Research/custom-go-c2-beacon
MalwareHighPublic

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: 149c5295423ba834292d1ad58107bb14

  • SHA256: 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, executeTask

  • A task menu: taskShell, taskDownload, taskUpload, taskScreenshot, taskPersist, taskSelfDelete, taskSocksStart, taskPortForward, and friends

  • Crypto: encryptGCM, decryptGCM, getAESKey

  • Framing: 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

taskShell, taskExec

Run commands, preferably where nobody is looking

File system

taskLs, taskPwd, taskCd, taskDownload, taskUpload

Walk the disk and take souvenirs

Process

taskPs, taskKillProc

List and evict

Surveillance

taskScreenshot

Steal the desktop

Persistence

taskPersist / persistWindows

Registry + schtasks

Self-protection

taskSelfDelete

Leave no coat on the hook

Evasion / control

taskSleep, applyJitter

Beacon interval with jitter

Advanced

taskLoadAssembly

Pull in extra payloads

Lateral / proxy

taskPortForward, taskSocksStart/Stop, handleSocks5

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 sliverpb, no mTLS/protobuf implant path, no BishopFox packages. Sliver does not narrate itself at /check_in and /result.

Cobalt Strike

The file is literally called beacon.exe

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 github.com/Ne0nd0g/merlin symbols. Not a one-package main.checkIn.

AdaptixC2 Gopher

Closest public cousin: Go, AES, framed TCP, taskShell / taskLs / taskScreenshot

Stock Gopher is TCP/mTLS + msgpack. Its loop is CreateInfoSendMsg / RecvMsgTaskProcess. No checkIn, no persistWindows, no HTTPS /check_in.

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)

  1. 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.

  2. 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.

  3. Enrich 38.55.232.233 and sit on the network story. The Adaptix listener on :8443/file/ is a lead, not a conclusion. Disposable VPS does not mean disposable evidence.

  4. 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.

  5. Hunt the rest of the estate for the same hashes, the same /check_in callback, and the registry/schtasks persistence 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.

Under Contruction (Beta) - No Bully, Please 😊