diff --git a/bloomoo_trace.js b/bloomoo_trace.js new file mode 100644 index 0000000..f9d77c3 --- /dev/null +++ b/bloomoo_trace.js @@ -0,0 +1,558 @@ +'use strict'; + +// ============================================================ +// BlooMoo Script Engine Tracer -- Frida instrumentation +// Target: bloomoodll.dll (loaded by nemo.exe) +// +// Usage: +// frida -l bloomoo_trace.js -f nemo.exe +// python frida_launcher.py +// ============================================================ + +// ---------- configuration ---------- + +var CONFIG = { + // Hook categories (set false to silence) + hookBehaviourEntry: true, // CMC_Behaviour_Entry constructor (command parser) + hookStaticValidText: true, // CMC_Condition::valid(container, condText) - MAIN condition entry + hookStaticValidCompare: true, // CMC_Condition::valid(container, lhs, rhs, op) - parsed compare + hookStaticValidTextOp: true, // CMC_Condition::valid(container, lhs, opTxt, rhs) - text-op compare + hookConditionValid: true, // CMC_Condition::valid (virtual instance method) + hookComplexCondition: true, // CMC_ComplexCondition::valid (AND / OR) + hookExpression: true, // CMC_Expression constructor (arithmetic tokeniser) + hookVariableResolve: true, // CMC_Variable::resolve (literal -> typed value) + hookResolveParameter: true, // CMC_Behaviour::resolveParameter (operand resolution) + hookGetObject: false, // CMC_ObjectsContainer::getObject (very noisy) + hookGetVariable: false, // CMC_ObjectsContainer::getVariable (very noisy) + + // Detail levels + showParsedFields: true, // dump parsed fields after BehaviourEntry ctor + showConditionResult: true, // print TRUE/FALSE after condition eval + + // Display + maxStringLen: 200, // truncate strings longer than this +}; + +// ---------- address table ---------- +// All values are Ghidra VA (preferred base 0x10000000). +// We hook the *implementations*, not the 5-byte JMP thunks. + +var GHIDRA_BASE = 0x10000000; + +var IMPL = { + CMC_Behaviour_Entry_ctor: 0x10042670, + + // Static condition overloads (the MAIN entry points for condition eval!) + CMC_Condition_valid_text: 0x10050330, // static valid(container, CXString condText) + CMC_Condition_valid_compare: 0x1004fe80, // static valid(container, CXString lhs, CXString rhs, int op) + CMC_Condition_valid_textop: 0x100504c0, // static valid(container, CXString lhs, CXString opTxt, CXString rhs) + + // Virtual instance methods (may fire less often) + CMC_Condition_valid_instance: 0x1004fc50, + CMC_ComplexCondition_valid: 0x1004f490, + + CMC_Expression_ctor: 0x1005a420, + CMC_Object_getType: 0x100759d0, + CMC_ObjectsContainer_getObject: 0x100231b0, + CMC_ObjectsContainer_getVariable: 0x10026530, + CMC_Variable_resolve: 0x10099c20, + CMC_Behaviour_resolveParameter: 0x100422e0, // resolveParameter(container, obj, CXString, behaviour) +}; + +// ---------- look-up tables ---------- + +var CMP_OP = { 1:'==', 2:'!=', 3:'<', 4:'<=', 5:'>', 6:'>=', 7:'instanceof' }; +var LOGIC_OP = { 1:'AND', 2:'OR' }; +var TYPE_NAME = { + 1:'Integer', 2:'String', 3:'Bool', 4:'Double', + 0xC:'Special(0xC)' +}; +// Text-form operators used in the 3-CXString valid() overload +var TEXT_OP = { + '_':'==', '!_':'!=', '<>':'!=', '<':'<', '<_':'<=', '>':'>', '>_':'>=', '?':'instanceof' +}; + +// ---------- helpers ---------- + +var callDepth = 0; + +/** Central logging: prints to Frida console AND sends to Python host (for file logging). */ +function log(msg) { + console.log(msg); + send({ type: 'trace', text: msg }); +} + +function pad() { return ' '.repeat(callDepth); } + +function trunc(s) { + if (!s) return ''; + if (s.length > CONFIG.maxStringLen) + return s.substring(0, CONFIG.maxStringLen) + '\u2026'; + return s; +} + +function safeAnsi(p, maxLen) { + try { + if (p.isNull()) return ''; + return maxLen > 0 ? p.readAnsiString(maxLen) : p.readAnsiString(); + } catch (_) { return ''; } +} + +/** + * Read a CXString that lives *inside* an object at a known offset. + * + * CXString layout (16 bytes / 0x10): + * +0x00 vtable (ptr) + * +0x04 length (int32) + * +0x08 capacity (int32) + * +0x0C charPtr (char*) + */ +function readCXStr(base, off) { + try { + var s = base.add(off); + var len = s.add(0x04).readS32(); + if (len <= 0) return ''; + var cp = s.add(0x0C).readPointer(); + return safeAnsi(cp, len); + } catch (_) { return ''; } +} + +/** Read a CXString passed by-value starting at a raw pointer. */ +function readCXStrRaw(p) { return readCXStr(p, 0); } + +function typeName(id) { return TYPE_NAME[id] || ('type#' + id); } + +// ============================================================ +// Hook installer +// ============================================================ + +function installHooks(base) { + function at(ghidra) { return base.add(ghidra - GHIDRA_BASE); } + + // ---------------------------------------------------------- + // 1. CMC_Behaviour_Entry::ctor + // __thiscall(CMC_Behaviour *owner, CXString code) + // ECX = this + // Stack: [ret][owner:4][CXString vtable:4][len:4][cap:4][charPtr:4] + // ---------------------------------------------------------- + if (CONFIG.hookBehaviourEntry) { + Interceptor.attach(at(IMPL.CMC_Behaviour_Entry_ctor), { + onEnter: function (args) { + this._self = ptr(this.context.ecx); + // CXString 'code' starts at ESP + 8 (skip retaddr + owner ptr) + var cxs = ptr(this.context.esp).add(8); + var len = cxs.add(0x04).readS32(); + var cp = cxs.add(0x0C).readPointer(); + this._code = (len > 0 && !cp.isNull()) ? safeAnsi(cp, len) : ''; + + log(pad() + '[ENTRY] code="' + trunc(this._code) + '"'); + callDepth++; + }, + onLeave: function (_ret) { + callDepth = Math.max(0, callDepth - 1); + if (!CONFIG.showParsedFields) return; + try { + var t = this._self; + var opName = readCXStr(t, 0x74); + var opcode = t.add(0x84).readS32(); + var argsStr = readCXStr(t, 0x88); + var token = readCXStr(t, 0xa8); + var fTarget = t.add(0xa0).readU8(); + var fThis = t.add(0xa1).readU8(); + var fStatic = t.add(0xa2).readU8(); + var fDyn = t.add(0xbc).readU8(); + var fExprTgt = t.add(0xbe).readU8(); + + var flags = []; + if (fTarget) flags.push('*TGT'); + if (fThis) flags.push('THIS'); + if (fStatic) flags.push('STATIC'); + if (fDyn) flags.push('DYN$'); + if (fExprTgt) flags.push('EXPR_TGT'); + + var parts = []; + if (opName) parts.push('op="' + opName + '"'); + if (opcode !== -1) parts.push('idx=' + opcode); + if (argsStr) parts.push('args="' + trunc(argsStr) + '"'); + if (token && token !== 'XXX') parts.push('tok="' + trunc(token) + '"'); + if (flags.length) parts.push('[' + flags.join(',') + ']'); + + if (parts.length) + log(pad() + ' => ' + parts.join(' ')); + } catch (e) { + log(pad() + ' => '); + } + } + }); + log('[+] CMC_Behaviour_Entry::ctor @ ' + at(IMPL.CMC_Behaviour_Entry_ctor)); + } + + // ========================================================== + // STATIC CONDITION OVERLOADS (main entry points!) + // ========================================================== + + // ---------------------------------------------------------- + // 2a. CMC_Condition::valid(ObjectsContainer*, CXString condText) + // static __cdecl - parses condition text like "x'=y" + // Operators in text: ' = equal, !' = notequal, < > ? etc. + // Stack: [ret][container:4][CXString condText (16b)] + // ---------------------------------------------------------- + if (CONFIG.hookStaticValidText) { + Interceptor.attach(at(IMPL.CMC_Condition_valid_text), { + onEnter: function (_args) { + var esp = ptr(this.context.esp); + var cxs = esp.add(8); // skip ret + container ptr + this._text = readCXStrRaw(cxs); + log(pad() + '[COND:TXT] "' + trunc(this._text) + '"'); + callDepth++; + }, + onLeave: function (retval) { + callDepth = Math.max(0, callDepth - 1); + if (CONFIG.showConditionResult) { + var r = retval.toInt32() & 0xFF; + log(pad() + ' => ' + (r ? 'TRUE' : 'FALSE')); + } + } + }); + log('[+] static valid(condText) @ ' + at(IMPL.CMC_Condition_valid_text)); + } + + // ---------------------------------------------------------- + // 2b. CMC_Condition::valid(ObjectsContainer*, CXString lhs, + // CXString rhs, int op) + // static __cdecl - resolves operands via resolveParameter + // Stack: [ret][container:4][CXString lhs(16)][CXString rhs(16)][op:4] + // ---------------------------------------------------------- + if (CONFIG.hookStaticValidCompare) { + Interceptor.attach(at(IMPL.CMC_Condition_valid_compare), { + onEnter: function (_args) { + var esp = ptr(this.context.esp); + var lhs = readCXStrRaw(esp.add(0x08)); + var rhs = readCXStrRaw(esp.add(0x18)); + var op = esp.add(0x28).readS32(); + var opStr = CMP_OP[op] || ('cmp#' + op); + this._desc = '"' + trunc(lhs) + '" ' + opStr + ' "' + trunc(rhs) + '"'; + log(pad() + '[COND:CMP] ' + this._desc); + callDepth++; + }, + onLeave: function (retval) { + callDepth = Math.max(0, callDepth - 1); + if (CONFIG.showConditionResult) { + var r = retval.toInt32() & 0xFF; + log(pad() + ' => ' + (r ? 'TRUE' : 'FALSE')); + } + } + }); + log('[+] static valid(lhs,rhs,op) @ ' + at(IMPL.CMC_Condition_valid_compare)); + } + + // ---------------------------------------------------------- + // 2c. CMC_Condition::valid(ObjectsContainer*, CXString lhs, + // CXString opText, CXString rhs) + // static __cdecl - operator as text ("_","!_","<",">_","?" etc.) + // Stack: [ret][container:4][CXString(16)][CXString(16)][CXString(16)] + // ---------------------------------------------------------- + if (CONFIG.hookStaticValidTextOp) { + Interceptor.attach(at(IMPL.CMC_Condition_valid_textop), { + onEnter: function (_args) { + var esp = ptr(this.context.esp); + var s1 = readCXStrRaw(esp.add(0x08)); + var s2 = readCXStrRaw(esp.add(0x18)); + var s3 = readCXStrRaw(esp.add(0x28)); + // Determine which is operator: try s2 first, then s3 + var opStr = TEXT_OP[s2] || TEXT_OP[s3] || s2; + var lhs, rhs; + if (TEXT_OP[s2]) { lhs = s1; rhs = s3; } + else { lhs = s1; rhs = s2; opStr = TEXT_OP[s3] || s3; } + this._desc = '"' + trunc(lhs) + '" ' + opStr + ' "' + trunc(rhs) + '"'; + log(pad() + '[COND:TOP] ' + this._desc); + callDepth++; + }, + onLeave: function (retval) { + callDepth = Math.max(0, callDepth - 1); + if (CONFIG.showConditionResult) { + var r = retval.toInt32() & 0xFF; + log(pad() + ' => ' + (r ? 'TRUE' : 'FALSE')); + } + } + }); + log('[+] static valid(lhs,opTxt,rhs)@ ' + at(IMPL.CMC_Condition_valid_textop)); + } + + // ========================================================== + // VIRTUAL INSTANCE METHODS (may fire less often) + // ========================================================== + + // ---------------------------------------------------------- + // 3. CMC_Condition::valid (instance) + // __thiscall(bool triggerBehaviours) + // +0x54 / +0x58 : CMC_Behaviour_Entry* left / right operand + // +0x8c : comparison opcode (1..7) + // ---------------------------------------------------------- + if (CONFIG.hookConditionValid) { + Interceptor.attach(at(IMPL.CMC_Condition_valid_instance), { + onEnter: function (_args) { + var t = ptr(this.context.ecx); + var op = t.add(0x8c).readS32(); + var opStr = CMP_OP[op] || ('cmp#' + op); + + var lhsE = t.add(0x54).readPointer(); + var rhsE = t.add(0x58).readPointer(); + var lhs = !lhsE.isNull() + ? (readCXStr(lhsE, 0xa8) || readCXStr(lhsE, 0x88) || '?') + : ''; + var rhs = !rhsE.isNull() + ? (readCXStr(rhsE, 0xa8) || readCXStr(rhsE, 0x88) || '?') + : ''; + + this._desc = '"' + trunc(lhs) + '" ' + opStr + ' "' + trunc(rhs) + '"'; + log(pad() + '[COND] ' + this._desc); + callDepth++; + }, + onLeave: function (retval) { + callDepth = Math.max(0, callDepth - 1); + if (CONFIG.showConditionResult) { + var r = retval.toInt32() & 0xFF; + log(pad() + ' => ' + (r ? 'TRUE' : 'FALSE')); + } + } + }); + log('[+] CMC_Condition::valid(inst) @ ' + at(IMPL.CMC_Condition_valid_instance)); + } + + // ---------------------------------------------------------- + // 3. CMC_ComplexCondition::valid + // __thiscall(bool triggerBehaviours) + // +0x8c : 1=AND, 2=OR + // children are named conditions at +0x5c, +0x6c (CXString) + // ---------------------------------------------------------- + if (CONFIG.hookComplexCondition) { + Interceptor.attach(at(IMPL.CMC_ComplexCondition_valid), { + onEnter: function (_args) { + var t = ptr(this.context.ecx); + var op = t.add(0x8c).readS32(); + var opStr = LOGIC_OP[op] || ('logic#' + op); + var lName = readCXStr(t, 0x5c); + var rName = readCXStr(t, 0x6c); + this._desc = lName + ' ' + opStr + ' ' + rName; + log(pad() + '[CCOND] ' + this._desc); + callDepth++; + }, + onLeave: function (retval) { + callDepth = Math.max(0, callDepth - 1); + if (CONFIG.showConditionResult) { + var r = retval.toInt32() & 0xFF; + log(pad() + ' => ' + (r ? 'TRUE' : 'FALSE')); + } + } + }); + log('[+] CMC_ComplexCondition::valid @ ' + at(IMPL.CMC_ComplexCondition_valid)); + } + + // ---------------------------------------------------------- + // 4. CMC_Expression::ctor + // __thiscall(CMC_ObjectsContainer *container, CXString expressionText) + // ECX = this + // Stack: [ret][container:4][CXString: vtable|len|cap|charPtr] + // ---------------------------------------------------------- + if (CONFIG.hookExpression) { + Interceptor.attach(at(IMPL.CMC_Expression_ctor), { + onEnter: function (_args) { + var cxs = ptr(this.context.esp).add(8); // skip retaddr + container ptr + var len = cxs.add(0x04).readS32(); + var cp = cxs.add(0x0C).readPointer(); + var txt = (len > 0 && !cp.isNull()) ? safeAnsi(cp, len) : ''; + log(pad() + '[EXPR] "' + trunc(txt) + '"'); + } + }); + log('[+] CMC_Expression::ctor @ ' + at(IMPL.CMC_Expression_ctor)); + } + + // ---------------------------------------------------------- + // 5. CMC_Variable::resolve (static, __cdecl) + // CXString by value on stack: + // args[0]=vtable, args[1]=len, args[2]=cap, args[3]=charPtr + // Returns CMC_Variable* with type at +0x20 + // ---------------------------------------------------------- + if (CONFIG.hookVariableResolve) { + Interceptor.attach(at(IMPL.CMC_Variable_resolve), { + onEnter: function (args) { + var len = args[1].toInt32(); + var cp = args[3]; + this._txt = safeAnsi(cp, len); + }, + onLeave: function (retval) { + var resolved = ''; + if (!retval.isNull()) { + try { + var tid = retval.add(0x20).readS32(); + resolved = ' -> ' + typeName(tid); + } catch (_) {} + } + log(pad() + '[RESOL] "' + trunc(this._txt) + '"' + resolved); + } + }); + log('[+] CMC_Variable::resolve @ ' + at(IMPL.CMC_Variable_resolve)); + } + + // ---------------------------------------------------------- + // 6a. CMC_Behaviour::resolveParameter (static, __cdecl) + // resolveParameter(ObjectsContainer*, CMC_Object*, CXString text, CMC_Behaviour*) + // Resolves an operand text to a CMC_Variable* + // Stack: [ret][container:4][obj:4][CXString(16)][behaviour:4] + // ---------------------------------------------------------- + if (CONFIG.hookResolveParameter) { + Interceptor.attach(at(IMPL.CMC_Behaviour_resolveParameter), { + onEnter: function (_args) { + var esp = ptr(this.context.esp); + var cxs = esp.add(0x0C); // skip ret + container + obj + this._text = readCXStrRaw(cxs); + }, + onLeave: function (retval) { + var info = ''; + if (!retval.isNull()) { + try { + var tid = retval.add(0x20).readS32(); + info = ' -> ' + typeName(tid); + } catch (_) {} + } else { + info = ' -> NULL'; + } + log(pad() + '[PARAM] "' + trunc(this._text) + '"' + info); + } + }); + log('[+] resolveParameter @ ' + at(IMPL.CMC_Behaviour_resolveParameter)); + } + + // ---------------------------------------------------------- + // 7. CMC_ObjectsContainer::getObject(char*) + // __thiscall, args[0] = char* name + // ---------------------------------------------------------- + if (CONFIG.hookGetObject) { + Interceptor.attach(at(IMPL.CMC_ObjectsContainer_getObject), { + onEnter: function (args) { + this._name = safeAnsi(args[0], 0); + }, + onLeave: function (retval) { + var info = retval.isNull() ? 'NULL' : ''; + if (!retval.isNull()) { + try { + var tid = retval.add(0x20).readS32(); + info = typeName(tid) + ' @' + retval; + } catch (_) { info = '@' + retval; } + } + log(pad() + '[OBJ] "' + trunc(this._name) + '" => ' + info); + } + }); + log('[+] getObject @ ' + at(IMPL.CMC_ObjectsContainer_getObject)); + } + + // ---------------------------------------------------------- + // 8. CMC_ObjectsContainer::getVariable(char*) + // __thiscall, args[0] = char* name + // ---------------------------------------------------------- + if (CONFIG.hookGetVariable) { + Interceptor.attach(at(IMPL.CMC_ObjectsContainer_getVariable), { + onEnter: function (args) { + this._name = safeAnsi(args[0], 0); + }, + onLeave: function (retval) { + var info = retval.isNull() ? 'NULL' : ''; + if (!retval.isNull()) { + try { + var tid = retval.add(0x20).readS32(); + info = typeName(tid); + } catch (_) { info = '@' + retval; } + } + log(pad() + '[VAR] "' + trunc(this._name) + '" => ' + info); + } + }); + log('[+] getVariable @ ' + at(IMPL.CMC_ObjectsContainer_getVariable)); + } + + log(''); + log('[*] All hooks installed. Tracing BlooMoo script engine...'); + log('Legend:'); + log(' [ENTRY] BehaviourEntry ctor (command parser)'); + log(' [COND:TXT] static valid(conditionText) - main condition entry point'); + log(' [COND:CMP] static valid(lhs, rhs, intOp) - parsed comparison'); + log(' [COND:TOP] static valid(lhs, textOp, rhs) - text-op comparison'); + log(' [COND] virtual Condition::valid (instance)'); + log(' [CCOND] ComplexCondition (AND/OR)'); + log(' [EXPR] Expression ctor'); + log(' [RESOL] Variable::resolve'); + log(' [PARAM] resolveParameter'); + log(' [OBJ/VAR] getObject/getVariable (off by default)'); + log(''); +} + +// ============================================================ +// Runtime: toggle hooks on-the-fly via rpc +// ============================================================ + +rpc.exports = { + /** Toggle a CONFIG flag by name. E.g. toggle('hookGetObject') */ + toggle: function (key) { + if (key in CONFIG) { + CONFIG[key] = !CONFIG[key]; + return key + ' is now ' + CONFIG[key]; + } + return 'unknown key: ' + key; + }, + /** Set max string truncation length */ + setMaxLen: function (n) { + CONFIG.maxStringLen = n; + return 'maxStringLen = ' + n; + }, + /** Reset call depth (in case of mismatched enter/leave) */ + resetDepth: function () { + callDepth = 0; + return 'depth reset'; + } +}; + +// ============================================================ +// Module load detection +// ============================================================ + +function tryAttach() { + var mod = Process.findModuleByName('bloomoodll.dll'); + if (mod !== null) { + console.log('[*] bloomoodll.dll base=' + mod.base + ' size=0x' + mod.size.toString(16)); + installHooks(mod.base); + return true; + } + return false; +} + +// If DLL is already loaded (attach-to-running-process case), hook immediately. +// Otherwise, intercept LoadLibrary to catch it being loaded. +if (!tryAttach()) { + console.log('[*] bloomoodll.dll not yet loaded, waiting...'); + + var _hookA = Interceptor.attach( + Module.findExportByName('kernel32.dll', 'LoadLibraryA'), { + onEnter: function (args) { this._n = safeAnsi(args[0], 0); }, + onLeave: function (_ret) { + if (this._n && this._n.toLowerCase().indexOf('bloomoodll') !== -1) { + console.log('[*] LoadLibraryA -> bloomoodll.dll'); + _hookA.detach(); + tryAttach(); + } + } + }); + + var _hookW = Interceptor.attach( + Module.findExportByName('kernel32.dll', 'LoadLibraryW'), { + onEnter: function (args) { + try { this._n = args[0].readUtf16String(); } catch (_) { this._n = ''; } + }, + onLeave: function (_ret) { + if (this._n && this._n.toLowerCase().indexOf('bloomoodll') !== -1) { + console.log('[*] LoadLibraryW -> bloomoodll.dll'); + _hookW.detach(); + tryAttach(); + } + } + }); +} diff --git a/frida_launcher.py b/frida_launcher.py new file mode 100644 index 0000000..dcb7759 --- /dev/null +++ b/frida_launcher.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +BlooMoo Script Engine Tracer - Frida launcher +Spawns nemo.exe and instruments bloomoodll.dll. + +Usage: + python frida_launcher.py # spawn nemo.exe + python frida_launcher.py --attach # attach to running process + python frida_launcher.py --exe path/nemo.exe # custom exe path + +Alternatively, use the Frida CLI directly: + frida -l bloomoo_trace.js -f nemo.exe +""" + +import argparse +import os +import sys +import threading +import shlex + +try: + import frida +except ImportError: + print("Frida not installed. Run: pip install frida-tools") + sys.exit(1) + + +def format_trace(message): + """Extract printable text from a Frida message. Returns (text, is_trace).""" + mtype = message.get("type", "") + if mtype == "send": + payload = message.get("payload", "") + if isinstance(payload, dict) and payload.get("type") == "trace": + return payload.get("text", ""), True + return f"[send] {payload}", False + if mtype == "error": + desc = message.get("description", "") + stack = message.get("stack", "") + return f"[error] {desc}\n{stack}" if stack else f"[error] {desc}", False + return f"[{mtype}] {message}", False + + +def main(): + parser = argparse.ArgumentParser( + description="BlooMoo Script Engine Tracer" + ) + parser.add_argument( + "--exe", default="nemo.exe", + help="Path to nemo.exe (default: nemo.exe in script dir)" + ) + parser.add_argument( + "--attach", type=int, default=0, metavar="PID", + help="Attach to an already-running process instead of spawning" + ) + parser.add_argument( + "--log", default="bloomoo_trace.log", metavar="FILE", + help="Write trace output to a file (default: bloomoo_trace.log, use --no-log to disable)" + ) + parser.add_argument( + "--no-log", action="store_true", + help="Disable file logging" + ) + parser.add_argument( + "--args", default="-window", metavar="ARGS", + help="Additional arguments to pass to the spawned process" + ) + args = parser.parse_args() + + script_dir = os.path.dirname(os.path.abspath(__file__)) + js_path = os.path.join(script_dir, "bloomoo_trace.js") + + if not os.path.isfile(js_path): + print(f"[!] Script not found: {js_path}") + sys.exit(1) + + with open(js_path, "r", encoding="utf-8") as f: + js_code = f.read() + + # log file (on by default) + log_file = None + if args.log and not args.no_log: + log_path = os.path.join(script_dir, args.log) if not os.path.isabs(args.log) else args.log + log_file = open(log_path, "w", encoding="utf-8") + print(f"[*] Logging to {log_path}") + + def on_msg_wrapper(message, data): + text, is_trace = format_trace(message) + if not text: + return + # trace lines already go to stdout via Frida's console.log; + # non-trace messages (errors etc.) we print ourselves + if not is_trace: + print(text, flush=True) + # write ALL messages (trace + errors) to the log file + if log_file: + try: + log_file.write(text + "\n") + log_file.flush() + except Exception: + pass + + device = frida.get_local_device() + + if args.attach: + print(f"[*] Attaching to PID {args.attach} ...") + session = device.attach(args.attach) + else: + exe_path = args.exe + if not os.path.isabs(exe_path): + exe_path = os.path.join(script_dir, exe_path) + if not os.path.isfile(exe_path): + print(f"[!] Executable not found: {exe_path}") + sys.exit(1) + print(f"[*] Spawning {exe_path} with args {args.args} ...") + pid = device.spawn([exe_path] + shlex.split(args.args), cwd=script_dir) + print(f"[*] PID = {pid}") + session = device.attach(pid) + + script = session.create_script(js_code) + script.on("message", on_msg_wrapper) + script.load() + + if not args.attach: + print("[*] Resuming process ...") + device.resume(pid) + + print("[*] Tracer running. Press Ctrl+C to detach and exit.") + print("[*] Tip: hookGetObject / hookGetVariable are OFF by default (noisy).") + print() + + try: + # keep the script alive + sys.stdin.read() + except KeyboardInterrupt: + pass + + print("\n[*] Detaching ...") + session.detach() + if log_file: + log_file.close() + print("[*] Done.") + + +if __name__ == "__main__": + main()