beacon.exe: custom Go C2 found in the wild
Found beacon.exe in the wild. The filename begged for Sliver. The pclntab and the C2 URLs said otherwise: a privately built Go implant with HTTPS plus framed TCP, AES-256-GCM, and a full post-ex menu. Not Sliver. Not Cobalt. Not stock Adaptix.
- Name
- beacon.exe: custom Go C2 found in the wild
- Type
- Malware reverse-engineering
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- n/a (private C2; callback on PEG TECH / AROSSCLOUD)
- Affected
- Windows x64 hosts (PE32+ Go 1.26.5)
- Published
- 19 Aug 2026
- Updated
- 19 Aug 2026
- Tags
- malware, golang, c2, custom-c2, beacon, windows, reverse-engineering, socks5, incident-response
It was already too late for the filename to be cute
This one did not arrive as a “please analyze this attachment.” It turned up in the wild — a Windows host that was already having a worse day than I was. No installer. No dropper theater. Just beacon.exe, already on disk, already named like it wanted to be in a detection-engineering slide deck.
I told myself it would be Sliver. Then Cobalt. Then some cracked Havoc build with the serial still in the strings. What I actually got was more interesting, and slightly more annoying: a privately developed Go C2 implant. Custom protocol. Custom symbols. Professional, bored, and not wearing anyone else’s jersey.
This is the static pass. Isolated lab, authorized sample, no live tasking against the C2. I am not publishing a decoder kit. I am publishing the map, and I am correcting the name on the door.
First contact — the file introduces itself badly
6.5 MB. PE32+. GUI subsystem. x86-64. Compiled with Go 1.26.5, which you can smell from across the room once you have seen enough Go binaries: high entropy (~6.29), epoch timestamp (1 January 1970, Go’s little joke), stripped symbol table, the whole runtime packed in because Go does not believe in traveling light.
Hashes
MD5:
149c5295423ba834292d1ad58107bb14SHA256:
fb4eff03328e845f9a2eeaad61018111f42bbc366177813f733ca855b58c2d99
ASLR (DYNAMIC_BASE), DEP/NX, high-entropy VAs. The GUI subsystem is the quiet part: no console window. Not stealth in the “nation-state art project” sense. Stealth in the “please do not pop a black box on the user’s desktop” sense. I respect the courtesy. The compromised server did not.
Then the pclntab started talking
Early strings were the usual Go karaoke: runtime, net, crypto, Windows APIs. Useful, not diagnostic. The tell was in the pclntab — Go’s little phone book of function names that survives even when someone has taken a belt sander to the symbol table.
Out fell a resume, all in main:
checkIn,fetchTasks,reportResult,executeTaskA task menu:
taskShell,taskDownload,taskUpload,taskScreenshot,taskPersist,taskSelfDelete,taskSocksStart,taskPortForward, and friendsCrypto:
encryptGCM,decryptGCM,getAESKeyFraming:
tcpWriteFrame,tcpReadFrame,tcpRoundTrip
I sat back. That is not a utility. That is a beacon that went to finishing school. It is also, importantly, a beacon that lives in one package. Framework implants leave a hallway of import paths. This one left a studio apartment.
C2, written in permanent marker
The next clue did not even have the decency to hide.
https://38.55.232.233:443/check_in
https://38.55.232.233:443/result
Hardcoded. HTTPS. Two endpoints with names that would fail a code-review for “maybe don’t narrate the protocol in the URL.” The IP sits in PEG TECH INC / AROSSCLOUD INC space (AS400619) — the kind of VPS neighborhood where machines are born on Monday and forgotten on Thursday.
Traffic is AES-256-GCM. There is a second, framed TCP channel beside HTTPS. I am treating that as the bulk path: file transfer, SOCKS5 data, anything that would look silly as a REST call. Hybrid C2. Classic. Still effective.
At check-in the implant fingerprints the host into a UUID from:
first MAC address
first internal IP
hostname
current username
whether it is feeling administrative today
That UUID, plus basic process metadata, goes out on every check-in. It wants to know who it woke up as. In the wild, that is not a hypothetical.
Capability map — the implant’s LinkedIn
Recovered main package symbols, organized the way I wish all malware would organize itself:
Category | Functions | Purpose |
|---|---|---|
Execution |
| Run commands, preferably where nobody is looking |
File system |
| Walk the disk and take souvenirs |
Process |
| List and evict |
Surveillance |
| Steal the desktop |
Persistence |
| Registry + |
Self-protection |
| Leave no coat on the hook |
Evasion / control |
| Beacon interval with jitter |
Advanced |
| Pull in extra payloads |
Lateral / proxy |
| Port forward and a full SOCKS5 proxy |
The loop is the loop every C2 textbook draws on page three:
check in → fetch tasks → execute asynchronously → report results → sleep with jitter.
I have seen this loop in more languages than I have seen good comments. Go just makes it tidy.
Deeper — reconstructing main with the lights off
Stripped binary, so entry points came from parsing the Go pclntab (magic 0xfffffff1) and chasing RIP-relative references to unique strings. The C2 URL https://38.55.232.233:443/check_in lives at 0x14026feab. Disassembly around it is Go doing Go things: runtime helpers, string construction, interface noise, HTTP assembly. Consistent with checkIn. Not pretty. Very identifiable.
The logic, as I would write it on a whiteboard
Beacon loop (main.main)
for {
checkIn()
tasks := fetchTasks()
for _, task := range tasks {
handleTaskAsync(task)
}
time.Sleep(applyJitter(baseInterval))
}
Check-in (main.checkIn)
Collect host IDs, build a metadata blob, AES-GCM it, ship it to /check_in. Hello, operator.
Task dispatch (main.executeTask)
A switch on task type. No mystery. Results go through buildTaskReport / encodeReportText and home via reportResult. I almost missed it because it was so straightforward. Straightforward is a choice.
Encryption (getAESKey, encryptGCM, decryptGCM)
AES-256-GCM on C2 and the secondary TCP channel. Key is either static or derived from implant-specific material. I have not pulled the exact bytes in this pass. That is a next-lab problem, not a this-paragraph problem.
Persistence (persistWindows)
Registry Run keys and scheduled tasks. The imports (RegCreateKeyExW and company) confirm they did not invent a new persistence religion. They used the one that still works. On a server that was already compromised, this is how “we found a file” becomes “it came back after the reboot.”
SOCKS5 and port forward
Start/stop controls, multiple goroutines, actual proxy support. This is how a single beachhead becomes a hallway.
So which C2 is it?
This is the part where the original folder name gets evicted.
beacon.exe is a marketing department. It is not a family. I lined the usual suspects up against the pclntab and the wire format and watched them fail the dress code.
Framework | Why it was tempting | Why it is not this sample |
|---|---|---|
Sliver | Go, mature post-ex, people name everything “beacon” | No |
Cobalt Strike | The file is literally called | CS Beacon is C, not a 6.5 MB statically linked Go runtime. Different century of tradecraft, same generic noun. |
Havoc | Open-source CS-shaped C2, often the next guess | Demon is C/C++. This binary is Go 1.26.5 with a pclntab. |
Merlin | Go HTTP C2 | Merlin’s agent is HTTP/2 + JWT and a hallway of |
AdaptixC2 Gopher | Closest public cousin: Go, AES, framed TCP, | Stock Gopher is TCP/mTLS + msgpack. Its loop is |
AdaptixC2 Beacon | Adaptix literally ships an agent family named Beacon | That Beacon is C++ with RC4 “beat” blobs, not Go AES-GCM REST. |
Adaptix still gets a footnote, because I am not allergic to infrastructure. A different sample on Tria.ge (bc2220b32d5aa397…) extracted an AdaptixC2 config pointing at the same IP, port 8443, path /file/. Same cheap VPS, different listener. That is “operator likes this neighborhood,” not “this PE is Gopher.” I will not launder an IP overlap into a family name.
Most likely C2: a private, purpose-built framework. Custom Go implant, hybrid HTTPS + framed TCP, AES-256-GCM, textbook English symbols all stuffed into main. It implements the same tradecraft the public frameworks already productized — which is either impressive or exhausting, depending on how much coffee is left.
The folder used to be called sliver-c2-beacon. I renamed it. Hypotheses do not get to keep the lease after they bounce.
What I would do next (authorized lab only)
Dynamic analysis in an isolated sandbox with controlled egress. Watch a live check-in. Do not gift the operator a real callback from a machine you still care about.
Pull the AES key material and the exact persistence command lines in Ghidra or IDA with Go support. The static map is done. The constants are not.
Enrich
38.55.232.233and sit on the network story. The Adaptix listener on:8443/file/is a lead, not a conclusion. Disposable VPS does not mean disposable evidence.Memory forensics on any host that actually ran this. Config and in-memory task queues do not always make it to disk. This family looks like it knows that.
Hunt the rest of the estate for the same hashes, the same
/check_incallback, and the registry/schtaskspersistence this implant already knows how to plant.
Static work ends here. The sample is capable, modern, and rude in a professional way. It was found in the wild. Handle it like you would any implant that already knows the hostname.