// Exercises the real receive hook against mock native memory. Never attaches to
// a process, runs Frida, or invokes a real send function.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const base = 0x400000;
const cells = new Map();
let nextAddress = 0x30000000;
const signatures = new Map(Object.entries({
0x0816FFB0: '6690558bec', 0x001F81DA: '558bec5156', 0x0026D0F0: '568b710485',
0x0A04CDC4: '558beceb0d', 0x026988F0: '8bd10f57c0', 0x015D8B90: '6690558bec',
0x097D11B0: '8b414485c0', 0x096358B0: '6690558bec', 0x001F8530: 'e9c2ffffff',
0x00496C30: '558bec6aff', 0x00496160: '558bec6aff'
}).map(([rva, signature]) => [base + Number(rva), signature]));
class Pointer {
constructor(address) { this.address = Number(address); }
add(offset) { return new Pointer(this.address + Number(offset)); }
sub(other) { return new Pointer(this.address - other.address); }
toUInt32() { return this.address >>> 0; }
equals(other) { return this.address === other.address; }
compare(other) { return Math.sign(this.address - other.address); }
isNull() { return this.address === 0; }
readU32() { return Number(cells.get(this.address) ?? 0); }
readU64() { return BigInt(cells.get(this.address) ?? 0); }
readPointer() { return new Pointer(cells.get(this.address) ?? 0); }
readByteArray() { assert(signatures.has(this.address)); return Buffer.from(signatures.get(this.address), 'hex'); }
readUtf8String() { return cells.get(this.address) ?? ''; }
}
function alloc() { const address = nextAddress; nextAddress += 4096; return new Pointer(address); }
function word(pointer, offset, value) { cells.set(pointer.address + offset, value instanceof Pointer ? value.address : value); }
function string(pointer, offset, value) {
const storage = pointer.add(offset); word(storage, 16, Buffer.byteLength(value));
word(storage, 20, value.length > 15 ? 256 : 15);
if (value.length > 15) { const text = alloc(); word(storage, 0, text); cells.set(text.address, value); }
else cells.set(storage.address, value);
}
const managerControl = alloc(), manager = managerControl.add(12), environment = alloc();
word(managerControl, 0, new Pointer(base + 0x0BA69DC0)); word(managerControl, 4, 1); word(manager, 8, environment);
let notification;
const events = [];
const context = vm.createContext({
Process: { arch: 'ia32', id: 1, mainModule: { name: 'WXWork.exe', base: new Pointer(base) } },
ptr: value => new Pointer(value),
NativeFunction: function (address) {
if (address.address === base + 0x097D11B0) return () => 9000n;
return () => { throw new Error('This receive-only test must never call native send functions.'); };
},
Interceptor: { attach(_address, callbacks) { notification = callbacks.onEnter; return { detach() {} }; } },
send: event => events.push(event), rpc: { exports: {} }, Date, Uint8Array,
});
const source = fs.readFileSync(new URL('../XFEWorkflow/XFEWorkflow.Plugins.WeCom/NativeBridge/agent.js', import.meta.url), 'utf8')
.replace('__PROFILE__', JSON.stringify({ managerControl: managerControl.address, managerObject: manager.address,
baselineIds: [], binding: { GroupId: 'R:legacy', SenderId: '1001' } }));
vm.runInContext(source, context);
function receive(conversation, sender, id, type = 2) {
const message = alloc(), control = alloc(), vector = alloc(), holder = alloc(), holderPointer = alloc(), name = alloc();
word(control, 0, new Pointer(base + 0x0AF984F0)); word(message, 0, 73); word(message, 8, 800);
string(message, 0x10, id); string(message, 0x28, conversation); word(message, 0x48, sender);
word(message, 0x50, type); word(message, 0x58, Math.floor(Date.now() / 1000));
word(vector, 0, message); word(vector, 4, control);
word(holder, 0, new Pointer(base + 0x0B0343D8)); word(holder, 4, vector); word(holder, 8, vector.add(8)); word(holder, 12, vector.add(8));
word(holderPointer, 0, holder); string(name, 0, 'ConversationMessagesDidReceiveNotification');
notification([new Pointer(0), name, holderPointer]);
}
receive('R:other', '2002', 'before-watch');
assert.equal(events.length, 0);
context.rpc.exports.watch(new Date(Date.now() - 1000).toISOString(), ['R:other:cached']);
receive('R:other', '2002', 'before-watch');
receive('R:other', '2002', 'cached');
receive('R:other', '2002', 'same-id', 2);
receive('R:another', '1002', 'same-id', 4);
receive('2002', '3003', 'direct', 99);
receive('R:other', '2002', 'same-id');
receive('R:other', '9000', 'self');
receive('R:other', '0', 'invalid-sender');
assert.deepEqual(events.map(x => [x.conversationId, x.senderId, x.rawType]), [
['R:other', '2002', 2], ['R:another', '1002', 4], ['2002', '3003', 99]
]);
context.rpc.exports.unwatch(); receive('R:other', '2002', 'after-unwatch');
assert.equal(events.length, 3);
console.log('PASS: native hook receives all conversations, senders and types; keeps session activation, baseline, self and duplicate guards.');
// Exercises the real receive hook against mock native memory. Never attaches to
// a process, runs Frida, or invokes a real send function.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const base = 0x400000;
const cells = new Map();
let nextAddress = 0x30000000;
const signatures = new Map(Object.entries({
0x0816FFB0: '6690558bec', 0x001F81DA: '558bec5156', 0x0026D0F0: '568b710485',
0x0A04CDC4: '558beceb0d', 0x026988F0: '8bd10f57c0', 0x015D8B90: '6690558bec',
0x097D11B0: '8b414485c0', 0x096358B0: '6690558bec', 0x001F8530: 'e9c2ffffff',
0x00496C30: '558bec6aff', 0x00496160: '558bec6aff'
}).map(([rva, signature]) => [base + Number(rva), signature]));
class Pointer {
constructor(address) { this.address = Number(address); }
add(offset) { return new Pointer(this.address + Number(offset)); }
sub(other) { return new Pointer(this.address - other.address); }
toUInt32() { return this.address >>> 0; }
equals(other) { return this.address === other.address; }
compare(other) { return Math.sign(this.address - other.address); }
isNull() { return this.address === 0; }
readU32() { return Number(cells.get(this.address) ?? 0); }
readU64() { return BigInt(cells.get(this.address) ?? 0); }
readPointer() { return new Pointer(cells.get(this.address) ?? 0); }
readByteArray() { assert(signatures.has(this.address)); return Buffer.from(signatures.get(this.address), 'hex'); }
readUtf8String() { return cells.get(this.address) ?? ''; }
}
function alloc() { const address = nextAddress; nextAddress += 4096; return new Pointer(address); }
function word(pointer, offset, value) { cells.set(pointer.address + offset, value instanceof Pointer ? value.address : value); }
function string(pointer, offset, value) {
const storage = pointer.add(offset); word(storage, 16, Buffer.byteLength(value));
word(storage, 20, value.length > 15 ? 256 : 15);
if (value.length > 15) { const text = alloc(); word(storage, 0, text); cells.set(text.address, value); }
else cells.set(storage.address, value);
}
const managerControl = alloc(), manager = managerControl.add(12), environment = alloc();
word(managerControl, 0, new Pointer(base + 0x0BA69DC0)); word(managerControl, 4, 1); word(manager, 8, environment);
let notification;
const events = [];
const context = vm.createContext({
Process: { arch: 'ia32', id: 1, mainModule: { name: 'WXWork.exe', base: new Pointer(base) } },
ptr: value => new Pointer(value),
NativeFunction: function (address) {
if (address.address === base + 0x097D11B0) return () => 9000n;
return () => { throw new Error('This receive-only test must never call native send functions.'); };
},
Interceptor: { attach(_address, callbacks) { notification = callbacks.onEnter; return { detach() {} }; } },
send: event => events.push(event), rpc: { exports: {} }, Date, Uint8Array,
});
const source = fs.readFileSync(new URL('../XFEWorkflow/XFEWorkflow.Plugins.WeCom/NativeBridge/agent.js', import.meta.url), 'utf8')
.replace('__PROFILE__', JSON.stringify({ managerControl: managerControl.address, managerObject: manager.address,
baselineIds: [], binding: { GroupId: 'R:legacy', SenderId: '1001' } }));
vm.runInContext(source, context);
function receive(conversation, sender, id, type = 2) {
const message = alloc(), control = alloc(), vector = alloc(), holder = alloc(), holderPointer = alloc(), name = alloc();
word(control, 0, new Pointer(base + 0x0AF984F0)); word(message, 0, 73); word(message, 8, 800);
string(message, 0x10, id); string(message, 0x28, conversation); word(message, 0x48, sender);
word(message, 0x50, type); word(message, 0x58, Math.floor(Date.now() / 1000));
word(vector, 0, message); word(vector, 4, control);
word(holder, 0, new Pointer(base + 0x0B0343D8)); word(holder, 4, vector); word(holder, 8, vector.add(8)); word(holder, 12, vector.add(8));
word(holderPointer, 0, holder); string(name, 0, 'ConversationMessagesDidReceiveNotification');
notification([new Pointer(0), name, holderPointer]);
}
receive('R:other', '2002', 'before-watch');
assert.equal(events.length, 0);
context.rpc.exports.watch(new Date(Date.now() - 1000).toISOString(), ['R:other:cached']);
receive('R:other', '2002', 'before-watch');
receive('R:other', '2002', 'cached');
receive('R:other', '2002', 'same-id', 2);
receive('R:another', '1002', 'same-id', 4);
receive('2002', '3003', 'direct', 99);
receive('R:other', '2002', 'same-id');
receive('R:other', '9000', 'self');
receive('R:other', '0', 'invalid-sender');
assert.deepEqual(events.map(x => [x.conversationId, x.senderId, x.rawType]), [
['R:other', '2002', 2], ['R:another', '1002', 4], ['2002', '3003', 99]
]);
context.rpc.exports.unwatch(); receive('R:other', '2002', 'after-unwatch');
assert.equal(events.length, 3);
console.log('PASS: native hook receives all conversations, senders and types; keeps session activation, baseline, self and duplicate guards.');