Added Frida script for events listening
This commit is contained in:
212
bloomoo_event_trace.js
Normal file
212
bloomoo_event_trace.js
Normal 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();
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user