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