Intro
This year I only managed to solve 9 challenges. Compared to last year, I guess I have improved myself. Some of the challenges were pretty hard(3) and some of them required too much detective work and guessing. Even though number 3 was hard, it was a fair challenge. However, number 10 was just a riddle. It was like someone dealt with that CPU or maybe wrote an emulator for it and wanted us to suffer too lol. I hope next year they don’t ask questions related to the abacus or some ancient computers.
In this blog post, I will only show the solutions to the challenges that I enjoyed solving.
3 mypassion
This was a very hard challenge even though it was the 3rd one. My biggest mistake was patching everything. However, there is no patching necessary to solve this challenge. You were required to run the exe with a command line so that it shows the flag. Every part of the command line is there for a reason.
I will put the solution first and then I will explain the parts
01c34R@brUc3E/1337pr.ost/20ABCDEFGH/)pizza/AMu$E`0R.MAZe/YPXEKCZXYIGMNOXNMXPYCXGXNE/ob5cUr3/fin/
- 01c34R@brUc3E: This is pretty much a constant string. The first char must be 0. (second char << 2) + third = 0x127
- 1337pr.ost: Constant
- 20ABCDEFGH: This string is made of 2 parts 20 and ABCDEFGH. 20 is actually a number in base 4. So 20 is equal to 8 which is the length of the next part. This number(8) is being used for Sleep function. If you don’t let the program sleep for a while it crashes.
- )pizza: The first character is based on the day of the month. This value is important. Because the app first adds the day of the month + 0x1F to buffer and then uses the first char of pizza to subtract it. Therefore, to cancel the effect of the day, you need to get the day of the month and add 0x1F to calculate the character.
- AMu$E`0R.MAZe: This is the missing bytes of the shellcode. It was a pain to recover.
- YPXEKCZXYIGMNOXNMXPYCXGXNE: Constant
- ob5cUr3: Constant
- fin: Constant
If you run the app with the correct cmd line, you will get the flag b0rn_t0_5truc7_b4by@flare-on.com
6 Flaresay
This is a two-part challenge. First, you need to run the game in DOS. I used DOSBox to debug the application. You need to do the exact movements the game shows so that you can score. I first thought the game was using the moves of the game directly and self-modifying itself. I even wrote a brute-forcer for it. It gave me MPHMKMHMPHPMHPMH
I put that string into the position it expected. It passed the hash check of the main exe but decryption failed lol. Time to go back to the DOS part and analyze again.
Every time you start the game, it starts with different moves. Even if you beat the game, you can’t know if that was the correct game to beat. Let’s see how the seed is calculated for random moves.
SetRandomSeed proc near
pusha
mov di, 953h
xor cx, cx
mov al, 0
out 70h, al
in al, 71h
mov byte_10952, al
loc_108D2:
call sub_10076 ; Check the moves and fill the buffer
jz short loc_10908
cmp al, 0Dh
jz short loc_10916
sub al, 20h ; ' '
cmp al, 41h ; 'A'
jz short loc_108FB
cmp al, 42h ; 'B'
jz short loc_108FB
cmp ah, 48h ; 'H'
jz short loc_108FF
cmp ah, 50h ; 'P'
jz short loc_108FF
cmp ah, 4Bh ; 'K'
jz short loc_108FF
cmp ah, 4Dh ; 'M'
jz short loc_108FF
jmp short loc_10908
loc_108FB:
mov [di], al
jmp short loc_10901
loc_108FF:
mov [di], ah
loc_10901:
inc di
inc cx
cmp cx, 0Bh
jz short loc_10916
loc_10908:
mov al, 0
out 70h, al
in al, 71h
sub al, byte_10952
cmp al, 0Ah
jl short loc_108D2
loc_10916:
mov cx, 0Ah
mov si, offset unk_10953
mov di, offset aHhppkmkmba ; "HHPPKMKMBA"
cld
repe cmpsb
jnz short loc_1093B
mov cx, 5
mov si, 953h
xor ax, ax
loc_1092C:
mov bx, [si]
shr bx, 5
add ax, bx
inc si
loop loc_1092C
call sub_10732
jmp short loc_1094D
loc_1093B:
xor bx, bx
mov al, 2
out 70h, al
in al, 71h
mov bl, al
mov al, 0
out 70h, al
in al, 71h
mov ah, bl
loc_1094D:
call sub_10094
popa
retn
SetRandomSeed endp
Here is the fun part! As you can see from this code, it checks special movements before you start the game. If those special movements are correct, it jumps to loc_1092C
and sets the correct seed. The special move is the sequence of HHPPKMKMBA
which is also called as Konami Code. Just do Up, Up, Down, Down, Left, Right, Left, Right, B, A and start the game. If you don’t want to waste time, you can patch the game so that it can play itself. I also removed the code responsible for the preview. After you run the DOS game, it will correctly modify the exe. Running the main exe on Windows will show us the correct flag: Ha$htag_n0_rc4_aNd_Th3n_s0me@flare-on.com
8 AmongRust
Challenge says that this is malware. Therefore, I run this on VM with a Sandboxie-Plus. It infected all the exe files on my user’s folder and created svchost.exe
inside AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
for persistence. You can easily decrypt payloads from the main exe by using HcPeterr
as the xor key. If we run the server part of the ransomware, we can easily see that it is listening on port 8345
If we open the WireShark packet dump and filter by port tcp.port == 8345
, we can easily see the traffic. The client first sends the key and nonce. After that, the client issues a command to get the name of the computer and uploads files to the infected machine
00000000 65 74 21 2c 9b 4d 93 34 d8 93 be c2 47 7c b8 6a et!,.M.4 ....G|.j
00000010 70 98 3b 3c 33 95 2d 68 a8 cc 5c 02 26 07 0a bf p.;<3.-h ..\.&...
00000000 41 43 4b 5f 4b 0d ACK_K.
00000020 0e 02 f4 a9 a8 b5 be ea ba 83 48 d6 d2 f8 7c 60 ........ ..H...|`
00000030 68 49 df 9a 5e ef 49 a6 5c 98 cf 07 d4 c2 38 a6 hI..^.I. \.....8.
00000006 41 43 4b 5f 4e 0d ACK_N.
00000040 65 78 65 63 20 77 68 6f 61 6d 69 0d 0a exec who ami..
0000000C 64 65 73 6b 74 6f 70 2d 31 63 6d 72 33 71 6c 5c desktop- 1cmr3ql\
0000001C 75 73 65 72 0a 0d user..
0000004D 65 78 65 63 20 6d 6b 64 69 72 20 43 3a 5c 55 73 exec mkd ir C:\Us
0000005D 65 72 73 5c 75 73 65 72 5c 41 6d 6f 6e 67 52 75 ers\user \AmongRu
0000006D 73 74 0d 0a st..
00000022 0d .
00000071 75 70 6c 6f 61 64 20 43 3a 5c 55 73 65 72 73 5c upload C :\Users\
00000081 75 73 65 72 5c 41 6d 6f 6e 67 52 75 73 74 5c 77 user\Amo ngRust\w
00000091 61 6c 6c 70 61 70 65 72 2e 50 4e 47 20 31 32 32 allpaper .PNG 122
000000A1 32 31 38 0d 0a 218..
00000023 41 43 4b 5f 55 50 4c 4f 41 44 0d ACK_UPLO AD.
Instead of patching exe, and dealing with files, let’s write a client code and emulate the communication. Dump the file from wireshark dump and save it as wall.png
next to the below script. Running the below script will automatically create the decrypted wallpaper in a given directory.
import socket
import os
print("[+] Connecting to the server")
server_address = ('localhost', 8345)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(server_address)
print("[+] Connection successful!")
key = bytes.fromhex('6574212c9b4d9334d893bec2477cb86a70983b3c33952d68a8cc5c0226070abf')
nonce = bytes.fromhex('0e02f4a9a8b5beeaba8348d6d2f87c606849df9a5eef49a65c98cf07d4c238a6')
client_socket.send(key)
ack_k = client_socket.recv(12)
print("[+] Key exchange result %s" % ack_k)
client_socket.send(nonce)
ack_n = client_socket.recv(12)
print("[+] Nonce exchange result %s" % ack_n)
upload_cmd = b'upload C:\output\wall.png 122218\x0D\x0A'
client_socket.send(upload_cmd)
print("[+] Upload cmd sent")
upload_result = client_socket.recv(12)
print("[+] Upload cmd result %s" % upload_result)
file_path = "wall.png"
data = None
try:
with open(file_path, 'rb') as file:
data = file.read()
client_socket.send(data)
upload_result = client_socket.recv(12)
print("[+] Upload binary result %s", upload_result)
except FileNotFoundError:
print(f"File '{file_path}' not found.")
except Exception as e:
print(f"An error occurred: {str(e)}")
client_socket.close()
Decrypted PNG will reveal our flag: n0T_SuS_4t_aLl@flare-on.com
9 MBRansom
This one was the most fun challenge for me. When you run this disk inside an emulator, you’re greeted with a ransomware message. You’re supposed to write the correct key so that it will decrypt the disk. You can use Bochs to debug MBR code. It first decrypts itself gets the HD serial and finally jumps to the 1000
address. I run this on Bochs so addresses could be different. Finally, it checks your serial with the following code
seg000:1296 push si
seg000:1297 push di
seg000:1298 sub sp, 8
seg000:129B mov si, offset user_key
seg000:129E mov di, offset key_table
seg000:12A1 mov cx, 804h
seg000:12A4
seg000:12A4 loc_12A4:
seg000:12A4 lodsw
seg000:12A5 shl al, cl
seg000:12A7 or al, ah
seg000:12A9 stosb
seg000:12AA dec ch
seg000:12AC jnz short loc_12A4
seg000:12AE mov si, offset key_table
seg000:12B1 mov di, offset hd_serial
seg000:12B4 dec cx
seg000:12B5
seg000:12B5 loc_12B5:
seg000:12B5 lodsw
seg000:12B6 xor ax, [di]
seg000:12B8 inc di
seg000:12B9 inc di
seg000:12BA cmp ax, 5555h
seg000:12BD jnz short loc_1304
Basically, it xors your serial with HD serial and checks if the result is 5555h for each word. It looks so easy. Let’s write a basic code for that
hd_serial = bytes([0x34, 0x87, 0xB3, 0xB4, 0x1F, 0x20])
result = bytearray()
for i in range(0, len(hd_serial), 2):
word = (hd_serial[i + 1] << 8) | hd_serial[i]
word ^= 0x5555
xor_bytes = bytearray([word & 0xFF, (word >> 8) & 0xFF])
result.extend(xor_bytes)
hex_result = "".join(["{:02X}".format(byte) for byte in result])
print("Partial Key:", hex_result)
This code gives only the partial key. We need to find 4 more chars to correctly decrypt the disk. We need a way to brute force it. It is cumbersome to convert 16-bit code to run it on modern machines. The key check routine is below
seg000:11FB push dx
seg000:11FC call sub_1296
seg000:11FF pop dx
seg000:1200 test ax, ax
seg000:1202 jnz short loc_1205
If the key is wrong, ax returns the offset of the error message. The correct key will give us 0 in the ax register.
I decided to try something different. I dumped the MBR code after it correctly decrypted itself and put the HD serial.
I used unicorn to emulate the environment and bruteforce it. Here is the script that solves the challenge.
from unicorn import *
from unicorn.x86_const import *
import os
mu = Uc(UC_ARCH_X86, UC_MODE_16)
if __name__ == "__main__":
image_path = os.path.join(os.path.dirname(__file__), "rawimage.bin") # dumped MBR code
with open(image_path, "rb") as f:
file_data = f.read()
hd_serial = bytes([0x34, 0x87, 0xB3, 0xB4, 0x1F, 0x20]) # hd serial
result = bytearray()
for i in range(0, len(hd_serial), 2):
word = (hd_serial[i + 1] << 8) | hd_serial[i]
word ^= 0x5555
xor_bytes = bytearray([word & 0xFF, (word >> 8) & 0xFF])
result.extend(xor_bytes)
partial_serial = "".join(["{:02X}".format(byte) for byte in result])
print("Partial Key:", partial_serial)
byte_serial = b''
for char in partial_serial:
byte_value = int(char, 16)
byte_serial += bytes([byte_value])
img_base = 0x0600
entry_point = 0x11FB
# seg000:11FB push dx
# seg000:11FC call sub_1296 ; check serial
# seg000:11FF pop dx
# seg000:1200 test ax, ax
try:
# Initialize CPU emulator
# Write image to the emulator's memory
mem_size = 0x10000
mu.mem_map(0, mem_size)
mu.mem_write(img_base, file_data) # Write MBR
mu.mem_write(0x2A4C,byte_serial) # Partial Key
mu.mem_write(0x19FC,hd_serial) # HD Serial
print("Bruteforcing the last 4 chars...")
for value in range(65536):
byte1 = (value >> 12) & 0x0F
byte2 = (value >> 8) & 0x0F
byte3 = (value >> 4) & 0x0F
byte4 = value & 0x0F
byte_array = bytes([byte1, byte2, byte3, byte4])
mu.mem_write(0x2A58, byte_array)
mu.reg_write(UC_X86_REG_DX, 0x1224)
# run the serial check routine
mu.emu_start(entry_point, 0x1200)
ax_reg = mu.reg_read(UC_X86_REG_AX)
if (ax_reg == 0): # if ax == 0x18fb serial is wrong.
# Serial is correct!
result = byte_serial + byte_array
hex_string = ''.join(f'{byte:X}' for byte in result)
print("Full Key: %s" % hex_string)
exit(0)
print("Emulation done")
except UcError as e:
print("ERROR: %s" % e)
Running this script gave me 61D2E6E14A754ADC
as the correct key. When we put our key and hit Enter, it correctly decrypted the disk and booted into FreeDOS. Typing type flag.txt
shows us the flag: bl0wf1$h_3ncrypt10n_0f_p@rt1t10n_1n_r3al_m0d3@flare-on.com
You can get all the source codes of this blog post from this repo