diff options
Diffstat (limited to 'frida_mode/Scripting.md')
-rw-r--r-- | frida_mode/Scripting.md | 828 |
1 files changed, 719 insertions, 109 deletions
diff --git a/frida_mode/Scripting.md b/frida_mode/Scripting.md index 8b961e18..4c6fe6b2 100644 --- a/frida_mode/Scripting.md +++ b/frida_mode/Scripting.md @@ -99,142 +99,752 @@ const address = module.base.add(0xdeadface); Afl.setPersistentAddress(address); ``` -# API +# Persisent Hook +A persistent hook can be implemented using a conventional shared object, sample +source code for a hook suitable for the prototype of `LLVMFuzzerTestOneInput` +can be found [here](hook/hook.c). This can be configured using code similar to +the following. + ```js -/* - * Print a message to the STDOUT. This should be preferred to - * FRIDA's `console.log` since FRIDA will queue it's log messages. - * If `console.log` is used in a callback in particular, then there - * may no longer be a thread running to service this queue. - */ -Afl.print(msg); +const path = Afl.module.path; +const dir = path.substring(0, path.lastIndexOf("/")); +const mod = Module.load(`${dir}/frida_mode/build/hook.so`); +const hook = mod.getExportByName('afl_persistent_hook'); +Afl.setPersistentHook(hook); +``` -/* - * This must always be called at the end of your script. This lets - * FRIDA mode know that your configuration is finished and that - * execution has reached the end of your script. Failure to call - * this will result in a fatal error. - */ +Alternatively, the hook can be provided by using FRIDAs built in support for `CModule`, powered by TinyCC. + +```js +const cm = new CModule(` + + #include <string.h> + #include <gum/gumdefs.h> + + void afl_persistent_hook(GumCpuContext *regs, uint8_t *input_buf, + uint32_t input_buf_len) { + + memcpy((void *)regs->rdi, input_buf, input_buf_len); + regs->rsi = input_buf_len; + + } + `, + { + memcpy: Module.getExportByName(null, 'memcpy') + }); +Afl.setPersistentHook(cm.afl_persistent_hook); +``` + +# Advanced Persistence +Consider the following target code... +```c + +#include <fcntl.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +void LLVMFuzzerTestOneInput(char *buf, int len) { + + if (len < 1) return; + buf[len] = 0; + + // we support three input cases + if (buf[0] == '0') + printf("Looks like a zero to me!\n"); + else if (buf[0] == '1') + printf("Pretty sure that is a one!\n"); + else + printf("Neither one or zero? How quaint!\n"); + +} + +int run(char *file) { + + int fd = -1; + off_t len; + char * buf = NULL; + size_t n_read; + int result = -1; + + do { + + dprintf(STDERR_FILENO, "Running: %s\n", file); + + fd = open(file, O_RDONLY); + if (fd < 0) { + + perror("open"); + break; + + } + + len = lseek(fd, 0, SEEK_END); + if (len < 0) { + + perror("lseek (SEEK_END)"); + break; + + } + + if (lseek(fd, 0, SEEK_SET) != 0) { + + perror("lseek (SEEK_SET)"); + break; + + } + + buf = malloc(len); + if (buf == NULL) { + + perror("malloc"); + break; + + } + + n_read = read(fd, buf, len); + if (n_read != len) { + + perror("read"); + break; + + } + + dprintf(STDERR_FILENO, "Running: %s: (%zd bytes)\n", file, n_read); + + LLVMFuzzerTestOneInput(buf, len); + dprintf(STDERR_FILENO, "Done: %s: (%zd bytes)\n", file, n_read); + + result = 0; + + } while (false); + + if (buf != NULL) { free(buf); } + + if (fd != -1) { close(fd); } + + return result; + +} + +void slow() { + + usleep(100000); + +} + +int main(int argc, char **argv) { + + if (argc != 2) { return 1; } + slow(); + return run(argv[1]); + +} +``` + +FRIDA mode supports the replacement of any function, with an implementation +generated by CModule. This allows for a bespoke harness to be written as +follows: + +``` +const slow = DebugSymbol.fromName('slow').address; +Afl.print(`slow: ${slow}`); + +const LLVMFuzzerTestOneInput = DebugSymbol.fromName('LLVMFuzzerTestOneInput').address; +Afl.print(`LLVMFuzzerTestOneInput: ${LLVMFuzzerTestOneInput}`); + +const cm = new CModule(` + + extern unsigned char * __afl_fuzz_ptr; + extern unsigned int * __afl_fuzz_len; + extern void LLVMFuzzerTestOneInput(char *buf, int len); + + void slow(void) { + + LLVMFuzzerTestOneInput(__afl_fuzz_ptr, *__afl_fuzz_len); + } + `, + { + LLVMFuzzerTestOneInput: LLVMFuzzerTestOneInput, + __afl_fuzz_ptr: Afl.getAflFuzzPtr(), + __afl_fuzz_len: Afl.getAflFuzzLen() + }); + +Afl.setEntryPoint(cm.slow); +Afl.setPersistentAddress(cm.slow); +Afl.setInMemoryFuzzing(); +Interceptor.replace(slow, cm.slow); +Afl.print("done"); Afl.done(); +``` -/* - * This function can be called within your script to cause FRIDA - * mode to trigger a fatal error. This is useful if for example you - * discover a problem you weren't expecting and want everything to - * stop. The user will need to enable `AFL_DEBUG_CHILD=1` to view - * this error message. - */ -Afl.error(); +Here, we replace the function `slow` with our own code. This code is then +selected as the entry point as well as the persistent loop address. -/* - * This has the same effect as setting `AFL_ENTRYPOINT`, but has the - * convenience of allowing you to use FRIDAs APIs to determine the - * address you would like to configure, rather than having to grep - * the output of `readelf` or something similarly ugly. This - * function should be called with a `NativePointer` as its - * argument. - */ -Afl.setEntryPoint(address); +**WARNING** There are two key limitations in replacing a function in this way: +- The function which is to be replaced must not be `main` this is because this +is the point at which FRIDA mode is initialized and at the point the the JS has +been run, the start of the `main` function has already been instrumented and +cached. +- The replacement function must not call itself. e.g. in this example we +couldn't replace `LLVMFuzzerTestOneInput` and call itself. -/* - * This is equivalent to setting `AFL_FRIDA_PERSISTENT_ADDR`, again a - * `NativePointer` should be provided as it's argument. - */ -Afl.setPersistentAddress(address); +# Patching +Consider the [following](test/js/test2.c) test code... +```c /* - * This is equivalent to setting `AFL_FRIDA_PERSISTENT_RET`, again a - * `NativePointer` should be provided as it's argument. + american fuzzy lop++ - a trivial program to test the build + -------------------------------------------------------- + Originally written by Michal Zalewski + Copyright 2014 Google Inc. All rights reserved. + Copyright 2019-2020 AFLplusplus Project. All rights reserved. + 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 */ -Afl.setPersistentReturn(address); -/* - * This is equivalent to setting `AFL_FRIDA_PERSISTENT_CNT`, a - * `number` should be provided as it's argument. - */ -Afl.setPersistentCount(count); +#include <fcntl.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdint.h> +#include <stdlib.h> +#include <unistd.h> + +const uint32_t crc32_tab[] = { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, + + ... + + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d +}; + +uint32_t +crc32(const void *buf, size_t size) +{ + const uint8_t *p = buf; + uint32_t crc; + crc = ~0U; + while (size--) + crc = crc32_tab[(crc ^ *p++) & 0xFF] ^ (crc >> 8); + return crc ^ ~0U; +} /* - * See `AFL_FRIDA_PERSISTENT_DEBUG`. + * Don't you hate those contrived examples which CRC their data. We can use + * FRIDA to patch this function out and always return success. Otherwise, we + * could change it to actually correct the checksum. */ -Afl.setPersistentDebug(); +int crc32_check (char * buf, int len) { + if (len < sizeof(uint32_t)) { return 0; } + uint32_t expected = *(uint32_t *)&buf[len - sizeof(uint32_t)]; + uint32_t calculated = crc32(buf, len - sizeof(uint32_t)); + return expected == calculated; +} /* - * See `AFL_FRIDA_DEBUG_MAPS`. + * So you've found a really boring bug in an earlier campaign which results in + * a NULL dereference or something like that. That bug can get in the way, + * causing the persistent loop to exit whenever it is triggered, and can also + * cloud your output unnecessarily. Again, we can use FRIDA to patch it out. */ -Afl.setDebugMaps(); +void some_boring_bug(char c) { + switch (c) { + case 'A'...'Z': + case 'a'...'z': + __builtin_trap(); + break; + } +} -/* - * This is equivalent to setting a value in `AFL_FRIDA_INST_RANGES`, - * it takes as arguments a `NativePointer` and a `number`. It can be - * called multiple times to include several ranges. - */ -Afl.addIncludedRange(address, size); +void LLVMFuzzerTestOneInput(char *buf, int len) { + + if (!crc32_check(buf, len)) return; + + some_boring_bug(buf[0]); + + if (buf[0] == '0') { + printf("Looks like a zero to me!\n"); + } + else if (buf[0] == '1') { + printf("Pretty sure that is a one!\n"); + } + else if (buf[0] == '2') { + if (buf[1] == '3') { + if (buf[2] == '4') { + printf("Oh we, weren't expecting that!"); + __builtin_trap(); + } + } + } + else + printf("Neither one or zero? How quaint!\n"); -/* - * This is equivalent to setting a value in `AFL_FRIDA_EXCLUDE_RANGES`, - * it takes as arguments a `NativePointer` and a `number`. It can be - * called multiple times to exclude several ranges. - */ -Afl.addExcludedRange(address, size); +} -/* - * See `AFL_INST_LIBS`. - */ -Afl.setInstrumentLibraries(); +int main(int argc, char **argv) { -/* - * See `AFL_FRIDA_INST_DEBUG_FILE`. This function takes a single `string` as - * an argument. - */ -Afl.setInstrumentDebugFile(file); + int fd = -1; + off_t len; + char * buf = NULL; + size_t n_read; + int result = -1; -/* - * See `AFL_FRIDA_INST_NO_PREFETCH`. - */ -Afl.setPrefetchDisable(); + if (argc != 2) { return 1; } -/* - * See `AFL_FRIDA_INST_NO_OPTIMIZE` - */ -Afl.setInstrumentNoOptimize(); + printf("Running: %s\n", argv[1]); -/* - * See `AFL_FRIDA_INST_TRACE`. - */ -Afl.setInstrumentEnableTracing(); + fd = open(argv[1], O_RDONLY); + if (fd < 0) { return 1; } -/* - * See `AFL_FRIDA_INST_TRACE_UNIQUE`. - */ -Afl.setInstrumentTracingUnique() + len = lseek(fd, 0, SEEK_END); + if (len < 0) { return 1; } -/* - * See `AFL_FRIDA_OUTPUT_STDOUT`. This function takes a single `string` as - * an argument. - */ -Afl.setStdOut(file); + if (lseek(fd, 0, SEEK_SET) != 0) { return 1; } -/* - * See `AFL_FRIDA_OUTPUT_STDERR`. This function takes a single `string` as - * an argument. - */ -Afl.setStdErr(file); + buf = malloc(len); + if (buf == NULL) { return 1; } -/* - * See `AFL_FRIDA_STATS_FILE`. This function takes a single `string` as - * an argument. - */ -Afl.setStatsFile(file); + n_read = read(fd, buf, len); + if (n_read != len) { return 1; } -/* - * See `AFL_FRIDA_STATS_INTERVAL`. This function takes a `number` as an - * argument - */ -Afl.setStatsInterval(interval); + printf("Running: %s: (%zd bytes)\n", argv[1], n_read); + + LLVMFuzzerTestOneInput(buf, len); + printf("Done: %s: (%zd bytes)\n", argv[1], n_read); + + return 0; +} +``` + +There are a couple of obstacles with our target application. Unlike when fuzzing +source code, though, we can't simply edit it and recompile it. The following +script shows how we can use the normal functionality of FRIDA to modify any +troublesome behaviour. + +```js +Afl.print('******************'); +Afl.print('* AFL FRIDA MODE *'); +Afl.print('******************'); +Afl.print(''); + +const main = DebugSymbol.fromName('main').address; +Afl.print(`main: ${main}`); +Afl.setEntryPoint(main); +Afl.setPersistentAddress(main); +Afl.setPersistentCount(10000000); + +const crc32_check = DebugSymbol.fromName('crc32_check').address; +const crc32_replacement = new NativeCallback( + (buf, len) => { + Afl.print(`len: ${len}`); + if (len < 4) { + return 0; + } + + return 1; + }, + 'int', + ['pointer', 'int']); +Interceptor.replace(crc32_check, crc32_replacement); + +const some_boring_bug = DebugSymbol.fromName('some_boring_bug').address +const boring_replacement = new NativeCallback( + (c) => { }, + 'void', + ['char']); +Interceptor.replace(some_boring_bug, boring_replacement); + +Afl.done(); +Afl.print("done"); +``` + +# Advanced Patching +Consider the following code fragment... +```c +extern void some_boring_bug2(char c); + +__asm__ ( + ".text \n" + "some_boring_bug2: \n" + ".global some_boring_bug2 \n" + ".type some_boring_bug2, @function \n" + "mov %edi, %eax \n" + "cmp $0xb4, %al \n" + "jne ok \n" + "ud2 \n" + "ok: \n" + "ret \n"); + +void LLVMFuzzerTestOneInput(char *buf, int len) { + + ... + + some_boring_bug2(buf[0]); + + ... + +} +``` + +Rather than using FRIDAs `Interceptor.replace` or `Interceptor.attach` APIs, it +is possible to apply much more fine grained modification to the target +application by means of using the Stalker APIs. + +The following code locates the function of interest and patches out the UD2 +instruction signifying a crash. + +```js +/* Modify the instructions */ +const some_boring_bug2 = DebugSymbol.fromName('some_boring_bug2').address +const pid = Memory.alloc(4); +pid.writeInt(Process.id); + +const cm = new CModule(` + #include <stdio.h> + #include <gum/gumstalker.h> + + typedef int pid_t; + + #define STDERR_FILENO 2 + #define BORING2_LEN 10 + + extern int dprintf(int fd, const char *format, ...); + extern void some_boring_bug2(char c); + extern pid_t getpid(void); + extern pid_t pid; + + gboolean js_stalker_callback(const cs_insn *insn, gboolean begin, + gboolean excluded, GumStalkerOutput *output) + { + pid_t my_pid = getpid(); + GumX86Writer *cw = output->writer.x86; + + if (GUM_ADDRESS(insn->address) < GUM_ADDRESS(some_boring_bug2)) { + + return TRUE; + + } + + if (GUM_ADDRESS(insn->address) >= + GUM_ADDRESS(some_boring_bug2) + BORING2_LEN) { + + return TRUE; + + } + + if (my_pid == pid) { + + if (begin) { + + dprintf(STDERR_FILENO, "\n> 0x%016lX: %s %s\n", insn->address, + insn->mnemonic, insn->op_str); + + } else { + + dprintf(STDERR_FILENO, " 0x%016lX: %s %s\n", insn->address, + insn->mnemonic, insn->op_str); + + } + + } + + if (insn->id == X86_INS_UD2) { + + gum_x86_writer_put_nop(cw); + return FALSE; + + } else { + + return TRUE; + + } + } + `, + { + dprintf: Module.getExportByName(null, 'dprintf'), + getpid: Module.getExportByName(null, 'getpid'), + some_boring_bug2: some_boring_bug2, + pid: pid + }); +Afl.setStalkerCallback(cm.js_stalker_callback) +Afl.setStdErr("/tmp/stderr.txt"); +``` + +Note that you will more likely want to find the +patch address by using: + +```js +const module = Process.getModuleByName('target.exe'); +/* Hardcoded offset within the target image */ +const address = module.base.add(0xdeadface); +``` +OR +``` +const address = DebugSymbol.fromName("my_function").address.add(0xdeadface); +``` +OR +``` +const address = Module.getExportByName(null, "my_function").add(0xdeadface); +``` + +The function `js_stalker_callback` should return `TRUE` if the original +instruction should be emitted in the instrumented code, or `FALSE` otherwise. +In the example above, we can see it is replaced with a `NOP`. + +Lastly, note that the same callback will be called when compiling instrumented +code both in the child of the forkserver (as it is executed) and also in the +parent of the forserver (when prefetching is enabled) so that it can be +inherited by the next forked child. It is **VERY** important that the same +instructions be generated in both the parent and the child, or if prefetching is +disabled that the same instructions are generated every time the block is +compiled. Failure to do so will likely lead to bugs which are incredibly +difficult to diagnose. The code above only prints the instructions when running +in the parent process (the one provided by `Process.id` when the JS script is +executed). + +# API +```js +class Afl { + + /** + * Field containing the `Module` object for `afl-frida-trace.so` (the FRIDA mode + * implementation). + */ + public static module: Module = Process.getModuleByName("afl-frida-trace.so"); + + /** + * This is equivalent to setting a value in `AFL_FRIDA_EXCLUDE_RANGES`, + * it takes as arguments a `NativePointer` and a `number`. It can be + * called multiple times to exclude several ranges. + */ + public static addExcludedRange(addressess: NativePointer, size: number): void { + Afl.jsApiAddExcludeRange(addressess, size); + } + + /** + * This is equivalent to setting a value in `AFL_FRIDA_INST_RANGES`, + * it takes as arguments a `NativePointer` and a `number`. It can be + * called multiple times to include several ranges. + */ + public static addIncludedRange(addressess: NativePointer, size: number): void { + Afl.jsApiAddIncludeRange(addressess, size); + } + + /** + * This must always be called at the end of your script. This lets + * FRIDA mode know that your configuration is finished and that + * execution has reached the end of your script. Failure to call + * this will result in a fatal error. + */ + public static done(): void { + Afl.jsApiDone(); + } + + /** + * This function can be called within your script to cause FRIDA + * mode to trigger a fatal error. This is useful if for example you + * discover a problem you weren't expecting and want everything to + * stop. The user will need to enable `AFL_DEBUG_CHILD=1` to view + * this error message. + */ + public static error(msg: string): void { + const buf = Memory.allocUtf8String(msg); + Afl.jsApiError(buf); + } + + /** + * Function used to provide access to `__afl_fuzz_ptr`, which contains the length of + * fuzzing data when using in-memory test case fuzzing. + */ + public static getAflFuzzLen(): NativePointer { + + return Afl.jsApiGetSymbol("__afl_fuzz_len"); + } + + /** + * Function used to provide access to `__afl_fuzz_ptr`, which contains the fuzzing + * data when using in-memory test case fuzzing. + */ + public static getAflFuzzPtr(): NativePointer { + + return Afl.jsApiGetSymbol("__afl_fuzz_ptr"); + } + + /** + * Print a message to the STDOUT. This should be preferred to + * FRIDA's `console.log` since FRIDA will queue it's log messages. + * If `console.log` is used in a callback in particular, then there + * may no longer be a thread running to service this queue. + */ + public static print(msg: string): void { + const STDOUT_FILENO = 2; + const log = `${msg}\n`; + const buf = Memory.allocUtf8String(log); + Afl.jsApiWrite(STDOUT_FILENO, buf, log.length); + } + + /** + * See `AFL_FRIDA_DEBUG_MAPS`. + */ + public static setDebugMaps(): void { + Afl.jsApiSetDebugMaps(); + } + + /** + * This has the same effect as setting `AFL_ENTRYPOINT`, but has the + * convenience of allowing you to use FRIDAs APIs to determine the + * address you would like to configure, rather than having to grep + * the output of `readelf` or something similarly ugly. This + * function should be called with a `NativePointer` as its + * argument. + */ + public static setEntryPoint(address: NativePointer): void { + Afl.jsApiSetEntryPoint(address); + } + + /** + * Function used to enable in-memory test cases for fuzzing. + */ + public static setInMemoryFuzzing(): void { + Afl.jsApiAflSharedMemFuzzing.writeInt(1); + } + + /** + * See `AFL_FRIDA_INST_DEBUG_FILE`. This function takes a single `string` as + * an argument. + */ + public static setInstrumentDebugFile(file: string): void { + const buf = Memory.allocUtf8String(file); + Afl.jsApiSetInstrumentDebugFile(buf); + } + + /** + * See `AFL_FRIDA_INST_TRACE`. + */ + public static setInstrumentEnableTracing(): void { + Afl.jsApiSetInstrumentTrace(); + } + + /** + * See `AFL_INST_LIBS`. + */ + public static setInstrumentLibraries(): void { + Afl.jsApiSetInstrumentLibraries(); + } + + /** + * See `AFL_FRIDA_INST_NO_OPTIMIZE` + */ + public static setInstrumentNoOptimize(): void { + Afl.jsApiSetInstrumentNoOptimize(); + } + + /** + * See `AFL_FRIDA_INST_TRACE_UNIQUE`. + */ + public static setInstrumentTracingUnique(): void { + Afl.jsApiSetInstrumentTraceUnique(); + } + + /** + * This is equivalent to setting `AFL_FRIDA_PERSISTENT_ADDR`, again a + * `NativePointer` should be provided as it's argument. + */ + public static setPersistentAddress(address: NativePointer): void { + Afl.jsApiSetPersistentAddress(address); + } + + /** + * This is equivalent to setting `AFL_FRIDA_PERSISTENT_CNT`, a + * `number` should be provided as it's argument. + */ + public static setPersistentCount(count: number): void { + Afl.jsApiSetPersistentCount(count); + } + + /** + * See `AFL_FRIDA_PERSISTENT_DEBUG`. + */ + public static setPersistentDebug(): void { + Afl.jsApiSetPersistentDebug(); + } + + /** + * See `AFL_FRIDA_PERSISTENT_ADDR`. This function takes a NativePointer as an + * argument. See above for examples of use. + */ + public static setPersistentHook(address: NativePointer): void { + Afl.jsApiSetPersistentHook(address); + } + + /** + * This is equivalent to setting `AFL_FRIDA_PERSISTENT_RET`, again a + * `NativePointer` should be provided as it's argument. + */ + public static setPersistentReturn(address: NativePointer): void { + Afl.jsApiSetPersistentReturn(address); + } + + /** + * See `AFL_FRIDA_INST_NO_PREFETCH`. + */ + public static setPrefetchDisable(): void { + Afl.jsApiSetPrefetchDisable(); + } + + /* + * Set a function to be called for each instruction which is instrumented + * by AFL FRIDA mode. + */ + public static setStalkerCallback(callback: NativePointer): void { + Afl.jsApiSetStalkerCallback(callback); + } + + /** + * See `AFL_FRIDA_STATS_FILE`. This function takes a single `string` as + * an argument. + */ + public static setStatsFile(file: string): void { + const buf = Memory.allocUtf8String(file); + Afl.jsApiSetStatsFile(buf); + } + + /** + * See `AFL_FRIDA_STATS_INTERVAL`. This function takes a `number` as an + * argument + */ + public static setStatsInterval(interval: number): void { + Afl.jsApiSetStatsInterval(interval); + } + + /** + * See `AFL_FRIDA_STATS_TRANSITIONS` + */ + public static setStatsTransitions(): void { + Afl.jsApiSetStatsTransitions(); + } + + /** + * See `AFL_FRIDA_OUTPUT_STDERR`. This function takes a single `string` as + * an argument. + */ + public static setStdErr(file: string): void { + const buf = Memory.allocUtf8String(file); + Afl.jsApiSetStdErr(buf); + } + + /** + * See `AFL_FRIDA_OUTPUT_STDOUT`. This function takes a single `string` as + * an argument. + */ + public static setStdOut(file: string): void { + const buf = Memory.allocUtf8String(file); + Afl.jsApiSetStdOut(buf); + } + +} -/* - * See `AFL_FRIDA_STATS_TRANSITIONS` - */ -Afl.setStatsTransitions() ``` |