..

Anti Debugging For Noobs Part 3

In previous part, we covered anti-debugging using a trivial self-modifying code. Here, instead of blocking debugging completely, we will detect various debugger-induced activities.

Breakpoints

A breakpoint is intentional “pause” in normal execution of a program, generally used to inspect the internals of said process in more detail. This is the most used feature of any debugger.

On x86 CPUs, there are two types of breakpoints: hardware breakpoints and software breakpoints. While they overlap to a certain degree they are not exactly the same.

In most of debugging cases, you will be using software breakpoints, which do not need any special hardware support. These are implemented using same interrupt mechanism which is used by pretty much everything else. On x86, 3rd interrupt is used to implement a breakpoint. When you set a breakpoint, your debugger overwrites target address (where you want to put the breakpoint) with INT 3 (0xCC in hex). When this instruction gets executed, debugger gets the control back from target process, and can inspect its state (registers, memory etc). To resume the execution, debugger will silently remove breakpoint, execute the instruction, and set the breakpoint again before letting the process resume (until it terminates, or breaks). Features like step over, step out are also implemented using “transparent” software breakpoints, which are set and removed automatically by debugger. Generally, you can set any number of software breakpoints; however these cannot be set on non-code address (i.e. these can break the program only when target address content is executed; but not if the address is read from or write to).

Hardware breakpoints, on the other hand, are much more powerful and flexible than software breakpoints. These can be set to break not only on execution, but also on memory access (read and write both), I/O port access etc. These debuggers are set by writing into special “debug registers” which are largely platform specific (and not all platforms will have support for hardware breakpoints). On x86, registers DR0-3 and DR6-7 are used to set these breakpoints (DR4-5 are reserved as of now). If you have ever used “watchpoints” which let you break when certain memory address is accessed, you have used hardware breakpoints.

Here, one can try looking this inside a debugger, and then claim that this is not how software breakpoints work:

(gdb) break main
Breakpoint 1 at 0x116d
(gdb) disassemble main 
Dump of assembler code for function main:
   0x0000000000001169 <+0>:	push   rbp
   0x000000000000116a <+1>:	mov    rbp,rsp
   0x000000000000116d <+4>:	lea    rsi,[rip+0xe91]        # 0x2005
   0x0000000000001174 <+11>:	lea    rdi,[rip+0x2f05]        # 0x4080 <[email protected]@GLIBCXX_3.4>
   0x000000000000117b <+18>:	call   0x1040 <[email protected]>
   0x0000000000001180 <+23>:	mov    rdx,rax
   0x0000000000001183 <+26>:	mov    rax,QWORD PTR [rip+0x2e46]        # 0x3fd0
   0x000000000000118a <+33>:	mov    rsi,rax
   0x000000000000118d <+36>:	mov    rdi,rdx
   0x0000000000001190 <+39>:	call   0x1050 <[email protected]>
   0x0000000000001195 <+44>:	mov    eax,0x0
   0x000000000000119a <+49>:	pop    rbp
   0x000000000000119b <+50>:	ret    
End of assembler dump.
(gdb)

Here, we cannot see any interrupt instruction; not because there is none; but because our debugger is lying here. It will show you disassembly as it looked before setting any breakpoints so that it matches with what compiler generated from source.

Detecting Software Breakpoint

Since we know that software breakpoints are set by overwriting 0xCC at first byte of instruction, we can easily check for such breakpoints in our code:

A trivial implementation looks something like this:

#include <iostream>

bool isBreakpointPresent(unsigned char *func)
{
    bool result = *func == 0xCC;
    return result;
}

void secret()
{
    for (int i = 0; i < 10; ++i)
    {
        std::cout << "Try a breakpoint at secret()" << std::endl;
    }
}

int main()
{
    auto *ptr_secret = (unsigned char*)secret;
    if (isBreakpointPresent(ptr_secret))
        std::cerr << "Breakpoint detected" << std::endl;
    else
        secret();
    return 0;
}

Let us try to see what happens if we run this under debugger:

$ gdb -q ./detect-breakpoint1                                                                                                                                                                                  
Reading symbols from ./detect-breakpoint1...
(gdb) break secret
Breakpoint 1 at 0x118e: file detect-breakpoint1.cpp, line 15.
(gdb) run
Starting program: detect-breakpoint1 

Breakpoint 1, secret () at detect-breakpoint1.cpp:15
15	    for (int i = 0; i < 10; ++i)
(gdb)

Wait, WHAT? It should have printed Breakpoint detected; but it went ahead to call secret() instead. Let us see where our breakpoint has been put in memory:

(gdb) bt
#0  secret () at detect-breakpoint1.cpp:15
#1  0x000055555555521e in main () at detect-breakpoint1.cpp:27
(gdb) disassemble secret
Dump of assembler code for function secret():
   0x0000555555555186 <+0>:	push   rbp
   0x0000555555555187 <+1>:	mov    rbp,rsp
   0x000055555555518a <+4>:	sub    rsp,0x10
=> 0x000055555555518e <+8>:	mov    DWORD PTR [rbp-0x4],0x0
   0x0000555555555195 <+15>:	cmp    DWORD PTR [rbp-0x4],0x9
   0x0000555555555199 <+19>:	jg     0x5555555551c9 <secret()+67>
   0x000055555555519b <+21>:	lea    rsi,[rip+0xe62]        # 0x555555556004
   0x00005555555551a2 <+28>:	lea    rdi,[rip+0x2ed7]        # 0x555555558080 <[email protected]@GLIBCXX_3.4>
   0x00005555555551a9 <+35>:	call   0x555555555040 <[email protected]>
   0x00005555555551ae <+40>:	mov    rdx,rax
   0x00005555555551b1 <+43>:	mov    rax,QWORD PTR [rip+0x2e18]        # 0x555555557fd0
   0x00005555555551b8 <+50>:	mov    rsi,rax
   0x00005555555551bb <+53>:	mov    rdi,rdx
   0x00005555555551be <+56>:	call   0x555555555050 <[email protected]>
   0x00005555555551c3 <+61>:	add    DWORD PTR [rbp-0x4],0x1
   0x00005555555551c7 <+65>:	jmp    0x555555555195 <secret()+15>
   0x00005555555551c9 <+67>:	nop
   0x00005555555551ca <+68>:	leave  
   0x00005555555551cb <+69>:	ret    
End of assembler dump.
(gdb)

If you notice, the debugger has not set breakpoint at very beginning. It has set breakpoint where assembly corresponding to our code starts (right after function prologue). This is why our breakpoint check is failing. But, if we set breakpoint manually on very first instruction, we can see that our detection is working:

(gdb) disassemble secret
Dump of assembler code for function secret():
   0x0000555555555186 <+0>:	push   rbp
   0x0000555555555187 <+1>:	mov    rbp,rsp
   0x000055555555518a <+4>:	sub    rsp,0x10
   0x000055555555518e <+8>:	mov    DWORD PTR [rbp-0x4],0x0
   0x0000555555555195 <+15>:	cmp    DWORD PTR [rbp-0x4],0x9
   0x0000555555555199 <+19>:	jg     0x5555555551c9 <secret()+67>
   0x000055555555519b <+21>:	lea    rsi,[rip+0xe62]        # 0x555555556004
   0x00005555555551a2 <+28>:	lea    rdi,[rip+0x2ed7]        # 0x555555558080 <[email protected]@GLIBCXX_3.4>
   0x00005555555551a9 <+35>:	call   0x555555555040 <[email protected]>
   0x00005555555551ae <+40>:	mov    rdx,rax
   0x00005555555551b1 <+43>:	mov    rax,QWORD PTR [rip+0x2e18]        # 0x555555557fd0
   0x00005555555551b8 <+50>:	mov    rsi,rax
   0x00005555555551bb <+53>:	mov    rdi,rdx
   0x00005555555551be <+56>:	call   0x555555555050 <[email protected]>
   0x00005555555551c3 <+61>:	add    DWORD PTR [rbp-0x4],0x1
   0x00005555555551c7 <+65>:	jmp    0x555555555195 <secret()+15>
   0x00005555555551c9 <+67>:	nop
   0x00005555555551ca <+68>:	leave  
   0x00005555555551cb <+69>:	ret    
End of assembler dump.
(gdb) break * 0x0000555555555186
Breakpoint 2 at 0x555555555186: file detect-breakpoint1.cpp, line 14.
(gdb) continue 
Continuing.
Breakpoint detected
[Inferior 1 (process 52508) exited normally]
(gdb)

Here, we have correctly detected that a breakpoint has been set.

Improved Detection

We can improve the detection to catch breakpoints on any instruction in our protected function:

Getting Offsets

We can compile the source code fully first; and then can use gdb to dump offsets for all instructions. For our previous example, we can list disassembled instructions and offsets as shown below:

(gdb) disassemble secret
Dump of assembler code for function secret():
   0x000000000000128f <+0>:	push   rbp
   0x0000000000001290 <+1>:	mov    rbp,rsp
   0x0000000000001293 <+4>:	sub    rsp,0x10
   0x0000000000001297 <+8>:	mov    DWORD PTR [rbp-0x4],0x0
   0x000000000000129e <+15>:	cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000000012a2 <+19>:	jg     0x12d2 <secret()+67>
   0x00000000000012a4 <+21>:	lea    rsi,[rip+0xd5d]        # 0x2008
   0x00000000000012ab <+28>:	lea    rdi,[rip+0x2e0e]        # 0x40c0 <[email protected]@GLIBCXX_3.4>
   0x00000000000012b2 <+35>:	call   0x1060 <[email protected]>
   0x00000000000012b7 <+40>:	mov    rdx,rax
   0x00000000000012ba <+43>:	mov    rax,QWORD PTR [rip+0x2d0f]        # 0x3fd0
   0x00000000000012c1 <+50>:	mov    rsi,rax
   0x00000000000012c4 <+53>:	mov    rdi,rdx
   0x00000000000012c7 <+56>:	call   0x1090 <[email protected]>
   0x00000000000012cc <+61>:	add    DWORD PTR [rbp-0x4],0x1
   0x00000000000012d0 <+65>:	jmp    0x129e <secret()+15>
   0x00000000000012d2 <+67>:	nop
   0x00000000000012d3 <+68>:	leave  
   0x00000000000012d4 <+69>:	ret    
End of assembler dump.
(gdb)

The data between < and > is the offset that we are looking for. We can run one-liner to extract all offsets in nice usable format:

$ gdb -batch -ex 'file ./detect-breakpoint2' -ex 'disassemble secret' 2>/dev/null | grep "0x" | grep "<+" | awk -F ' ' '{print $2}' | cut -c3- | rev | cut -c3- | rev | sed ':a;N;$!ba;s/\n/, /g'              
0, 1, 4, 8, 15, 19, 21, 28, 35, 40, 43, 50, 53, 56, 61, 65, 67, 68, 69

This one liner can get us offsets in comma separated list, which we can copy and use in our code. Instead of checking only first byte, we have to iterate over list of offsets, find actual address of instructions, and then check if first byte is 0xCC. Sample implementation goes below:

#include <iostream>
#include <unistd.h>
#include <vector>

bool isBreakpointPresent(const unsigned char *func, const std::vector<unsigned int>& offsets)
{
    bool result = false;

    for (auto &i : offsets) {
        if (*(func + i) == 0xCC)
        {
            result = true;
            break;
        }
    }

    return result;
}

void secret()
{
    for (int i = 0; i < 10; ++i)
    {
        std::cout << "Try a breakpoint at secret()" << std::endl;
    }
}

int main()
{
    auto *ptr_secret = (unsigned char*)secret;

    std::vector<unsigned int> offsets = {0, 1, 4, 8, 15, 19, 21, 28, 35, 40, 43, 50, 53, 56, 61, 65, 67, 68, 69};

    if (isBreakpointPresent(ptr_secret, offsets))
        std::cerr << "Breakpoint detected" << std::endl;
    else
        secret();
    return 0;
}

Now let us test this version under debugger:

$ gdb -q ./detect-breakpoint2                                                                                                                                                                                  
Reading symbols from ./detect-breakpoint2...
(gdb) break secret
Breakpoint 1 at 0x1297: file detect-breakpoint2.cpp, line 26.
(gdb) run
Starting program: detect-breakpoint2 
Breakpoint detected
[Inferior 1 (process 70087) exited normally]
(gdb)

And it works!