One of the most useful tools for the reverse engineer (and forward engineer :P) is the program execution tracer. This yields either program coverage (which lines have been executed) or a full trace (list of executed instructions in order). Qira is a very good example, leveraging either qemu or PIN to take an execution trace and display it in a nice GUI. But many other tracers exist, including DynamoRIO drcov, Frida and Intel PIN based tools. Finally, there are whole-system tracers, such as PANDA. I want to compare some of these, point out some shortcomings and illustrate some countermeasures.
In a previous post on ptrace, I showed some simple programs that will detect a debugger using ptrace. Let's start with those and then explore tougher cookies....
#include <stdio.h>#include <sys/ptrace.h>int main() { if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) { printf("Debugger\n"); } else { printf("Normal\n"); } return 0;}qira
$ qira ./superSimplePtraceDebuggerqira using PIN
$ qira --pin ./superSimplePtraceNormaldrcov
$ bin64/drrun -t drcov -- ./superSimplePtracefrida
PANDA
#include <stdio.h>#include <sys/ptrace.h>int main() { if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) // first call { printf("Debugger (first check)\n"); } else { if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) // second call { printf("Normal\n"); } else { printf("Debugger (second check)\n"); } }return 0;}qira
$ qira ./lessSimplePtraceDebugger (first check)qira using PIN
$ qira --pin ./lessSimplePtrace Normaldrcov
$ bin64/drrun -t drcov -- ./lessSimplePtraceNormalfrida
PANDA
This code is adapted from [5]
#include <unistd.h>#include <sys/ptrace.h>#include <sys/wait.h>#include <stdio.h>int main(){ pid_t child; int status; switch((child = fork())) { case 0: ptrace(PTRACE_TRACEME); // this is the actual 'useful' program code should run for a while - simulate with delay usleep(1000000); printf("no debugger detected!"); return 2; case -1: perror("fork"); return 1; default: if (ptrace(PTRACE_ATTACH, child)) { kill(child, SIGKILL); printf("ptrace_attach failed - debugger?"); return 3; } while (waitpid(child, &status,0) != -1) ptrace(PTRACE_CONT,child,0,0); return 0; } return 0;}qira
$ qira ./selfdebugptrace_attach failed - debugger?qira using PIN
$ qira --pin ./selfdebugno debugger detected!drcov
$ bin64/drrun -t drcov -- ./selfdebugno debugger detected!frida
PANDA
Next, I am running a binary challenge from root-me.org that contains nanomites [2],[3].
qira
This fails, even when the correct flag is passed.
$ qira ./ch28.bin So you want to trace me?!Wrong! try hard! :)qira using PIN
This fails, even when the correct flag is passed. But in a different place than before.
$ qira --pin ./ch28.bin Please input the flag:Hummmmmmm NO WAY.drcov
This fails, even when the correct flag is passed, same place as qira with PIN
$ bin64/drrun -t drcov -- ./ch28.bin Please input the flag:Hummmmmmm NO WAY.frida
I am using a script by lighthouse [5] to collect coverage information using frida.
$ sudo python frida-drcov.py ch28.bin[*] Attaching to pid '6171' on device 'local'...[+] Attached. Loading script...[+] Got module info.Starting to stalk threads...Stalking thread 6171.Done stalking threads.[*] Now collecting info, control-D to terminate....and in a separate window
$ ./ch28.bin Please input the flag:Hummmmmmm NO WAY.PANDA
details here: PANDA for code coverage with IDA pro
$ ./ch28.bin Please input the flag:POOOOOOOOOOOOOOOOOOOOOOOOO God damn!! You won!Whole system tracing is the only method that can yield code coverage and execution tracing in this and many other cases...
References: