GoSerpent Malware Internals: Reversing the Undocumented Go Implant Silently Espionaging Southeast Asian Governments Since Late 2025

Somebody sat inside a Southeast Asian foreign ministry for the better part of a year, quietly copying Word docs and PDFs into an encrypted archive, and never once phoned that archive straight home. They staged it. They waited. Then they walked the data out over an authenticated network share. That is not smash-and-grab crime. That is intelligence work, and GoSerpent, the Go-based implant Kaspersky’s GReAT team detailed in July 2026, is the tooling built to do it patiently. This post pulls the binary apart: the Go runtime you have to fight through, the AES-CBC argument that hides the C2, the ChaCha20 channel, the svchost injection, and the detection content that actually catches it.


Why Go, and why this campaign matters

Here is my thesis up front: Go has become the default language for a certain tier of state-nexus espionage implant, and GoSerpent is a near-textbook example of why. A statically-linked Go binary drops a full runtime, garbage collector, and scheduler into a single fat executable that runs on Windows and Linux with the same source. The stripped binary buries the operator’s actual logic under tens of thousands of runtime.* functions. Antivirus heuristics tuned on decades of C/C++ malware get noisy and unreliable against it. For an actor who cares about dwell time over speed, that trade is worth it.

The catch, and this is the part defenders should internalize, is that Go’s runtime is also the biggest gift the analyst gets. The pclntab (program counter line table) structure that the Go runtime needs for stack traces and panics carries function and package names even in “stripped” builds. Mandiant’s GoReSym exists precisely because that metadata is nearly impossible to fully erase without breaking the binary. So the same feature that frustrates signature engines hands the reverse engineer a symbol table on a plate. GoSerpent’s authors knew this, which is why the interesting secrets (the C2 address, the channel password) live outside the binary, delivered as an encrypted launch argument.

Kaspersky discovered the campaign in February 2026, traced first activity back to late 2025, and watched the toolset evolve through a fresh second-stage push in May 2026. Victims are government and diplomatic entities across Southeast Asia. The operators overlap, by Kaspersky’s read, with TetrisPhantom, the APAC-focused actor first documented in October 2023 for spying on government bodies through hardware-encrypted USB drives. Attribution is not nailed down, and I will not pretend otherwise, but the targeting and tradecraft rhyme.


Campaign timeline and operational posture

PhaseTimeframeWhat happened
Initial footholdLate 2025GoSerpent RAT deployed as lass.exe / updates.exe, encrypted argument delivers C2
Reconnaissance dwellDays after footholdOperators sit idle several days before staging additional components
CollectionLate 2025 onwardThumbcacheService DLL harvests Office docs and PDFs into thumbcache_605a.db
Credential accessOngoingMimikatz against LSASS, QuarksDumpLocalHash against the SAM hive
Kaspersky discoveryFebruary 2026Campaign identified and tracked
Toolset evolutionMay 2026Stowaway RAT and the TmcLoader/TmcPayload exfiltration chain introduced
ExfiltrationAfter months of stagingArchives moved over authenticated network shares, not direct C2 uploads

The single most important behavioural fact in that table is the last row. The threat actor deliberately avoided pushing collected data out through the C2 channel. They staged locally, harvested credentials to reach file servers, and used legitimate SMB access to move the archives. That decision starves most network-centric DLP and C2 detection of the signal it depends on. If your entire detection strategy is “watch for large outbound uploads to weird IPs,” GoSerpent walks right past you.


Reversing the GoSerpent RAT: fighting the runtime

Set up a throwaway Windows 10/11 analysis VM with no route to the internet. Load Ghidra 11.x, and before you touch the disassembler, run the binary through GoReSym.

GoReSym.exe -t -d -p goserpent_sample.exe > symbols.json

The -t flag pulls type information, -d recovers the standard-library package metadata, and -p extracts user package paths. GoReSym walks the pclntab (whose header magic for Go 1.20 and later is 0xFFFFFFF1, laid out little-endian as the bytes F1 FF FF FF 00 00) and reconstructs the function name table. Feed the resulting JSON into a Ghidra script to relabel functions, and a wall of FUN_00401a20 blobs turns into main.decryptArgs, main.deriveKey, crypto/aes.NewCipher, and so on.

That relabelling is the whole game. In a C++ implant you would grind through call graphs guessing at intent. Here, once symbols land, you can go straight to the function you care about. Search the recovered symbol tree for anything under main.* (the malware author’s own package) and for the crypto primitives: crypto/aes, crypto/sha256, and golang.org/x/crypto/chacha20.

A word on Go runtime noise. You will see runtime.morestack_noctxt prologues on nearly every function, goroutine stack-growth checks, and the scheduler machinery. Ignore it. It is boilerplate the compiler emits. The analyst-relevant logic is a rounding error in code volume compared to the runtime, and the symbol names tell you exactly which few hundred bytes matter.


Argument decryption and the C2 handshake

GoSerpent does not carry its C2 address in plaintext, or in any static form the strings tool would reveal. Instead it expects an encrypted, Base64-encoded command-line argument at launch. That argument, once decrypted, contains the C2 server address and a communication password. This is smart operational hygiene: an implant captured on disk without its launcher gives up almost nothing about the infrastructure.

The decryption is AES in CBC mode with a fixed IV of 31323334353637383930616263646566 (that hex is the ASCII string 1234567890abcdef) and keys derived from predefined strings baked into the binary. Reconstructed, the routine looks like this:

func decryptArgs(b64Arg string, key []byte) ([]byte, error) {
    ct, err := base64.StdEncoding.DecodeString(b64Arg)
    if err != nil {
        return nil, err
    }
    block, err := aes.NewCipher(key) // key from predefined in-binary string
    if err != nil {
        return nil, err
    }
    iv := []byte("1234567890abcdef") // fixed IV: 31323334...61626364656 6
    mode := cipher.NewCBCDecrypter(block, iv)
    pt := make([]byte, len(ct))
    mode.CryptBlocks(pt, ct)
    return pkcs7Unpad(pt), nil // yields "c2_addr:port|password"
}

A fixed IV in CBC is a genuine cryptographic weakness (it makes identical plaintext blocks under the same key produce identical ciphertext), but the operators do not care because this is obfuscation, not confidentiality against a cryptanalyst. It exists to defeat a lazy strings pass, and it does.

Once the password is in hand, GoSerpent derives its ChaCha20 session key from the SHA-256 hash of that password:

key := sha256.Sum256([]byte(password))   // 32-byte ChaCha20 key
// stream, _ := chacha20.NewUnauthenticatedCipher(key[:], nonce)

In the lab, set an x64dbg breakpoint right after the CBC decrypt call. When it fires, dump the plaintext buffer to read the C2 address and password in memory, then let the SHA-256 derivation run and grab the 32-byte key. With key and address recovered, stand up a Python listener that speaks the same ChaCha20 framing and replay the handshake while Wireshark captures it. That capture is what you turn into network signatures later.

The operator’s post-handshake capability set is broad and modular: open a command shell, upload and download files, pivot to additional systems, bind a listener, forward traffic between hosts, and flip the compromised machine into a SOCKS5 proxy. That last capability is central to the tradecraft. By tunnelling through infected government hosts, the actor keeps its real infrastructure off the wire and enables lateral movement deeper into networks that trust those hosts.


Flowchart showing GoSerpent decrypting its encrypted launch argument via Base64 and AES-CBC with a fixed IV to obtain the C2 address and password, then deriving a ChaCha20 session key from SHA-256 of the password
GoSerpent keeps all C2 secrets outside the binary: the launch argument is AES-CBC-decrypted to yield the server address, and the channel is ChaCha20-encrypted with a key derived from the recovered password.

The collection toolchain: ThumbcacheService, Mimikatz, QuarksDumpLocalHash

After the days-long dwell, the operators bring in collection. ThumbcacheService is a DLL that registers as a Windows service and supplements GoSerpent with a proper file-hunting engine. It walks the filesystem for .docx, .xlsx, and .pdf files, monitors deleted documents (a nice touch for catching things users tried to clean up), and packs its loot into encrypted archives.

The staging artefact is where this gets catchable. Everything lands at:

C:\Users\Public\thumbcache_605a.db

There is no legitimate reason for a file named thumbcache_605a.db to sit in C:\Users\Public\. Real thumbnail cache databases live under each user’s AppData\Local\Microsoft\Windows\Explorer\. The name is chosen to look boring in a directory listing, and it works on a human eyeballing the disk. It does not survive a rule that knows the correct path.

Credential access rides alongside collection with two well-known tools:

ToolTargetTechnique
MimikatzLSASS process memoryDumps credential material (T1003.001)
QuarksDumpLocalHashSAM registry hiveExtracts local account password hashes (T1003.002)

Neither is novel, and that is the point. State actors reach for commodity credential tools constantly because they work and because a Mimikatz hit muddies attribution. The SAM extraction matters specifically because it yields local hashes the operators can use to authenticate to those network shares later, closing the loop on the exfiltration plan.


Stage 2: Stowaway, TmcLoader, and the injected payload

In May 2026 the operators upgraded. Stowaway is a weaponised build of a publicly available Go proxy framework, customised to make the infection stealthier. It talks over TCP, HTTP, or WebSocket, wraps traffic in AES-256-GCM or TLS, and offers SOCKS5 proxies, port forwarding, reverse tunnels, remote shells, and file transfers. Its real value to the operator is topology: Stowaway has both admin and agent roles, so the actor can build chained proxy paths across multiple compromised hosts, hopping deeper into segmented networks while every leg looks like ordinary internal traffic.

The exfiltration muscle is TmcLoader, a C++ service, paired with its embedded TmcPayload. TmcLoader carries an encrypted payload in its PE .data section, decrypts it at runtime, and injects it directly into the memory space of a legitimate svchost.exe. Fileless second stage, no payload ever written to disk in decrypted form.

Two implementation details are worth reversing carefully:

API obfuscation via circular XOR. TmcLoader hides its Windows API names from static analysis by XORing each byte of an obfuscated name against the value of the following byte, then Base64-encoding the result. Decoded and resolved at runtime, the API names never appear as clean strings. The decode loop looks like this:

// circular XOR: each byte XORed with the next byte's value
void decode_api(unsigned char *buf, size_t len) {
    for (size_t i = 0; i + 1 < len; i++) {
        buf[i] ^= buf[i + 1];
    }
    // buf now holds a Base64 string; Base64-decode, then
    // pass to GetProcAddress via resolved GetModuleHandle
}

This is why a naive import-table or string scan for CreateRemoteThread or VirtualAllocEx comes up empty. You have to find the decode stub, run it in a debugger or reimplement it, and recover the API list dynamically.

Config polling and share-based exfil. TmcPayload builds the path:

C:\Users\Public\Libraries\{BBF061R2-BE25-4F6D-8B2D-1A6A39C3FSA2}.db

It checks for that configuration file. If it is absent, the payload sleeps for a random interval and rechecks, rather than failing loudly or beaconing. That randomised delay is deliberate anti-analysis: in a sandbox with a short timeout, the payload does nothing observable and gets scored clean. When the config does exist, it contains encrypted network-share credentials and destination paths. The payload then moves the staged archives out over authenticated SMB. No direct C2 upload, no anomalous egress to a suspicious IP, just an internal file copy that blends into normal Windows behaviour.


Hierarchy diagram showing TmcLoader decrypting an embedded payload at runtime, resolving obfuscated API names via circular XOR, injecting TmcPayload into svchost.exe, which then polls a config file and exfiltrates staged archives over SMB
TmcLoader never writes a decrypted payload to disk: it resolves obfuscated Windows APIs at runtime, injects directly into svchost.exe, and the in-memory TmcPayload reads share credentials from a staging config to exfiltrate data over authenticated SMB.

Anti-analysis and evasion, catalogued

Pulling the tradecraft together, GoSerpent layers evasion at every stage rather than betting on a single trick:

  • No secrets in the binary. C2 and password arrive as an encrypted launch argument, so a captured sample leaks nothing about infrastructure.
  • Layered crypto for obfuscation. AES-CBC on the argument, ChaCha20 on the channel, AES-256-GCM/TLS on Stowaway. All chosen to defeat string and signature scanning, not to stop a cryptographer.
  • Name masquerading. lass.exe, updates.exe, thumbcache_605a.db, ThumbcacheService, all borrow the visual grammar of legitimate Windows components.
  • Fileless second stage. TmcLoader decrypts and injects into svchost.exe, leaving no cleartext payload on disk.
  • Sandbox-resistant timing. TmcPayload’s randomised recheck loop stalls automated analysis.
  • Legitimate cloud C2. Infrastructure sits on Alibaba Cloud and UCLOUD HK, so beacon traffic blends with the enormous volume of legitimate connections to those providers.
  • Decoy key material. Strings like www.microsoft.com, www.spacex.com, and github.code are embedded as secret-key material, a small piece of misdirection for an analyst grepping for indicators.
  • Exfil over trusted channels. SMB shares instead of C2 uploads sidesteps network exfil detection entirely.

None of these is exotic on its own. Stacked, and executed by an operator willing to wait months, they produce an implant that stays quiet.


Conceptual illustration of layered evasion defenses as concentric fortress walls made of locks, camouflage, mirrors, and hourglasses protecting a dark inner chamber
GoSerpent stacks evasion at every layer – encrypted arguments, fileless injection, decoy strings, cloud-hosted C2, and sandbox-stalling timers – so no single detection technique sees the full picture.

Comparative attribution: GoSerpent, BRICKSTORM, and GopherWhisper

I want to be careful here, because attribution is where good technical writeups go to die. Kaspersky’s overlap call is with TetrisPhantom, and it is explicitly unconfirmed. The comparison below to BRICKSTORM and GopherWhisper is my own analytical exercise in the Go-implant pattern space, not a claim that these are the same crew.

AttributeGoSerpentBRICKSTORM (UNC5221)GopherWhisper
LanguageGoGoGo (+ C++ loaders)
Disclosed byKaspersky GReAT, Jul 2026Mandiant/Google 2024; NVISO 2025ESET, Apr 2026
AttributionTetrisPhantom overlap (unconfirmed)China-nexus cluster UNC5221China-aligned (ESET)
Primary targetsSEA government/diplomaticEuropean industry; US tech/legalMongolian government
C2 transportChaCha20 over TCPWebSocket, DNS-over-HTTPS resolutionLoLS via Slack/Discord/Outlook
Platform reachWindows (Go cross-compilable)Windows and Linux variantsWindows
Dwell postureMonths, staged exfilVery long dwell, appliance-focusedPatient, low-noise

What these share is a pattern, not a fingerprint. All three chose Go for cross-platform reach and signature evasion. All three lean on infrastructure that blends in, whether that is cloud-provider hosting, DNS-over-HTTPS, or living-off-legitimate-services C2 through Slack and Outlook. All three prioritise dwell over speed. That convergence tells you something real about the current China-nexus toolkit meta, even if it tells you nothing definitive about which desk built GoSerpent. Treat the TetrisPhantom link as the working hypothesis Kaspersky offers, and treat the broader pattern as context, not proof.


YARA: fingerprinting the runtime and the unique artefacts

Two detection ideas drive these rules. First, gate on the Go pclntab magic to confirm you are looking at a Go binary. Second, require a couple of GoSerpent-specific string artefacts so you are not flagging every Go program on the planet.

rule GoSerpent_Backdoor_GoRuntime_Strings {
    meta:
        description = "GoSerpent backdoor: Go runtime + unique string constants"
        author      = "GenXCyber Research"
        reference   = "securelist.com/goserpent-backdoor-in-southeast-asia/120687/"
        date        = "2026-07-22"
    strings:
        // Go 1.20+ pclntab header magic (0xFFFFFFF1, LE) + pad
        $go_pclntab = { F1 FF FF FF 00 00 }
        // Fixed AES-CBC IV, ASCII "1234567890abcdef"
        $aes_iv     = "31323334353637383930616263646566" ascii wide
        $thumb_db   = "thumbcache_605a.db" ascii wide
        $tmc_guid   = "BBF061R2-BE25-4F6D-8B2D-1A6A39C3FSA2" ascii wide
        $staging    = "C:\\Users\\Public\\Libraries\\" ascii wide
        $decoy1     = "www.spacex.com" ascii wide
        $decoy2     = "github.code" ascii wide
        $mask1      = "lass.exe" ascii wide
        $mask2      = "updates.exe" ascii wide
    condition:
        uint16(0) == 0x5A4D and $go_pclntab and
        2 of ($aes_iv, $thumb_db, $tmc_guid, $staging, $decoy1, $decoy2, $mask1, $mask2)
}

For TmcLoader, the injection target and the config path anchor the rule, and the circular-XOR decode stub is the strongest discriminator once you extract its exact byte pattern from a confirmed sample:

rule TmcLoader_CXX_Injector {
    meta:
        description = "TmcLoader: svchost injection + XOR API-name obfuscation"
        author      = "GenXCyber Research"
        reference   = "securelist.com/goserpent-backdoor-in-southeast-asia/120687/"
    strings:
        $svchost     = "svchost.exe" ascii wide nocase
        $config_path = "C:\\Users\\Public\\Libraries\\" ascii wide
        $svc_install = "CreateServiceW" ascii wide
        // Extract the exact circular-XOR loop bytes from a confirmed
        // TmcLoader sample and add as a hex string here.
    condition:
        uint16(0) == 0x5A4D and $svchost and $config_path and $svc_install
}

And a rule for the Stowaway Stage 2 build, keyed on the framework package names GoReSym recovers plus its GCM/WebSocket transport strings:

rule Stowaway_Go_RAT_Customized {
    meta:
        description = "Customized Stowaway RAT used in GoSerpent Stage 2"
        author      = "GenXCyber Research"
    strings:
        $go_pclntab = { F1 FF FF FF 00 00 }
        $pkg1       = "Stowaway" ascii wide
        $pkg2       = "socks5" ascii wide
        $aes_gcm    = "NewGCM" ascii wide
        $ws         = "websocket" ascii wide nocase
    condition:
        uint16(0) == 0x5A4D and $go_pclntab and
        2 of ($pkg1, $pkg2, $aes_gcm, $ws)
}

Verify every string marked for confirmation against real samples from Kaspersky’s IOC release before you deploy these in production. The fixed IV and the config GUID path are sourced directly from Securelist and are solid. The XOR stub bytes you must lift yourself.


Forensic illustration of a serpent dissected on an examination board with glowing internal organs representing unique binary artifacts used as YARA detection anchors
Effective YARA detection anchors on GoSerpent’s durable artifacts – the fixed AES IV, the staging database name, and the config GUID path – rather than on network IOCs that rotate with infrastructure.

Detection and defense

Telemetry to have on

Sysmon does most of the heavy lifting here, provided you have a decent config and the right audit policies enabled underneath it.

Sysmon EventSignal
EID 1 (Process Create)lass.exe/updates.exe from odd parents; svchost.exe with service-atypical command lines
EID 3 (Network Connect)Beacons to Alibaba Cloud / UCLOUD HK ASNs (45102, 59019) from government hosts; SOCKS5 negotiation from non-browser images
EID 7 (Image Load)Unexpected DLL loaded into svchost.exe matching ThumbcacheService naming
EID 11 (File Create)Creation of C:\Users\Public\thumbcache_605a.db or C:\Users\Public\Libraries\*.db
EID 13 (Registry Set)New service key with a DLL ImagePath outside System32/SysWOW64
EID 25 (Process Tampering)Hollowing/injection events targeting svchost.exe

Pair that with the Microsoft-Windows-Security-Auditing provider for LSASS handle access (Event 4656/4663) and SAM hive reads (4663), and Microsoft-Windows-Kernel-Process for injection heuristics.

Sigma rules

title: SOCKS5 Proxy Negotiation by Non-Browser Process
logsource:
  product: windows
  category: network_connection
detection:
  selection:
    Initiated: 'true'
    DestinationPort: 1080
    Image|endswith:
      - '\updates.exe'
      - '\lass.exe'
  condition: selection
level: high
tags: [attack.command_and_control, attack.t1090.001]
title: Staging DB File Created in Public Libraries
logsource:
  product: windows
  category: file_event
detection:
  selection:
    TargetFilename|startswith: 'C:\Users\Public\Libraries\'
    TargetFilename|endswith: '.db'
  condition: selection
level: high
tags: [attack.collection, attack.t1074.001]
title: Service Registered with DLL ImagePath Mimicking System Component
logsource:
  product: windows
  category: registry_set
detection:
  selection:
    TargetObject|contains: '\CurrentControlSet\Services\'
    TargetObject|endswith: '\ImagePath'
    Details|endswith: '.dll'
  filter_legit:
    Details|contains: ['\System32\', '\SysWOW64\']
  condition: selection and not filter_legit
level: high
tags: [attack.persistence, attack.t1543.003]

Hunting queries

Because GoSerpent exfiltrates over SMB rather than C2, hunt the collection and staging behaviour, not the egress. A KQL starting point for Microsoft Defender:

DeviceFileEvents
| where FolderPath startswith @"C:\Users\Public\"
| where FileName in~ ("thumbcache_605a.db")
      or FolderPath startswith @"C:\Users\Public\Libraries\"
| project Timestamp, DeviceName, InitiatingProcessFileName, FolderPath, FileName

Then pivot on any process that both touched those paths and later authenticated to a file server, which is the exfiltration tell.

Hardening that actually blunts this chain

  • Turn on Credential Guard and LSA Protection (PPL) to break the Mimikatz LSASS dump.
  • Restrict SeDebugPrivilege to SYSTEM via Group Policy.
  • Enforce WDAC or AppLocker to block unsigned DLL service registration.
  • Apply egress filtering on government workstations against Alibaba Cloud / UCLOUD HK ranges; a diplomatic endpoint has no business beaconing there.
  • Drop canary files named thumbcache_605a.db in C:\Users\Public\ and alert on any read. GoSerpent will walk right into them.
  • Audit service registrations for newly added DLLs mimicking system component names.

Key takeaways

  • Go’s pclntab is your friend. Run GoReSym first, always. The runtime metadata that frustrates AV hands you a symbol table, and GoSerpent’s real logic is a few hundred bytes of relabelled main.* functions inside a mountain of scheduler noise.
  • The secrets are in the launch argument, not the binary. AES-CBC (fixed IV 31323334...) protects the C2 and password; ChaCha20 keyed on SHA-256 of that password protects the channel. Break at the decrypt call and read them from memory.
  • Detect the staging, not the exfil. GoSerpent moves data over authenticated SMB, so thumbcache_605a.db, the C:\Users\Public\Libraries\*.db config, and canary files matter far more than outbound-upload alerts.
  • TmcLoader hides its APIs with circular XOR plus Base64 and injects fileless into svchost.exe. Static import scans miss it. Find and run the decode stub.
  • Attribution is a hypothesis. TetrisPhantom overlap is Kaspersky’s working call, not a verdict. The stronger, safer claim is the pattern: Go, blended cloud/legit-service C2, and multi-month patient dwell now define this tier of espionage tooling, GoSerpent, BRICKSTORM, and GopherWhisper alike.
  • Pull the exact hashes and 11 C2 IPs from the primary Securelist report before you operationalise any of this. The behavioural indicators here are durable; the network IOCs rotate.

References