I have spent the last couple of weeks exploring packers and process injection from an attacker’s perspective in Windows environments. When paired together, both packing and process injection have proven effective in evading antivirus software, specifically Windows Defender. In this post I will discuss the concepts behind packing and process injection as it relates to antivirus evasion.
A packer is a software program that accepts a binary executable file and transforms it into an encrypted version. The encrypted file decrypts itself at runtime an executes the original program. Signature based antivirus software works by detecting known patterns in malicious software files to block their execution. Signature based antivirus can be evaded by encrypting a malicious file as represented on disk giving it a unique signature not cataloged in the antivirus signature database.
To understand the basics of packing Portable Executable files, a general understanding of the PE format and PE loader is required.
The packer I have developed has two components. The first component is a crypter. The crypter takes a PE file as input, encrypts the file and encodes it in a way that is suitable for the packer to decrypt. This is the payload. The second component is the packer source. The output from the crypter is embedded in the .rsrc section of the packer PE file once compiled. The packer source acts as a decrypter and PE loader for the encrypted payload.
The crypter encrypts the input PE file using AES 256 ECB. This was an arbitrary decision that will likely be revisited in a rewrite in the coming weeks (more on that later). The encrypted file is Base64 encoded and saved to a text file for input into the packer build process.
When the packer is executed, the Base64 encoded file is located in the .rsrc section and loaded into memory. Once in memory, the file is Base64 decoded and decrypted using a hard-coded key. This is the reason I plan on revisiting the encryption scheme. The plan is to change this implementation detail in a future rewrite such that a key is chosen in a limited key space so that it can be brute forced at startup from the packer. Brute forcing the key at startup will allow the packer to decrypt the payload without the need for a hard-coded key. A hard-coded key makes static analysis and potentially antivirus detection more feasible.
Once the file has been decrypted, it is handed off to a PE loader to begin execution.
The packer is written in C++ and makes use of both documented and undocumented Windows APIs to load the PE in memory at runtime and execute the embedded payload.
The PE loader makes use of important structures to load the PE into memory, fix up the import address table, and rebase the image if needed.
The PIMAGE_DOS_HEADER is used to validate the PE in addition to acting as a reference to the base of the image.
The PIMAGE_NT_HEADERS structure contains a four-byte signature identifying the file as a PE image. Again, this is used to validate the PE in addition to walking the structure to the IMAGE_OPTIONAL_HEADER and other addresses of importance.
The IMAGE_OPTIONAL_HEADER points to the image base and entry point address. Additionally, we have access to the size of the image allowing us to allocate memory via VirtualAllocEx for the payload.
The PIMAGE_SECTION_HEADER provides access to the address and sizes of the different PE sections. Each section is copied into a new address space for execution.
Once memory is allocated for the image, the packer will copy over the image headers and each of the corresponding image sections. After that, the image is rebased if it is not written to the expected region of memory. The includes looping over the BASE_RELOCATION_TABLE and adjusting each relocation block as the relocation block patching instructions are relative to the image base which has changed.
If we were to execute the code now it would fail. We would need to adjust the import address table by loading the required libraries using LoadLibraryA and using GetProcAddress to set the expected function addresses for the in-memory import address table. I’m not going to go into this in detail. My experience with this approach is that the payload is quickly discovered by the behavior analysis of Windows Defender. To get around this issue I opted to use a process injection technique to further disguise malicious activity.
Instead of executing a malicious payload in the context of the packer, we can instead inject code into a host program. In essence, process injection is the act of running foreign code within the address space of another process. This improves stealth and, in my experience, helps avoid some behavior analysis executed by antivirus on running code.
Process hollowing is one of many different types of process injection techniques. With process hollowing, code is injected into a target program by unmapping the legitimate code from memory and overwriting the memory space with malicious code.
To execute process hollowing, a target process is created in a suspended state. NtUnmapViewOfSection is used to unmap a region of memory from the virtual address space of the running process. From there, memory is allocated at that region using VirtualAllocEx. The payload’s image headers and sections are copied into that region of memory. Just as previously mentioned, the image is rebased if it is not written to the expected region of memory. The includes looping over the BASE_RELOCATION_TABLE and adjusting each relocation block as the relocation block patching instructions are relative to the image base which has changed.
After the packer has finished writing the payload to memory of the target process and rebasing the image, it calls SetThreadContext to point to the new address of entry point. Finally, ResumeThread is called to continue execution of the target process transferring control to the malicious code.
When paired together, both packing and process injection have proven effective in evading antivirus software. In my testing, commonly detected PE files were executed using the above techniques without being picked up by antivirus. I plan to release some code using some of the techniques discussed in this post over the coming weeks. This post will be updated when the time comes.