diff --git a/bloomoo_event_trace.js b/bloomoo_event_trace.js new file mode 100644 index 0000000..616726c --- /dev/null +++ b/bloomoo_event_trace.js @@ -0,0 +1,212 @@ +'use strict'; + +// ============================================================ +// BlooMoo global event tracer -- Frida instrumentation +// Target: bloomoodll.dll loaded by nemo.exe +// +// Usage: +// frida -l bloomoo_event_trace.js -f nemo.exe +// +// Core idea: +// Hook the shared script-event dispatcher instead of individual +// producers like CMC_Animo / CMC_Button / CMC_Keyboard. +// +// Ghidra: +// CMC_Object::onSignal(CXString) 0x10005880 +// CMC_Object::onSignal(CXString, CXArray *) 0x10006983 +// CMC_Scene::onSignal(CXString) 0x10001c71 +// CMC_Object::getName() reads CXString at this+0x10 +// ============================================================ + +var CONFIG = { + moduleName: 'bloomoodll.dll', + maxStringLen: 160, + + // Main global hooks. + hookObjectOnSignal: true, + hookObjectOnSignalWithArgs: true, + + // Scene override calls the base implementation, so it is noisy. + hookSceneOnSignal: false, + + // Optional filters. Empty string means "log everything". + objectFilter: '', + signalFilter: '', + + // Show return value from dispatcher. + showReturnValue: true +}; + +var RVA = { + CMC_Scene_onSignal: 0x1c71, + CMC_Object_onSignal: 0x5880, + CMC_Object_onSignal_withArgs: 0x6983 +}; + +var hooksInstalled = false; + +function log(msg) { + console.log(msg); + send({ type: 'trace', text: msg }); +} + +function trunc(s) { + if (!s) return ''; + if (s.length > CONFIG.maxStringLen) { + return s.substring(0, CONFIG.maxStringLen) + '...'; + } + return s; +} + +function safeAnsi(p, maxLen) { + try { + if (p.isNull()) return ''; + return maxLen > 0 ? p.readAnsiString(maxLen) : p.readAnsiString(); + } catch (_) { + return ''; + } +} + +function readCXStringRaw(p) { + try { + if (p.isNull()) return ''; + var len = p.add(0x04).readS32(); + if (len <= 0) return ''; + var buf = p.add(0x0c).readPointer(); + return safeAnsi(buf, len); + } catch (_) { + return ''; + } +} + +function readCXStringField(base, off) { + try { + if (base.isNull()) return ''; + return readCXStringRaw(base.add(off)); + } catch (_) { + return ''; + } +} + +function readObjectName(objPtr) { + return readCXStringField(objPtr, 0x10); +} + +function shouldLog(objectName, signalName) { + if (CONFIG.objectFilter && objectName.indexOf(CONFIG.objectFilter) === -1) { + return false; + } + if (CONFIG.signalFilter && signalName.indexOf(CONFIG.signalFilter) === -1) { + return false; + } + return true; +} + +function bool01(value) { + return value ? '1' : '0'; +} + +function installHooks(base) { + if (hooksInstalled) return; + hooksInstalled = true; + + function at(rva) { + return base.add(rva); + } + + log('[+] bloomoodll.dll base = ' + base); + + if (CONFIG.hookObjectOnSignal) { + Interceptor.attach(at(RVA.CMC_Object_onSignal), { + onEnter: function (_args) { + this.self = ptr(this.context.ecx); + this.objectName = readObjectName(this.self); + this.signalName = readCXStringRaw(ptr(this.context.esp).add(4)); + this.hit = shouldLog(this.objectName, this.signalName); + if (!this.hit) return; + + log( + '[SIGNAL] ' + + 'object="' + trunc(this.objectName) + '" ' + + 'this=' + this.self + ' ' + + 'event="' + trunc(this.signalName) + '"' + ); + }, + onLeave: function (retval) { + if (!this.hit || !CONFIG.showReturnValue) return; + log( + '[SIGNAL:RET] ' + + 'object="' + trunc(this.objectName) + '" ' + + 'event="' + trunc(this.signalName) + '" ' + + 'handled=' + bool01((retval.toInt32() & 0xff) !== 0) + ); + } + }); + + log('[+] Hooked CMC_Object::onSignal(CXString) @ ' + at(RVA.CMC_Object_onSignal)); + } + + if (CONFIG.hookObjectOnSignalWithArgs) { + Interceptor.attach(at(RVA.CMC_Object_onSignal_withArgs), { + onEnter: function (_args) { + this.self = ptr(this.context.ecx); + this.objectName = readObjectName(this.self); + this.signalName = readCXStringRaw(ptr(this.context.esp).add(4)); + this.argArray = ptr(this.context.esp).add(0x14).readPointer(); + this.hit = shouldLog(this.objectName, this.signalName); + if (!this.hit) return; + + log( + '[SIGNAL+ARGS] ' + + 'object="' + trunc(this.objectName) + '" ' + + 'this=' + this.self + ' ' + + 'event="' + trunc(this.signalName) + '" ' + + 'args=' + this.argArray + ); + }, + onLeave: function (retval) { + if (!this.hit || !CONFIG.showReturnValue) return; + log( + '[SIGNAL+ARGS:RET] ' + + 'object="' + trunc(this.objectName) + '" ' + + 'event="' + trunc(this.signalName) + '" ' + + 'handled=' + bool01((retval.toInt32() & 0xff) !== 0) + ); + } + }); + + log('[+] Hooked CMC_Object::onSignal(CXString,CXArray*) @ ' + at(RVA.CMC_Object_onSignal_withArgs)); + } + + if (CONFIG.hookSceneOnSignal) { + Interceptor.attach(at(RVA.CMC_Scene_onSignal), { + onEnter: function (_args) { + this.self = ptr(this.context.ecx); + this.objectName = readObjectName(this.self); + this.signalName = readCXStringRaw(ptr(this.context.esp).add(4)); + this.hit = shouldLog(this.objectName, this.signalName); + if (!this.hit) return; + + log( + '[SCENE:SIGNAL] ' + + 'object="' + trunc(this.objectName) + '" ' + + 'this=' + this.self + ' ' + + 'event="' + trunc(this.signalName) + '"' + ); + } + }); + + log('[+] Hooked CMC_Scene::onSignal(CXString) @ ' + at(RVA.CMC_Scene_onSignal)); + } +} + +function maybeInstall() { + if (hooksInstalled) return; + + var mod = Process.findModuleByName(CONFIG.moduleName); + if (mod === null) return; + installHooks(mod.base); +} + +setInterval(maybeInstall, 250); +maybeInstall(); diff --git a/frida_launcher.py b/frida_launcher.py index dcb7759..0a3743d 100644 --- a/frida_launcher.py +++ b/frida_launcher.py @@ -7,6 +7,7 @@ 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 + python frida_launcher.py --script bloomoo_event_trace.js Alternatively, use the Frida CLI directly: frida -l bloomoo_trace.js -f nemo.exe @@ -48,6 +49,10 @@ def main(): "--exe", default="nemo.exe", help="Path to nemo.exe (default: nemo.exe in script dir)" ) + parser.add_argument( + "--script", default="bloomoo_trace.js", + help="Frida JS script to load (default: bloomoo_trace.js)" + ) parser.add_argument( "--attach", type=int, default=0, metavar="PID", help="Attach to an already-running process instead of spawning" @@ -67,7 +72,9 @@ def main(): args = parser.parse_args() script_dir = os.path.dirname(os.path.abspath(__file__)) - js_path = os.path.join(script_dir, "bloomoo_trace.js") + js_path = args.script + if not os.path.isabs(js_path): + js_path = os.path.join(script_dir, js_path) if not os.path.isfile(js_path): print(f"[!] Script not found: {js_path}")