Flare-On 5 Challenge
Author: shiki7

Challenge 1. Minesweeper Championship Registration
We're given a jar
file. Using jad
to decompile the contained class
files gives us the key:
The flag is: GoldenTicket2018@flare-on.com
Challenge 2. Ultimate Minesweeper
We're given an .NET assembly which is a minesweeper game. According to the decompilation result of .NET Reflector
, the coordinates of the normal fields are generated dynamically. I extracted the algorithm and wrote a simple program which prints out the coordinates:
The flag is: Ch3aters_Alw4ys_W1n@flare-on.com
Challenge 3. FLEGGO
In this challenge we get dozens of executables. A quick glance at some of the binaries revealed that every program compares the input with some embedded resource, and extract an image along with a character if correct input is provided.
Notably every image extracted is marked with an number, presumably the index of the character inside the flag string. So I wrote a simple script to help me recover the flag:
And yes, I would use OCR if I could.
The flag is: mor3_awes0m3_th4n_an_awes0me_p0ssum@flare-on.com
Challenge 4. binstall
We're provided with a malicious dotNet assembly which is obfuscated. I first ran de4dot
on this binary and started using dnSpy
to analyze the deobfuscated file. The malware will first delete some files, and install an DLL file which is called browserassist.dll
.
The DLL is configured as an AppInitDLL, but according to the challenge description, this DLL will only work in the context of firefox.exe
. Unfortunately I couldn't get this running, so let's begin our static analysis as usual.
Inspecting the DLL I found that there is a general decryption routine, which will use hashed FL@R3ON.EXE
as key and decrypt some data. Decrypting some of the data I found a suspicious pastebin
URL, using the same decryption routine to decrypt the content of the URL gives us several JSON object which contains some javascript snippets. Looks like this DLL will inject these javascript to some of the web pages.
Combining and analyzing the javascript gives us the flag.
Details forgotten.
Sadly I lost the progress due to the death of my VM…
Challenge 5. Web 2.0
This is a WebAssembly
reversing challenge. We're provided with:
index.html
, html page running the challenge.
test.wasm
, WebAssembly executable
main.js
, javascript referenced by the index page, fetching and executing test.wasm
By reading main.js
, we found there's an function exported by test.wasm
named Match
. main.js
calls this function with two parameters, an array containing some data, and our input, and if the function returns 1
, we win the challenge.
So the verifing logic must be in the WebAssembly file, we need to dig through it.
I cat't tell you how to efficiently read the WebAssembly specification and manually decompile the whole thing, actually it's a quite painful process. Fortunately I found a shortcut which greatly accelerates my analysis: I used wasm2c
in wabt, converting the WebAssembly into a single c
source file, and compiled the generated source file into a native binary, and this makes the logic human-readable.
Reversing the compiled native binary and it showed us it's actually an VM and the provided array is the bytecode, so I can finally wrote the solver script:
from ctypes import c_uint8
code = [
0xE4, 0x47, 0x30, 0x10, 0x61, 0x24, 0x52, 0x21, 0x86, 0x40, 0xAD, 0xC1, 0xA0, 0xB4, 0x50, 0x22, 0xD0, 0x75, 0x32, 0x48, 0x24, 0x86, 0xE3, 0x48, 0xA1, 0x85, 0x36, 0x6D, 0xCC, 0x33, 0x7B, 0x6E, 0x93, 0x7F, 0x73, 0x61, 0xA0, 0xF6, 0x86, 0xEA, 0x55, 0x48, 0x2A, 0xB3, 0xFF, 0x6F, 0x91, 0x90, 0xA1, 0x93, 0x70, 0x7A, 0x06, 0x2A, 0x6A, 0x66, 0x64, 0xCA, 0x94, 0x20, 0x4C, 0x10, 0x61, 0x53, 0x77, 0x72, 0x42, 0xE9, 0x8C, 0x30, 0x2D, 0xF3, 0x6F, 0x6F, 0xB1, 0x91, 0x65, 0x24, 0x0A, 0x14, 0x21, 0x42, 0xA3, 0xEF, 0x6F, 0x55, 0x97, 0xD6
]
result = ''
idx = 0
while idx < len(code):
func = code[idx] & 0xF
if func == 0:
result += chr(code[idx + 1])
idx += 2
elif func == 1:
result += chr(code[idx + 1] ^ 0xff)
idx += 2
elif func == 2:
result += chr(code[idx + 1] ^ code[idx + 2])
idx += 3
elif func == 3:
result += chr(code[idx + 1] & code[idx + 2])
idx += 3
elif func == 4:
result += chr(code[idx + 1] | code[idx + 2])
idx += 3
elif func == 5:
result += chr(c_uint8(code[idx + 1] + code[idx + 2]).value)
idx += 3
elif func == 6:
result += chr(c_uint8(code[idx + 2] - code[idx + 1]).value)
idx += 3
else:
raise "Invalid instruction"
print result
The flag is: wasm_rulez_js_droolz@flare-on.com
Challenge 6. Magic
A traditional Linux ELF
reversing challenge, we're provided an stripped x64 ELF binary.
The binary roughly does several things:
- call
srand
with an static seed
- for each part of the input, decrypts the corresponding checking function and calls it, and encrypts it again after return.
- xor's some data with the input
- goes back to step 2, this loops 666 times.
Interestingly, this binary overwrites it self after each input, shuffling the input checker sequence, and saves it to the disk, overriding the original file.
I wrote the following script to extract the input checking sequence:
Further reverse engineering tells us the algorithms used inside the checker function. And I wrote the following script to immitate the program behaviour and found the correct input.
Feeding the original program with the inputs gives us the flag. The flag is: mag!iC_mUshr00ms_maY_h4ve_g!ven_uS_Santa_ClaUs@flare-on.com
Challenge 7. WoW
This challenge is pretty interesting. The given binary is a 32-bit Windows PE executable, but this binary switches to 64-bit code segment and loads a 64-bit DLL in memory.
The loaded DLL hooks NtDeviceIoControlFile
and extract another 32-bit DLL and the check logic seems to be inside this 32-bit DLL, but the strange thing is, it will take user input as a port number and connects to 127.0.0.1:{input}
and at last, recv
the flag.
This gives me a hint about why the program hooks NtDeviceIoControlFile
. On Windows, most of the socket-related functions will make IoCtls to afd.sys
, with AFD_XXX
as the IoControlCode. The hook intercepts all the connect
, recv
calls and redirects it to its own logic. After figuring out the working mechanisms of this challenge, the rest is just a piece of cake.
import struct
keys = [15, 87, 97, 119, 11, 250, 181, 209, 129, 153, 172, 167, 144, 88, 26, 82, 12, 160, 8, 45, 237, 213, 109, 231, 224, 242, 188, 233, 242]
data = [95, 104, 68, 98, 35, 186, 33, 84, 51, 115, 4, 101, 80, 151, 114, 38, 1, 196, 205, 17, 182, 11, 214, 249, 88, 118, 126, 101, 105]
assert len(keys) == len(data)
def solve():
for i in xrange(len(keys)):
data[i] ^= keys[i]
for j in xrange(i + 1, len(keys)):
keys[j] ^= keys[i]
return
solve()
print ''.join(map(chr, data)) + '@flare-on.com'
Challenge 8. Doogie Hacker
The challenge gives us a dump of the first few sectors of a disk.
Running this binary inside QEMU gives us a prompt asking for password. So our goal is to find the password.
Doing a little reversing shows that the program first read the code from disk to 0x8000
and jumps to it, and then:
- get current time and stores it to
0x87f2
(4 bytes).
keyed_xor(0x8809, 0x87f2, 4)
- read user input
keyed_xor(0x8809, user_input, len(user_input))
write_string(0x8809)
So the first problem is the time. Actually the time comes from the prompt, it tells us the correct time is February 06, 1990
, so I used a small utility called faketime
to run QEMU.
And now I can extract the content at 0x8809
xored with the time:
And the second problem is about the key, we don't know how long the key is and what could be inside the decrypted data. So I found this fascinating tool xortool suitable for this situation. Brute-forcing all the possible most frequent characters shows that the original data could be some kind of ascii art and gives us a pretty good finding of the key:
… and tweaking this a little bit gives us the correct key: ioperateonmalware
The flag is: R3_PhD@flare-on.com
Challenge 9. leet editr
This is a pretty annoying challenge, as it cost me a lot of time to figure out why the challenge refuses to run on a zh_CN
Windows 7.
The program first allocated some pages and copied some data to it, and then it installed a VEH
, and jumps to the allocated page.
The VEH
is actually the key part of this challenge. It will dynamically decrypt the data and code when the faulting instruction needs it to, and encrypt it back after the execution of the instruction. I don't know how to use dynamic approaches against this anti-debug technique as my x64dbg
always gets deceived by the single trap exception
even if I told him to ignore it. So I wrote a python script to extract the code and data from the binary:
This gives us some sort of vbscript
and the code. The code actually calls the windows script host
and inserts several custom objects, and executes the script.
The script will open a Internet explorer
, set the content and monitors the input editarea. Interestingly, there's a snippet encrypted with RC4
which will detect several debuggers, the script will dynamically decrypt and executes this snippet.
So the script will first check if the ascii art is the correct one, I put the ascii art FLARE
extracted from the data to the input box and it asks for the title. Actually the title input comes from the code (a string embedded inside the code). Input the correct title gives us the flag.
The flag is: scr1pt1ng_sl4ck1ng_and_h4ck1ng@flare-on.com
Challenge 10. golf
This challenge is my personal favourite, though it didn't fraustrated me too much, it did give me an inspiration of how we can utilize hardware features to create RE challenges.
So the challenge is a Windows executable, which will extract a driver and load it while running. Something interesting is that the executable has a special instruction vmcall
inside, with the challenge description Did you bring your visor?
, this immediately reminds me that the driver could be a hypervisor
.
Since I already had some experience working with VMX
related stuffs, understanding how the driver works isn't too hard to me. The most fascinating part is that the driver utilizes EPT_VIOLATION
vmexit to implement a whole different instruction set, which is an awesome idea.
The script:
import os, sys, struct
u8 = lambda x: struct.unpack("<B", x)[0]
u16 = lambda x: struct.unpack("<h", x)[0]
u32 = lambda x: struct.unpack("<L", x)[0]
u64 = lambda x: struct.unpack("<Q", x)[0]
'''
0x01: ret
0xBB [reg]: pop reg
0xAA [reg]: push reg
0xC2 [r1] [r2]: add r2, r1
0x00 [r1] [r2]: set r2, r1
0xC3 [reg] [imm32]: sub reg, imm32
0xC1 [reg] [imm32]: add reg, imm32
0xD1 [reg] [imm32]: add32 reg, imm32
0xD3 [reg] [imm32]: sub32 reg, imm32
0xD2 [r1] [r2]: add32 r1, r2
0xD4 [r1] [r2]: sub32 r2, r1
0xC9 [imm32] [imm64]: set [rsp+imm32], imm64
0xD5 [r1] [r2]: xor32 r1, r2
0xC5 [r1] [r2]: xor r2, r1
0xD6 [reg] [imm32]: mov32 reg, imm32
0xC6 [reg] [imm64]: mov reg, imm64
0x30: memset(rdi, *rax, rcx)
0xC8 [reg] [imm32]: set [rsp+imm32], reg
0xD8 [reg] [imm32]: set32 [rsp+imm32], reg
0x1A [reg] [imm32]: set reg, [rsp + imm32]
0xC7 [r1] [r2]: set r1, r2
0x4A [r1] [imm32]: lea r1, rsp + imm32
0x44 [r1] [r2]: testz r1, r2
0x40 [r1] [r2]: cmp32 r1, r2
0x42 [off32] [imm32]: cmp32 [rsp+off32], imm32
0x50 [imm32]: set rip, rip + imm32
0x51 [imm16]: setnz rip, rip + imm16
0x52 [imm16]: setz rip, rip + imm16
0x54 [imm16]: setxx rip, rip + imm16
0xd7 [r1] [imm32]: xor32 r1, [rsp+imm32]
0x19 [r1] [imm32]: set32 r1, [rsp+imm32]
0x1b [imm32]: setal [rsp+imm32]
0x17 [imm32]: set8 [rsp+imm32], al
0xc0 [r1] [imm32]: xor32 r1, [imm32]
0x02 [r1] [r2]: set32 r2, r1
0x43 [r1] [r2]: testz32 r1, r2
0xbe [r1] [imm32]: and32 r1, imm32
0xbc [r1] [imm8]: shr32 r1, imm8
0xb9 [r1] [imm8]: shr r1, imm8
0x41 [r1] [imm32]: cmp32 r1, imm32
0x1d [r1] [r...] [imm32]: set r1, [rsp + imm32]
0x1e [r1] [r...] [imm32]: set r1, [imm32 + r... + rsp]
0x1f [r1] [r...] [imm32]: set r1, [imm32 + r...]
0x20 [r1] [r...] [imm32]:
'''
def _getreg(idx):
if idx == 0xF4:
return "rip"
elif idx == 0xEE:
return "rax"
elif idx == 0xEF:
return "rbx"
elif idx == 0xF0:
return "rcx"
elif idx == 0xF1:
return "rdx"
elif idx == 0xF2:
return "rsi"
elif idx == 0xF3:
return "rdi"
elif idx == 0xf5:
return "rsp"
elif idx == 0xf6:
return 'rbp'
elif idx == 0xf7:
return 'r8'
elif idx == 0xf8:
return 'r9'
elif idx == 0xf9:
return 'r10'
elif idx == 0xfa:
return 'r11'
elif idx == 0xfb:
return 'r12'
elif idx == 0xfc:
return 'r13'
elif idx == 0xfd:
return 'r14'
elif idx == 0xfe:
return 'r15'
return None
def _getreg32(idx):
if idx == 0xF4-9:
return 'eip'
elif idx == 0xEE-9:
return 'eax'
elif idx == 0xEF-9:
return 'ebx'
elif idx == 0xF0-9:
return 'ecx'
elif idx == 0xF1-9:
return 'edx'
elif idx == 0xF2-9:
return 'esi'
elif idx == 0xF3-9:
return 'edi'
elif idx == 0xF5-9:
return 'esp'
elif idx == 0xf6-9:
return 'ebp'
return None
def disasm(buffer):
index = 0
while True:
print '%d:' % index,
opcode = ord(buffer[index])
if opcode == 0x01:
print 'ret'
index += 1
elif opcode == 0xbb:
print 'pop {}'.format(_getreg(u8(buffer[index + 1])))
index += 2
elif opcode == 0xaa:
print 'push {}'.format(_getreg(u8(buffer[index + 1])))
index += 2
elif opcode == 0xc2:
print 'add {}, {}'.format(_getreg(u8(buffer[index + 2])), _getreg(u8(buffer[index + 1])))
index += 3
elif opcode == 0x00:
print 'mov {}, {}'.format(_getreg(u8(buffer[index + 2])), _getreg(u8(buffer[index + 1])))
index += 3
elif opcode == 0xc3:
print 'sub {}, {}'.format(_getreg(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0xc1:
print 'add {}, {}'.format(_getreg(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0xd1:
print 'add32 {}, {}'.format(_getreg32(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0xd3:
print 'sub32 {}, {}'.format(_getreg32(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0xd2:
print 'add32 {}, {}'.format(_getreg32(u8(buffer[index + 2])), _getreg32(u8(buffer[index + 1])))
index += 3
elif opcode == 0xd4:
print 'sub32 {}, {}'.format(_getreg32(u8(buffer[index + 2])), _getreg32(u8(buffer[index + 1])))
index += 3
elif opcode == 0xc9:
print 'set64 [rsp + {}], {}'.format(u32(buffer[index + 1: index + 5]), u64(buffer[index + 5: index + 13]))
index += 13
elif opcode == 0xd5:
print 'xor32 {}, {}'.format(_getreg32(u8(buffer[index + 1])), _getreg32(u8(buffer[index + 2])))
index += 3
elif opcode == 0xc5:
print 'xor {}, {}'.format(_getreg(u8(buffer[index + 2])), _getreg(u8(buffer[index + 1])))
index += 3
elif opcode == 0xd6:
print 'set32 {}, {}'.format(_getreg32(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0xc6:
print 'set64 {}, {}'.format(_getreg(u8(buffer[index + 1])), u64(buffer[index + 2: index + 10]))
index += 10
elif opcode == 0x30:
print 'memset rdi, *rax, rcx'
index += 1
elif opcode == 0xc8:
print 'set64 [rsp + {}], {}'.format(u32(buffer[index + 2: index + 6]), _getreg(u8(buffer[index + 1])))
index += 6
elif opcode == 0xd8:
print 'set32 [rsp + {}], {}'.format(u32(buffer[index + 2: index + 6]), _getreg32(u8(buffer[index + 1])))
index += 6
elif opcode == 0x1a:
print 'set64 {}, [rsp + {}]'.format(_getreg(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0xc7:
print 'set64 {}, {}'.format(_getreg(u8(buffer[index + 2])), _getreg(u8(buffer[index + 1])))
index += 3
elif opcode == 0x4a:
print 'lea {}, [rsp + {}]'.format(_getreg(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0x44:
print 'testz {}, {}'.format(_getreg(u8(buffer[index + 2])), _getreg(u8(buffer[index + 1])))
index += 3
elif opcode == 0x40:
print 'cmp32 {}, {}'.format(_getreg32(u8(buffer[index + 1])), _getreg32(u8(buffer[index + 2])))
index += 3
elif opcode == 0x42:
print 'cmp32 [rsp + {}], {}'.format(u32(buffer[index + 1: index + 5]), u32(buffer[index + 5: index + 9]))
index += 9
elif opcode == 0x50:
print 'set64 rip, rip + {}'.format(u16(buffer[index + 1: index + 3]))
index += 3
elif opcode == 0x51:
print 'set64nz rip, rip + {}'.format(u16(buffer[index+1:index+3]))
index += 3
elif opcode == 0x52:
print 'set64z rip, rip + {}'.format(u16(buffer[index+1:index+3]))
index += 3
elif opcode == 0x54:
print 'set64xx rip, rip + {}'.format(u16(buffer[index+1:index+3]))
index += 3
elif opcode == 0xd7:
print 'xor32 {}, [rsp + {}]'.format(_getreg32(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0x19:
print 'set32 {}, [rsp + {}]'.format(_getreg32(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0x1b:
print 'setal [rsp + {}]'.format(u32(buffer[index + 1: index + 5]))
index += 5
elif opcode == 0x17:
print 'set8 [rsp + {}], al'.format(u32(buffer[index + 1: index + 5]))
index += 5
elif opcode == 0xc0:
print 'xor32 {}, {}'.format(_getreg32(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0x02:
print 'set32 {}, {}'.format(_getreg32(u8(buffer[index + 2])), _getreg32(u8(buffer[index + 1])))
index += 3
elif opcode == 0x43:
print 'testz32 {}, {}'.format(_getreg32(u8(buffer[index + 2])), _getreg32(u8(buffer[index + 1])))
index += 3
elif opcode == 0xbe:
print 'and32 {}, {}'.format(_getreg32(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode == 0xbc:
print 'shr32 {}, {}'.format(_getreg32(u8(buffer[index + 1])), u8(buffer[index + 2]))
index += 3
elif opcode == 0xb9:
print 'shr {}, {}'.format(_getreg(u8(buffer[index + 1])), u8(buffer[index + 2]))
index += 3
elif opcode == 0x41:
print 'cmp32 {}, {}'.format(_getreg32(u8(buffer[index + 1])), u32(buffer[index + 2: index + 6]))
index += 6
elif opcode >= 0x1c and opcode <= 0x20:
ptr = index
if opcode == 0x1d:
ptr += 1
target = _getreg(u8(buffer[ptr]))
ptr += 1
while _getreg(u8(buffer[ptr])) != None:
ptr += 1
imm32 = u32(buffer[ptr: ptr + 4])
ptr += 4
print 'set64sx {}, dword ptr [rsp + {}]'.format(target, imm32)
elif opcode == 0x1e:
ptr += 1
target = _getreg32(u8(buffer[ptr]))
ptr += 1
s = []
while _getreg(u8(buffer[ptr])) != None:
s.append(_getreg(u8(buffer[ptr])))
ptr += 1
imm32 = u32(buffer[ptr: ptr + 4])
ptr += 4
print 'set8sx {}, byte ptr [rsp + {} + {}]'.format(target, '+'.join(s), imm32)
elif opcode == 0x1f:
ptr += 1
target = _getreg32(u8(buffer[ptr]))
ptr += 1
s = []
while _getreg(u8(buffer[ptr])) != None:
s.append(_getreg(u8(buffer[ptr])))
ptr += 1
imm32 = u32(buffer[ptr: ptr + 4])
ptr += 4
print 'set8sx {}, byte ptr [{} + {}]'.format(target, '+'.join(s), imm32)
elif opcode == 0x20:
ptr += 1
target = _getreg32(u8(buffer[ptr]))
ptr += 1
s = []
while _getreg(u8(buffer[ptr])) != None:
s.append(_getreg(u8(buffer[ptr])))
ptr += 1
imm32 = u32(buffer[ptr: ptr + 4])
ptr += 4
print 'set32 {}, byte ptr [{} + {}]'.format(target, '+'.join(s), imm32)
elif opcode == 0x1c:
ptr += 1
target = _getreg32(u8(buffer[ptr]))
ptr += 1
s = []
while _getreg(u8(buffer[ptr])) != None:
s.append(_getreg(u8(buffer[ptr])))
ptr += 1
imm32 = u32(buffer[ptr: ptr + 4])
ptr += 4
print 'set32 {}, byte ptr [rsp + {} + {}]'.format(target, '+'.join(s), imm32)
index = ptr
else:
print 'ud%d' % opcode
break
return
code = open("code4.bin", 'rb').read()
disasm(code)
The checker are broke into several parts, with each of them having a relatively easy checking algorithm.
Challenge 11. malware skillz
This challenge is cool, but reverse engineering the c2 module is a time-consuming job…
Script to extract main c2 module from traffic:
Script to find native function by hash:
Script to extract c2 traffic:
import lief, dpkt, os, sys, IPython, socket, struct
from hexdump import hexdump
dehex = lambda x: x.replace(' ', '').replace('\n', '').decode('hex')
chex = lambda x: ','.join(map(lambda y: '0x%02x' % ord(y), x))
u32 = lambda x: struct.unpack("<L", x)[0]
u16 = lambda x: struct.unpack("<H", x)[0]
client_decstub = '''
{{
char* a1 = NULL;
int a2;
char* decfunc = RVATOVA(char*, module, 0x65A0);
THISCALL(5, decfunc, crypto_c, {encrypted}, sizeof({encrypted}), {iv}, &a1, &a2);
hexdump(a1, a2);
void* ptr = halloc(a2);
memcpy(ptr, a1, a2);
char* sa1 = NULL;
int sa2;
char* encfunc = RVATOVA(char*, module, 0x64F0);
THISCALL(5, encfunc, crypto_s, ptr, a2, {iv}, &sa1, &sa2);
write_file("server_{idx}.bin", a1, a2); // sent from server
}}
'''
server_decstub = '''
{{
char* a1 = NULL;
int a2;
char* decfunc = RVATOVA(char*, module, 0x65A0);
THISCALL(5, decfunc, crypto_s, {encrypted}, sizeof({encrypted}), {iv}, &a1, &a2);
void* ptr = halloc(a2);
memcpy(ptr, a1, a2);
char* sa1 = NULL;
int sa2;
char* encfunc = RVATOVA(char*, module, 0x64F0);
THISCALL(5, encfunc, crypto_c, ptr, a2, {iv}, &sa1, &sa2);
free(ptr);
write_file("client_{idx}.bin", a1, a2);
}}
'''
defglobals = []
defcode = []
def uchar_arr(name, buf):
return 'unsigned char {name}[] = {{ {buf} }};'.format(name=name, buf=chex(buf))
def add_server_packet(data, iv, idx):
assert len(iv) == 16
ivname = 'siv%d' % idx
dname = 'sd%d' % idx
defglobals.append(uchar_arr(dname, data))
defglobals.append(uchar_arr(ivname, iv))
defcode.append(client_decstub.format(encrypted=dname, iv=ivname, idx=idx))
return
def add_client_packet(data, iv, idx):
assert len(iv) == 16
ivname = 'civ%d' % idx
dname = 'cd%d' % idx
defglobals.append(uchar_arr(dname, data))
defglobals.append(uchar_arr(ivname, iv))
defcode.append(server_decstub.format(encrypted=dname, iv=ivname, idx=idx))
return
pcaps = dpkt.pcapng.Reader(open("./pcap.pcap", 'rb'))
if not os.path.exists("./splitted/"):
os.mkdir("./splitted/")
'''
# this is the first stage, attacking 192.168.221.91
def is_client(pkt):
# ethernet pkt
if socket.inet_ntoa(pkt.data.src) == '192.168.221.91' and socket.inet_ntoa(pkt.data.dst) == '52.0.104.200':
return True
return False
def is_server(pkt):
if socket.inet_ntoa(pkt.data.dst) == '192.168.221.91' and socket.inet_ntoa(pkt.data.src) == '52.0.104.200':
return True
return False
def is_valid(pkt):
# tcp pkt
if len(pkt.data) >= 58 and pkt.data[3] == '\x8f':
return True
return False
c = 0
s = 0
while True:
try:
ts, buf = pcaps.next()
except StopIteration:
break
packet = dpkt.ethernet.Ethernet(buf)
if is_client(packet) and isinstance(packet.data.data, dpkt.tcp.TCP) and is_valid(packet.data.data):
tcp_data = packet.data.data.data
size = u32(tcp_data[:4]) & 0xFFFFFF
print 'Valid client packet (%d), extracting...' % size
#if size > 2000: hexdump(tcp_data)
buffer = tcp_data
rem_len = size - len(tcp_data)
while rem_len != 0:
ts, buf = pcaps.next()
assert is_client(packet) and isinstance(packet.data.data, dpkt.tcp.TCP)
pkt = dpkt.ethernet.Ethernet(buf)
buffer += pkt.data.data.data
rem_len -= len(pkt.data.data.data)
assert len(buffer) == size
iv = buffer[42:58]
data = buffer[58:]
add_client_packet(data, iv, c)
open("splitted/c_{}.bin".format(c), 'wb').write(buffer)
c += 1
elif is_server(packet) and isinstance(packet.data.data, dpkt.tcp.TCP) and is_valid(packet.data.data):
tcp_data = packet.data.data.data
size = u32(tcp_data[:4]) & 0xFFFFFF
print 'Valid server packet (%d), extracting...' % size
#if size > 2000: hexdump(tcp_data)
buffer = tcp_data
rem_len = size - len(tcp_data)
while rem_len != 0:
ts, buf = pcaps.next()
assert is_server(packet) and isinstance(packet.data.data, dpkt.tcp.TCP)
pkt = dpkt.ethernet.Ethernet(buf)
buffer += pkt.data.data.data
rem_len -= len(packet.data.data.data)
iv = buffer[42:58]
data = buffer[58:]
add_server_packet(data, iv, s)
open("splitted/s_{}.bin".format(s), 'wb').write(buffer)
s += 1
'''
def is_client(pkt):
if socket.inet_ntoa(pkt.data.src) == '192.168.221.91' and socket.inet_ntoa(pkt.data.dst) == '192.168.221.105' and pkt.data.data.dport == 445:
return True
return False
def is_server(pkt):
if socket.inet_ntoa(pkt.data.dst) == '192.168.221.91' and socket.inet_ntoa(pkt.data.src) == '192.168.221.105' and pkt.data.data.sport == 445:
return True
return False
def is_valid_client(pkt):
data = pkt.data[4:]
if len(data) < 4: return False
if data[0:4] == '\xfeSMB':
flags = u32(data[0x10:0x14])
if flags & 1 == 0:
cmd = u16(data[0xc:0xe])
if cmd == 9:
return True
return False
def is_valid_server(pkt):
data = pkt.data[4:]
if len(data) < 4: return False
if data[0:4] == '\xfeSMB':
flags = u32(data[0x10:0x14])
if (flags & 1) != 0:
cmd = u16(data[0xc:0xe])
if cmd == 8:
status = u32(data[0x8:0xc])
if status == 0:
return True
return False
c = 0
s = 0
while True:
try:
ts, buf = pcaps.next()
except StopIteration:
break
packet = dpkt.ethernet.Ethernet(buf)
if isinstance(packet.data.data, dpkt.tcp.TCP):
if is_client(packet) and is_valid_client(packet.data.data):
wreq = packet.data.data.data[4+64+48:]
if len(wreq) <= 58 or wreq[3] != '\x8f': continue
size = u32(wreq[0:4]) & 0xffffff
print 'Valid client packet (%d), extracting...' % size
buffer = ''
buffer += wreq
rem_size = size - len(wreq)
while rem_size != 0:
ts, buf = pcaps.next()
packet = dpkt.ethernet.Ethernet(buf)
assert is_client(packet)
buffer += packet.data.data.data
rem_size -= len(packet.data.data.data)
assert len(buffer) == size
iv = buffer[42:58]
data = buffer[58:]
add_client_packet(data, iv, c)
c += 1
elif is_server(packet) and is_valid_server(packet.data.data):
rrsp = packet.data.data.data[4+64+16:]
if len(rrsp) <= 58 or rrsp[3] != '\x8f': continue
size = u32(rrsp[0:4]) & 0xffffff
print 'Valid server packet (%d), extracting...' % size
buffer = ''
buffer += rrsp
rem_size = size - len(rrsp)
while rem_size != 0:
ts, buf = pcaps.next()
packet = dpkt.ethernet.Ethernet(buf)
assert is_client(packet)
buffer += packet.data.data.data
rem_size -= len(packet.data.data.data)
assert len(buffer) == size
iv = buffer[42:58]
data = buffer[58:]
add_server_packet(data, iv, s)
s += 1
open("data.h", 'wb').write('\n'.join(defglobals))
open("code.h", 'wb').write('\n'.join(defcode))
print 'Done!'
Program to decrypt c2 traffic:
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
void* load_mem(char* filename) {
void* buffer = NULL;
FILE* fp = fopen(filename, "rb");
if(!fp) {
return buffer;
}
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
fseek(fp, 0, SEEK_SET);
buffer = VirtualAlloc((void*)(0x400000), size, MEM_COMMIT | MEM_PRIVATE, PAGE_EXECUTE_READWRITE);
if(!buffer) {
fclose(fp);
return buffer;
}
fread(buffer, 1, size, fp);
fclose(fp);
return buffer;
}
void write_file(char* filename, void* buffer, size_t size) {
FILE* fp = fopen(filename, "wb");
fwrite(buffer, 1, size, fp);
fclose(fp);
return;
}
char* load_module(char* filename) {
return (char*)LoadLibraryA(filename);
}
#define RVATOVA(type, base, rva) (type)((char*)base + rva)
#define THISCALL_CALLER __asm__ volatile ( \
"mov (%0), %%ecx\n\t" \
"mov (%1), %%eax\n\t" \
"jmp *%%eax\n\t" \
: \
: "m" (g_this), "m" (g_callsite) \
: "cc" \
)
int g_this, g_callsite;
int __stdcall __attribute__((naked, noinline)) __thiscall0() { THISCALL_CALLER; }
int __stdcall __attribute__((naked, noinline)) __thiscall1(int arg0) { THISCALL_CALLER; }
int __stdcall __attribute__((naked, noinline)) __thiscall2(int arg0, int arg1) { THISCALL_CALLER; }
int __stdcall __attribute__((naked, noinline)) __thiscall3(int arg0, int arg1, int arg2) { THISCALL_CALLER; }
int __stdcall __attribute__((naked, noinline)) __thiscall4(int arg0, int arg1, int arg2, int arg3) { THISCALL_CALLER; }
int __stdcall __attribute__((naked, noinline)) __thiscall5(int arg0, int arg1, int arg2, int arg3, int arg4) { THISCALL_CALLER; }
int __stdcall __attribute__((naked, noinline)) __thiscall6(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { THISCALL_CALLER; }
#define THISCALL(nargs, callsite, this, ...) \
g_this = (int)this; \
g_callsite = (int)callsite; \
__thiscall##nargs(__VA_ARGS__);
#define halloc(x) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, x)
#define hfree(x) HeapFree(GetProcessHeap(), 0, x)
void hexdump(unsigned char* buf, int size) {
for(int base = 0; base < size; base += 0x10) {
printf("%08X: ", base);
for(int offset = 0; offset < 0x10 && offset + base < size; offset++) {
printf("%02x ", buf[base + offset]);
}
putchar('\n');
}
return;
}
char* mod;
unsigned char client[] = {0xe7,0x66,0xe6,0x5a,0xe8,0x50,0x9d,0x68,0x33,0xd7,0x3a,0x37,0xd1,0xec,0x4a,0xd8};
unsigned char server[] = {0x5f,0xa5,0x29,0x40,0x57,0x65,0x44,0xd4,0x4d,0x01,0xfa,0x2a,0x37,0xf4,0x9f,0xc4};
#include "data.h"
int main(int argc, char* argv[]) {
char* module = load_module("mal.dll");
if(!module) {
printf("Cannot load module, %d\n", GetLastError());
return -1;
}
mod = module;
void* crypto_c = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x148);
char* initfunc = RVATOVA(char*, module, 0x63E0);
THISCALL(1, initfunc, crypto_c, 0);
void* crypto_s = halloc(0x148);
THISCALL(1, initfunc, crypto_s, 0);
void* comp_c = halloc(32);
char* compinit = RVATOVA(char*, module, 0xA5B0);
THISCALL(1, compinit, comp_c, 0);
void* comp_s = halloc(32);
THISCALL(1, compinit, comp_s, 0);
unsigned char key[16];
for(int i = 0; i < 16; i++) {
key[i] = client[i] ^ server[i] ^ 0xAA;
}
char* keyfunc = RVATOVA(char*, module, 0x64B0);
THISCALL(2, keyfunc, crypto_c, key, 16);
THISCALL(2, keyfunc, crypto_s, key, 16);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wint-conversion"
#pragma clang diagnostic ignored "-Wpointer-sign"
#include "code.h"
#pragma clang diagnostic pop
printf("Survived!\n");
return 0;
}
Challenge 12. Suspicious floppy disk
This is an insane challenge, you feel real despair when you unveiled the first layer of the abstruse logic, only to see another layer of terror.
Generally we could break this challenge into 3 parts.
Playing around with the image given, I found something interesting: type MESSAGE.DAT
command also triggers the checker routine. Moreover, if we extract infohelp.exe
and put it into dosbox
, we can see that the checker routine magically disappeared. Further reversing showed that infohelp.exe
involves no sequence checking login. So what is actually going on?
Debugging the image inside bochs
gave me the answer: The IVT
for int 13h
is replaced! This turns out to be BIOS interrupt
. Looking into the detoured logic shows there's more action when reading/writing specific sectors:
- When writing to the sector containing
KEY.DAT
data, the hook copies the data written to a global buffer.
- When reading from the sector containing
MESSAGE.DAT
data, the hook enters another function which is the checker function presumably.
So given the checker function, reversing result showed it's actually a subleq
VM. Let's first extract the VM environment and goes on to the second part:
2. The first layer of terror
I first crafted a small python script which implements the subleq
VM (the script is enhanced gradually in the process of analysis, shown is the final script I used):
import struct, os, sys, ctypes, IPython, hashlib
from hexdump import hexdump
c16 = lambda x: ctypes.c_int16(x).value
cu16 = lambda x: ctypes.c_uint16(x).value
u16 = lambda x: struct.unpack("<H", x)[0]
s16 = lambda x: struct.unpack("<h", x)[0]
p16 = lambda x: struct.pack("<H", c16(x))[0]
data = bytearray(open("subleq.vm", 'rb').read())
assert len(data) % 2 == 0
vmmem = []
for i in xrange(len(data) / 2):
vmmem.append(s16(data[i * 2: i * 2 + 2]))
class TaggedInt(object):
def __init__(self, v, tag):
self.v = v
self.tag = tag
return
def __int__(self):
print "Calling int() on '%s'" % (self.tag)
return self.v
def __sub__(self, x):
value = int(x)
print "Calling sub(%d) on '%s'" % (value, self.tag)
return TaggedInt(self.v - value, self.tag)
def __add__(self, x):
value = int(x)
print "Calling add(%d) on '%s'" % (value, self.tag)
return TaggedInt(self.v + value, self.tag)
def __neg__(self):
print "Calling neg() on '%s'" % (self.tag)
return TaggedInt(-self.v, self.tag)
def tagged_subtract(a, b):
if isinstance(a, TaggedInt):
return a - b
elif isinstance(b, TaggedInt):
return - b + a
return a - b
def set_input(inp, tag=True):
start = 0x1208 / 2
for idx, ch in enumerate(inp):
if tag:
vmmem[start] = TaggedInt(ord(ch), 'char%d' % (idx))
else:
vmmem[start] = ord(ch)
start += 1
if tag:
vmmem[start] = TaggedInt(0, 'term')
else:
vmmem[start] = 0
return
set_input('\x00' * 63, False)
breakpoints = [0x5]
def subleq_vm(data, start, end):
'''Some debugging helpers'''
step = False
stop = False
def interactive():
step = False
stop = False
def disasm(addr=None):
if addr == None: addr = current
if isinstance(addr, str): addr = int(addr, 0)
a = data[addr]
b = data[addr + 1]
pc = data[addr + 2]
print '%x: subleq %x, %x' % (addr, b, a),
if pc != 0:
print 'jump=%x' % pc,
print ''
return
def bp(addr=None):
if addr == None:
print 'Usage: bp <addr>'
return
if isinstance(addr, str): addr = int(addr, 0)
breakpoints.append(addr)
return
def bd(addr=None):
if addr == None:
print 'Usage: bd <addr>'
return
if isinstance(addr, str): addr = int(addr, 0)
breakpoints.remove(addr)
return
def bl():
for idx, bkpt in enumerate(breakpoints):
print '%d: %x' % (idx, bkpt)
return
def get(addr=None):
if addr == None:
print 'Usage: get <addr>'
return
if isinstance(addr, str): addr = int(addr, 0)
print 'vmmem[%x] = %d' % (addr, vmmem[addr])
return
def set(addr=None, val=None):
if addr == None or val == None:
print 'Usage: set <addr> <val>'
return
if isinstance(addr, str): addr = int(addr, 0)
if isinstance(val, str): val = int(val, 0)
vmmem[addr] = val
return
funcs = {'disasm': disasm, 'bp': bp, 'bd': bd, 'bl': bl, 'get': get, 'set': set}
while True:
comm = raw_input('>>> ').split()
if comm[0] == 'quit':
break
elif comm[0] == 'kill':
stop = True
elif comm[0] == 'stepi':
step = True
else:
try:
funcs[comm[0]](*comm[1:])
except KeyError:
print 'No such command'
return (step, stop)
'''VM Code'''
addr_dict = {}
current = start
inscount = 0
vmins = 0
while True:
if current + 3 > end:
break
'''
if current in breakpoints or step:
print 'Break! pc = %x' % current
if step: step = False
step, stop = interactive()
if stop:
break
'''
if current == 0x520:
print '%x: %d' % (vmmem[0x7f6], vmmem[0x7f6 + vmmem[0x7f6]])
vmins += 1
a = data[current]
b = data[current + 1]
pc = data[current + 2]
inscount += 1
if not addr_dict.has_key(current):
'''
print current, ': subleq', a, b,
if pc != 0:
print pc,
print ''
'''
addr_dict[current] = True
if exec_subleq(data, a, b, pc):
if cu16(pc) == 0xFFFF:
break
current = pc
else:
current += 3
if data[4] != 0:
char = cu16(data[2]) & 0xFF
sys.stderr.write(chr(char))
data[4] = 0
data[2] = 0
print 'Inscount:', inscount
print 'VMIns:', vmins
return
def exec_subleq(data, a, b, pc):
a_lit = data[int(a)]
b_lit = data[int(b)]
b_res = c16(int(tagged_subtract(b_lit, a_lit)))
data[b] = b_res
if pc != 0:
if b_res <= 0:
return True
return False
subleq_vm(vmmem, 5, 0x2dad)
Analysing this VM could be a pain in the ass, it's really painful staring at thousands lines of subleq x, x, x
trying to figure out the what the program actual does. Hopefully we have some resource on this topic already, including a writeup with Binary ninja plugin and an official writeup published last year about the same topic.
Finally I came to the result of this manually-written pseudocode:
… What the hell is this? Why the logic doesn't look like print(banner); if(check(input)) { print(good); } else { print(fail); }
? Oh my…
3. The final layer of despair
Fortuanately I didn't close my chrome tab browsing esolang. Review the page for a few seconds and I found this. Looks like this is a subleq
VM running a rssb
VM. Holy fuck.
So as I did in the second part, I crafted another rssb
vm script first:
from hexdump import hexdump
import IPython, sys, os, struct, ctypes
c16 = lambda x: ctypes.c_int16(x).value
cu16 = lambda x: ctypes.c_uint16(x).value
u16 = lambda x: struct.unpack("<H", x)[0]
s16 = lambda x: struct.unpack("<h", x)[0]
p16 = lambda x: struct.pack("<H", c16(x))[0]
data = bytearray(open("subleq.vm", 'rb').read())
assert len(data) % 2 == 0
vmmem = []
for i in xrange(len(data) / 2):
vmmem.append(s16(data[i * 2: i * 2 + 2]))
vmstate = 0x7f6
def set_input(inp):
start = 0x1208 / 2
for idx, ch in enumerate(inp):
vmmem[start] = ord(ch)
start += 1
for i in xrange(64 - (start - (0x1208 / 2))):
vmmem[start] = 0
start += 1
return
set_input(raw_input('Input: '))
def extract_result():
start = 0xa79
for idx in xrange(15):
print vmmem[vmstate + start + idx], ',',
sys.exit(0)
return
def get_name(v):
if v == 0:
return 'pc'
elif v < 0:
return str(v)
elif v == 1:
return 'acc'
elif v == 2:
return 'zero'
elif v == 4:
return 'out'
elif v == 6:
return 'outst'
return hex(v)
breakpoints = [0x15b]
def rssb_vm():
def interactive():
stop = False
step = False
def get(addr):
if addr == None:
print 'Usage: get <addr>'
return
if isinstance(addr, str): addr = int(addr, 0)
print 'VM[%x] = %d (%x)' % (addr, vmmem[vmstate + addr], vmmem[vmstate + addr])
return
def disasm(addr, length):
if addr == None:
print 'Usage: disasm <addr> [len]'
return
if isinstance(addr, str): addr = int(addr, 0)
if isinstance(length, str): length = int(length, 0)
elif length == None: length = 1
for i in xrange(length):
print '%x: rssb %s' % (addr + i, get_name(vmmem[vmstate + addr + i]))
return
def set(addr, val):
if addr == None or val == None:
print 'Usage: set <addr> <val>'
return
if isinstance(addr, str): addr = int(addr, 0)
if isinstance(val, str): val = int(val, 0)
vmmem[vmstate + addr] = val
return
def bp(addr):
if addr == None:
print 'Usage: bp <addr>'
return
if isinstance(addr, str): addr = int(addr, 0)
breakpoints.append(addr)
return
def bd(addr):
if addr == None:
print 'Usage: bd <addr>'
return
if isinstance(addr, str): addr = int(addr, 0)
breakpoints.remove(addr)
return
def bl():
for idx, bpkt in enumerate(breakpoints):
print '%d: %x' % (idx, bpkt)
return
def evaluate(stmt):
if stmt == None:
print 'Usage: eval <stmt>'
return
print eval(stmt)
return
def interact():
IPython.embed()
return
functions = {'get': get, 'set': set, 'bp': bp, 'bd': bd, 'bl': bl, 'eval': evaluate, 'disasm': disasm, 'ipy': interact}
while True:
command = raw_input('>>> ').split()
if command[0] == 'quit':
break
elif command[0] == 'kill':
stop = True
elif command[0] == 'stepi':
step == True
else:
try:
functions[command[0]](*command[1:])
except KeyError:
print 'No such command'
return (stop, step)
debug = False
stop = False
step = False
trace = False
output = True
inscount = 0
if trace:
tracefile = open("runtrace.txt", 'wb')
while vmmem[vmstate] < 0x25b7:
if trace:
trace_pc = vmmem[vmstate]
print >>tracefile, '%x: rssb %s' % (vmmem[vmstate], get_name(vmmem[vmstate + vmmem[vmstate]])),
if debug and (vmmem[vmstate] in breakpoints or step):
print 'Break! pc = %x' % vmmem[vmstate]
step = False
stop, step = interactive()
'''
if vmmem[vmstate] == 0x1f62:
print 'Intermediate:', vmmem[vmstate + 0x1ef6], vmmem[vmstate + 0x1ef2]
if vmmem[vmstate] == 0x1ff1:
print 'Result:', vmmem[vmstate + 0x1ef6]
# this is trigger 1
if vmmem[vmstate] == 0x20c2:
if vmmem[vmstate + 0x20aa] != 0:
print 'Trigger 1'
vmmem[vmstate + 0x20aa] = 0
# this is trigger 2
if vmmem[vmstate] == 0x21ba:
if vmmem[vmstate + 0x21a2] == 0:
print 'Trigger 2'
vmmem[vmstate + 0x21a2] = 1
'''
if stop:
break
if vmmem[vmstate + vmmem[vmstate]] == -2:
break
exec_rssb()
if trace:
print >>tracefile, '[acc = %d, vmmem[pc] = %d]' % (vmmem[vmstate + 1], vmmem[vmstate + vmmem[vmstate + trace_pc]])
inscount += 1
if vmmem[vmstate + 6] != 0:
if output: sys.stderr.write(chr(vmmem[vmstate + 4]))
vmmem[vmstate + 6] = 0
print 'Inscount:', inscount
return
def exec_rssb():
pc = vmmem[vmstate] + vmstate
if vmmem[pc] == -1:
vmmem[vmstate] = c16(vmmem[vmstate] + 1)
return
a = vmmem[vmstate + 1]
b = vmmem[vmstate + vmmem[pc]]
c = c16(b - a)
vmmem[vmstate + 1] = c
if vmmem[pc] != 2:
vmmem[vmstate + vmmem[pc]] = c
if c < 0:
vmmem[vmstate] += 1
vmmem[vmstate] += 1
return
rssb_vm()
… and went on analyzing this. It's much more frustrating since we don't have the references mentioned above, I have to figure out the high-level primitives on my own.
With the ability to debug this VM and 2 days of dedicated analysis, I finally found the algorithm used to check the password:
The final answer is: Av0cad0_Love_2018@flare-on.com