Added Frida script for debugging parser
This commit is contained in:
558
bloomoo_trace.js
Normal file
558
bloomoo_trace.js
Normal file
@@ -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 '<null>';
|
||||
return maxLen > 0 ? p.readAnsiString(maxLen) : p.readAnsiString();
|
||||
} catch (_) { return '<read-err>'; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 '<err>'; }
|
||||
}
|
||||
|
||||
/** 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() + ' => <field read error: ' + e.message + '>');
|
||||
}
|
||||
}
|
||||
});
|
||||
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) || '?')
|
||||
: '<null>';
|
||||
var rhs = !rhsE.isNull()
|
||||
? (readCXStr(rhsE, 0xa8) || readCXStr(rhsE, 0x88) || '?')
|
||||
: '<null>';
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
145
frida_launcher.py
Normal file
145
frida_launcher.py
Normal file
@@ -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 <PID> # 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()
|
||||
Reference in New Issue
Block a user