Reversing the Almanahe worm
Table of contents
- Table of contents
- Introduction
- Executive summary
- Hashes
- Infection lifecycle
- The two injections
- Technique 1: Thread execution hijacking
- Technique 2: APC injection
- IOCs
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:
- A shellcode stub runs first: decompresses the malware DLL, drops it to
%TEMP%\linkinfo.dll. - Loads the DLL:
DllMainchecks “am I in explorer?”. If not, it does nothing. - Calls ordinal 101: installs a rootkit driver, then APC-injects into explorer.exe (1. injection method).
- Explorer loads the DLL:
DllMainchecks again “am I in explorer?”. If yes, full activation. - Spawns 5 worker threads: file infector, network worm, browser injection, rootkit/AV killer, C2 handler.
- Browser injection thread uses thread hijacking (2. injection method) to spawn hidden browsers for C2 traffic.
- 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
CreateProcessAusingCREATE_SUSPENDED(flag0x4) (reference).
Then it hijacks the thread (FUN_10002096_hijack_thread_inject_shellcode):
- Calls
GetThreadContextwithCONTEXT_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
LoadLibraryAwith 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.Eipto point at the shellcode, then commits it withSetThreadContext. - 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#DL5exists. If it does, the malware is already in explorer, so return early (FUN_100039fb_apc_injection_entry). - Enables
SeDebugPrivilege(requires admin) viaOpenProcessToken–>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_ACCESSand callsQueueUserAPC(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.