From 2343ec8ffcd1084bafd52dde0c930f1e29d3eac0 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 20 Aug 2024 19:27:24 +0100 Subject: [PATCH 1/5] start implementing the debugging step command on arm64 --- package-lock.json | 17 ++-- package.json | 2 +- src/agent/index.ts | 1 + src/agent/lib/debug/index.ts | 157 +++++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba26ead1..a33c678d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "LGPL-3.0", "devDependencies": { "@frida/events": "^4.0.4", - "@types/frida-gum": "^18.5.1", + "@types/frida-gum": "^18.7.0", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", @@ -465,10 +465,11 @@ } }, "node_modules/@types/frida-gum": { - "version": "18.5.1", - "resolved": "https://registry.npmjs.org/@types/frida-gum/-/frida-gum-18.5.1.tgz", - "integrity": "sha512-99geyCbWB+YBCqxcO+ue7dJUQJti7kQ5CHGQtKoz0ENtRswKULGMFKW6QgL657sMiztqhcDHWJjYSPv5GKT1ig==", - "dev": true + "version": "18.7.0", + "resolved": "https://registry.npmjs.org/@types/frida-gum/-/frida-gum-18.7.0.tgz", + "integrity": "sha512-HhBomXE23fLDAWXEKi3BjJLrlH9wAv9IEQNfO/PaYHQNNbh0Bi06gx6JbXTspVpbqlbVqkWAuU7n6HaS9B6yXA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.11", @@ -2418,9 +2419,9 @@ } }, "@types/frida-gum": { - "version": "18.5.1", - "resolved": "https://registry.npmjs.org/@types/frida-gum/-/frida-gum-18.5.1.tgz", - "integrity": "sha512-99geyCbWB+YBCqxcO+ue7dJUQJti7kQ5CHGQtKoz0ENtRswKULGMFKW6QgL657sMiztqhcDHWJjYSPv5GKT1ig==", + "version": "18.7.0", + "resolved": "https://registry.npmjs.org/@types/frida-gum/-/frida-gum-18.7.0.tgz", + "integrity": "sha512-HhBomXE23fLDAWXEKi3BjJLrlH9wAv9IEQNfO/PaYHQNNbh0Bi06gx6JbXTspVpbqlbVqkWAuU7n6HaS9B6yXA==", "dev": true }, "@types/json-schema": { diff --git a/package.json b/package.json index 04f52b2a..45f807f5 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@frida/events": "^4.0.4", - "@types/frida-gum": "^18.5.1", + "@types/frida-gum": "^18.7.0", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", diff --git a/src/agent/index.ts b/src/agent/index.ts index fb8c119d..bc81affe 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -70,6 +70,7 @@ const commandHandlers = { 'e*': [config.evalConfigR2, 'display eval config vars in r2 format'], 'e/': [config.evalConfigSearch, 'eval config search (?)'], db: [debug.breakpointNative, 'list or add a native breakpoint', '[addr]'], + ds: [debug.breakpointStep, 'step to next instruction'], dbj: debug.breakpointJson, dbc: [debug.breakpointNativeCommand, 'associate an r2 command when the native breakpoint is hit', '[addr] [cmd]'], 'db-': [debug.breakpointUnset, 'unset the native breakpoint in the given address', '[addr]'], diff --git a/src/agent/lib/debug/index.ts b/src/agent/lib/debug/index.ts index d572d221..1cfd906f 100644 --- a/src/agent/lib/debug/index.ts +++ b/src/agent/lib/debug/index.ts @@ -175,6 +175,163 @@ export function breakpointNative(args: string[]) : string { return ""; } +// Required to access the register in a map like fashion... +interface Arm64CpuContextAccessible extends Arm64CpuContext { + [key: string]: any; +} + +function handleBranchOrCall(operands: string[]) { + // For b and bl, the destination is in the first operand. + const targetAddress = operands[0]; + console.log(`Branch or call to: ${targetAddress}`); + return targetAddress; +} + +function handleBranchToRegister(context: Arm64CpuContext, operands: string[]) { + // For br, the destination is to the address held by the register name, denoted in the first operand. + const registerName = operands[0]; + const registerValue = (context as Arm64CpuContextAccessible)[registerName]; + console.log(`Branch to reg: ${registerName}, i.e. ${registerValue}`); + return registerValue; +} + +function handleConditionalBranch(context: Arm64CpuContext, instruction: Instruction, operands: string[]) { + const parts = instruction.mnemonic.split(".") + if (!parts.length) return null + + const condition = parts[1]; + const targetAddress = operands[1]; + + // Evaluate the condition to determine if the branch will be taken + if (evaluateCondition(context, condition)) { + return targetAddress; + } + + return instruction.next; +} + +function handleTestAndBranch(context: Arm64CpuContext, instruction: Instruction, operands: string[]) { + if (operands.length < 3) { + return null; + } + + const bitPosition = parseInt(operands[1]); + const targetAddress = operands[2]; + const registerName = operands[0]; + const registerValue = (context as Arm64CpuContextAccessible)[registerName]; + + const mnemonic = instruction.mnemonic; + + const bitIsZero = (registerValue & (1 << bitPosition)) === 0; + + if ((mnemonic === "tbz" && bitIsZero) || (mnemonic === "tbnz" && !bitIsZero)) { + return targetAddress; + } else { + return instruction.next; + } +} + +function evaluateCondition(context: Arm64CpuContext, condition: string): boolean { + const nzcv = context.nzcv; + const n = (nzcv & 0x80000000) != 0; + const z = (nzcv & 0x40000000) != 0; + const c = (nzcv & 0x20000000) != 0; + const v = (nzcv & 0x10000000) != 0; + + console.log(`Evaluating condition: ${condition}, nzcv: ${nzcv}`); + + switch (condition) { + case "eq": return z; // Equal + case "ne": return !z; // Not equal + case "hs": return c; // Unsigned higher or same (carry set) + case "lo": return !c; // Unsigned lower (carry clear) + case "mi": return n; // Minus (negative) + case "pl": return !n; // Plus (positive or zero) + case "vs": return v; // Overflow + case "vc": return !v; // No overflow + case "hi": return c && !z; // Unsigned higher + case "ls": return !c || z; // Unsigned lower or same + case "ge": return n == v; // Signed greater than or equal + case "lt": return n != v; // Signed less than + case "gt": return !z && (n == v); // Signed greater than + case "le": return z || (n != v); // Signed less than or equal + default: return false; + } +} + +export function breakpointStep() { + const isArm64 = Process.arch === "arm64"; + if (!isArm64) { + console.log("Step is currently only implemented for arm64"); + return; + } + + if (!currentThreadContext) { + console.log("There is currently CPUContext set. Please ensure you have hit a breakpoint already, otherwise file a bug..."); + return; + } + + const arm64Context = currentThreadContext as Arm64CpuContext; + + const pc = currentThreadContext.pc; + const currentInstruction = Instruction.parse(pc); + const { mnemonic, next, opStr } = currentInstruction; + + const operands = opStr.split(/,(?![^\[]*\])/).map(operand => operand.trim()); + + /* + Attempt to evaluate the branches, tests, etc. + Possibly incomplete, but should be the majority of use-cases. + */ + let nextAddress = null; + switch (mnemonic) { + case "b": // Unconditional branch + case "bl": // Branch with link (function call) + nextAddress = handleBranchOrCall(operands); + break; + + case "br": // Branch to value in register + nextAddress = handleBranchToRegister(arm64Context, operands); + break; + + case "ret": // Return from subroutine + nextAddress = arm64Context.lr; + break; + + case "cbz": // Compare and branch on zero + case "cbnz": // Compare and branch on non-zero + case "b.eq": // Conditional branch examples (there are several) + case "b.ne": + case "b.hs": + case "b.lo": + nextAddress = handleConditionalBranch(arm64Context, currentInstruction, operands); + break; + + case "tbz": // Test and branch on zero + case "tbnz": // Test and branch on non-zero + handleTestAndBranch(arm64Context, currentInstruction, operands); + break; + + default: // Default to just instruction.next + nextAddress = next; + break; + } + + if (nextAddress) { + const target = nextAddress.toString(); + console.log(`Stepping to ${target}`); + _breakpointSet([target]); + + console.log("Attempting to continue (:dc)"); + breakpointContinue([]); + + console.log(`Removing ephemeral bp @ ${target}`); + breakpointUnset([target]); + } else { + console.log("Couldn't figure out the next address..."); + } +} + export function breakpointJson() { const json: any = {}; for (const [address, bp] of newBreakpoints.entries()) { From 10452dff19a23036630e8679f174583bfb9fbdb5 Mon Sep 17 00:00:00 2001 From: Grant Douglas Date: Wed, 21 Aug 2024 18:03:14 +0100 Subject: [PATCH 2/5] small fixes --- src/agent/lib/debug/index.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/agent/lib/debug/index.ts b/src/agent/lib/debug/index.ts index 1cfd906f..c2f7a21f 100644 --- a/src/agent/lib/debug/index.ts +++ b/src/agent/lib/debug/index.ts @@ -183,7 +183,7 @@ interface Arm64CpuContextAccessible extends Arm64CpuContext { function handleBranchOrCall(operands: string[]) { // For b and bl, the destination is in the first operand. const targetAddress = operands[0]; - console.log(`Branch or call to: ${targetAddress}`); + console.log(`Branch or call to: ${targetAddress}\n`); return targetAddress; } @@ -191,7 +191,7 @@ function handleBranchToRegister(context: Arm64CpuContext, operands: string[]) { // For br, the destination is to the address held by the register name, denoted in the first operand. const registerName = operands[0]; const registerValue = (context as Arm64CpuContextAccessible)[registerName]; - console.log(`Branch to reg: ${registerName}, i.e. ${registerValue}`); + console.log(`Branch to reg: ${registerName}, i.e. ${registerValue}\n`); return registerValue; } @@ -238,7 +238,7 @@ function evaluateCondition(context: Arm64CpuContext, condition: string): boolean const c = (nzcv & 0x20000000) != 0; const v = (nzcv & 0x10000000) != 0; - console.log(`Evaluating condition: ${condition}, nzcv: ${nzcv}`); + console.log(`Evaluating condition: ${condition}, nzcv: ${nzcv}\n`); switch (condition) { case "eq": return z; // Equal @@ -267,18 +267,24 @@ export function breakpointStep() { } if (!currentThreadContext) { - console.log("There is currently CPUContext set. Please ensure you have hit a breakpoint already, otherwise file a bug..."); + console.log("There is currently no CPUContext set. Please ensure you have hit a breakpoint already, otherwise file a bug..."); return; } const arm64Context = currentThreadContext as Arm64CpuContext; - const pc = currentThreadContext.pc; + + // We need to unpatch pc, to be able to parse the instruction... + breakpointUnset([pc.toString()]); + const currentInstruction = Instruction.parse(pc); const { mnemonic, next, opStr } = currentInstruction; const operands = opStr.split(/,(?![^\[]*\])/).map(operand => operand.trim()); + // Re-set the breakpoint at PC since we removed it temporarily: + _breakpointSet([pc.toString()]); + /* Attempt to evaluate the branches, tests, etc. Possibly incomplete, but should be the majority of use-cases. @@ -319,16 +325,13 @@ export function breakpointStep() { if (nextAddress) { const target = nextAddress.toString(); - console.log(`Stepping to ${target}`); + console.log(`Stepping to ${target}\n`); _breakpointSet([target]); - console.log("Attempting to continue (:dc)"); + console.log("Attempting to continue (:dc)\n"); breakpointContinue([]); - - console.log(`Removing ephemeral bp @ ${target}`); - breakpointUnset([target]); } else { - console.log("Couldn't figure out the next address..."); + console.log("Couldn't figure out the next address...\n"); } } From de68e37db98873e9df6acc4a1d873af6e48dbfac Mon Sep 17 00:00:00 2001 From: Grant Douglas Date: Wed, 21 Aug 2024 18:08:39 +0100 Subject: [PATCH 3/5] attempt to unblock the client after stepping --- src/agent/lib/debug/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/agent/lib/debug/index.ts b/src/agent/lib/debug/index.ts index c2f7a21f..02b59327 100644 --- a/src/agent/lib/debug/index.ts +++ b/src/agent/lib/debug/index.ts @@ -327,12 +327,15 @@ export function breakpointStep() { const target = nextAddress.toString(); console.log(`Stepping to ${target}\n`); _breakpointSet([target]); - - console.log("Attempting to continue (:dc)\n"); - breakpointContinue([]); + setTimeout(() => { + breakpointUnset([target]); + }, 1000); } else { console.log("Couldn't figure out the next address...\n"); } + + console.log("Attempting to continue (:dc)\n"); + breakpointContinue([]); } export function breakpointJson() { From 45036d2bc26328e4aba8d78dad3ae9a1c8d38736 Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 25 Aug 2024 20:44:11 +0100 Subject: [PATCH 4/5] fix bug where it would hang. Though it no longer auto-continues... --- src/agent/lib/debug/index.ts | 48 +++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/agent/lib/debug/index.ts b/src/agent/lib/debug/index.ts index 02b59327..f79dfa2f 100644 --- a/src/agent/lib/debug/index.ts +++ b/src/agent/lib/debug/index.ts @@ -4,6 +4,7 @@ import sys from '../sys.js'; import { autoType, getPtr, padPointer, byteArrayToHex } from '../utils.js'; const newBreakpoints = new Map(); +let ephemeralBreakpoints: string[] = []; let suspended = false; let currentThreadContext: CpuContext | null = null; @@ -76,10 +77,16 @@ export function setSuspended(v: boolean): void { /* breakpoint handler */ Process.setExceptionHandler(({ address, context }) => { - const bp = newBreakpoints.get(address.toString()); + const addressStr = address.toString(); + const bp = newBreakpoints.get(addressStr); if (!bp) { return false; } + if (ephemeralBreakpoints.includes(addressStr)) { + // If this is from a step, we should now remove the breakpoint automatically. + ephemeralBreakpoints = ephemeralBreakpoints.filter(element => element != addressStr); + breakpointUnset([addressStr]); + } const index = bp.patches.findIndex((p: any) => p.address.equals(address)); if (index === 0) { send({ name: 'breakpoint-event', stanza: { cmd: bp.cmd } }); @@ -182,8 +189,11 @@ interface Arm64CpuContextAccessible extends Arm64CpuContext { function handleBranchOrCall(operands: string[]) { // For b and bl, the destination is in the first operand. - const targetAddress = operands[0]; - console.log(`Branch or call to: ${targetAddress}\n`); + let targetAddress = operands[0]; + if (targetAddress.startsWith("#0x")) { + targetAddress = targetAddress.slice(1); + } + // console.log(`Branch or call to: ${targetAddress}\n`); return targetAddress; } @@ -191,7 +201,7 @@ function handleBranchToRegister(context: Arm64CpuContext, operands: string[]) { // For br, the destination is to the address held by the register name, denoted in the first operand. const registerName = operands[0]; const registerValue = (context as Arm64CpuContextAccessible)[registerName]; - console.log(`Branch to reg: ${registerName}, i.e. ${registerValue}\n`); + // console.log(`Branch to reg: ${registerName}, i.e. ${registerValue}\n`); return registerValue; } @@ -238,7 +248,7 @@ function evaluateCondition(context: Arm64CpuContext, condition: string): boolean const c = (nzcv & 0x20000000) != 0; const v = (nzcv & 0x10000000) != 0; - console.log(`Evaluating condition: ${condition}, nzcv: ${nzcv}\n`); + // console.log(`Evaluating condition: ${condition}, nzcv: ${nzcv}\n`); switch (condition) { case "eq": return z; // Equal @@ -266,11 +276,10 @@ export function breakpointStep() { return; } - if (!currentThreadContext) { + if (currentThreadContext === null) { console.log("There is currently no CPUContext set. Please ensure you have hit a breakpoint already, otherwise file a bug..."); return; } - const arm64Context = currentThreadContext as Arm64CpuContext; const pc = currentThreadContext.pc; @@ -280,11 +289,7 @@ export function breakpointStep() { const currentInstruction = Instruction.parse(pc); const { mnemonic, next, opStr } = currentInstruction; - const operands = opStr.split(/,(?![^\[]*\])/).map(operand => operand.trim()); - - // Re-set the breakpoint at PC since we removed it temporarily: - _breakpointSet([pc.toString()]); - + let operands: string[] = opStr.split(/,(?![^\[]*\])/).map(operand => operand.trim()); /* Attempt to evaluate the branches, tests, etc. Possibly incomplete, but should be the majority of use-cases. @@ -315,7 +320,7 @@ export function breakpointStep() { case "tbz": // Test and branch on zero case "tbnz": // Test and branch on non-zero - handleTestAndBranch(arm64Context, currentInstruction, operands); + nextAddress = handleTestAndBranch(arm64Context, currentInstruction, operands); break; default: // Default to just instruction.next @@ -325,17 +330,11 @@ export function breakpointStep() { if (nextAddress) { const target = nextAddress.toString(); - console.log(`Stepping to ${target}\n`); - _breakpointSet([target]); - setTimeout(() => { - breakpointUnset([target]); - }, 1000); + console.log(`Set a breakpoint @ ${target}. Use :dc to continue...\n`); + _breakpointSet([target], true); } else { - console.log("Couldn't figure out the next address...\n"); + console.log("Couldn't figure out where to step to...\n"); } - - console.log("Attempting to continue (:dc)\n"); - breakpointContinue([]); } export function breakpointJson() { @@ -425,7 +424,7 @@ function _breakpointList(args: string[]) : string { return bps.join("\n"); } -function _breakpointSet(args: string[]) : string { +function _breakpointSet(args: string[], ephemeral: boolean = false) : string { const address = args[0]; if (address.startsWith("java:")) { return "Breakpoints only work on native code"; @@ -438,6 +437,9 @@ function _breakpointSet(args: string[]) : string { }; newBreakpoints.set(p1.address.toString(), bp); newBreakpoints.set(p2.address.toString(), bp); + if (ephemeral === true) { + ephemeralBreakpoints.push(address); + } p1.toggle(); return ""; } From 5f330c99c90394e98bcd17142d03f832457c3d86 Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 25 Aug 2024 20:54:05 +0100 Subject: [PATCH 5/5] fix a bug which prevented stepping more than once --- src/agent/lib/debug/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/agent/lib/debug/index.ts b/src/agent/lib/debug/index.ts index f79dfa2f..f1057e36 100644 --- a/src/agent/lib/debug/index.ts +++ b/src/agent/lib/debug/index.ts @@ -284,7 +284,9 @@ export function breakpointStep() { const pc = currentThreadContext.pc; // We need to unpatch pc, to be able to parse the instruction... - breakpointUnset([pc.toString()]); + if (newBreakpoints.has(pc.toString())) { + breakpointUnset([pc.toString()]); + } const currentInstruction = Instruction.parse(pc); const { mnemonic, next, opStr } = currentInstruction;