This commit is contained in:
WitherOrNot 2023-07-30 14:05:52 -04:00
parent dee5bf889f
commit cf89342b36
5 changed files with 438 additions and 61 deletions

144
notes.txt
View File

@ -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 ; where reg is one of: eax, ecx, edx, ebx, ebp, esi, edi
; push is sometimes replaced with the following equivalent instructions ; push is sometimes replaced with the following equivalent instructions
@ -6,7 +17,7 @@
; mov [esp], reg ; mov [esp], reg
; IDA (and perhaps other disassemblers idk) confuses the first parts of the stub for constant bytes ; 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
push reg push reg
@ -27,18 +38,32 @@ ret 4
; after verifier finishes, it adds 0x10 to the value at [esp] ; 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 ; 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======= ; =======BYTE MARKERS=======
; ; Useful for finding obfuscated jumps in a disassembler
mov reg, [esp+4]
ret 4 lea esp, [esp - 4]
; eax - 8b 44 24 04 c2 04 00 mov [esp], reg
; ecx - 8b 4c 24 04 c2 04 00 ; eax - 8d 64 24 fc 89 04 24
; edx - 8b 54 24 04 c2 04 00 ; ecx - 8d 64 24 fc 89 0c 24
; ebx - 8b 5c 24 04 c2 04 00 ; edx - 8d 64 24 fc 89 14 24
; ebp - 8b 6c 24 04 c2 04 00 ; ebx - 8d 64 24 fc 89 1c 24
; esi - 8b 74 24 04 c2 04 00 ; ebp - 8d 64 24 fc 89 2c 24
; edi - 8b 7c 24 04 c2 04 00 ; 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 ; 0x11223344 is an example address
lea reg, [0x11223344] lea reg, [0x11223344]
@ -49,3 +74,96 @@ lea reg, [0x11223344]
; ebp - 8d 2d 44 33 22 11 ; ebp - 8d 2d 44 33 22 11
; esi - 8d 35 44 33 22 11 ; esi - 8d 35 44 33 22 11
; edi - 8d 3d 44 33 22 11 ; 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.

24
notes2.txt Normal file
View File

@ -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?

63
obf_block_data.hexpat Normal file
View File

@ -0,0 +1,63 @@
#include <std/io.pat>
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);
};

131
obfu_block.py Normal file
View File

@ -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

View File

@ -2,13 +2,22 @@ from qiling import *
from qiling.const import * from qiling.const import *
from capstone import * from capstone import *
from keystone import * from keystone import *
import pefile
import json import json
import re
# Set to binary name # Set to binary name
BIN_NAME = "sppsvc.exe" BIN_NAME = "sppsvc.exe"
# Set to function to be analyzed # 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 = { REG_NAMES = {
19: "EAX", 19: "EAX",
@ -31,7 +40,12 @@ sym_addrs = sorted(sym_data)
ks = Ks(KS_ARCH_X86, KS_MODE_32) ks = Ks(KS_ARCH_X86, KS_MODE_32)
md = Cs(CS_ARCH_X86, CS_MODE_32) md = Cs(CS_ARCH_X86, CS_MODE_32)
md.detail = True md.detail = True
md.skipdata = True
ql = Qiling(["./rootfs/sppsvc.exe"], "./rootfs") 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): def func_boundary(fun_name):
f_start = sym_data_inv[ANLZ_FUNC] f_start = sym_data_inv[ANLZ_FUNC]
@ -41,90 +55,117 @@ def func_boundary(fun_name):
return f_start, f_end return f_start, f_end
def save_patched_exe(): def save_patched_exe():
for region in ql.mem.get_mapinfo(): print("Fixing up sections...")
if region[3] == BIN_NAME:
exe_start = region[0]
exe_end = region[1]
with open(BIN_NAME.replace(".exe", ".stoned.exe"), "wb") as f: with open(BIN_NAME.replace(".exe", ".stoned.exe"), "wb") as f:
f.write(ql.mem.read(exe_start, exe_end - exe_start)) f.write(ql.mem.read(exe_start, exe_end - exe_start))
def assemble(instrs):
return bytes(ks.asm(instrs)[0])
"""
def remove_verify_stubs(): def remove_verify_stubs():
global ql 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: if __name__ == "__main__":
stop = False print("peacestone copyleft UMSKT project 2023")
f_code = ql.mem.read(f_start, f_end - f_start) print()
instrs = list(md.disasm(f_code, f_start))
print("INSTRS @ " + hex(f_start)) # remove_verify_stubs()
# save_patched_exe()
for i in instrs: pe_data = ql.mem.read(image_start, image_size)
print(i) 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)
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 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 ret = i
break break
if ret < 8: 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 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": 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": elif instrs[stub_start_index].mnemonic != "lea":
stop = True print("CANT DEAL WITH THIS")
continue
stub_start = instrs[stub_start_index].address stub_start = instrs[stub_start_index].address
try: try:
used_reg = list(md.disasm(instrs[stub_start_index].bytes, 0))[0].operands[0].value.reg used_reg = list(md.disasm(instrs[stub_start_index].bytes, 0))[0].operands[0].value.reg
except: 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() used_reg_name = REG_NAMES[used_reg].lower()
if stop:
print("VERIFY STUB REMOVAL FINISHED")
break
print("ADDRESS ASSIGNED @ " + hex(stub_start)) 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 len_stub = (instrs[ret].address + 3) - stub_start
ql.mem.write(stub_start, NOP * len_stub) # ql.mem.write(stub_start, NOP * len_stub)
ql.mem.write(chksum_data, NOP * 16) # 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}"])) 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 jmp_insert_addr = 0
for inst in instrs[max(0,stub_start_index-4):stub_start_index][::-1]: for inst in instrs[max(0,stub_start_index-4):stub_start_index][::-1]:
print(inst)
if inst.bytes in push_instrs: 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 jmp_insert_addr = inst.address
else: else:
break 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("NOPPED STARTING @ " + hex(jmp_insert_addr))
print("NEXT: " + hex(f_start + offset))
if __name__ == "__main__": try:
print("peacestone copyleft UMSKT project 2023") 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() print()
remove_verify_stubs()
save_patched_exe()