diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8989ee --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# frida-stack + +Small frida module for ensuring you get the stack information you wanted. + +## What? + +Often when using (Frida)[https://github.com/frida/frida], I would run into issues +with wanting specific stack traces. Then I realized I didn't have a specific context +window, or the stack traces didn't contain the correct shared libraries in them. This +resulted in me re-writing the same functions all the time. + +In other instances, mostly when reverse engineering heavily obfuscated or packed code, +I would have discovered functions or places in memory which had been created without any +exports available. This would lead to questions like, what process/library owned this? Where +am I inside those libraries? + +To answer the above questions, I wrapped some of the standard `Thread.Backtrace` functions +and added some scanning of the `Process` memory ranges. + +## Installing + +```sh +$ npm install frida-extended-stack +``` + +## Usage + +```typescript + +import { Stack } from 'frida-extended-stack' + +function hook_exit() { + const _exitPtr = Module.findExportByName('libc.so', '_exit'); + + if (_exitPtr) { + const _exit = new NativeFunction(_exitPtr, 'int', ['int']); + + Interceptor.replace( + _exitPtr, + new NativeCallback( + function (status) { + console.log(`[+] _exit : ${status} from ${Stack.getModuleInfo(this.context.pc)}`); + console.log(Stack.native(this.context) + return _exit(status); + }, + 'int', + ['int'], + ), + ); + } +} +``` + +Output: +``` +[Pixel 4::com.example.package ]-> [+] _exit : 0 from 0x7713d25000 libexamplesharedlib.so:0x1aae8 +0x7713d25000 libexamplesharedlib.so:0x1aae8 +``` + +Now you have a library and the exact offset into the library for reversing. + + +## License + +``` +Copyright 2020-2024 Tim 'diff' Strazzere + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` \ No newline at end of file diff --git a/lib/index.ts b/lib/index.ts index 8a0d92b..d1c2e7c 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,44 +1,129 @@ -const THUMB_HOOK_REDIRECT_SIZE = 8; -const THUMB_BIT_REMOVAL_MASK = ptr(1).not(); +/** + * Copyright (C) 2024 Red Naga, LLC - Tim Strazzere + * + * Helper class for getting stack traces and backtraces while debugging with + * Frida, primarily used for Android. + * + */ +export class Stack { + private threadObj!: Java.Wrapper; -const trampolines: NativePointer[] = []; -const replacements: NativePointer[] = []; + constructor() { + if (!Java.available) { + throw new Error(`Unable to initialize a Java stacktrace object when Java is unavailable`); + } -export function makeTrampoline(target: NativePointer): NativePointer { - const targetAddress = target.and(THUMB_BIT_REMOVAL_MASK); - const trampoline = Memory.alloc(Process.pageSize); + Java.perform(() => { + const ThreadDef = Java.use('java.lang.Thread'); + this.threadObj = ThreadDef.$new(); + }); + } - Memory.patchCode(trampoline, 128, code => { - const writer = new ThumbWriter(code, { pc: trampoline }); - const relocator = new ThumbRelocator(targetAddress, writer); + /** + * @returns {string} a java stack trace of where this was called + */ + java(): string { + if (!this.threadObj) { + throw new Error(`No java stack available as no thread object available`); + } + let stackString = ''; + this.threadObj + .currentThread() + .getStackTrace() + .map((stackLayer: string, index: number) => { + // Ignore our own creations on the stack (getStackStrace/getThreadStackTrace) + if (index > 1) { + stackString = stackString.concat(`${index - 1} => ${stackLayer.toString()}\n`); + } + }); - let n: number; - do { - n = relocator.readOne(); - } while (n < THUMB_HOOK_REDIRECT_SIZE); + return stackString; + } - relocator.writeAll(); + /** + * @param context in which to get a native backtrace + * @returns string of backtrace + */ + static native(context: CpuContext) { + return ( + Thread.backtrace(context, Backtracer.ACCURATE) + .map(this.getModuleInfo) + .join('\n') + '\n' + ); + } - if (!relocator.eoi) { - writer.putLdrRegAddress("pc", target.add(n)); - } + /** + * Return a decorated string, similar to DebugSymbol.fromAddress + * and Process.getModuleFromAddress, however if those fail we + * will forcefully look up the address association via the mappings. + * + * For some reason, DebugSymbol.fromAddress doesn't always work, + * nor does Process.getModuleFromAddress, so utilize enumerating the + * addresses manually to figure out what the module is and the local + * offset inside it. + * + * @param address Address to look up details for. + * @returns string of relevant data `0x7713d25000 libsharedlib.so:0x1aae8` + */ + static getModuleInfo(address: NativePointer) { + const debugSymbol = DebugSymbol.fromAddress(address); + + if (debugSymbol.moduleName) { + // Add local offset? + return debugSymbol.toString(); + } + + // When hooking we might get something interesting like the following; + // [ + // { + // "base": "0x76fa7000", <==== [anon:dalvik-free list large object space] + // "protection": "rw-", we don't actually care about this + // "size": 536870912 + // }, + // { + // "base": "0x771e939000", <==== this isn't the actual base, we need to refind that + // "file": { + // "offset": 663552, + // "path": "/apex/com.android.runtime/lib64/bionic/libc.so", + // "size": 0 + // }, + // "protection": "rwx", + // "size": 4096 + // } + // ] - writer.flush(); - }); + const builtSymbol = { + base: ptr(0x0), + moduleName: '', + path: '', + size: 0, + }; - trampolines.push(trampoline); + let ranges = Process.enumerateRanges('').filter( + (range) => range.base <= address && range.base.add(range.size) >= address, + ); - return trampoline.or(1); -} + ranges.forEach((range) => { + if (range.file) { + builtSymbol.path = range.file.path; + const moduleNameChunks = range.file.path.split('/'); + builtSymbol.moduleName = moduleNameChunks[moduleNameChunks.length - 1]; -export function replace(target: NativePointer, replacement: NativePointer): void { - const targetAddress = target.and(THUMB_BIT_REMOVAL_MASK); + builtSymbol.base = range.base.sub(range.file.offset); + } + }); + + ranges = Process.enumerateRanges('').filter( + (range) => range.base <= builtSymbol.base && range.base.add(range.size) >= builtSymbol.base, + ); - Memory.patchCode(targetAddress, 128, code => { - const writer = new ThumbWriter(code, { pc: targetAddress }); - writer.putLdrRegAddress("pc", replacement); - writer.flush(); - }); + ranges.forEach((range) => { + if (builtSymbol.base === ptr(0x0) || builtSymbol.base < range.base) { + builtSymbol.base = range.base; + } + builtSymbol.size += range.size; + }); - replacements.push(replacement); -} \ No newline at end of file + return `${builtSymbol.base} ${builtSymbol.moduleName}:${address.sub(builtSymbol.base)}`; + } + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b0da224..7260490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "frida-module-example", + "name": "frida-stacks", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frida-module-example", + "name": "frida-stacks", "version": "1.0.0", "devDependencies": { "@types/frida-gum": "^18.5.1", @@ -14,24 +14,24 @@ } }, "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==", + "version": "18.7.0", + "resolved": "https://registry.npmjs.org/@types/frida-gum/-/frida-gum-18.7.0.tgz", + "integrity": "sha512-HhBomXE23fLDAWXEKi3BjJLrlH9wAv9IEQNfO/PaYHQNNbh0Bi06gx6JbXTspVpbqlbVqkWAuU7n6HaS9B6yXA==", "dev": true }, "node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "version": "18.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", + "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 84e1268..bef5354 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "frida-module-example", + "name": "frida-stack", "version": "1.0.0", - "description": "Example Frida module written in TypeScript", + "description": "Getting better stacks and backtraces in Frida", "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [