Tradecraft Improvement 1 - Creating PE files with no imports
Introduction
Sometimes there are few steps that we would want to take in order to improve our tradecraft. We might want to make our PE files more difficult to detect or difficult to create signatures for. For this reason, I wanted to start a blog series regarding this particular topic of improving tradecrafts.
In this series I will mainly go through, how one can take steps to make their payloads a little more difficult to be detected or a little more difficult to create signatures for. This particular blog will focus on making a PE file which will make no imports. This means there will be no imported functions in the import table of the PE.
This will mainly going to help you reduce detection through static signatures, where the AV engine is trying to detect a certain type of malware by checking the imported functions.
For example, we a PE file has a certain sequence of functions like, CreateProcessA, WriteProcessMemory, VirtualProtect, WaitForSingleObject
etc. a signature based AV engine might be able to flag the binary as a malicious file.
Checking the PE file with imports table
We will be using the following code as a test sample.
test.c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main()
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcessA(NULL, "C:\\Windows\\System32\\cmd.exe", NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
return -1;
}
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
The above code will spawn a cmd.exe
instance. It also imports various functions, which are used to perform this task. Even though this file is not inherently malicious, but this will server our purpose of explaining how we can use it for more calling other Win32 APIs
Compile using the following command :
x86_64-w64-mingw32-gcc test.c -o test.exe
In our Windows machine we will use PE Bear to check the imports table of the binary.
We see that we have 2 imports in the Imports table. We can use the dumpbin
utility to check if we have the functions that we used are imported or not
We find our functions to be there.
Dynamically resolving Win32 APIs
- The first step towards reducing the number of imports in the imports table is to dynamically resolve Win32 APIs
- This can be done by using
GetProcAddress
andGetModuleHandle
Win32 APIs - First write the function definition
typedef BOOL (WINAPI *CreateProcessA_t)(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
- Then create a pointer to the function using the above mentioned APIs
CreateProcessA_t CreateProcessA_p = (CreateProcessA_t)MyGetProcAddress(MyGetModuleHandle(L"KERNEL32.DLL"), "CreateProcessA");
- Now this pointer can be used to invoke the Win32 API as required
Replacing GetProcAddress and GetModuleHandle APIs
If we are using the APIs GetProcAddress
and GetModuleHandle
APIs in our binary, then what if the AV engine is trying to check specifically these two APIs? In this case we need to replace these 2 APIs.
In my previous blog about Perun’s Fart I have discussed about how we can get the base address of a DLL by parsing the Loader data in the PE file. Here I have modified this function a bit so that it resembles the original GetModuleHandle
function
HMODULE WINAPI MyGetModuleHandle(LPCWSTR sModuleName)
{
//Get pointer to PEB
_PPEB ppeb = (_PPEB)__readgsqword(0x60);
ULONG_PTR pLdr = (ULONG_PTR)ppeb->pLdr;
ULONG_PTR val1 = (ULONG_PTR)((PPEB_LDR_DATA)pLdr)->InMemoryOrderModuleList.Flink;
PVOID dllBase = NULL;
while (val1)
{
PWSTR DllName = ((PLDR_DATA_TABLE_ENTRY)val1)->BaseDllName.Buffer;
dllBase = (PVOID)((PLDR_DATA_TABLE_ENTRY)val1)->DllBase;
if (strcmp((const char*)sModuleName, (const char*)DllName) == 0)
{
break;
}
val1 = DEREF_64(val1);
}
return (HMODULE)dllBase;
}
-
This is similar to the one that I have discussed in my previous blog.
-
It uses the
PEB
to get to the loader data -
Then traverses the
InMemoryOrderModuleList
to look for the particular DLL and returns its base address. -
The only difference it that it takes wide char like the original
GetModuleHandle
-
Now we need to replace the
GetProcAddress
function -
This also I had discussed previously in my previous blog.
-
But this implementation is much more closer to the original
GetProcAddress
function
FARPROC WINAPI MyGetProcAddress(HMODULE hMod, char *sProcName)
{
UINT64 dllAddress = (UINT64)hMod;
PIMAGE_DOS_HEADER ImageDosHeader = (PIMAGE_DOS_HEADER)dllAddress;
PIMAGE_NT_HEADERS NTHeaders = (PIMAGE_NT_HEADERS)(dllAddress + ImageDosHeader->e_lfanew);
PIMAGE_DATA_DIRECTORY ImageDataDirectory = (PIMAGE_DATA_DIRECTORY)&NTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
PIMAGE_EXPORT_DIRECTORY ppImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(dllAddress + ImageDataDirectory->VirtualAddress);
PDWORD addressOfFunctions = (PDWORD)(dllAddress + ppImageExportDirectory->AddressOfFunctions);
PDWORD addressOfNames = (PDWORD)(dllAddress + ppImageExportDirectory->AddressOfNames);
PWORD addressOfNameOrdinals = (PWORD)(dllAddress + ppImageExportDirectory->AddressOfNameOrdinals);
PVOID funcAddress = 0x00;
for (WORD cx = 0; cx < ppImageExportDirectory->NumberOfNames; cx++)
{
PCHAR functionName = (PCHAR)(dllAddress + addressOfNames[cx]);
if (strcmp(sProcName, functionName) == 0)
{
PVOID funcAddress = (PVOID)(dllAddress + addressOfFunctions[addressOfNameOrdinals[cx]]);
return (FARPROC)funcAddress;
}
}
return (FARPROC)funcAddress;
}
- We first use the DLL base address to extract the Export Directory.
- From the Export Directory we can get the names of the functions and check for our required function
- When we find this function we return the address of the function
Further improving our payload
The above steps will reduce the number of imported functions. But there are some important APIs that are imported by default by the compiler. To improve upon this we can use a few steps.
Changing the entry point of the binary
- First we need to write a custom entry point for our PE file
- Use the
WinMain
convention for this. This is because, we can use this to create a GUI application as well
int WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
);
- This will be our new
main
function and we will implement our actual workings here
int WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
//CreateProcessA_t CreateProcessA_p = (CreateProcessA_t)GetProcAddress(GetModuleHandle("kernel32.dll"), "CreateProcessA");
//WaitForSingleObject_t WaitForSingleObject_p = (WaitForSingleObject_t)GetProcAddress(GetModuleHandle("kernel32.dll"), "WaitForSingleObject");
//CloseHandle_t CloseHandle_p = (CloseHandle_t)GetProcAddress(GetModuleHandle("kernel32.dll"), "CloseHandle");
CreateProcessA_t CreateProcessA_p = (CreateProcessA_t)MyGetProcAddress(MyGetModuleHandle(L"KERNEL32.DLL"), "CreateProcessA");
WaitForSingleObject_t WaitForSingleObject_p = (WaitForSingleObject_t)MyGetProcAddress(MyGetModuleHandle(L"KERNEL32.DLL"), "WaitForSingleObject");
CloseHandle_t CloseHandle_p = (CloseHandle_t)MyGetProcAddress(MyGetModuleHandle(L"KERNEL32.DLL"), "CloseHandle");
if (!CreateProcessA_p(NULL, "C:\\Windows\\System32\\cmd.exe", NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
return -1;
}
WaitForSingleObject_p(pi.hProcess, INFINITE);
CloseHandle_p(pi.hProcess);
CloseHandle_p(pi.hThread);
return 0;
}
- Here we see the full implementation of
GetProcAddress, GetModuleHandle
and our custom entry point which will beWinMain
Compiling the binary
- In order to quickly compile the binary, we will writing a
Makefile
- The
Makefile
should look something like following:
make:
x86_64-w64-mingw32-gcc main.c -Wall -ffunction-sections -O2 -o noimports.exe -nostdlib -fno-ident -Wl,-eWinMain,-subsystem,windows,--no-seh
- For compiling, we will be using the
mingw
compiler
-Wall
-> This tells the compiler to display all warnings-ffunction-sections
-> This will place each function into its own section in the output file, if the target supports arbitrary sections-O2
-> This is more increasing optimization-nostdlib
-> This tells the compiler to not use the standard system startup files or libraries when linking. This also makes sure that no startup files and only the libraries that we specify will be passed to the linker.-Wl
-> This is used to pass arguments to the linker-e
-> This is used to specify the entry point of the PE.- When we write
-eWinMain
, it will change the entry point toWinMain
. -subsystem,windows
-> Will tell the compiler that the PE file is a windows GUI program and not a console application. This will prevent the popping up of console in the background whenever the program is launched.
- When we write
Testing the final binary
- Now we run the binary to check if it is working or not
- Then check the PE file in
PE Bear
- This shows us that the PE file has no imports. Not even the ones imported by compiler for startup and other purposes
- Also checking the
dumpbin
utility shows us that there are no imports in the PE file
Conclusions
This blog is just a short note on how we can create a PE file without any imports. This might be helpful in avoiding some static signatures in some cases. There can be other improvements that we can do to make our tradecraft even better, like encrypting the strings that we use in our code. This will entirely remove any reference or strings to the Win32 APIs that we have used.
The source code to this blog can be found here : https://github.com/dosxuz/TradecraftImrprovement/tree/main/Part-1