Added Frida script for debugging parser

This commit is contained in:
2026-03-31 08:22:18 +00:00
parent 5979c718fe
commit 55436fd562
2 changed files with 703 additions and 0 deletions

558
bloomoo_trace.js Normal file
View 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
View 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()