Writing a simple self-injecting packer

David S January 07, 2024 Updated: January 07, 2024 #Malware #Evasion #Windows

I 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):

  1. unpack the payload binary by decompressing/decrypting it
  2. parse the PE header of the unpacked payload
  3. allocate rwx memory for the aligned binary
  4. parse the section table and load the sections aligned into the allocated memory
  5. parse the import table, find each to-be-imported function and write it into the address table
  6. 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
  7. 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:

  1. generate a n bytes long key
uint8_t *generate_key(size_t len) {
  uint8_t *key = malloc(len * sizeof(uint8_t));
  if(key == NULL) {
    return NULL;
  }

  srand(time(NULL));

  for(size_t i=0;i<len;++i){
    key[i]=rand()&0xff;
  }

  return key;
}
  1. xor the payload with the key
  2. 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(size_t p=0; p<payload_len; ++p) {
    payload[p]^=key[p&km];
}

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 *generate_key(void) {
  list_t *list = NULL;
  uint8_t *key = NULL;

  list = list_new(256);
  if(list == NULL)
    goto error;

  for(size_t i=0;i<=255;++i)
    list_append(list, (uint8_t) i);

  key = malloc(256 * sizeof(uint8_t));
  if(key == NULL)
    goto error;

  srand(time(NULL));

  uint8_t val;
  for(size_t i=0;i<256;++i){
    if(!list_remove(list, rand()%list->len, &val))
      goto error;

    key[i]=val;
  }

  return key;

error:
  list_free(list);
  free(key);

  return NULL;
}

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(size_t s=0; s<16; ++s) {
  for(size_t p=s; p<payload_len; p+=16) {
    payload[p]=key[payload[p]];
  }
}

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.

overall entropy with serially xor-ed payload

overall entropy with bytewise substituted payload

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

mimikatz, packed by the low-entropy version 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.