Skip to main content

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 and GetModuleHandle 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 be WinMain

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
  1. -Wall -> This tells the compiler to display all warnings
  2. -ffunction-sections -> This will place each function into its own section in the output file, if the target supports arbitrary sections
  3. -O2 -> This is more increasing optimization
  4. -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.
  5. -Wl -> This is used to pass arguments to the linker
  6. -e -> This is used to specify the entry point of the PE.
    • When we write -eWinMain , it will change the entry point to WinMain.
    • -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.

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

References

  1. https://manpages.debian.org/unstable/binutils-mingw-w64-x86-64/x86_64-w64-mingw32-ld.1.en.html
  2. https://man7.org/linux/man-pages/man1/gcc.1.html
  3. https://github.com/paranoidninja/PIC-Get-Privileges/blob/main/addresshunter.h