Ground Zero: Part 3-3 – Reverse Engineering – Bypassing Breakpoint Detection – ARM64

A very common difficulty often faced by fellow reverse engineers is defeating anti-debugging measures. While it is important to many proprietary vendors to make their code as difficult to debug as possible, to avoid misuse according to their terms of conduct. It is also a technique used while writing malwares to throw off analyst from their “analysis mindset”, and confuse them to the point of calling it quits.

The most common example of an anti-debugging technique is the use of isDebuggerPresent() function in Windows, which checks if the running process is attached to any debugger or not. There are also many plugins available for your choice of Windows based debugger to bypass these anti-debugging techniques.

In this post, we will explore a similar anti anti-debugging technique, bypassing breakpoint detection in a program, from scratch using GDB.

Breakpoints

A breakpoint is a system interrupt that causes the operating system to trigger an interrupt to the execution of current process. In the case of Linux, it causes a SIGTRAP to be thrown, this signals the debugger that the child process has reached a point that we are interested in. The execution is halted and the debugger takes control. The op-code for a breakpoint in non-Apple aarch64 architecture is 0xd4200000.

The Program

The program we will work with.

We will use GDB to load the program and set a breakpoint in the main function. At the very start, our custom isBreakpointPresent() function is called and it checks for the presence of a breakpoint, from _start (entry point of our program) to __etext (end of .text section). We cast the address pointer variable start to volatile unsigned and then de-reference it to store the full raw word from that address.

Running the program

We set a breakpoint on the call to foo function, which is at line 41.

On running the program, we see the intended behaviour. The breakpoint address is displayed and program quits with a message. Our intention is to make the program continue in execution and avoid breakpoint detection.

There are two ways to address this –

  1. We identify part of binary that is responsible for checking and deciding if breakpoint is present, and edit the bytes with NOP instructions. (Like we did in our previous blog – Patching Binaries with Radare2)
  2.  We identify part of binary which is responsible for handling situation after breakpoint is discovered, which is a shared library in most cases (software’s with huge code base will have multiple modules as a shared library), and patch functions from those libraries.

In this case, we will patch a function call to a shared library. But first, we will look into the objdump to understand how to work this from a crackers perspective, who does not have access to the source code.

We observe that after the call to bar() function, there is a call to exit() function, where our program is terminating. As the exit() function is a library function, it is linked by GCC during compilation using shared libraries accessible via environment variables. This is where we can hook our patch code using LD_PRELOAD environment variable.

Note: This explanation is only limited to patching one function, which happens to be a widely used common shared library function. IRL, this won’t be the case. You need to derive your own action plan from this blog post.

The Patch

We need to replicate the function declaration and modify the function body that satisfies our intent in context of the program.

We simply have a return statement which would return control back to the program, and the program will continue it’s execution.

Next, we make the patch library using the following commands. And finally we export the shared library into LD_PRELOAD variable.

As our new patched shared library is available from the same directory path as the executable, the function being called is the one from our patched library due to Search Patch precedence.

Finally, we load the program into GDB, insert a breakpoint and run it.

The program stops at our breakpoint and does not exit like it was intended from the isBreakpointPresent() function. On continuing from the breakpoint, we see the program executes further and terminates with another message.

We have successfully bypassed a software breakpoint detection from scratch !

Epilogue

This is one of many anti anti-debugging techniques in software reversing and understanding the internals of a debugger really helps in knowing debugging / anti-debugging techniques and successfully bypassing them. We will dive into more such interesting topics in future posts. Till then, goodbye!

Security Analyst, Threat Hunter, Reversing Binaries in search for that POP POP RET.

Tags: , , , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *

*