Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add message listening system #4

Merged
merged 6 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"scripts/"
],
"scripts": {
"prebuild": "rimraf dist/*",
"prebuild": "rimraf dist/",
pimterry marked this conversation as resolved.
Show resolved Hide resolved
"build": "tsc",
"prepack": "npm run build",
"pretest": "npm-run-all --parallel pretest:download-frida build",
Expand Down Expand Up @@ -70,7 +70,7 @@
"typescript": "^5.0.4"
},
"dependencies": {
"@httptoolkit/dbus-native": "^0.1.1",
"@httptoolkit/dbus-native": "^0.1.3",
"@httptoolkit/websocket-stream": "^6.0.1",
"isomorphic-ws": "^4.0.1",
"long": "^4.0.0",
Expand Down
111 changes: 105 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,44 @@ interface AgentSession {
LoadScript(scriptId: [number]): Promise<void>;
}

/**
* A message from a Frida script to the runner.
* https://github.com/frida/frida-core/blob/main/lib/base/session.vala#L124C2-L146C3
* kind is the AgentMessageKind, "1" is a script message. There is also Debugger but no enum number is specified.
* script_id is the script id that sent the message. It is part of the AgentScriptId type.
* text is the message in plain text.
* has_data is a boolean that indicates if there is data attached to the message.
* data is the data attached to the message. It is a byte array.
*/
type AgentMessage = [kind: number, script_id: number[], text: string, has_data: boolean, data: Buffer | null]

/**
* A message sent from a script to the agent.
* https://github.com/frida/frida-node/blob/main/lib/script.ts#L103-L115
*/
export enum MessageType {
Send = "send",
Error = "error"
}

export type Message = ScriptAgentSendMessage | ScriptAgentErrorMessage;
export type ScriptAgentSendMessage = {
type: MessageType.Send,
payload: any
}
export type ScriptAgentErrorMessage = {
type: MessageType.Error;
description: string;
stack?: string;
fileName?: string;
lineNumber?: number;
columnNumber?: number;
}
enum AgentMessageKind {
Script = 1,
Debugger = 2,
}

export class FridaSession {

constructor(
Expand All @@ -92,10 +130,11 @@ export class FridaSession {
.getInterface<HostSession>('/re/frida/HostSession', 're.frida.HostSession16');
}

private getAgentSession(sessionId: string) {
return this.bus
private async getAgentSession(sessionId: string) {
const agentSession = await this.bus
.getService('re.frida.AgentSession16')
.getInterface<AgentSession>('/re/frida/AgentSession/' + sessionId, 're.frida.AgentSession16');
return new FridaAgentSession(this.bus, sessionId, agentSession);
}

/**
Expand Down Expand Up @@ -155,8 +194,13 @@ export class FridaSession {
const [sessionId] = await hostSession.Attach(pid, {});
const agentSession = await this.getAgentSession(sessionId);

const scriptId = await agentSession.CreateScript(fridaScript, {});
await agentSession.LoadScript(scriptId);
const script = await agentSession.createScript(fridaScript, {});
await script.loadScript();

return {
session: agentSession,
script
}
}

/**
Expand Down Expand Up @@ -188,10 +232,65 @@ export class FridaSession {
const [sessionId] = await hostSession.Attach(pid, {});
const agentSession = await this.getAgentSession(sessionId);

const scriptId = await agentSession.CreateScript(fridaScript, {});
await agentSession.LoadScript(scriptId);
const script = await agentSession.createScript(fridaScript, {});
await script.loadScript();

await hostSession.Resume(pid);
return {
pid,
session: agentSession,
script
}
}
}

export class FridaAgentSession {
constructor(
private bus: dbus.DBusClient,
private sessionId: string,
private agentSession: AgentSession
) {}

/**
* This method sets up a message handler for messages sent from the agent.
* @param cb Callback to be called when a message is received from the agent.
*/
onMessage(cb: (message: Message) => void) {
this.bus.setMethodCallHandler(`/re/frida/AgentMessageSink/${this.sessionId}`, "re.frida.AgentMessageSink16", "PostMessages", [(messages: AgentMessage[]) => {
for(const message of messages) {
const msg = JSON.parse(message[2]) as Message;
switch(message[0]) { // message[0] is the message kind
case AgentMessageKind.Script:
cb(msg)
break;
}
}
}, null]);
}

/**
* Create a new Frida script within this agent session.
* @param script The Frida script in plain text to create.
* @param options Options to pass to the script.
*/
async createScript(script: string, options: {}): Promise<FridaScript> {
const [scriptId] = await this.agentSession.CreateScript(script, options);
return new FridaScript(this.bus, this.agentSession, [scriptId]);
}
}

export class FridaScript {
constructor(
private bus: dbus.DBusClient,
private agentSession: AgentSession,
private scriptId: [number],
) {}

/**
* Load the script into the target process.
* @returns Promise that resolves when the script is loaded.
*/
async loadScript() {
return this.agentSession.LoadScript(this.scriptId);
}
}
2 changes: 1 addition & 1 deletion test/run-frida-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { delay } from './test-util';
let fridaServer: ChildProcess | undefined;

export const FRIDA_SERVER_DIR = path.join(__dirname, '.frida-server');
export const FRIDA_SERVER_BIN = path.join(FRIDA_SERVER_DIR, 'frida-server');
export const FRIDA_SERVER_BIN = path.join(FRIDA_SERVER_DIR, `frida-server${process.platform === 'win32' ? '.exe' : ''}`);

const FRIDA_PORT = 27042;

Expand Down
53 changes: 52 additions & 1 deletion test/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { expect } from 'chai';
import { fetch } from 'cross-fetch';

import { delay, isNode } from './test-util';
import { connect, FridaSession } from '../src/index';
import { connect, FridaSession, Message, MessageType, ScriptAgentSendMessage } from '../src/index';

const FIXTURES_BASE = isNode
? path.join(__dirname, 'fixtures')
Expand Down Expand Up @@ -226,6 +226,57 @@ describe("Frida-JS", () => {
expect(exitCode).to.equal(0);
expect(output).to.equal('Hello from injected script!\n');
});

it("can get the send message from agent", async () => {
// Start a demo subprocess to inject into:
const childProc = ChildProc.spawn(
// Fixture that loops until should_continue() returns false (which it never does).
// Source is in fixtures-setup/rust-loop.
path.join(__dirname, 'fixtures', `loop-${process.platform}-${process.arch}${process.platform === 'win32' ? '.exe' : ''}`),
{ stdio: 'pipe' }
);
childProc.unref();
spawnedProc = childProc; // Ensure this is killed after testing

const outputPromise = new Promise<{
exitCode: number | null,
output: string
}>((resolve, reject) => {
let output = '';
childProc.stdout.on('data', (msg) => output += msg.toString());
childProc.stderr.on('data', (msg) => output += msg.toString());
childProc.stdout.pipe(process.stdout);
childProc.stderr.pipe(process.stderr);
childProc.on('close', (exitCode) => resolve({ exitCode, output }));
childProc.on('error', reject);
});

// Wait for the target to start up:
await new Promise((resolve, reject) => {
childProc.on('spawn', resolve);
childProc.on('error', reject);
});

// Inject into it:
const expectedMessage = 'Hello from injected script!';
fridaClient = await connect();
const data = await fridaClient.injectIntoProcess(childProc.pid!, `
setTimeout(() => {
send('${expectedMessage}');
}, 1000);
`);

let message = await new Promise<Message | null>((resolve, reject) => {
data.session.onMessage(resolve);
setTimeout(() => {
reject(new Error('Timed out waiting for message'));
}, 5000);
});
pimterry marked this conversation as resolved.
Show resolved Hide resolved

// Inject into it:
expect(message?.type).to.equal(MessageType.Send);
expect((message as ScriptAgentSendMessage)?.payload).to.equal(expectedMessage);
});
}

})