mountain range under blue sky

Payload obfuscation: Encoding payload in Base64

Overview

In today's series, we will explore how to encode a payload or shellcode in Base64 format using the Windows APIs, and then carry out the reverse process to decode and execute the shellcode. So let`s dive in!

Requirements
  1. Headers:

    • Windows.h

    • Wincrypt.h

    • <iostream>

  2. APIS:

    • CryptBinaryToString*

    • CryptStringToBinary*

    • HeapAlloc

    • HeapFree

    • VirtualAlloc

    • CreateThread

    • VirtualProtect

    • WaitForSingleObject

    • CloseHandle

    • VirtualFree

Steps
  1. import required headers:

  1. We define the shellcode and global variables:

    • g_pBuffer will store a pointer to the shellcode formatted in Base64.

    • g_dwSize will store the g_pBuffer size

  1. Create a function to print the formatted shellcode:

    We create a function to print the result (payload formatted in Base64), where PrintBs64Payload receives a pointer to a constant char as a parameter.

  1. Load Crypt32.dll module in memory:

    We need to load the Crypt32.dll module to use some of it`s functions, such as CryptBinaryToString*, CryptStringToBinary*, etc.

  1. call CryptBinaryToStringA for the first time:

    We call this function for the first time to determine the number of characters that need to be allocated to hold the formatted strings.

  • payload: represents the shellcode, while sizeof(payload) indicates its size.

  • CRYPT_STRING_BASE64: specifies the formatting applied to each byte of the shellcode, whereas CRYPT_STRING_NOCRLF ensures no line breaks are added essential for storing the formatted string in a std::string variable.

  • nullptr: the fourth parameter is set to nullptr, allowing the function to determinate the required number of characters and store the result in the g_dwSize variable.

  • g_dwSize: a pointer to a variable containing the g_pBuffer size .

  1. Dynamic memory allocation:

    We dynamically allocate memory at runtime to store the strings.

  • GetProcessHeap(): retrieves the default handle from the calling process. We can do this by calling the function GetProcessHeap() or HeapCreate()

  • HEAP_ZERO_MEMORY: the allocated memory will be initialized to zero.

  • g_dwSize: the number of bytes to allocate.

  • static_cast<>(): we use it for explicit type conversion.

  1. Call CryptBinaryToString again:

    Call the CryptBinaryToStringA function again to convert each byte of the shellcode into Base64 format, but this time pass the buffer (g_pBuffer) that was previously allocated with HeapAlloc as a parameter.

  1. Clean up the buffer:

    Next, we call PrintBase64Payload() to print the strings and then clean the buffer by releasing its memory with HeapFree().

Final code

After compiling the program, we will obtain something similar to this:

Reversing the process

After briefly exploring and analyzing the output of our program, we now transition to the second phase of this blog, which focuses on reversing the process and subsequently executing the shellcode.

  1. Setting up global variables:

  • Bs64String: this is the shellcode in Base64 format.

  • g_pBuffer: a variable of type PBYTE that stores the bytes of the shellcode after the reverse process is applied.

  • g_pcbSize: a pointer that contains the size, in bytes, of the g_pBuffer variable.

  • g_oldProtect: it is necessary to call the VirtualProtec() function.

  • g_hThread: stores a handle for a newly created thread.

  1. Convert the strings to binary data and allocate memory:

Before moving on to the thread phase, I would like to highlight and break down some parameters and functions from the previous code block:

  • CrypStringToBinary(): it is the function that allows us to reverse the process of formatted shellcode.

    • Bs64String.c_str(): we use the c_str() function to return a pointer to a character array (shellcode), which will be passed as the first parameter to the CryptStringToBinaryA() function.

    • Bs64String.size(): the bs64String() size.

  • g_pBuffer: it is used to allocate a block of memory using VirtualAlloc().

  • VirtualAlloc():

    • g_pcbSize: it contains the size in bytes returned by the first call to CryptStringToBinaryA().

    • MEM_COMMIT | MEM_RESERVE: the type of memory allocation that will be assigned to g_pBuffer.

    • PAGE_READWRITE: a constant for the type of memory protection; in this case, we want, FOR NOW, to have read and write permissions.

Finally, we call CryptStringToBinary() again, almost in the same way as in the previous stage of the original shellcode formatting process, to obtain a pointer to a sequence of bytes.

Create thread and execute shellcode

To simplify things a bit, I have decided to execute the shellcode using the CreateThread() function, which creates a thread in the process's address space.

  • CreateThread(): crucial for thread creation.

    • (LPTHREAD_START_ROUTINE)g_pBuffer: A pointer to our shellcode, and we perform an explicit type conversion using parentheses ().

  • VirtualProtect(): it is useful for modifying memory permissions.

    • PAGE_EXECUTION_READWRITE: We changed the permissions to read, write, and execute (RWX).

    • &oldProtect: receives the access protection value for the page.

  • WaitForSingleObject(): waits until the specified object is in the signaled state or the time-out interval elapses.

Finally, we will perform some cleanup by closing the thread object using CloseHandle() and deallocating the memory block previously allocated with VirtualAlloc().

Final code

Now comes the invigorating part: compiling our project and running it.