230526

Tiny Tracer for x86_64

h2hack

CPU features are sometimes weird. And for an architecture like x86, there are some funny flags which can be used to do pretty interesting things. In this post, we want to talk about the Trap Flag (TF) and introduce a slow and dirty implementation of a poor man's instruction tracer.


    
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@%%%%###########************+++++====+++++************############%%@
@@%%%%########****++++====------::::::------====++++****########%%%%@@@
@%%%#####***+++==--:::::::.......................:::::--==+++***#####%@
%%####**++==--:::.............::::::::::::.............:::--==++**####%
###**++=--::.............::::::::::::::::::::.............::--=++**####
##*++=--:............:::::::.................:::::::........:--=++*####
#*+=--:..........:::::::........................::::::.......:--=+*##%#
#*+=-:.........:::::.............:::::.............:::::.......:-=+*###
*+=-:........::::.............:::::::::::.............::::......:-=+*#*
+=-:........:::............:::::.......::::............:::.......:-=+*+
=-:........:::..........::::..............::::..........:::.......:-=+=
-:.........::.........:::.....................:::.........::........:--
-:.........:.........::.......::::::::::.......::.........:.........:--
::.........:........::......::::::::::::::.....::.........:.........:::
::.........:.......::......:::::......:::::.....::........:.........:::
::.........:.......::.....::::..........::::.....::.................:::
::.........:......::.....::::............::::.....::.................::
:..........:......::.....::::............::::.....::..................:
:..........:......::.....::::............::::.....::..................:
::.........:......::......::::..........::::......::.................::
::.........:.......::......:::::......::::::......::.................::
-:.........::.......::.......::::::::::::::.......::................:--
=-:........:::.......::.........::::::::.........::...............:-===
+=-:........:::.........:::...............:::.................:-=+**##+
*+=-:.........::::..........:::::.....::::::...............:-=+**#####*
#*+=--:.........:::::............:::::::............::::---=+*###%%%%%#
##*++=--::.........:::::.........................::::--==++*####%%%%@@#
###**++==--:::.........::::::::........:::::::--===++**####%%%%@@@@@@@#
%%####**++===--::::........:::::::::::::::::---==++**####%%%@@@@@@@@@@%
@@@%%%%####**+++===---::::::::-----------===+++**####%%%%@@@@@@@@@@@@@@
@@@@@@@@%%%%%%%%############***************########%%%%@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    

General

Obviously, this tracer is not performant. It is more of a toy than anything else, and features like Intel PT exist for a reason. So let's start with the basics.

The Trap Flag is the 8th bit in the RFLAGS register which, when set, causes the CPU to generate an exception after each instruction. Historically, this was created to support hardware-assisted single-step debugging. Debuggers needed a reliable way to execute one instruction and then regain control as on older systems, doing this purely in software was impractical due to performance constraints.

Implementation

So the easiest way to gain some control over the execution of a process is not to use ptrace, but rather to implement a shared library which can be injected into the target with LD_PRELOAD. The basic idea is on init, to set the Trap Flag and install a SIGTRAP handler, which will then be called after each instruction and logs the current program counter to a file.

__attribute__((constructor))
static void preload_ctor(void)
{
    struct sigaction sa = {0};
    [...]
    sa.sa_sigaction = sigtrap_handler;
    sa.sa_flags = SA_SIGINFO | SA_RESTART;
    sigemptyset(&sa.sa_mask);
    if (sigaction(SIGTRAP, &sa, NULL) != 0) return;

    __asm__ volatile(
        "pushfq\n\t"
        "orl $0x100, (%%rsp)\n\t"
        "popfq\n\t"
        :
        :
        : "cc", "memory");
}

The handler then catches the exception, reads the instruction pointer, and logs it to a file. It then returns, allowing the next instruction to be executed.

static void sigtrap_handler(int signum, siginfo_t *info, void *context)
{
    ucontext_t *uc = (ucontext_t *)context;
    uintptr_t rip = uc->uc_mcontext.gregs[REG_RIP];
    log_instruction(rip);
}

And that's basically it. For better usability, we also implemented a TRACE_FILE variable and a TRACE_MODULES environment whitelist. We read out /proc/self/maps to get the module names and their base addresses. This allows us to log the instructions with their module and offset, which is pretty nice. The whole implementation can be found on our github.

$ TRACE_MODULES=date LD_PRELOAD=./libtrace.so /bin/date
Sa 16 Mai 2026 13:04:15 CEST

$ tail /tmp/date.log
date+0x482e
date+0x4831
date+0x4848
date+0x48bc
date+0x48c3
date+0x48c4
date+0x13e54
date+0x13e58
date+0x13e5c
date+0x13e60

The output can then be used to reconstruct the control flow of the program, and it is fully compatible with the Lighthouse plugin for IDA or Binary Ninja.

Further Work

Not only can it be used for tracing, but think about obfuscated code like nanomites and self-modifying code. This CPU feature makes their implementation trivial. But this might be needed for a future CTF challenge, so we won't talk about it here yet...