diff --git a/notes.txt b/notes.txt index 37a00f2..e69a53b 100644 --- a/notes.txt +++ b/notes.txt @@ -1,4 +1,15 @@ -; Obfuscated jmp structure +NOTES + +These are observations made while looking through WARBIRD code. + +Some are relevant to deobfuscation, while some are just oddities and curiosities. + +Hopefully, once I have a better understanding of everything, these notes will look a lot nicer! + +=========================================================== + +Obfuscated jmp structure + ; where reg is one of: eax, ecx, edx, ebx, ebp, esi, edi ; push is sometimes replaced with the following equivalent instructions @@ -6,7 +17,7 @@ ; mov [esp], reg ; IDA (and perhaps other disassemblers idk) confuses the first parts of the stub for constant bytes -; This combined with the random push replacement will make the stub look different, but its infact always the same +; This combined with the random instruction replacement will make the stub look different, but its infact always the same push reg push reg @@ -27,18 +38,32 @@ ret 4 ; after verifier finishes, it adds 0x10 to the value at [esp] ; now esp points to the target jump address, and another ret is done to "return" to it +; rarely (~1% of the time), the last three instructions are replaced with: +; xchg [esp], reg +; ret +; the principle of operation and results are the same, except only 2 values are placed on the stack + ; =======BYTE MARKERS======= -; -mov reg, [esp+4] -ret 4 -; eax - 8b 44 24 04 c2 04 00 -; ecx - 8b 4c 24 04 c2 04 00 -; edx - 8b 54 24 04 c2 04 00 -; ebx - 8b 5c 24 04 c2 04 00 -; ebp - 8b 6c 24 04 c2 04 00 -; esi - 8b 74 24 04 c2 04 00 -; edi - 8b 7c 24 04 c2 04 00 -; +; Useful for finding obfuscated jumps in a disassembler + +lea esp, [esp - 4] +mov [esp], reg +; eax - 8d 64 24 fc 89 04 24 +; ecx - 8d 64 24 fc 89 0c 24 +; edx - 8d 64 24 fc 89 14 24 +; ebx - 8d 64 24 fc 89 1c 24 +; ebp - 8d 64 24 fc 89 2c 24 +; esi - 8d 64 24 fc 89 34 24 +; edi - 8d 64 24 fc 89 3c 24 + +push reg +; eax - 50 +; ecx - 51 +; edx - 52 +; ebx - 53 +; ebp - 55 +; esi - 56 +; edi - 57 ; 0x11223344 is an example address lea reg, [0x11223344] @@ -48,4 +73,97 @@ lea reg, [0x11223344] ; ebx - 8d 1d 44 33 22 11 ; ebp - 8d 2d 44 33 22 11 ; esi - 8d 35 44 33 22 11 -; edi - 8d 3d 44 33 22 11 \ No newline at end of file +; edi - 8d 3d 44 33 22 11 + +mov reg, [esp+4] +ret 4 +; eax - 8b 44 24 04 c2 04 00 +; ecx - 8b 4c 24 04 c2 04 00 +; edx - 8b 54 24 04 c2 04 00 +; ebx - 8b 5c 24 04 c2 04 00 +; ebp - 8b 6c 24 04 c2 04 00 +; esi - 8b 74 24 04 c2 04 00 +; edi - 8b 7c 24 04 c2 04 00 + +xchg [esp], reg +ret +; eax - 87 04 24 c3 +; ecx - 87 0c 24 c3 +; edx - 87 14 24 c3 +; ebx - 87 1c 24 c3 +; ebp - 87 2c 24 c3 +; esi - 87 34 24 c3 +; edi - 87 3c 24 c3 + +=========================================================== + +Hidden functions in CRT initialization + +Upon launch, msvcrt will run a list of functions to initialize the C++ runtime like so: + +static bool __cdecl initialize_c() +{ + _initialize_onexit_table(&__acrt_atexit_table); + _initialize_onexit_table(&__acrt_at_quick_exit_table); + + // Do C initialization: + if (_initterm_e(__xi_a, __xi_z) != 0) + { + return false; + } + + // Do C++ initialization: + _initterm(__xc_a, __xc_z); + return true; +} + +Of particular interest is the _initterm function, with the following implementation: + +// Calls each function in [first, last). [first, last) must be a valid range of +// function pointers. Each function is called, in order. +extern "C" void __cdecl _initterm(_PVFV* const first, _PVFV* const last) +{ + for (_PVFV* it = first; it != last; ++it) + { + if (*it == nullptr) + continue; + + (**it)(); + } +} + +(Both code examples taken from UCRT 10.0.14393.0) + +The addresses __xc_a and __xc_z are symbols in the pdb, so we can find the list of these functions. + +Many are not interesting (just allocating class memory), but some odd functions are included as well (see following sections). + +When in doubt on where a function is being executed, consider checking at __xc_a! + +=========================================================== + +Debugger detection + +In _initterm initialization routines, the following device names are referenced: + +\\.\SICE +\\.\NTICE +\\.\SIWVID + +these correspond to the SoftICE kernel-level debugger. It seems to be rather famous for software cracking. + +I have not looked hard into how these strings are used, but they appear to be XORed against some constant data, with the result held in an array somewhere in .data. + +Not like its much of a hindrance anyway, since we have x64dbg :P + +=========================================================== + +EnterObfuscatedMode + +This function is also called from _initterm, and is always called from an obfuscated jump block. + +The first steps are to perform a binary search within the block of data specified by the symbol WARBIRD::g_ObfuscatedBlockData. + +(Fill in later) + +Lastly, the return is called, and the function returns to the decrypted code. \ No newline at end of file diff --git a/notes2.txt b/notes2.txt new file mode 100644 index 0000000..1070201 --- /dev/null +++ b/notes2.txt @@ -0,0 +1,24 @@ +EnterObfuscatedMode notes + +Stack: +A8 ebp +++ AC to all below +00 ecx +04 edi +08 ebx +0C eax / old stub (120F957) +10 esi / old stub offset (01001400) +14 edx +18 GetLastError() +1C EFLAGS +20 RetAddr +24 OldRetAddr (_initterm) + +;; not fastcall calling convention! +ecx <- [esp+1C] // Right below RetAddr +edx <- STUB_FRAME (??) + +val_3 -> {???}{7 bits select offset for something}{12 bits size decrypted data} +offset_1 -> key1 +offset_2 -> {byte1:last_bit = skip_verify_offset_1}{verify_byte}{key2 as word} +offset_3 -> some addr? \ No newline at end of file diff --git a/obf_block_data.hexpat b/obf_block_data.hexpat new file mode 100644 index 0000000..d432cba --- /dev/null +++ b/obf_block_data.hexpat @@ -0,0 +1,63 @@ +#include + +struct ObfBlockData { + u32 u0; + u32 u1; + u32 u2; + u32 u3; + u32 u4; +}; + +ObfBlockData entries[0x323B] @ 0; + +fn main() { + u32 first = 0; + u32 first_prev = 0; + u32 last = 0x323A; + u32 last_prev = 0x323A; + u32 sum = 0; + u32 stub_ret_offset = 0x20F989; + u32 index = 0; + u32 xor_value = 0; + + while (true) { + index = (first + last) / 2; + xor_value = entries[index].u0 ^ sum; + + if (stub_ret_offset >= xor_value) { + if (stub_ret_offset >= xor_value + (entries[index].u3 & 0xFFF)) { + first = index + 1; + last = last_prev; + first_prev = first; + sum = entries[index].u0 - entries[index].u3; + std::print("STAY {0:04X} >= {1:04X}\n", stub_ret_offset, xor_value + (entries[index].u3 & 0xFFF)); + } else { + std::print("BREAK {0:04X} < {1:04X}\n", stub_ret_offset, xor_value + (entries[index].u3 & 0xFFF)); + std::print("INDEX {}\n", index); + break; + } + } else { + last = index - 1; + last_prev = last; + sum = entries[index].u0 + entries[index].u3; + } + + if (last < first) { + // set_handle_ret(handle_ret_addr); + std::print("ERROR\n", xor_value); + return; + } + + std::print("XOR {0:04X}\n", xor_value); + std::print("FIRST {0:04X}\n", first); + std::print("LAST {0:04X}\n", last); + std::print("SUM {0:04X}\n", sum); + } + + u32 val1 = entries[index].u1 + entries[index].u0; + u32 val2 = entries[index].u2 - entries[index].u0; + u32 val3 = entries[index].u4 + entries[index].u3; + std::print("VAL1 {0:04X}\n", val1); + std::print("VAL2 {0:04X}\n", val2); + std::print("VAL3 {0:04X}\n", val3); +}; \ No newline at end of file diff --git a/obfu_block.py b/obfu_block.py new file mode 100644 index 0000000..147c1bd --- /dev/null +++ b/obfu_block.py @@ -0,0 +1,131 @@ +from qiling import * +from qiling.const import * +from capstone import * +from keystone import * +import pefile +import json +import re +import numpy as np + +# Set to binary name +BIN_NAME = "sppsvc.exe" + +# Set to function to be analyzed +# ANLZ_FUNC = "?VerifyPKeyByInstalledPidConfig@CSLSLicenseManager@@IBEJPAVCSLPKey@@PBGPAUtagPKEY_BASIC_INFO@@PAPAGPAUDECODED_PKEY_DATA@@@Z" +ANLZ_FUNC = "_wmain" + +# These magic regexes are derived from the byte markers in notes.txt +PUSH_REGEX = rb"(?:\x8dd\$\xfc\x89[\x04\x0c\x14\x1c,4<]\$|[PQRSUVW]){2}\x8d[x\x05\r\x15\x1d-5=].{4}" +STUB_RET4_REGEX = rb"\x8b[DLT\\lt\|]\$\x04\xc2\x04\x00" +STUB_RET0_REGEX = rb"\x87[\x04\x0c\x14\x1c,4<]\$\xc3" +STUB_MAX_SIZE = 0x40 # Maximum size of jump stub + +REG_NAMES = { + 19: "EAX", + 20: "EBP", + 21: "EBX", + 22: "ECX", + 23: "EDI", + 24: "EDX", + 29: "ESI" +} +NOP = b"\x90" + +with open("syms.json", "r") as f: + sym_data = json.loads(f.read()) + +sym_data = {int(a, 16): b for a, b in sym_data.items()} +sym_data_inv = {b: a for a, b in sym_data.items()} +sym_addrs = sorted(sym_data) + +ks = Ks(KS_ARCH_X86, KS_MODE_32) +md = Cs(CS_ARCH_X86, CS_MODE_32) +md.detail = True +md.skipdata = True +ql = Qiling(["./rootfs/sppsvc.exe"], "./rootfs") +image_start = ql.loader.images[0].base +image_end = ql.loader.images[0].end +image_size = image_end - image_start +pe = pefile.PE(data=ql.mem.read(image_start, image_size)) + +num_obd = ql.unpack(ql.mem.read(sym_data_inv["?g_nNumObfuscatedBlockData@WARBIRD@@3KA"], 4)) +obd_addr = sym_data_inv["?g_ObfuscatedBlockData@WARBIRD@@3PAU_OBFUSCATED_BLOCK_DATA@1@A"] +obd = [] + +for i in range(num_obd): + obf_block = [] + + for j in range(5): + obf_block.append(ql.unpack(ql.mem.read(obd_addr + 4*(5*i + j), 4))) + + obd.append(obf_block) + +stub_frame = 0x120f989 +stub_frame_offset = stub_frame - image_start + +first = 0 +first_prev = first +last = num_obd - 1 +last_prev = last +sum_diff = 0 + +while True: + index = (first + last) // 2 + xor_value = obd[index][0] ^ sum_diff + + if stub_frame_offset >= xor_value: + if stub_frame_offset >= xor_value + (obd[index][3] & 0xFFF): + first = index + 1 + last = last_prev + first_prev = first + sum_diff = (obd[index][0] - obd[index][3]) % (1 << 32) + else: + print(f"XOR {hex(xor_value)} SUMDIFF {hex(sum_diff)} INDEX {hex(index)}") + break + else: + last = index - 1 + last_prev = last + sum_diff = (obd[index][0] + obd[index][3]) % (1 << 32) + + print(f"XOR {hex(xor_value)} SUMDIFF {hex(sum_diff)} INDEX {hex(index)}") + +val1 = (obd[index][1] + obd[index][0]) % (1 << 32) +val2 = (obd[index][2] - obd[index][0]) % (1 << 32) +val3 = (obd[index][4] + obd[index][3]) % (1 << 32) +unk3 = obd[index][3] +data_size = unk3 & 0xfff +xor_plus_binstart = image_start + xor_value +val1_bytes = val1.to_bytes(4, "little") +val2_bytes = val2.to_bytes(4, "little") +enc_bytes = ql.mem.read(xor_plus_binstart, data_size + 1) +dec_bytes = [0] * data_size +chksum = 0xa5 + +for i in range(data_size - 1, 0, -1): + enc_byte = enc_bytes[i] + b = enc_bytes[i - 1] + + if i % 2 == 1: + if (enc_byte ^ val1_bytes[3]) % 2 == 1: + a = (enc_byte ^ val1_bytes[3] ^ val1_bytes[2] ^ 0x100) >> 1 + else: + a = (enc_byte ^ val1_bytes[3]) >> 1 + + if (a ^ b) % 2 == 1: + dec_byte = (a ^ b ^ val2_bytes[1] ^ 0x100) >> 1 + else: + dec_byte = (a ^ b) >> 1 + else: + if (enc_byte ^ val1_bytes[1]) % 2 == 1: + a = (enc_byte ^ val1_bytes[1] ^ val1_bytes[0] ^ 0x100) >> 1 + else: + a = (enc_byte ^ val1_bytes[1]) >> 1 + + if (a ^ b) % 2 == 1: + dec_byte = (a ^ b ^ val2_bytes[0] ^ 0x100) >> 1 + else: + dec_byte = (a ^ b) >> 1 + + chksum ^= dec_byte + print(hex(dec_byte)) + dec_bytes[i] = dec_byte \ No newline at end of file diff --git a/peacestone.py b/peacestone.py index 10a70e6..3f8ed71 100644 --- a/peacestone.py +++ b/peacestone.py @@ -2,13 +2,22 @@ from qiling import * from qiling.const import * from capstone import * from keystone import * +import pefile import json +import re # Set to binary name BIN_NAME = "sppsvc.exe" # Set to function to be analyzed -ANLZ_FUNC = "?VerifyPKeyByInstalledPidConfig@CSLSLicenseManager@@IBEJPAVCSLPKey@@PBGPAUtagPKEY_BASIC_INFO@@PAPAGPAUDECODED_PKEY_DATA@@@Z" +# ANLZ_FUNC = "?VerifyPKeyByInstalledPidConfig@CSLSLicenseManager@@IBEJPAVCSLPKey@@PBGPAUtagPKEY_BASIC_INFO@@PAPAGPAUDECODED_PKEY_DATA@@@Z" +ANLZ_FUNC = "_wmain" + +# These magic regexes are derived from the byte markers in notes.txt +PUSH_REGEX = rb"(?:\x8dd\$\xfc\x89[\x04\x0c\x14\x1c,4<]\$|[PQRSUVW]){2}\x8d[x\x05\r\x15\x1d-5=].{4}" +STUB_RET4_REGEX = rb"\x8b[DLT\\lt\|]\$\x04\xc2\x04\x00" +STUB_RET0_REGEX = rb"\x87[\x04\x0c\x14\x1c,4<]\$\xc3" +STUB_MAX_SIZE = 0x40 # Maximum size of jump stub REG_NAMES = { 19: "EAX", @@ -31,7 +40,12 @@ sym_addrs = sorted(sym_data) ks = Ks(KS_ARCH_X86, KS_MODE_32) md = Cs(CS_ARCH_X86, CS_MODE_32) md.detail = True +md.skipdata = True ql = Qiling(["./rootfs/sppsvc.exe"], "./rootfs") +image_start = ql.loader.images[0].base +image_end = ql.loader.images[0].end +image_size = image_end - image_start +pe = pefile.PE(data=ql.mem.read(image_start, image_size)) def func_boundary(fun_name): f_start = sym_data_inv[ANLZ_FUNC] @@ -41,90 +55,117 @@ def func_boundary(fun_name): return f_start, f_end def save_patched_exe(): - for region in ql.mem.get_mapinfo(): - if region[3] == BIN_NAME: - exe_start = region[0] - exe_end = region[1] - + print("Fixing up sections...") with open(BIN_NAME.replace(".exe", ".stoned.exe"), "wb") as f: f.write(ql.mem.read(exe_start, exe_end - exe_start)) +def assemble(instrs): + return bytes(ks.asm(instrs)[0]) + +""" def remove_verify_stubs(): global ql +""" - f_start, f_end = func_boundary(ANLZ_FUNC) - f_start_orig = f_start - orig_ql_state = ql.save() - offset = 0 - while f_start < f_end: - stop = False - f_code = ql.mem.read(f_start, f_end - f_start) - instrs = list(md.disasm(f_code, f_start)) - - print("INSTRS @ " + hex(f_start)) +if __name__ == "__main__": + print("peacestone copyleft UMSKT project 2023") + print() + + # remove_verify_stubs() + # save_patched_exe() + + pe_data = ql.mem.read(image_start, image_size) + f = open("log.txt", "w") + + for match in re.finditer(STUB_RET4_REGEX, pe_data): + match_addr = image_start + match.start() + stub_code = ql.mem.read(match_addr - 0x1000, 0x1000) - for i in instrs: - print(i) - + try: + stub_start_offset = list(re.finditer(PUSH_REGEX, stub_code))[0].start() + except: + continue + + stub_start_addr = match_addr - 0x1000 + stub_start_offset + instrs = list(md.disasm(ql.mem.read(stub_start_addr, 0x47), stub_start_addr)) ret = 0 - for i, inst in enumerate(instrs): - if inst.mnemonic == "ret" and inst.op_str == "4": + + for i, instr in enumerate(instrs): + print(instr) + + if instr.mnemonic == "ret" and instr.op_str == "4": ret = i break if ret < 8: - stop = True - + continue + + # min 7 backwards -> first push instr, then stop + # much better than whatever this is supposed to be + stub_start_index = ret - 8 + if instrs[ret-2].mnemonic == "mov": + stub_start_index -= 1 + if instrs[stub_start_index].mnemonic == "mov" or instrs[stub_start_index].mnemonic == "push": - stub_start_index = ret - 7 + stub_start_index += 1 elif instrs[stub_start_index].mnemonic != "lea": - stop = True + print("CANT DEAL WITH THIS") + continue stub_start = instrs[stub_start_index].address try: used_reg = list(md.disasm(instrs[stub_start_index].bytes, 0))[0].operands[0].value.reg except: - stop = True + raise Exception("CANT DEAL WITH THIS") + + if used_reg not in REG_NAMES: + print("CANT DEAL WITH THIS") + continue used_reg_name = REG_NAMES[used_reg].lower() - if stop: - print("VERIFY STUB REMOVAL FINISHED") - break - print("ADDRESS ASSIGNED @ " + hex(stub_start)) - ql.run(begin=stub_start, end=instrs[stub_start_index+2].address) - chksum_data = ql.arch.regs.read(used_reg) - - print("CHKSUM @ " + hex(chksum_data)) - len_stub = (instrs[ret].address + 3) - stub_start - ql.mem.write(stub_start, NOP * len_stub) - ql.mem.write(chksum_data, NOP * 16) + # ql.mem.write(stub_start, NOP * len_stub) + # ql.mem.write(chksum_data, NOP * 16) push_instrs = list(map(lambda c: bytearray(ks.asm(c)[0]), [f"push {used_reg_name}", f"lea esp, [esp-4]", f"mov [esp], {used_reg_name}"])) jmp_insert_addr = 0 for inst in instrs[max(0,stub_start_index-4):stub_start_index][::-1]: + print(inst) if inst.bytes in push_instrs: - ql.mem.write(inst.address, NOP * len(inst.bytes)) + # ql.mem.write(inst.address, NOP * len(inst.bytes)) jmp_insert_addr = inst.address else: break - f_start = chksum_data + 0x10 + if jmp_insert_addr == 0: + print("CANT DEAL WITH THIS") + continue print("NOPPED STARTING @ " + hex(jmp_insert_addr)) - print("NEXT: " + hex(f_start + offset)) - -if __name__ == "__main__": - print("peacestone copyleft UMSKT project 2023") - print() - - remove_verify_stubs() - save_patched_exe() \ No newline at end of file + + try: + ql.run(begin=jmp_insert_addr, end=instrs[ret].address) + + handler_addr = ql.arch.stack_pop() + ql.arch.stack_pop() + next_addr = ql.arch.stack_pop() + except: + handler_addr = -1 + next_addr = -1 + + print("HANDLER @ " + hex(handler_addr)) + print("JUMP TARGET @ " + hex(next_addr)) + print(ql.arch.regs.esp) + + f.write(f"J R4 S {hex(jmp_insert_addr)} H {hex(handler_addr)} N {hex(next_addr)}\n") + + # input() + print() \ No newline at end of file