'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(); } } }); }