Files
BlooMooExperiments/bloomoo_event_trace.js
2026-04-02 15:16:48 +02:00

213 lines
6.4 KiB
JavaScript

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