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 # spawn nemo.exe
|
||||||
python frida_launcher.py --attach <PID> # attach to running process
|
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 --exe path/nemo.exe # custom exe path
|
||||||
|
python frida_launcher.py --script bloomoo_event_trace.js
|
||||||
|
|
||||||
Alternatively, use the Frida CLI directly:
|
Alternatively, use the Frida CLI directly:
|
||||||
frida -l bloomoo_trace.js -f nemo.exe
|
frida -l bloomoo_trace.js -f nemo.exe
|
||||||
@@ -48,6 +49,10 @@ def main():
|
|||||||
"--exe", default="nemo.exe",
|
"--exe", default="nemo.exe",
|
||||||
help="Path to nemo.exe (default: nemo.exe in script dir)"
|
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(
|
parser.add_argument(
|
||||||
"--attach", type=int, default=0, metavar="PID",
|
"--attach", type=int, default=0, metavar="PID",
|
||||||
help="Attach to an already-running process instead of spawning"
|
help="Attach to an already-running process instead of spawning"
|
||||||
@@ -67,7 +72,9 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
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):
|
if not os.path.isfile(js_path):
|
||||||
print(f"[!] Script not found: {js_path}")
|
print(f"[!] Script not found: {js_path}")
|
||||||
|
|||||||
Reference in New Issue
Block a user