10.FingerPrint Locks

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.

What Is Anti Debugging?

Debugging malware code enables a malware analyst to run the malware step by step, introduce changes to memory space, variable values, configurations and more facilitating the understanding of the malware’s behavior, mechanisms and capabilities. For these reasons, debugging is something malware authors would want to prevent.

Anti-Debugging techniques are meant to ensure that a program is not running under a debugger, and if it is, to change its behavior correspondingly. 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 <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:

- Read **/proc/self/stat** word by word, and take fourth word. This is parent process ID.
- Resolve the symlink **/proc/<pid from step 1>/exe**
- Check if symlink is resolving to some known debugger process.

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:

- Call ptrace on own process with PTRACE_TRACEME.
- If the call fails, we are being traced.
- If call succeeds, no other process will be able to trace us.

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?

About the Author

Adhokshaj Mishra works as a security researcher (malware - Linux) at Uptycs. His interest lies in offensive and defensive side of Linux malware research. He has been working on attacks related to containers, kubernetes; and various techniques to write better malware targeting Linux platform. In his free time, he loves to dabble into applied cryptography, and present his work in various security meetups and conferences.