Halfmast Loader: Eleven Days Inside an Exposed Staging Directory
A world-readable build server, 1,184 logged check-ins, and a TLS certificate that has outlived three hosting providers.
- Published
- Reading time
- 9 min
- Analyst
- Nouman Ahmed
- Region
- Asia
- Loader
- Command & Control
- Exposed Directory
- Detection Engineering
Contents
An exposed directory on a staging host left a full loader toolkit readable to anyone who found it: build scripts, an operator log, and a configuration file that named the next three targets. This post walks through what was on the box, how the loader stages its payload, and what to look for on the wire.
The host stayed open for eleven days. That was long enough to watch the operators iterate — two rebuilds, a change of C2 domain, and a rushed cleanup that missed the log directory entirely.
Key findings
- A staged loader, tracked here as Halfmast, retrieved its second stage
over plain HTTP from a hard-coded IP and wrote it straight to
%LOCALAPPDATA%. - The operator log recorded 1,184 check-ins across eleven days, from hosts in three sectors — logistics, regional government, and a single managed service provider.
- Configuration on disk named three follow-on targets that had not yet been contacted at the time of collection.
- The build script embedded a compilation timestamp and a username, both of which survived into the shipped binaries.
- Infrastructure reuse links the staging host to two earlier clusters by TLS certificate serial.
The exposed directory
The host was found through routine sweeping for open directory listings on
non-standard ports. It answered on 8080 with a default index — no
authentication, no directory protection.
logs/ folder survived
the operators' cleanup.Contents changed twice during the collection window:
| Captured | Files | Notable change |
|---|---|---|
| Day 1 | 14 | Initial capture. build.sh, stage2.bin, logs/ |
| Day 6 | 17 | stage2.bin rebuilt; C2 domain changed |
| Day 9 | 6 | Cleanup attempt — binaries removed, logs/ left behind |
The day-9 cleanup is the useful one. Removing the binaries but leaving the operator log is the sort of mistake that happens when someone is working from a checklist rather than an inventory.
Loader behaviour
Halfmast is a small C loader. It does three things: resolve its configuration, pull a second stage, and hand over execution. There is no persistence in the loader itself — that is the second stage's job.
Configuration is XOR-encoded with a single-byte key and appended to the binary
after the .rdata section. Decoding it is trivial:
1import sys
2
3def decode(blob: bytes, key: int = 0x5C) -> bytes:
4 return bytes(b ^ key for b in blob)
5
6with open(sys.argv[1], "rb") as fh:
7 data = fh.read()
8
9# Config is the last 512 bytes, null-padded
10marker = data.rindex(b"\x00" * 16)
11print(decode(data[marker + 16:]).decode("utf-8", "replace"))Which yields the staging URL and the check-in interval:
1{
2 "c2": "http://192.0.2.44:8080/upd/stage2.bin",
3 "interval": 900,
4 "jitter": 0.3,
5 "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
6 "kill_date": "2026-09-30"
7}Retrieval and staging
The second stage is fetched with a plain WinHttpSendRequest call and written
to %LOCALAPPDATA%\Microsoft\Vault\vaultsvc.dat before being mapped and
executed in place. No signature check, no TLS.
Because the request is unencrypted and the URI path is fixed, this is straightforward to write a network rule for:
alert http $HOME_NET any -> $EXTERNAL_NET any (
msg:"Halfmast loader second-stage retrieval";
flow:established,to_server;
http.method; content:"GET";
http.uri; content:"/upd/stage2.bin"; endswith;
http.user_agent; content:"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; depth:44;
classtype:trojan-activity;
sid:9000001; rev:1;
)Host-side detection
The write path is specific enough to be worth a rule on its own. vaultsvc.dat
is not a real Windows artefact, and nothing legitimate creates it under the
Vault directory.
1title: Halfmast Second Stage Written to Vault Directory
2id: 6f0d5b18-9c41-4a27-b6e2-3f1c8a5d0e77
3status: experimental
4logsource:
5 category: file_event
6 product: windows
7detection:
8 selection:
9 TargetFilename|endswith: '\\Microsoft\\Vault\\vaultsvc.dat'
10 condition: selection
11falsepositives:
12 - None known
13level: highTargeting
The operator log recorded a check-in line per beacon. Fields were consistent across all 1,184 entries: timestamp, source address, hostname, OS build, and a campaign tag.
2026-07-11T04:22:19Z 198.51.100.19 WKS-LOG-114 10.0.19045 tag=logi-q3
2026-07-11T04:37:02Z 198.51.100.86 WKS-LOG-092 10.0.19045 tag=logi-q3
2026-07-11T05:01:44Z 203.0.113.7 SRV-GOV-004 10.0.20348 tag=gov-pilotTwo campaign tags were in use — logi-q3 against logistics operators, and
gov-pilot, a much smaller set, against regional government hosts. The
gov-pilot set accounted for 31 of the 1,184 check-ins but ran the entire
eleven days, which suggests it was the campaign that mattered.
The volume is in logistics. The persistence is in government. When those two diverge, the low-volume set is usually the objective and the high-volume set is the cover.
Infrastructure
Three hosts were involved. All shared a self-signed TLS certificate on port
443 with an identical serial, which is what ties them together.
| Host | Role | First seen | Last seen |
|---|---|---|---|
192.0.2.44 | Staging / payload delivery | 2026-07-08 | 2026-07-19 |
198.51.100.203 | C2 (initial) | 2026-07-08 | 2026-07-14 |
203.0.113.91 | C2 (post-rebuild) | 2026-07-14 | 2026-07-19 |
The certificate serial also appears on two hosts outside this cluster, first seen in March 2026. Those are not covered here but are included in the indicators below for anyone tracking the same certificate.
Mitigations
- Block outbound plain HTTP to non-standard ports at the perimeter. The loader
has no TLS fallback — denying
8080egress breaks it outright. - Alert on file creation under
%LOCALAPPDATA%\Microsoft\Vault\for any extension other than the two Windows genuinely writes there. - Hunt for the fixed user-agent string. It is a stale Chrome-era UA with no browser token, so it stands out in proxy logs.
- Pivot on the certificate serial in the indicators table — it has outlived three separate hosting providers.
MITRE ATT&CK
| ID | Technique | Where it shows up |
|---|---|---|
| T1105 | Ingress Tool Transfer | Second stage pulled over HTTP |
| T1071.001 | Application Layer Protocol: Web Protocols | Fixed-path HTTP check-in |
| T1027 | Obfuscated Files or Information | Single-byte XOR config blob |
| T1620 | Reflective Code Loading | Stage 2 mapped and run in place |
| T1036.005 | Masquerading: Match Legitimate Name or Location | vaultsvc.dat under Vault\ |
| T1573 | Encrypted Channel | Self-signed TLS on the C2 tier |
Indicators
Network
| Indicator | Type | Note |
|---|---|---|
192.0.2[.]44 | IPv4 | Staging host, port 8080 |
198.51.100[.]203 | IPv4 | C2, first infrastructure |
203.0.113[.]91 | IPv4 | C2, post-rebuild |
hxxp://192.0.2[.]44:8080/upd/stage2[.]bin | URL | Second-stage retrieval |
updates.example[.]net | Domain | Resolved to C2 tier |
Files
| SHA-256 | Filename | Note |
|---|---|---|
3f1c8a5d0e77b6e29c414a276f0d5b18... | vaultsvc.dat | Second stage |
a27b6e23f1c8a5d0e779c416f0d5b189... | svchost32.exe | Loader |
TLS
| Serial | Subject CN | Note |
|---|---|---|
00:c4:1a:9f:20:b7:e3:8d | localhost | Self-signed, shared across cluster |
Conclusion
Halfmast is not sophisticated. The interesting part is the operational discipline around it, or rather the lack of it: a hard-coded staging IP, a plaintext retrieval, a config that survives a single XOR, and an operator log left on a world-readable directory.
The certificate reuse is what will outlast this campaign. Hosts get rebuilt and IPs get rotated, but the same self-signed certificate has now followed these operators across three providers and five months. That is the indicator worth keeping.
Written by Muhammad Nouman Ahmed · Threat Intelligence Analyst