Writing a simple self-injecting packer
David S January 07, 2024 Updated: January 07, 2024 #Malware #Evasion #WindowsI wanted to know how easy it is for malware to evade anti-virus detection and decided to write my own self-injecting packer.
Packers
Packers are mostly used by malware during their first stage to hide from EDR when they are written on-disk. They create self-injecting binaries which store the packed (therefore the name) malware payload (which is also a PE file) mostly inside a data section. The payload may be compressed and/or encrypted.
Most of a packer's work is the same as the loader has, namely PE header parsing, memory aligning, import resolving and relocation.
Hence, when a packed binary is run it will (at some point):
- unpack the payload binary by decompressing/decrypting it
- parse the PE header of the unpacked payload
- allocate rwx memory for the aligned binary
- parse the section table and load the sections aligned into the allocated memory
- parse the import table, find each to-be-imported function and write it into the address table
- parse the relocation table and calculate the non-relative address from the base address of the allocated memory and the RVA for each table entry
- finally jump to the addressOfEntrypoint of the payload which lays inside the allocated memory
Implementation
You can find the code in this repository.
Most of the loading stage code is based on the tutorial by frank2.
Evasion by payload encryption
Since emulating the loader is straightforward, I want to take a closer look how one can store the payload inside the packed binary.
XOR-ing with a random key
This idea is quite similar to some kind of one-time pad (for that, key length == payload length). One may:
- generate a n bytes long key
uint8_t *
- xor the payload with the key
- finally store the key alongside the payload inside the packed binary
During unpacking one has to xor the encrypted payload with the key again.
size_t km=key_len-1;
for
Lowering the entropy
Just xor-ing each byte of the payload with the key leads to an increased entropy of the packed executable.
Instead, one can use a bytewise substitution using a 8-bit (one byte) lookup table.
uint8_t *
This way, each byte of the payload is substituted by the same value each time it occurs, which is an isomorphism. Hence, the entropy of the encrypted payload is not higher than the plain one.
During unpacking, one uses the inverse lookup table to decrypt the payload. Additionally, instead of decrypting the bytes consecutively, one can decrypt in congruence classes. This makes signature detection harder.
for
The following images display the entropy calculated of a packed version of mimikatz using the simple serial xor-ing with the key and a bytewise substitution, respectively.
As we can see, the entropy of the packed binary with bytewise substitution is much lower. One can now not conclude from the entropy of the binary whether it is packed or not. This makes it harder to flag the binary as malicious.
Conclusion
As you can see in the screenshot above, Windows Defender is unable to detect the packed mimikatz in the binary.
It seems to be quite easy to bypass the static analysis of Windows Defender by just encrypting the payload. One should use a better EDR.