gemesa@home:~$

Reversing the Almanahe worm

Table of contents

Introduction

This post analyses a worm/rootkit malware sample using Ghidra. The primary goal is to identify and document the process injection techniques used by the binary, map them to MITRE ATT&CK TTPs and trace the infection lifecycle. Additionally, we provide YARA and Sigma detection rules targeting the identified injection behaviors.

Executive summary

This is a worm that spreads by infecting EXE files on local drives, USB drives and network shares. The malware itself is a DLL pretending to be linkinfo.dll. This is a legit Windows library with the Windows Volume Tracking description. It proxies the real exports so nothing looks broken to the host process.

The malware uses two different process injection techniques (APC injection and thread hijacking) at different stages of its lifecycle to get its code running inside trusted processes.

Hashes

$ mv f74399cc0be275376dad23151e3d0c2e2a1c966e6db6a695a05ec1a30551c0ad worm.dll

$ md5sum < worm.dll 
26f5d66b94d3bf161a896323f753a06b  -

$ sha1sum < worm.dll 
974ca8f4a4e92b31ecb23eb351da5a0027c0b764  -

$ sha256sum < worm.dll
f74399cc0be275376dad23151e3d0c2e2a1c966e6db6a695a05ec1a30551c0ad  -

Infection lifecycle

The malware spreads by infecting EXE files on drives and network shares. When someone runs an infected EXE:

  1. A shellcode stub runs first: decompresses the malware DLL, drops it to %TEMP%\linkinfo.dll.
  2. Loads the DLL: DllMain checks “am I in explorer?”. If not, it does nothing.
  3. Calls ordinal 101: installs a rootkit driver, then APC-injects into explorer.exe (1. injection method).
  4. Explorer loads the DLL: DllMain checks again “am I in explorer?”. If yes, full activation.
  5. Spawns 5 worker threads: file infector, network worm, browser injection, rootkit/AV killer, C2 handler.
  6. Browser injection thread uses thread hijacking (2. injection method) to spawn hidden browsers for C2 traffic.
  7. The shellcode restores the original EXE code and jumps to the real entry point.

The cycle repeats: the file infector thread searches drives looking for more EXEs to infect.

The two injections

The two injection techniques serve completely different purposes at different stages:

  APC injection Thread hijacking
Purpose Bootstrap into explorer (activation). C2 communication.
When Once, when an infected EXE runs. Repeatedly, in a loop (so the malware always has a browser).
Target explorer.exe Default browser.
Why that target Malware only activates inside explorer. Network traffic looks normal from a browser.
Trigger Shellcode stub calls ordinal 101. Worker thread inside explorer.

So simply put, APC injection is “the door into the house”. Thread hijacking is “opening a window to talk to the outside”.

Technique 1: Thread execution hijacking

MITRE ATT&CK: T1055.003 — Process Injection: Thread Execution Hijacking

How it works

First, it spawns a frozen browser (FUN_10002232_create_suspended_browser):

  • Grabs the default browser path from HKCR\htmlfile\shell\open\command (reference).
  • Launches it with CreateProcessA using CREATE_SUSPENDED (flag 0x4) (reference).

Then it hijacks the thread (FUN_10002096_hijack_thread_inject_shellcode):

  • Calls GetThreadContext with CONTEXT_FULL (0x10007) to read the suspended thread’s registers (e.g. EIP, the instruction pointer).
  • Allocates a chunk of RWX memory inside the browser with VirtualAllocEx (PAGE_EXECUTE_READWRITE, 0x40) (reference).
  • Builds a small shellcode stub locally that calls LoadLibraryA with this DLL path, then calls ordinal 100 (FUN_10001e4c_c2_comm).
  • Writes the whole shellcode blob into the browser’s memory with WriteProcessMemory.
  • Overwrites ctx.Eip to point at the shellcode, then commits it with SetThreadContext.
  • Calls ResumeThread. The browser runs the shellcode instead of its normal startup code.

What actually gets executed: The shellcode calls LoadLibraryA with the malware DLL path, then calls ordinal 100 (FUN_10001e4c_c2_comm). The malware is now doing its network activity from inside the browser process.

API call sequence:

CreateProcessA(..., CREATE_SUSPENDED, ...)
GetThreadContext(hThread, CONTEXT_FULL)
VirtualAllocEx(hProcess, ..., PAGE_EXECUTE_READWRITE)
WriteProcessMemory(hProcess, shellcode + DLL path)
SetThreadContext(hThread, ...)   // EIP --> shellcode
ResumeThread(hThread)

Callgraph

entry (DllMain, DLL_PROCESS_ATTACH)
  - FUN_10005303_dll_init
    - FUN_10003f3c_load_real_linkinfo
    - FUN_100052a2_check_if_explorer
      - CreateThread --> FUN_1000515e_main_thread
        - FUN_10003edc_vm_check
        - CreateMutex("PNP#DMUTEX#1#DL5")
        - FUN_10003aa8_prepare_payload  // compress own DLL
        - FUN_100010bb_peb_unlink_self  // hide DLL from module lists
        - CreateThread --> FUN_100042cd_hosts_and_inject_browser
          - [loop]
            - FUN_10002232_create_suspended_browser
              - read default browser from HKCR\htmlfile\shell\open\command
              - CreateProcessA(..., CREATE_SUSPENDED)
              - FUN_10002096_hijack_thread_inject_shellcode
                - GetThreadContext(CONTEXT_FULL)
                - VirtualAllocEx(PAGE_EXECUTE_READWRITE)
                - WriteProcessMemory (shellcode + DLL path + ordinal 100 RVA)
                - SetThreadContext (EIP --> shellcode)
                - ResumeThread

Technique 2: APC injection

MITRE ATT&CK: T1055.004 — Process Injection: Asynchronous Procedure Call

How it works

This path only executes when someone runs an infected EXE. The shellcode stub drops the malware DLL, loads it and calls ordinal 101.

Ordinal 101 (FUN_10003e12_driver_and_apc_entry) is just two instructions:

CALL FUN_100036e4_drop_and_load_rootkit_driver
JMP  FUN_100039fb_apc_injection_entry

APC injection:

  • Checks if mutex PNP#DMUTEX#1#DL5 exists. If it does, the malware is already in explorer, so return early (FUN_100039fb_apc_injection_entry).
  • Enables SeDebugPrivilege (requires admin) via OpenProcessToken –> LookupPrivilegeValueA –> AdjustTokenPrivileges.
  • Takes a process snapshot, walks it to find the PID of explorer.exe.
  • Takes a thread snapshot, collects every thread ID of explorer.
  • Opens explorer with PROCESS_ALL_ACCESS (FUN_10003919_apc_inject_into_explorer).
  • Allocates memory in explorer, writes the malware DLL path into it.
  • For each explorer thread: opens it with THREAD_ALL_ACCESS and calls QueueUserAPC(LoadLibraryA, hThread, dllPath).

The next time any explorer thread enters an alertable wait (which happens often), Windows runs the queued APC. This calls LoadLibraryA, which loads the DLL and triggers DllMain inside explorer. Hitting every thread guarantees almost immediate execution.

API call sequence:

AdjustTokenPrivileges(SeDebugPrivilege)
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS) // find explorer.exe PID
CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD)  // get all explorer threads
OpenProcess(PROCESS_ALL_ACCESS, explorerPID)
VirtualAllocEx(...)
WriteProcessMemory(hProcess, dllPath)
for each thread:
  OpenThread(THREAD_ALL_ACCESS, threadID)
  QueueUserAPC(LoadLibraryA, hThread, remoteAddr)

Callgraph

Infected EXE runs:
  - Shellcode stub (DAT_100062a8, appended to infected EXE)
    - walks SEH chain, finds kernel32.dll base
    - resolves APIs from kernel32 export table (GetProcAddress, LoadLibraryA, CreateFileA, WriteFile, etc.)
    - decompresses malware DLL from appended data
    - drops to disk as %TEMP%\linkinfo.dll
    - LoadLibraryA("linkinfo.dll") --> DllMain --> not explorer --> does nothing
    - GetProcAddress(hModule, ordinal 101)
    - call ordinal 101 (FUN_10003e12_driver_and_apc_entry):
      - FUN_100036e4_drop_and_load_rootkit_driver // IsDrv118.sys
      - FUN_100039fb_apc_injection_entry
        - CreateMutex("PNP#DMUTEX#1#DL5") // skip if already exists
        - FUN_100011bd_enable_debug_privilege
          - OpenProcessToken --> LookupPrivilegeValueA --> AdjustTokenPrivileges
        - FUN_10003919_apc_inject_into_explorer
          - FUN_100037be_find_explorer_pid
            - CreateToolhelp32Snapshot --> Process32First/Next --> match "explorer.exe"
          - FUN_10003886_enumerate_threads_by_pid
            - CreateToolhelp32Snapshot --> Thread32First/Next --> collect thread IDs
          - OpenProcess(PROCESS_ALL_ACCESS, explorerPID)
          - VirtualAllocEx + WriteProcessMemory (DLL path)
          - for each thread:
            - OpenThread --> QueueUserAPC(LoadLibraryA, hThread, dllPath)
    - explorer loads DLL --> DllMain sees "I am in explorer" --> full activation
    - restores original overwritten code bytes to EXE's code section
    - jmp to original entry point --> program runs normally

IOCs

YARA

Note: the rules are available here as well.

$ yara worm.yar ../tmp/worm.dll
thread_execution_hijacking_suspended_browser ../tmp/worm.dll
APC_injection_explorer_LoadLibrary ../tmp/worm.dll
linkinfo_DLL_injection_shellcode ../tmp/worm.dll
/*
    YARA rules for process injection TTPs.
    Target: f74399cc0be275376dad23151e3d0c2e2a1c966e6db6a695a05ec1a30551c0ad
    https://www.virustotal.com/gui/file/f74399cc0be275376dad23151e3d0c2e2a1c966e6db6a695a05ec1a30551c0ad/details
    
*/

rule thread_execution_hijacking_suspended_browser
{
    meta:
        description = "Detects thread execution hijacking via suspended browser process."
        author = "gemesa@protonmail.com"
        hash = "f74399cc0be275376dad23151e3d0c2e2a1c966e6db6a695a05ec1a30551c0ad"
        mitre_attack = "T1055.003"
        reference = "FUN_10002096_hijack_thread_inject_shellcode, FUN_10002232_create_suspended_browser"

    strings:
        // Hijacking API sequence.
        $api_CreateProcessA     = "CreateProcessA" ascii
        $api_GetThreadContext   = "GetThreadContext" ascii
        $api_SetThreadContext   = "SetThreadContext" ascii
        $api_VirtualAllocEx     = "VirtualAllocEx" ascii
        $api_WriteProcessMemory = "WriteProcessMemory" ascii
        $api_ResumeThread       = "ResumeThread" ascii

        // Default browser in registry.
        $reg_htmlfile = "htmlfile\\shell\\open\\command" ascii nocase

        // Setting ContextFlags CONTEXT_FULL (0x10007) before GetThreadContext.
        /*
        100020c9 c7 85 08        MOV        dword ptr [EBP + local_2fc],0x10007
                 fd ff ff 
                 07 00 01 00
        100020d3 ff 15 fc        CALL       dword ptr [->KERNEL32.DLL::GetThreadContext]     = 000086da
                 60 00 10
        */
        $context_full = { C7 4? ?? 07 00 01 00 }

        /*
        100020d9 6a 40           PUSH       PAGE_EXECUTE_READWRITE
                             MEM_COMMIT: 0x00001000
                             MEM_RESERVE: 0x00002000
        100020db 68 00 30        PUSH       0x3000
                 00 00
        100020e0 68 00 04        PUSH       0x400
                 00 00
        100020e5 6a 00           PUSH       0x0
        100020e7 ff 75 08        PUSH       dword ptr [EBP + param_1]
        100020ea ff 15 00        CALL       dword ptr [->KERNEL32.DLL::VirtualAllocEx]       = 000086c8
                 61 00 10
        */
        $mem_alloc_rwx = { 6A 40 68 00 30 00 00 }

    condition:
        uint16(0) == 0x5A4D and
        (
            // Full hijacking pattern (all 6 APIs).
            all of ($api_*)
            or
            // Key APIs with browser target indicator.
            (4 of ($api_*) and $reg_htmlfile)
            or
            // Context manipulation with RWX allocation.
            ($api_GetThreadContext and $api_SetThreadContext and $context_full and $mem_alloc_rwx and $api_WriteProcessMemory)
        )
}

rule APC_injection_explorer_LoadLibrary
{
    meta:
        description = "Detects APC injection into explorer.exe threads using QueueUserAPC with LoadLibrary."
        author = "gemesa@protonmail.com"
        hash = "f74399cc0be275376dad23151e3d0c2e2a1c966e6db6a695a05ec1a30551c0ad"
        mitre_attack = "T1055.004"
        reference = "FUN_10003919_apc_inject_into_explorer, FUN_100039fb_apc_injection_entry"

    strings:
        // APC injection APIs.
        $api_QueueUserAPC             = "QueueUserAPC" ascii
        $api_VirtualAllocEx           = "VirtualAllocEx" ascii
        $api_WriteProcessMemory       = "WriteProcessMemory" ascii
        $api_OpenProcess              = "OpenProcess" ascii
        $api_OpenThread               = "OpenThread" ascii

        // Thread/process enumeration via Toolhelp32.
        $api_CreateToolhelp32Snapshot = "CreateToolhelp32Snapshot" ascii
        $api_Thread32First            = "Thread32First" ascii
        $api_Thread32Next             = "Thread32Next" ascii
        $api_Process32First           = "Process32First" ascii
        $api_Process32Next            = "Process32Next" ascii

        // Target.
        $target_explorer = "explorer.exe" ascii nocase

        // Privilege escalation.
        $priv_debug = "SeDebugPrivilege" ascii

        // Mutexes.
        $mutex1 = "PNP#DMUTEX#1#DL5" ascii
        $mutex2 = "PNP#NETMUTEX#1#DL5" ascii

    condition:
        uint16(0) == 0x5A4D and
        (
            // Generic: APC + memory write + thread/process enumeration.
            (
                3 of ($api_QueueUserAPC, $api_VirtualAllocEx, $api_WriteProcessMemory, $api_OpenProcess, $api_OpenThread) and
                3 of ($api_CreateToolhelp32Snapshot, $api_Thread32First, $api_Thread32Next, $api_Process32First, $api_Process32Next)
            )
            or
            // Targeted: APC + explorer + debug privilege.
            (
                $api_QueueUserAPC and $target_explorer and $priv_debug
            )
            or
            // Sample-specific: known mutex + APC.
            (
                any of ($mutex*) and $api_QueueUserAPC
            )
        )
}

rule linkinfo_DLL_injection_shellcode
{
    meta:
        description = "Detects the shellcode stub used by the file infector to drop and load the malicious linkinfo.dll."
        author = "gemesa@protonmail.com"
        hash = "f74399cc0be275376dad23151e3d0c2e2a1c966e6db6a695a05ec1a30551c0ad"
        mitre_attack = "T1055.004"
        reference = "DAT_100062a8 (shellcode stub appended to infected EXEs)"

    strings:
        // String embedded in shellcode stub.
        $shellcode_dllname = "\\linkinfo.dll" ascii

        // SEH chain walk. Find kernel32 base.
        /*
        $ echo "0x33,0xf6,0x64,0xad" | llvm-mc --disassemble -triple=i686-pc-windows-msvc --output-asm-variant=1
            xor	esi, esi
            lodsd	eax, dword ptr fs:[esi]
        */
        $seh_walk = { 33 F6 64 AD }

        // MZ header scan.
        /*
        $ echo "0x66,0x33,0xdb,0x66,0x81,0x3b,0x4d,0x5a" | llvm-mc --disassemble -triple=i686-pc-windows-msvc --output-asm-variant=1 --print-imm-hex
            xor	bx, bx
            cmp	word ptr [ebx], 0x5a4d
        */
        $mz_scan = { 66 33 DB 66 81 3B 4D 5A }

        // PE signature check.
        /*
        $ echo "0x81,0x3e,0x50,0x45,0x00,0x00" | llvm-mc --disassemble -triple=i686-pc-windows-msvc --output-asm-variant=1 --print-imm-hex
          cmp	dword ptr [esi], 0x4550
        */
        $pe_check = { 81 3E 50 45 00 00 }

        // Ordinal 101 call.
        /*
        $ echo "0x6a,0x65,0x50,0xff,0x13" | llvm-mc --disassemble -triple=i686-pc-windows-msvc --output-asm-variant=1 --print-imm-hex
            push	0x65
            push	eax
            call	dword ptr [ebx]
        */
        $ord101_call = { 6A 65 50 FF 13 }

        // Mutexes.
        $mutex1 = "PNP#DMUTEX#1#DL5" ascii
        $mutex2 = "PNP#NETMUTEX#1#DL5" ascii

    condition:
        uint16(0) == 0x5A4D and
        (
            // Shellcode pattern: SEH walk + MZ scan + PE check + inline DLL name.
            ($seh_walk and $mz_scan and $pe_check and $shellcode_dllname)
            or
            // Shellcode with ordinal call.
            ($ord101_call and $shellcode_dllname)
            or
            // Infected binary with both shellcode string and mutex.
            ($shellcode_dllname and any of ($mutex*))
        )
}

Sigma rules

Note: the rules are available here as well.

These rules are verified via sigma check file.yml but not tested otherwise. Additionally, they are formatted via npx prettier --write file.yml.

## Sigma rule 1: thread execution hijacking via suspended browser process.
## Detects T1055.003: Browser spawned by explorer without a URL.

title: Suspicious suspended browser process creation for thread hijacking
id: 4a52869c-6826-445e-8547-130a0f4317ac
status: experimental
description: |
  Detects a browser process spawned by explorer.exe without a URL on the command line.
  The malware reads the default browser from htmlfile\shell\open\command and launches it
  with CREATE_SUSPENDED. Since the browser is started for injection (not user browsing),
  it has no URL argument.
references:
  - https://attack.mitre.org/techniques/T1055/003/
author: gemesa@protonmail.com
date: 2025-02-16
tags:
  - attack.defense-evasion
  - attack.privilege-escalation
  - attack.t1055.003
logsource:
  category: process_creation
  product: windows
detection:
  selection:
    ParentImage|endswith: '\explorer.exe'
    Image|endswith:
      # Tune this list.
      - '\iexplore.exe'
      - '\firefox.exe'
      - '\chrome.exe'
      - '\msedge.exe'
      - '\opera.exe'
      - '\brave.exe'
  filter_user_browsing:
    CommandLine|contains:
      - "http://"
      - "https://"
      - ".html"
      - ".htm"
  condition: selection and not filter_user_browsing
level: high
falsepositives:
  - Software that opens browsers without a URL.

---
## Sigma rule 2: APC injection into explorer.exe.
## Detects T1055.004: Any non-standard process opening explorer with full access.

title: Suspicious full access to explorer.exe by non-standard process
id: 4aca5732-fe84-42bb-aee4-94baaf731004
status: experimental
description: |
  Detects any process opening explorer.exe with PROCESS_ALL_ACCESS
  permissions. The APC injection path is triggered from an infected
  EXE (which could be any process name), so this rule does not filter by source image.
  Instead it flags any non-OS process requesting full access to explorer.
references:
  - https://attack.mitre.org/techniques/T1055/004/
author: gemesa@protonmail.com
date: 2025-02-16
tags:
  - attack.defense-evasion
  - attack.privilege-escalation
  - attack.t1055.004
logsource:
  category: process_access
  product: windows
detection:
  selection:
    TargetImage|endswith: '\explorer.exe'
    GrantedAccess|contains:
      # https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights
      - "0x1F0FFF" # PROCESS_ALL_ACCESS (pre-Vista)
      - "0x1FFFFF" # PROCESS_ALL_ACCESS (Vista+)
  filter_os_processes:
    SourceImage|endswith:
      # Tune this list.
      - '\csrss.exe'
      - '\lsass.exe'
      - '\winlogon.exe'
      - '\dwm.exe'
      - '\sihost.exe'
      - '\SearchIndexer.exe'
      - '\ShellExperienceHost.exe'
      - '\RuntimeBroker.exe'
  filter_security_tools:
    SourceImage|endswith:
      # Tune this list.
      - '\MsMpEng.exe'
      - '\taskmgr.exe'
      - '\procexp.exe'
      - '\procexp64.exe'
  condition: selection and not 1 of filter_*
level: high
falsepositives:
  - Unknown. Tune based on environment.

---
## Sigma rule 3: linkinfo.dll loaded from non-system directory.
## Detects the malware DLL being loaded after drop or search-order hijack.

title: linkinfo.dll loaded from non-system directory
id: a8cb10b2-6be2-49f5-a7bc-14149573f4e5
status: experimental
description: |
  Detects linkinfo.dll being loaded from outside System32. The shellcode stub drops
  the malware DLL as %TEMP%\linkinfo.dll and loads it via LoadLibraryA. A legitimate
  linkinfo.dll load should always come from System32.
references:
  - https://attack.mitre.org/techniques/T1055/004/
  - https://attack.mitre.org/techniques/T1574/001/
author: gemesa@protonmail.com
date: 2025-02-16
tags:
  - attack.defense-evasion
  - attack.persistence
  - attack.t1055.004
  - attack.t1574.001
logsource:
  category: image_load
  product: windows
detection:
  selection:
    # https://github.com/trustedsec/SysmonCommunityGuide/blob/master/Build/Sysmon.md#image-loading
    ImageLoaded|endswith: '\linkinfo.dll'
  filter_legitimate:
    ImageLoaded|contains:
      - '\Windows\System32\linkinfo.dll'
      - '\Windows\SysWOW64\linkinfo.dll'
  condition: selection and not filter_legitimate
level: critical
falsepositives:
  - Very unlikely. linkinfo.dll should only exist in System32/SysWOW64.

---
## Sigma rule 4: SeDebugPrivilege escalation by non-standard process.
## Detects the privilege escalation step before the APC injection.

title: SeDebugPrivilege enabled by non-standard process
id: 08366b73-1242-450f-966b-f5703e67ceb9
status: experimental
description: |
  Detects a process enabling SeDebugPrivilege via AdjustTokenPrivileges. This is a
  prerequisite for the APC injection. The malware needs it to call
  OpenProcess(PROCESS_ALL_ACCESS) on explorer.exe. Since the source process is an
  infected EXE (any name), this rule flags any non-standard process enabling this
  privilege.
references:
  - https://attack.mitre.org/techniques/T1055/004/
author: gemesa@protonmail.com
date: 2025-02-16
tags:
  - attack.privilege-escalation
  - attack.t1055.004
logsource:
  product: windows
  service: security
detection:
  selection:
    # https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4703
    EventID: 4703
    EnabledPrivilegeList|contains: "SeDebugPrivilege"
  filter_known_legitimate:
    ProcessName|endswith:
      # Tune this list.
      - '\taskmgr.exe'
      - '\procexp.exe'
      - '\procexp64.exe'
      - '\procmon.exe'
      - '\procmon64.exe'
      - '\MsMpEng.exe'
      - '\csrss.exe'
      - '\windbg.exe'
      - '\x64dbg.exe'
      - '\devenv.exe'
      - '\msvsmon.exe'
      - '\WerFault.exe'
  condition: selection and not filter_known_legitimate
level: medium
falsepositives:
  - Debuggers and RE tools.
  - Security/EDR products.