..

Anti Debugging For Noobs Part 4

In previous part, we covered anti-debugging by detecting presence of software breakpoints. Today, we will move one step ahead, and remove detected breakpoints!

Removing Breakpoints

Since software breakpoints are set by overwriting first byte of target instruction, we can remove them by restoring the first byte of all affected instructions. There are two ways to get first byte of all instructions in your target function:

  1. Start from address of first instruction, and read first byte of all the required instructions.
  2. Use some utility like objdump to dump machine bytes as well as instructions.

If you want to go by route 1, you can modify program in previous blog post. A possible code to do this is below:

for (auto &offset : offsets)
{
    original_bytes.push_back(*(ptr_secret + offset));
}

std::ios_base::fmtflags f(std::cout.flags());

for (auto &raw_byte : original_bytes)
{
    std::cout << "0x" << std::hex << static_cast<unsigned short>(raw_byte) << ", ";
}

std::cout.flags(f);
std::cout<<std::flush;

For 2, objdump can be invoked like this:

$ objdump -d -M intel ./detect-breakpoint2 | grep "<_Z6secretv>:" -A 20                                                                                                                                        
000000000000128f <_Z6secretv>:
    128f:	55                   	push   rbp
    1290:	48 89 e5             	mov    rbp,rsp
    1293:	48 83 ec 10          	sub    rsp,0x10
    1297:	c7 45 fc 00 00 00 00 	mov    DWORD PTR [rbp-0x4],0x0
    129e:	83 7d fc 09          	cmp    DWORD PTR [rbp-0x4],0x9
    12a2:	7f 2e                	jg     12d2 <_Z6secretv+0x43>
    12a4:	48 8d 35 5d 0d 00 00 	lea    rsi,[rip+0xd5d]        # 2008 <_IO_stdin_used+0x8>
    12ab:	48 8d 3d 0e 2e 00 00 	lea    rdi,[rip+0x2e0e]        # 40c0 <[email protected]@GLIBCXX_3.4>
    12b2:	e8 a9 fd ff ff       	call   1060 <[email protected]>
    12b7:	48 89 c2             	mov    rdx,rax
    12ba:	48 8b 05 0f 2d 00 00 	mov    rax,QWORD PTR [rip+0x2d0f]        # 3fd0 <[email protected]XX_3.4>
    12c1:	48 89 c6             	mov    rsi,rax
    12c4:	48 89 d7             	mov    rdi,rdx
    12c7:	e8 c4 fd ff ff       	call   1090 <[email protected]>
    12cc:	83 45 fc 01          	add    DWORD PTR [rbp-0x4],0x1
    12d0:	eb cc                	jmp    129e <_Z6secretv+0xf>
    12d2:	90                   	nop
    12d3:	c9                   	leave  
    12d4:	c3                   	ret    

The number after -A will need some trial and error to correctly figure out. You can either manually copy the first bytes; or if you are lazy like me, can use this one-liner:

$ objdump -d -M intel ./detect-breakpoint2 | grep "<_Z6secretv>:" -A 19 | tail -n 19 | awk '{print $2}' | sed ':a;N;$!ba;s/\n/, 0x/g'                                                                          
55, 0x48, 0x48, 0xc7, 0x83, 0x7f, 0x48, 0x48, 0xe8, 0x48, 0x48, 0x48, 0x48, 0xe8, 0x83, 0xeb, 0x90, 0xc9, 0xc3

For those who are not super comfortable with that one-liner, this is what it does:

Now, we can copy the output and put it inside a vector (don’t forget to put 0x before first entry!).

Here, try restoring first bytes for instructions. Program will crash immediately. What went wrong?

Executable Space Protection

Executable-space protection marks memory regions as non-executable, such that an attempt to execute machine code in these regions will cause an exception. General implementations use hardware features like NX bit (on intel/AMD chips), or XN bit (ARM chips). Operating System may mark all or some parts of memory as non-executable. Generally, only the parts where some executable code is loaded are marked executable, and rest of memory is marked non-executable. Similarly, there are read and write protections as well (e.g., area containing code will be readable, but not writable).

Many operating systems also provide ways to set desired permissions on memory.

Changing Memory Permission

On Linux, we can use mprotect call to change permission, which takes following parameters:

Unlike operating systems like Windows, Linux does not save previous permission on memory range anywhere. If you want to restore the original permission, you have to parse /proc/<pid>/maps to figure out memory protection before changing that.

Now, before we can even call mprotect, we need to figure out three key details:

The page size can be found by

long pagesize = sysconf(_SC_PAGESIZE);

Starting address of page for any arbitrary address can be found by

unsigned long page_start = (unsigned long)address & ~(pagesize - 1);

NOTE: Finding number of pages is left as an excercise for the reader.

Now, code to change memory permission looks like this:

long pagesize = sysconf(_SC_PAGESIZE);
unsigned long page_start = (unsigned long)func & ~(pagesize - 1);

if (mprotect((void*)page_start, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0)
{
    ...
}

Here we are changing permission only on one page, because our function fits inside single page (which is not guaranteed for even small functions). Now we can overwrite instructions without getting a crash. A sample implementation is given below:

#include <iostream>
#include <sys/mman.h>
#include <unistd.h>
#include <vector>

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

    if (offsets.size() > original_bytes.size())
        return false;

    long pagesize = sysconf(_SC_PAGESIZE);
    unsigned long page_start = (unsigned long)func & ~(pagesize - 1);

    if (mprotect((void*)page_start, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0)
    {
        std::cerr << "mprotect() failed" << std::endl;
        return false;
    }

    for (auto i = 0; i < offsets.size(); ++i)
    {
        if (*(func + offsets[i]) != original_bytes[i]) {
            *(func + offsets[i]) = original_bytes[i];
            result = true;
        }
    }

    return result;
}

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};
    std::vector<unsigned char> original_bytes = {0x55, 0x48, 0x48, 0xc7, 0x83, 0x7f, 0x48, 0x48, 0xe8, 0x48, 0x48,
                                                 0x48, 0x48, 0xe8, 0x83, 0xeb, 0x90, 0xc9, 0xc3};

    if (isBreakpointPresent(ptr_secret, offsets)) {
        std::cerr << "Breakpoint detected" << std::endl;
        if (removeBreakpoint(ptr_secret, offsets, original_bytes)) {
            std::cout << "Breakpoint removed" << std::endl;
            secret();
        }
        else
            std::cerr << "Cannot remove breakpoint" << std::endl;
    }
    else
        secret();
    return 0;
}

To test this, we can run this code under debugger, set a breakpoint, and see what happens:

$ gdb -q ./detect-breakpoint3                                                                                                                                                                                  
Reading symbols from ./detect-breakpoint3...
(gdb) break secret
Breakpoint 1 at 0x1431: file detect-breakpoint3.cpp, line 55.
(gdb) run
Starting program: detect-breakpoint3 
Breakpoint detected
Breakpoint removed
Try a breakpoint at secret()
Try a breakpoint at secret()
Try a breakpoint at secret()
Try a breakpoint at secret()
Try a breakpoint at secret()
Try a breakpoint at secret()
Try a breakpoint at secret()
Try a breakpoint at secret()
Try a breakpoint at secret()
Try a breakpoint at secret()
[Inferior 1 (process 38154) exited normally]
(gdb)

And, we have successfully removed breakpoint from our code.

Excercise For The Readers

  1. Write code which sets a breakpoint at unrelated part of code, for every removed breakpoint from critical code.
  2. Write code which restores all breakpoints once critical function is finished.
  3. Modify the previous code to restore all breakpoints when Ctrl + C is hit.