--- tags: public --- Flare-On 5 Challenge === > Author: shiki7 ![](https://i.imgur.com/XvK2qCT.png) [TOC] ## Challenge 1. Minesweeper Championship Registration We're given a `jar` file. Using `jad` to decompile the contained `class` files gives us the key: ``` C:\Users\pc\Documents\CTF\flareon\1\m>jad -p InviteValidator.class // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) // Source File Name: InviteValidator.java import javax.swing.JOptionPane; public class InviteValidator { public InviteValidator() { } public static void main(String args[]) { String response = JOptionPane.showInputDialog(null, "Enter your invitation code:", "Minesweeper Championship 2018", 3); if(response.equals("GoldenTicket2018@flare-on.com")) JOptionPane.showMessageDialog(null, (new StringBuilder("Welcome to the Minesweeper Championship 2018!\nPlease enter the following code to the ctfd.flare-on.com website to compete:\n\n")).append(response).toString(), "Success!", -1); else JOptionPane.showMessageDialog(null, "Incorrect invitation code. Please try again next year.", "Failure", 0); } } ``` 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: ```csharp= using System; using System.IO; // Ch3aters_Alw4ys_W1n@flare-on.com namespace Test { class Test { private static UInt32 VALLOC_NODE_LIMIT = 30; private static UInt32 VALLOC_TYPE_HEADER_PAGE = 0xfffffc80; private static UInt32 VALLOC_TYPE_HEADER_POOL = 0xfffffd81; private static UInt32 VALLOC_TYPE_HEADER_RESERVED = 0xfffffef2; private static UInt32[] VALLOC_TYPES; private static uint DeriveVallocType(uint r, uint c) { return ~((r * VALLOC_NODE_LIMIT) + c); } private static void AllocateMemory() { for (uint i = 0; i < VALLOC_NODE_LIMIT; i++) { for (uint j = 0; j < VALLOC_NODE_LIMIT; j++) { bool flag = true; uint r = i + 1; uint c = j + 1; if (Array.IndexOf(VALLOC_TYPES, DeriveVallocType(r, c)) > -1) { Console.WriteLine("I : {0}, J : {1}", i, j); } } } } public static void Main(string[] args) { VALLOC_TYPES = new uint[] { VALLOC_TYPE_HEADER_PAGE, VALLOC_TYPE_HEADER_POOL, VALLOC_TYPE_HEADER_RESERVED }; AllocateMemory(); return; } } } ``` 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: ```python= # -*- coding:utf-8 -*- import pepy import os import subprocess import sys import glob import shutil import IPython from PIL import Image # get the key def parse_one(filename): pe = pepy.parse(filename) brick = pe.get_resources()[0] assert brick.type_str == "B\x00R\x00I\x00C\x00K\x00" # BRICK # read wchar_t string key = '' idx = 0 while True: if brick.data[idx] == 0: break key += chr(brick.data[idx]) idx += 2 return key # extract essentials def extract_essentials(filename, key): proc = subprocess.Popen(filename, stdin=subprocess.PIPE, stdout=subprocess.PIPE) proc.stdin.write(key + '\n') proc.wait() output = proc.stdout.read() essntials = map(lambda x: x.replace(' ', ''), output.split('\r\n')[2].split('=>')) return essntials # mor3_awes0m3_th4n_an_awes0me_p0ssum@flare-on.com def main(argv): # make image directory if not os.path.exists("images"): os.mkdir("images") results = [] for executable in glob.glob("FLEGGO\\*.exe"): print "Processing %s..." % executable key = parse_one(executable) essentials = extract_essentials(executable, key) print essentials shutil.move("FLEGGO\\" + essentials[0], "images\\" + essentials[0]) os.system("open %s" % ("images\\" + essentials[0])) number = int(raw_input("Index: ")) # I'm no good at tesseract... can't get it working... essentials[0] = number results.append(essentials) results.sort(key=lambda x: x[0]) flag = '' for idx, char in results: flag += char print flag IPython.embed() return 0 if __name__ == '__main__': sys.exit(main(sys.argv)) ``` ~~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](https://github.com/WebAssembly/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: ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- 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: ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- import idaapi import cPickle as pickle dataset_addr = 0x605100 check_rounds = 33 ''' 00000000 magic_t struc ; (sizeof=0x120, mappedto_6) 00000000 ; XREF: .data:dataset/r 00000000 code_ptr dq ? 00000008 code_len dd ? 0000000C input_offset dd ? 00000010 length dd ? 00000014 key_offset dd ? 00000018 xor_key dq ? 00000020 parameter db 256 dup(?) 00000120 magic_t ends ''' l2s = lambda x: ''.join(map(chr, x)) def get_contiguous(ea, length): result = [] for i in xrange(length): result.append(idaapi.get_byte(ea + i)) return result def xor(idx): addr = dataset_addr + 288 * idx code_ptr = idaapi.get_64bit(addr) code_len = idaapi.get_32bit(addr + 8) xor_key = idaapi.get_64bit(addr + 0x18) for i in xrange(code_len): orig = idaapi.get_byte(code_ptr + i) ^ idaapi.get_byte(xor_key + i) idaapi.patch_byte(code_ptr + i, orig) return code_len def dump_data(idx): addr = dataset_addr + 288 * idx code_ptr = idaapi.get_64bit(addr) code_len = idaapi.get_32bit(addr + 8) input_offset = idaapi.get_32bit(addr + 0xc) length = idaapi.get_32bit(addr + 0x10) key_offset = idaapi.get_32bit(addr + 0x14) xor_key = idaapi.get_64bit(addr + 0x18) parameter = get_contiguous(addr + 0x20, 256) name = idaapi.get_name(code_ptr) return { 'code_ptr': code_ptr, 'code_len': code_len, 'input_offset': input_offset, 'length': length, 'key_offset': key_offset, 'xor_key': xor_key, 'parameter': parameter, 'name': name } rounds = [] for i in xrange(check_rounds): rounds.append(dump_data(i)) open("/Users/arch/CTF/flareon/6/dump.pickle", 'wb').write(pickle.dumps(rounds)) print 'Done!' ``` 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. ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- # mag!iC_mUshr00ms_maY_h4ve_g!ven_uS_Santa_ClaUs@flare-on.com from IPython import embed import cPickle as pickle import struct, binascii, itertools, string, base64, ctypes libc = ctypes.CDLL("libc.so.6") u64 = lambda x: struct.unpack("<Q", x)[0] u32 = lambda x: struct.unpack("<L", x)[0] l2s = lambda x: ''.join(map(chr, x)) b64pad = lambda x: x + (4 - len(x) % 4) * '=' w32 = lambda x: ctypes.c_uint32(x).value DATA = "$\\u y:\x12E\x1e \x1dq\x197&\x17g\x03\x10>0g|J\x11\x1b^U\x08\x13b\x11hl|ZD\x17,\x12yY\t$c\x0cmW\x1fe'\x0cj\x0f]&CFY3\x14\\7\nc&\x02\x16,\x00\x00\x00" libc.srand(0x5f215742) def rand(): return w32(libc.rand()) def fib(x): result = 0 v5 = 1 v4 = 0 while x != 0: result = v4 + v5 v4 = v5 v5 = result x -= 1 return result def find_fib(x): for i in xrange(256): if fib(i) % pow(2, 64) == x: return i raise Exception("Not find in fib seq") KEY = 'Tis but a scratch.' def rc4_xor(data, key): key = map(ord, key) def KSA(key): keylength = len(key) S = range(256) j = 0 for i in range(256): j = (j + S[i] + key[i % keylength]) % 256 S[i], S[j] = S[j], S[i] # swap return S def PRGA(S): i = 0 j = 0 while True: i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] # swap K = S[(S[i] + S[j]) % 256] yield K S = KSA(key) KS = PRGA(S) keys = [] for i in xrange(len(data)): keys.append(KS.next()) return map(lambda x: x[0] ^ x[1], zip(data, keys)) data = pickle.loads(open("../dump.pickle").read()) def solve(): global data result = {} for i in xrange(33): chars = [] name = data[i]['name'] param = data[i]['parameter'] length = data[i]['length'] key_offset = data[i]['key_offset'] inp_off = data[i]['input_offset'] if name.startswith('xor'): for j in xrange(length): chars.append(chr(param[j] ^ 0x2a)) elif name.startswith('add'): for j in xrange(length): chars.append(chr(param[j] - 0xd)) elif name.startswith('strcmp'): for j in xrange(length): chars.append(chr(param[j])) elif name.startswith('fib'): idx = 0 for j in xrange(length): par = u64(l2s(param[idx:idx+8])) chars.append(chr(find_fib(par))) idx += 8 elif name.startswith('rc4'): dec = rc4_xor(param[0:length], KEY) for c in dec: chars.append(chr(c)) elif name.startswith('hash'): # crc32b h = u32(l2s(param[0:4])) for comb in itertools.product(string.printable, repeat=length): if w32(binascii.crc32(''.join(comb))) == h: for c in comb: chars.append(c) break else: raise Exception("CRC32B not found") elif name.startswith('hybrid'): newtable = '2a395f64c2a74623'.decode('hex') + 'BpM(GtkS'[::-1] + 'J@8bjR%I'[::-1] + 'P$1-YDEi'[::-1] + 'fqvL!Tyg'[::-1] + '0OWQmhc+'[::-1] + 'l3nu4ZNe'[::-1] + 'Kzaw2&H7'[::-1] origtable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' trans = string.maketrans(newtable, origtable) decode_len = (length * 4) / 3 + (length * 4) % 3 par = l2s(param[0:decode_len]).translate(trans) par = b64pad(par) #print par for c in base64.b64decode(par): chars.append(c) else: raise Exception("No coherent decoding function") for idx, c in enumerate(chars): result[inp_off + idx] = c solved = '' for i in xrange(69): solved += result[i] return solved def randomize(): inp_off = 0 for i in xrange(33): idx = i + (rand() % (33 - i)) data[idx]['input_offset'] = inp_off inp_off += data[idx]['length'] tmp = data[i] data[i] = data[idx] data[idx] = tmp rand() # this choose xor key for i in xrange(33): idx = i + (rand() % (33 - i)) tmp = data[i] data[i] = data[idx] data[idx] = tmp return results = [] for i in xrange(666): g = solve() print g results.append(g) randomize() open("./answers.pickle", 'wb').write(pickle.dumps(results)) print 'Done!' embed() ``` 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. ```python= import struct # P0rt_Kn0ck1ng_0n_he4v3ns_d00r@flare-on.com 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: ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- import idaapi key = '19900206'.decode('hex') def xor(data, key): result = '' for idx, c in enumerate(data): result += chr(c ^ ord(key[idx % len(key)])) return result d = [] start = 0x8809 while True: b = idaapi.get_byte(start) if b == 0: break d.append(b) start += 1 dt = xor(d, key) open("/Users/arch/CTF/flareon/8/dump.bin", 'wb').write(dt) ``` 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](https://github.com/hellman/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: ``` ioperal7dwvmalware ``` ... 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: ```python= import idaapi import ctypes ''' If I were to title this piece, it would be A_FLARE_f0r_th3_Dr4m4t1(C) ''' c32 = lambda x: ctypes.c_uint32(x).value c8 = lambda x: ctypes.c_uint8(x).value ss_size = 36 css_addr = 0x40C2B0 css_cnt = 6 dss_addr = 0x40CD00 dss_cnt = 1 def get_contiguous(ea, size): result = '' for i in xrange(size): result += chr(idaapi.get_byte(ea + i)) return result def xor1(data, key): result = '' for i in xrange(len(data)): result += chr(ord(data[i]) ^ key) return result def xorc(data, key, inc): result = '' key = c8(key) for i in xrange(len(data)): result += chr(ord(data[i]) ^ key) key = c8(key + inc) return result matome_code = '' ''' # extract code segments for i in xrange(css_cnt): desc = css_addr + i * ss_size enc_type = idaapi.get_32bit(desc) assert enc_type == 1 # xor enc size = idaapi.get_32bit(desc + 8) static_addr = idaapi.get_32bit(desc + 12) static_data = idaapi.get_32bit(static_addr) code = get_contiguous(static_data, size) encparam = idaapi.get_32bit(desc + 28) keyaddr = idaapi.get_32bit(encparam + 4) key = idaapi.get_byte(keyaddr) decrypted = xor1(code, key) matome_code += decrypted filename = r"C:\Users\pc\Documents\CTF\flareon\9\dump\code{}.bin".format(i) open(filename, 'wb').write(decrypted) open(r"C:\Users\pc\Documents\CTF\flareon\9\dump\all_code.bin", 'wb').write(matome_code) ''' def rc4_xor(data, key): key = map(ord, key) def KSA(key): keylength = len(key) S = range(256) j = 0 for i in range(256): j = (j + S[i] + key[i % keylength]) % 256 S[i], S[j] = S[j], S[i] # swap return S def PRGA(S): i = 0 j = 0 while True: i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] # swap K = S[(S[i] + S[j]) % 256] yield K S = KSA(key) KS = PRGA(S) keys = [] for i in xrange(len(data)): keys.append(KS.next()) return ''.join(map(lambda x: chr(x[0] ^ x[1]), zip(map(ord, data), keys))) ''' # extract data segments data = '' ptr = 0 size = idaapi.get_32bit(dss_addr + 8) anatomy = idaapi.get_32bit(dss_addr + 20) static_addr = idaapi.get_32bit(dss_addr + 12) static_data = idaapi.get_32bit(static_addr) idx = 0 encparam = idaapi.get_32bit(dss_addr + 28) k0s = idaapi.get_32bit(encparam) k0 = get_contiguous(idaapi.get_32bit(encparam + 4), k0s) k1s = idaapi.get_32bit(encparam + 8) k1 = get_contiguous(idaapi.get_32bit(encparam + 12), k1s) k2s = idaapi.get_32bit(encparam + 16) k2 = get_contiguous(idaapi.get_32bit(encparam + 20), k2s) while ptr < size: offset = idaapi.get_32bit(anatomy + 8 * idx) size = idaapi.get_32bit(anatomy + 4 + 8 * idx) if offset == 1: break encrypted = get_contiguous(static_data + offset, size) if idx % 3 == 1: decrypted = '' for i in xrange(len(encrypted)): decrypted += chr(ord(encrypted[i]) ^ c8(ord(k1[i % k1s]) + idx * i)) elif idx % 3 == 2: decrypted = xorc(rc4_xor(encrypted, k2), idx, idx - 1) else: decrypted = '' seed = idx for i in xrange(len(encrypted)): decrypted += chr(ord(encrypted[i]) ^ c8(57 - 0x93 * seed)) seed = c32((12345 - 0x3E39B193 * seed)) & 0x7FFFFFFF idx += 1 data += decrypted open(r"C:\Users\pc\Documents\CTF\flareon\9\dump\all_data.bin", 'wb').write(data) ''' # extract code code_addr = 0x40F110 code_size = 0x0C50 key = 'sn00gle-fl00gle-p00dlekins' encrypted = get_contiguous(code_addr, code_size) decrypted = rc4_xor(encrypted, key) dec2 = rc4_xor(decrypted.decode('base64'), 'yummy') open(r"C:\Users\pc\Documents\CTF\flareon\9\dump\phase2.bin", 'wb').write(dec2) print 'Done!' ``` 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: ```python= 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' #raise Exception("None") 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: ```python= # extract dns records import dpkt, struct, IPython from hexdump import hexdump pcap = dpkt.pcapng.Reader(open("pcap.pcap", 'rb')) u32 = lambda x: struct.unpack("<L", x)[0] collected = '' def is_txtdnsresp(buf): dns = dpkt.dns.DNS(buf) if dns.an[0].type == 16: return dns.an[0].text[0] return '' for ts, buf in pcap: eth = dpkt.ethernet.Ethernet(buf) ip = eth.data udp = ip.data if udp.sport == 53: collected += is_txtdnsresp(udp.data) def decode(data): assert len(data) % 2 == 0 idx = 0 result = '' while idx < len(data): low = ord(data[idx]) - ord('A') hi = ord(data[idx+1]) - ord('a') result += chr(low + (hi << 4)) idx += 2 return result decoded = decode(collected) #hexdump(decoded) def rc4_xor(data, key): # damn this is slightly modified! key = map(ord, key) data = map(ord, data) def KSA(key): keylength = len(key) S = range(255) j = 0 for i in range(255): j = (j + S[i] + key[i % keylength]) % 255 S[i], S[j] = S[j], S[i] # swap return S def PRGA(S): i = 0 j = 0 while True: i = (i + 1) % 255 j = (j + S[i]) % 255 S[i], S[j] = S[j], S[i] # swap K = S[(S[i] + S[j]) % 255] yield K S = KSA(key) KS = PRGA(S) keys = [] for i in xrange(len(data)): keys.append(KS.next()) return ''.join(map(lambda x: chr(x[0] ^ x[1]), zip(data, keys))) k = decoded[:0x10] size = u32(decoded[0x10:0x14]) print 'Size:', hex(size) d = decoded[0x14:] decrypted = rc4_xor(d, k) open("decrypted.bin", 'wb').write(decrypted) print 'Done!' ``` Script to find native function by hash: ```python= import lief, ctypes, os, sys, IPython c32 = lambda x: ctypes.c_uint32(x).value def ror4(x, s): return ((x >> s) | ((x << (32 - s)) & 0xFFFFFFFF)) def get_hash(modname, funcname): modhash = 0 modname += '\x00' # mimic unicode string for ch in map(ord, modname): modhash = c32(ror4(modhash, 0xd)) if ch >= 0x61: ch -= 0x20 modhash = c32(modhash + ch) # mimic unicode string modhash = c32(ror4(modhash, 0xd) + 0) funchash = ord(funcname[0]) funcname += '\x00' for ch in map(ord, funcname[1:]): funchash = c32(ror4(funchash, 0xd)) funchash = c32(funchash + ch) return c32(funchash + modhash) #print hex(get_hash('kernel32.dll', 'LoadLibraryA')) module_mem_list = ['ntdll.dll', 'kernel32.dll', 'kernelbase.dll', 'shell32.dll', 'user32.dll', 'advapi32.dll', 'ws2_32.dll', 'gdi32.dll', 'mpr.dll', 'wininet.dll'] def find_func_by_hash(h): for mod in module_mem_list: pe = lief.PE.parse("dllcoll/" + mod) for func in pe.exported_functions: hv = get_hash(mod, str(func)) if hv == h: return mod, str(func) return '', '' #print find_func_by_hash(get_hash('kernel32.dll', 'LoadLibraryA')) #print find_func_by_hash(0x95898DFF) # CreateMutexW #print find_func_by_hash(0x5DE2C5AA) # GetLastError #print find_func_by_hash(0xE035F044) # Sleep #print find_func_by_hash(0xCF448459) # RtlDeleteCriticalSection #print find_func_by_hash(0xEA61FCB1) # LocalFree #print find_func_by_hash(0x528176EE) # LocalAlloc #print find_func_by_hash(0xDC322193) # RtlInitializeCriticalSection #print find_func_by_hash(0x6B8029) # WSAStartup #print find_func_by_hash(0x42131B45) # CryptAcquireContextW #print find_func_by_hash(0xCEE535FF) # RtlEnterCriticalSection #print find_func_by_hash(0x3A182487) # RtlLeaveCriticalSection print 'Done!' ``` Script to extract c2 traffic: ```python= import lief, dpkt, os, sys, IPython, socket, struct from hexdump import hexdump # use this with mimic.c # we decontaminate this shit... 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): # ethernet 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:] # netbios if len(data) < 4: return False if data[0:4] == '\xfeSMB': flags = u32(data[0x10:0x14]) if flags & 1 == 0: # resp cmd = u16(data[0xc:0xe]) if cmd == 9: # write return True return False def is_valid_server(pkt): data = pkt.data[4:] # netbios if len(data) < 4: return False if data[0:4] == '\xfeSMB': flags = u32(data[0x10:0x14]) if (flags & 1) != 0: # resp cmd = u16(data[0xc:0xe]) if cmd == 8: # read 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: ```c // clang -m32 -o mim.exe mimic.c #include <stdio.h> #include <stdlib.h> #include <Windows.h> ////////////////////////////////////////// // Shit starts here 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); // So that we don't need to fix the relocation // NO IT'S NOT JUST ABOUT RELOC! We need to fix the import as well! 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) // Some little trick to placate the msvc __thiscall // This apparently does not comply to multithreaded environments #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; } ////////////////////////////////////////// // Holy god this ends.. char* mod; /* // these are from the first stage unsigned char client[] = {0x0b,0x7d,0xbe,0x80,0xe7,0xb8,0x44,0x1f,0xf3,0x05,0x5c,0xe8,0xd0,0x75,0x7b,0xcb}; unsigned char server[] = {0xc3,0x95,0x08,0x1d,0xe5,0xa3,0xf2,0xe5,0x44,0xe8,0x18,0xae,0x75,0x80,0xd4,0xd3}; */ // second stage 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"); // this loads a neutralized version... if(!module) { printf("Cannot load module, %d\n", GetLastError()); return -1; } mod = module; //THISCALL(3, 0x0, 0x0, 1, 2, 3); void* crypto_c = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x148); char* initfunc = RVATOVA(char*, module, 0x63E0); // init structure 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); // set key 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. ### 1. Finding the input checker 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: ```python= import os, sys, struct from hexdump import hexdump ''' Progress now: I found that int13h hook, but it looks like some kind of red herring, by the sentence "BE SURE TO XXX". I don't know if there's any lower-level hooks or modifications in this level.... ''' def ror8(b, r): return ((b >> r) | ((b << (8 - r)) & 0xFF)) assert ror8(1, 1) == 0x80 assert ror8(0x81, 1) == 0xC0 data = open("image.img", 'rb').read() ''' # dump decrypted boot section. sector6 = data[5*0x200: 5*0x200+0x200] data6 = '' for ch in sector6: data6 += chr(ror8(ord(ch), 1)) hexdump(data6) open("sector6.bin", 'wb').write(data6) ''' # the following comprehension could be wrong.... ### def chs2l(c, h, s): # chs to lba addressing return (c * 2 + h) * 0x12 + s - 1 # 3.5 inch floppy settings def l2o(lba): # lba to offset return lba * 0x200 # 512b sector ### assert chs2l(0, 0, 6) == 5 print hex(chs2l(0x21, 1, 0x11)) #forbid_section = data[l2o(0x4c8): l2o(0x593)] # these are the section forbidden from reading by the hook #hexdump(forbid_section) #message_section = data[l2o(chs2l(0x21, 1, 0x11)): l2o(chs2l(0x21, 1, 0x12))] #hexdump(message_section) # extract subleq commands dump = open("../dump/before.dump", 'rb').read() start = 0x97c00 + 0x223 end = start + 0x2DAD * 2 subleq = dump[start:end] hexdump(subleq) open("subleq.vm", 'wb').write(subleq) ``` ### 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): ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- 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 # output 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](https://blahcat.github.io/2017/10/13/flareon-4-writeups/#challenge-11) and an [official writeup](https://www.fireeye.com/content/dam/fireeye-www/global/en/blog/threat-research/Flare-On%202017/Challenge11.pdf) published last year about the same topic. Finally I came to the result of this manually-written pseudocode: ``` _start() { push(0x25b7, 0x7f6); call(0xfc); fix_stack(); HALT(); } 0xfc(arg1, arg2) { //_bp = _sp; DECLARE_ARGUMENT(arg2); // 0x10f DECLARE_ARGUMENT(arg1); // 0x148 DECLARE_VAR(v1) // 0x181 DECLARE_VAR(v2) // 0x185 DECLARE_VAR(v3) // 0x189 DECLARE_VAR(v4, -2) // 0x18d DECLARE_VAR(v5, 3) // 0x191 DECLARE_VAR(v6, 4) // 0x195 DECLARE_VAR(v7, 5) // 0x199 DECLARE_VAR(v8, 6) // 0x19d DECLARE_VAR(v9, 0) // 0x1a1 DECLARE_VAR(v10, 1) // 0x1a5 DECLARE_VAR(v11, 0) // 0x1a9 DECLARE_VAR(v12) // 0x1ad v2 = arg2; v12 = *v2; while(true) { v2 = arg2; v1 = *v2; if(v1 < arg1) { v2 += v1; v3 = *v2; if(v3 + 2 == 0) { break; } push(arg2); call(0x520); fix_stack(); v2 = arg2; v2 += v8; v9 = *v2; if(v9 == v10) { *v2 = v11; v2 = arg2 + v6; v9 = *v2; // OUT r2 = v9; r4 = 1; // OUT DONE *v2 = v11; } v2 = arg2; v2 += v7; v9 = *v2; if(v9 == v10) { *v2 = v11; r3 += 1; v9 = r1; v2 = arg2; v2 += v5; *v2 = v9; } } else { break; } } v2 = arg2; *v2 = v12; return; } 0x520(arg1) { _bp523 = _sp; DECLARE_ARGUMENT(arg1) // 0x533 DECLARE_VAR(v2, -2) // 0x537 DECLARE_VAR(v3) // 0x53b [0x7f1] = arg1; [0x7f5] = *[0x7f1]; [0x7f1] += [0x7f5]; [0x7ef] = *[0x7f1]; // 0x7ef = opcode if([0x7ef] == [0x7f2]) { // 0x7f2 == -1, nop [0x7f1] = arg1; [0x7f5] += 1; *[0x7f1] = [0x7f5]; return; } [0x7f1] = arg1 + 1; [0x7ee] = *(arg1 + 1); [0x7f1] = arg1 + [0x7ef]; [0x7f0] = *(arg1 + [0x7ef]); [0x7f4] = [0x7f0] - [0x7ee]; [0x7ee] = [0x7f4]; [0x7f1] = arg1 + 1; *[0x7f1] = [0x7ee]; if([0x7ef] != [0x7f3]) { // 0x7f3 == 2 [0x7f1] = arg1 + [0x7ef]; *[0x7f1] = [0x7ee]; } [0x7f5] = *arg1; if([0x7ee] < 0) { [0x7f5] += 1; } [0x7f5] += 1; *arg1 = [0x7f5]; return; } ``` ... 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](https://esolangs.org/wiki/OISC). Review the page for a few seconds and I found [this](https://esolangs.org/wiki/RSSB). 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: ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- 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('\x00' * 63) set_input(raw_input('Input: ')) #set_input(17 * 'A' + '@flare-on.com') #set_input(15 * 'B1' + '@flare-on.com') def extract_result(): start = 0xa79 for idx in xrange(15): print vmmem[vmstate + start + idx], ',', sys.exit(0) return #extract_result() 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] # https://en.wikipedia.org/wiki/One_instruction_set_computer # Looks like RSSB?, and he implements RSSB inside a SUBLEQ interpreter.... holyfuck 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: ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- import z3, ctypes c16 = lambda x: ctypes.c_int16(x).value z16 = lambda x: z3.BitVecVal(x, 16) def main(length): s = z3.Solver() answers = [] for i in xrange(length): bv = z3.BitVec('x%d' % i, 16) s.add(bv >= 0x20, bv <= 0x7f) s.add(bv != 0x40) answers.append(bv) vlen = length vb47 = sum(answers) + z16(length * 1024 * 3) if length % 2 == 1: answers.append(z16(ord('@'))) length += 1 vx = z16(0) ans = map(lambda x: z16(x), [-897 , -3313 , -3231 , -3759 , -1914 , -3119 , -9385 , -9771 , -7570 , -1843 , -1687 , -9972 , -2015 , -3711 , -1953]) for i in xrange(length / 2): x1 = (answers[i * 2 + 1] - z16(0x20)) * z16(128) + (answers[i * 2] - z16(0x20)) x1 ^= vx s.add(x1 + vb47 == ans[i]) vx += 33 print s.check(), length result = '' try: for idx in xrange(vlen): result += chr(s.model()[answers[idx]].as_long()) except z3.z3types.Z3Exception: return False print result return True for i in xrange(1, 31): main(i) ``` The final answer is: `Av0cad0_Love_2018@flare-on.com`