/advent

17 - Breakpoints and Registers

Welcome to Day 17 of the Advent of Radare!

Today, we’ll focus on examining and changing the register state at specific breakpoints, a fundamental step in debugging and understanding binary behavior when performing dynamic instrumentation.

Radare2 provides commands like db and dbc to manage breakpoints and display register values with dr or drr. In this guide, we’ll explore some common practices and techniques for manipulating program execution to bypass protections and security checks at runtime.

In need for a break?

We have already covered the topic of breakpoints in the 2nd day of the #aor24, so we won’t go into much extra details in here, but instead focus on the practical applications and inconvenients.

Am I being Debugged?

Protected software uses different techniques to detect when the application is being debugged and uses that as a way to misbehave, crash, or fail in different ways to confuse the analyst or avoid tampering to make it harder to change or inspect its internal functionalities.

This technique is commonly used not just for Anti-debugging mechanisms, but also for Malware detection evasion, Software protection schemes and Security-sensitive applications. In this section we will learn about some of them:

IsDebuggerPresent, this w32 api returns true when the current process have a debugger attached, so the program itself can determine if it’s being debugged.

The IsDebuggerPresent API on Windows can be replaced by directly checking the Process Environment Block (PEB) structure, which contains a byte flag indicating if a debugger is present. This information is accessible through the Thread Local Storage (TLS) via the Thread Environment Block (TEB), which points to the PEB.

IsDebuggerPresentPEB:
#if _WIN64
    mov rax, gs:[0x60]
#else
    mov eax, fs:[0x30]
#endif

Some little understandings on which structures are inside those pointers:

BOOL IsDebuggerPresentPEB() {
    typedef struct _PEB {
        BYTE Reserved1[2];
        BYTE BeingDebugged;
        BYTE Reserved2[1];
        // ... rest of PEB structure
    } PEB;

    typedef struct _TEB {
        NT_TIB NtTib;
        PVOID EnvironmentPointer;
        CLIENT_ID ClientId;
        PVOID ActiveRpcHandle;
        PVOID ThreadLocalStoragePointer;
        PPEB ProcessEnvironmentBlock;
        // ... rest of TEB structure
    } TEB;

    PTEB pTEB = NtCurrentTeb();
    return pTEB->ProcessEnvironmentBlock->BeingDebugged;
}

On Linux systems (as well as in BSD), the PTRACE_ME request for the ptrace syscall that will fail (return -1) when the process is being traced. The reason for that is because this call can be only made once by the child process.

On Linux we can check the stat and status files from the /proc/self/ directory to determine if we are being traced:

Anti-Debugging Techniques

Malware authors and software protection systems often implement various techniques to detect and prevent debugging. Understanding them will make us understand and find ways to bypass or workaround them.

Spend some time reading the following items and think (or google around) about how to beat them!

Code Integrity Checks

Timing-Based Detection

Virtual Machine Detection

Environment Analysis

System Behavior Checks

Other Techniques

Debugger-Specific tricks

Register Profiles

Let’s go back to the radare2 shell to learn how to display and manipulate the registers.

Register profiles are tab-separated text files that define how registers are mapped in memory. Each analysis engine and debugger backend maintains its own register profile, which specifies the exact memory layout of registers.

We can inspect them with the arp or drp commands. These files contain the following fields:

Here’s an excerpt from the reg profile used by esil on x64 binaries:

0x00000000]> drp | head -n20
=PC rip
=SP rsp
=BP rbp
=R0 rax
=A0 rdi
=A1 rsi
=A2 rdx
=A3 rcx
=A4 r8
=A5 r9
=A6 r10
=A7 r11
=SN rax
gpr rax .64 80  0
gpr eax .32 80  0
gpr ax  .16 80  0
gpr al  .8  80  0
gpr ah  .8  81  0
gpr rbx .64 40  0
gpr ebx .32 40  0
..

The register profile information is also available in XML format within GDB, and Radare2 includes functionality to convert between both formats. By default, the dr command displays registers corresponding to the current architecture’s bit width (as specified by asm.bits).

To pick the value of a register:

[0x100425074]> dr=

Show registers of size 64

[0x100425074]> dr 64

Dumping and Restoring regstate

As long as all r2 command can export the information in scripts by using the * suffix, we can use this characteristic to save the register values in a file and then restore it back by executing the script.

Let’s see how that works:

[0x100425074]> dr* > regs.r2
[0x100425074]> . regs.r2

But what’s in the regs.r2 now? is this exactly what we were looking for?

[0x100425074]> cat regs.r2
fs+registers
f rax 8 0x00000000
f rbx 8 0x00000000
f rcx 8 0x00000000
f rdx 8 0x00000000
f rsi 8 0x00000000
f rdi 8 0x00000000
...
fs-
[0x100425074]>

Well, it seems like it’s not. this script is just setting some flags with a given name, size and address. So, if we run the script what we will get is just the flags loaded, but flags are not tied to the debugger backend or the register state, why’s that useful for then?

Turns out that math operations, and many other commands end up using the flags as a way to find out references to named things like the register periscoping listing in drr.

So, to achieve the same behaviour we can just run it in one line:

[0x100425074]> .dr*

The dot tells r2 to take the output of the command that is written right after it and interpret it as an r2 script.

But then, which command should we use to save and restore registers? it’s dr.!

[0x100425074]> dr. > regs.r2
[0x100425074]> dr rax=33
[0x100425074]> . regs.r2
[0x100425074]> dr?rax
0x00000033

And the script now looks like expected:

[0x100425074]> dr.
dr rax=0x00000000
dr rbx=0x00000000
dr rcx=0x00000000
dr rdx=0x00000000
dr rsi=0x00000000
dr rdi=0x00000000
dr r8=0x00000000
...

Tracing, Breaking and Patching

We learned about the advantages and inconveniences of different protections and anti-debugging techniques, so we should now decide which are the best choices we can take to circumvent them.

Bypassing the ptraceme check

Let’s put the knowledge into practice with today’s challenge, we may try to solve it in all the possible ways we have.

Start by disassembling the main function of the binary attached (b64+xz), then load it into your favourite linux-x64 system and run it:

$ ./a.out
Hello, World!
$ r2 -qc 'dc' -d ./a.out
Debugger detected!
(1223217) Process exited with status=0x100
$

I’ll give you over some of these options:

Breakpoints

Set a breakpoint in the ptrace import. step over the libc call and check for the condition that happens in the main function and the values of the registers.

We can now use the ‘dr’ command right before the cmp to set the right value like if ptrace returned 0 instead of -1.

Binary patching in disk

Open the binary with -w to be able to write changes in the file, analyze with agfv the condition that makes the program fail when ptrace returns -1 and patch the code using wa or wao.

R2Frida

If we use Frida or r2frida as backend for debugging or tracing the target we will notice that the program runs without any issue:

$ frida ./a.out
Spawned `./a.out`. Resuming main thread!                                
Hello, World!
[Local::a.out ]-> Process terminated
Thank you for using Frida!

Same happens in r2frida:

$ r2 frida://./a.out
INFO: Mounted io on /r2f at 0x0
 -- Press 'C' in visual mode to toggle colors
[0x5caf81643169]> :dc
Hello, World!
INFO: resumed spawned process
[0x5caf81643169]>
INFO: DetachReason: FRIDA_SESSION_DETACH_REASON_PROCESS_TERMINATED

The reason for that is because frida uses ptrace only to spawn the proces and inject the frida agent inside the process, then it’s detaching it and resuming, we can use commands like :dtf ptrace to trace when the ptrace call is happening. But more interestingly, we can just fake the return value of ptrace to make the program think that is running

$ r2 frida://./a.out
INFO: Mounted io on /r2f at 0x0
[0x61a5cf02f169]> :dif-1 ptrace
[0x61a5cf02f169]> :dc
INFO: resumed spawned process
Debugger detected!
Target: ptrace was called, intercepting return value.
INFO: DetachReason: FRIDA_SESSION_DETACH_REASON_PROCESS_TERMINATED
[0x61a5cf02f169]> 

The dif-1 command intercepts the function return value and replace it with 0, 1, -1, any random inmediate a string, etc..

The difference with di is that one replaces the function and the other keeps the code being executed and changes the return value after that.

:di[0,1,-1,i,s,v] [addr]    Intercepts and replace return value of address without calling the function
:dif[0,1,-1,i,s] [addr]     Intercepts return value of address after calling the function

Blob!

/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4D6HB4tdAD+RRYRoPYmm2orhgzJO2QgDEQzOIJnhOIC5by
/ltn+ezABBUQWSImIw3m2GytJRiNQkZPCV3kjmM7n2HnITlJRsuZmFI1UE10XNxW+VChhvRHl5fGj2
J+xHkuneiFvGZVjLRXsLH+ejwCtW4fycAq+zrC3pCtl6OjIfWSGu4VRquhW0qbY4kgFGnU0pPvGR56
J9SbiL7LRA9MEo7XZSgG5KgZv8w4JMhmh/71xh6r5JEPOEUv73jNfcXf0o2saxza4WhR1nhG0rQ/M0
zJ3GU2x7ZRABu/1NCjPnUl/mIjhjKYrvuInbBUuDDZGnO52WnBL/NXNxyCFmW3hifYY+ZsvvNgYEj0
BmJeGkTBBpCYakJniaTPxkoamERCyPhnhY3Hs6FP/xqu0ryTPmbWp7KVqVzZdM/zINB+uDDdEfypx9
DgtIyl8gKNK+jdqZfrl2qzNP9mTm/LlnfiZ5L5xNmKcXSsJ+ScvYqCnsrdZFRfjBNu9+/BGOwZiISl
BuYy1EqSz3L4ghcWOvKdQCNpRAuM/3mpP7TznBu+/4ntUAb79JzSkaZLoQOpx/uKMB9rmp6fr9T2RC
nu4UyAd4+w4M9D8eJulFCbkvH6agg8EF3j+69saEbsikSyYK9RUgWeDExhT4IUxOsU5qse9+RfKM2K
8TkR88l/tPyZMu87zCgOkFan4EQksXJaO6kuLeyNWXTPI4tFDKIpiAONRmDvcvmlbkpgRDLlPUBJ6M
wIPX1ZRrB5QSz7XnScen3OuQdMpWU3g3Yz8trLeDyU5yH/i+MRgTHIrM8s/zjdpt573ox1tIGwozFZ
mu8gfgOJ/BYZcZdNP73csQVNBf1dQZNb2LnYTVkeE2vK2ReKUjDyEPkfE2fx8j1DiYqpwExK2xeWKz
8SFwyr4p2QdlV6EYk5w5UcDkJyrmihVVkelX7JjI5zsvtZHreRVgDnXqTXlncKBDvWAbspzCrmlPjO
qc+2NKzeqS1JSHS7CLU6/YQ6CnZyWeOc97ECKsQAN51j0ATD+8J7GOKV2DoqxICUNtWCsRO1ZeQEe/
FrjN/NuhJVPVSuRvc6fBivY4zBOdjwwfeofUYF9T0hn/UjTEPRojCbtDxT/Zj6Tooita478RnFpJu/
LHnBFMyg5lgy/+s5+H8+7DyqQbUOrN+qSdpmeCqY8M9rzNeBOG0h0Ri6gxTLbMQaAY5rj1+IZRFOx4
zJDBA+NC3qX2ZIpy2mcQhx0Q0KHLegUdfW5HIU2+MPsC9ZPWDkx6m8SDxvylcpZHibPMmIYCEHocU5
SMekVhC3ZNzwAAPQsCmMzRkZl5JpquJlm45QQsxNXmpJ6yWng+dnleIzkmxYV4D1kIsX9WfLJ394eo
ycWhLXPbHCThQP0AcSQBnFGD8LC/aGkL6x7wVlqAoFu/9vtB9FJhQOfIudj5WxFUceebxHNsgwho/K
xSvAKGT1uFkDs/gISwEYcG0QfT8TWLuVLnpTzJZjX1UU9RK2lGMydg65FftwecdO6BvFWBb+qT2TM2
p+TsqFWOW9bOEvPrpSQoMpUNfNIqe9mGBhdJ96VNqCatxGeBT0HEOaDItNfHlu6SWz9o7OZHCpdH4K
2USnveWWdbMFn8iyOwGEq5ZSZAt+BEVlIKSSsy1kI1KeZWtGzAIrJM5Ek5cHxk3fPN+uHqKbR/JeHP
BrIjetINZ/kmxNIXb64TP4N8ktG3Ds8y3Qfmtq7UJrFuKrZpLaAs/NFM4mLpVnmi5WSTEK8h16RG9Q
4/chp4JXJ26hZxvRCZ6Dxyl2EOmclJapKB/QzYwZ/dzJlDUyvZXEZulgX07COskprImttfjRSH+v4t
t/Qyz8suqkaoilKpcLmDbcdnljXdd1sK36axUCHNFJTAG2jsoB4Vj9sJIwbo/+e6OfTmO82lFIJKu3
dwLdGt40aGJwr4qwFO0wNlnZDnDvky68mj9qCv88Go2lVEVty+LCmQ5iZUY5gUtnYwJVyl/l6cbzuz
zT9b0cqRawcQjH6XrxSONzZ3V9WF0x8PAaf+4pLw3BQoy+IKznNNFb96FRfYetqeTH/+0FPRIsA8T1
HpSmXuRPK/zzpA4uotzYIdGl5mGe+BMXW01+nOLW9BGgzWSaEoA6KHTicp4d8eBTznktYIqyK++LtZ
sP8N+46wiM0jWFWj3175oyyn//iDkN5bV2kYPW96FPtySNw1kmOhvjnf0jX60+DPNAIMJlshWNCi9b
02+RCCz5ieq6taFRU2nuwzcpcPpTCkNlczPo4dd0Z6LTHQONZBn2MnWwJZkDtkGrtnh/VcPAgvB7dp
htyibCTLZGKza/4nkv+SQqxNKy1zavya4xmrFew9Yzvi86+3V/DqRi64zT4UPpXw456RSFVGjUhWQw
wJOunqOQy4lsm2V1O+e3XQA5aEyrkm0xEx/HAIUELeBopLm/7RpL38EZETucZNJEOkQ5SBm3RAjFzC
zbAWehvi+4LWJz3MC6SjXNMUnRAK3Z8UrjnXbfUvn8NBY5z4NMaoI2ziCgzS/siraiU8nneuP0aqBu
A40hRIfZ/4N6Sz2fuh/RAgFRfbEw04QIPaOHK46Yf5AABUe8K8ViBtbQABpw+IfQAAjQfYMbHEZ/sC
AAAAAARZWg==

Closing

Stay tuned for tomorrow’s Radare2 challenge as we explore more powerful analysis tools!