..

Anti Debugging For Noobs

What good is a keylogger (or any such tool, for that matter), that is reversed using a debugger within minutes? Let’s level up just a little bit, and try to make malware analyst’s job slightly more involved.

Anti Debugging

At very basic level, anti-debugging is act of detecting and preventing attempts of getting debugged. These techniques may be generic enough to be applicable to all debuggers; or maybe so specific that they are applicable only to one specific version of specific debugger.

There are many ways to do this:

  1. Timing analysis
  2. Detecting known processes
  3. Checking process status
  4. Self-debugging code
  5. Detecting breakpoints
  6. Detecting code patching
  7. In-Memory hypervisor
  8. Non-standard architecture emulation

We will look into some simple techniques.

Timing Analysis

The basic idea behind this that the analyst will probably pause the program, and cause too long delay in execution. If we measure time in normal execution, we can detect delays, and decide to change behaviour in such cases. This method is easiest to implement, easiest to get false positives, and easiest to disable.

A very basic example to do this is given below:

#include <iostream>
#include <chrono>
#include <filesystem>
#include <string>
#include <thread>

using namespace std::chrono_literals;

int main() {
    std::chrono::steady_clock::time_point begin, end;
    std::cout << "Hello, World!" << std::endl;

    while (true) {
        begin = std::chrono::steady_clock::now();
        for (auto &p : std::filesystem::directory_iterator("/proc")) {
            std::string pid = p.path().filename().string();

            try {
                long _pid = std::stol(pid);
                std::cout << _pid << " " << std::flush;
            }
            catch (...) {
                continue;
            }
        }
        std::this_thread::sleep_for(1s);
        end = std::chrono::steady_clock::now();

        unsigned long duration = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count();

        if (duration > 1010000) {
            std::cerr << "Debugging attempt detected (" << duration << " us)" << std::endl;
            break;
        }
    }

    return 0;
}

Checking Common Debugger Processes

Another commonly used technique is to check if some known debugging tool is your parent process (or if they are running at all). In linux, parent process can be looked up either by parsing /proc/self/stat or by parsing /proc/self/status.

The logic is something like this:

If you are going to use /proc/self/status instead, you can take line starting from PPid: to extract parent process ID.

An example implementation of this is given below:

#include <cstring>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <linux/limits.h>
#include <unistd.h>

bool hasEnding (std::string const &fullString, std::string const &ending) {
    if (fullString.length() >= ending.length()) {
        return (0 == fullString.compare (fullString.length() - ending.length(), ending.length(), ending));
    } else {
        return false;
    }
}

bool isUnderDebugger()
{
    bool result = false;
    /*
     * /proc/self/stat has PID of parent process as fourth parameter.
     */
    std::string stat;
    std::ifstream file("/proc/self/stat");

    for(int i = 0; i < 4; ++i)
        file >> stat;

    std::string parent_path = std::string("/proc/") + stat + "/exe";
    char path[PATH_MAX + 1];
    memset(path, 0, PATH_MAX + 1);
    readlink(parent_path.c_str(), path, PATH_MAX);

    std::vector<std::string> debuggers = {"gdb", "lldb-server"};

    for (auto &p: debuggers)
    {
        if (hasEnding(std::string(path), p))
        {
            result = true;
            break;
        }
    }

    return result;
}

int main() {
    if (isUnderDebugger())
        std::cout << "I am being debugged." << std::endl;
    else
        std::cout << "I am not being debugged" << std::endl;
    return 0;
}

Checking Process Status

This technique is Linux equivalent of IsDebuggerPresent from Windows land. Just like Windows indicates whether a process is being debugged in PEB, Linux indicates tracer process ID in /proc/self/status file. If the process is being traced/debugged, the tracer PID field will show process ID of debugger or tracer; otherwise it will be 0.

An example implementation to use this technique is as below:

#include <iostream>
#include <fstream>
#include <string>
#include <sstream>

bool isUnderDebugger()
{
    bool result = false;

    std::string line;
    std::ifstream file("/proc/self/status");

    while (std::getline(file, line))
    {
        std::istringstream _stream(line);
        std::string tag, value;
        _stream >> tag >> value;
        
        if (tag == "TracerPid:" && value != "0")
            result = true;
    }

    return result;
}

int main() {
    if (isUnderDebugger())
        std::cout << "I am being debugged." << std::endl;
    else
        std::cout << "I am not being debugged" << std::endl;
    return 0;
}

Self-debugging Code

This method relies on a limitation of debugging: a process can be debugged/traced by only one process at a time. In other words, if you try to trace a program already being traced by something else, your attempt will fail. This in turn can be used to check if our process is getting debugged or not (and if not, to prevent it from getting debugged).

The basic logic goes as follows:

A basic implementation goes as below:

#include <iostream>
#include <sys/ptrace.h>

int main()
{
    if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1)
    {
        std::cout<< "don't trace me !!" << std::endl;
        return 1;
    }

    std::cout << "normal execution" << std::endl;
    return 0;
}

While this method is really easy to implement, it is also trivially easy to remove: the ptrace() call be patched either statically or dynamically. An easy way to deal with this is to call ptrace() multiple times, and change some tracked internal state on each call. If internal state does not match with expected state, ptrace() was probably patched.

At very basic level, you may have some variable whose value gets changed on each ptrace(). Then somewhere else in your code, you can check if this variable holds expected value or not.

An example implementation goes as below:

#include <iostream>
#include <sys/ptrace.h>

int main()
{
    int offset = 0;

    if (ptrace(PTRACE_TRACEME, 0, 1, 0) == 0)
    {
        offset = 2;
    }

    if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1)
    {
        offset = offset * 3;
    }

    if (offset == 2 * 3)
    {
        std::cout << "normal execution" << std::endl;
    }
    else
    {
        std::cout<< "don't trace me !!" << std::endl;
    }

    return 0;
}

Exercise for you

Can you disable the ptrace with internal state mechanism? How can you improve the mechanism to makes it more stronger?