Added Frida script for events listening

This commit is contained in:
Patryk Gensch
2026-04-02 15:16:48 +02:00
parent 55436fd562
commit 71b4f3b4a3
2 changed files with 220 additions and 1 deletions

212
bloomoo_event_trace.js Normal file
View File

@@ -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 '<read-err>';
}
}
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 '<bad-cxstring>';
}
}
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();

View File

@@ -7,6 +7,7 @@ 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
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}")