Fused String API Decoding
A Go anti-analysis technique where Windows DLL names and their exported API names are concatenated into single large strings in .rdata, then sliced at runtime into individual syscall.LoadLibrary / GetProcAddress targets. Nets the attacker two things: strings tools only see the monolithic blob, and naive YARA/Sigma rules looking for VirtualAlloc in isolation miss the binary entirely.
Detection / Fingerprint
- Look for
.rdatastrings that fusekernel32.dllwith one or more API names with no delimiter:kernel32.dllVirtualAllocGetTempPathWinvalid_slothost_is_down...^[strings.txt:1615] - Look for Go binaries whose IAT imports only
kernel32.dllyet runtime resolves dozens of APIs viasyscall.LoadLibrary^[pefile.txt:292-347] - Function names are randomized Go main-package symbols (
sym.main.rxzsirwk,sym.main.lkezaikuw,sym.main.xhqsnoekoztspb) ^[r2:sym.main.rxzsirwk]
Implementation Patterns Observed
In lummastealer (e03dd36f), the implementation pattern is:
- Table construction (
sym.main.rxzsirwk) — allocates a slice of Gostringobjects viaruntime.newobject. Each object points to a substring inside a fused.rdatablob with a hardcoded length prefix. ^[r2:sym.main.rxzsirwk @ 0x140089ce0] - Index-based loading (
sym.main.lkezaikuw) — receives two integer indices (start byte, length), bounds-checks them against the fused blob, callssym.main.xhqsnoekoztspbto produce a Go string, then passes it tosyscall.LoadLibrary. ^[r2:sym.main.lkezaikuw @ 0x140087e80] - String helper (
sym.main.xhqsnoekoztspb) — pureruntime.slicebytetostringwrapper. ^[r2:sym.main.xhqsnoekoztspb @ 0x140088620]
Reproduce on Your Own VMs
package main
import (
"fmt"
"syscall"
"unsafe"
)
// fusedBlob contains: kernel32.dllVirtualAllocGetTempPathW...
var fusedBlob = []byte(
"kernel32.dllVirtualAllocGetTempPathWshell32.dllShellExecuteW")
func loadFromFused(offset, length int) uintptr {
sub := string(fusedBlob[offset : offset+length])
mod, _ := syscall.LoadLibrary(sub)
return uintptr(mod)
}
func main() {
// "VirtualAlloc" starts at byte 12, length 12
v := loadFromFused(12, 12)
fmt.Printf("resolved handle: %x\n", v)
}
Build with:
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -trimpath" fused.go
Then run strings against the binary — VirtualAlloc should not appear as a standalone string.
Defensive Countermeasures
- Memory-based string extraction after the table is built (e.g., floss dynamic) may still recover sliced strings.
- Behavioral detection: single-import
kernel32.dllGo binary that rapidly loadsadvapi32.dll,ws2_32.dll,crypt32.dll, etc. viaLoadLibraryis suspicious. - Sigma rule: monitor for
LoadLibrarycalls from a parent process with no corresponding import table entries.
Pages Where Observed
- lummastealer —
e03dd36fsibling - 9d2ca3 —
29149758sibling:main.Xjnzvbmnconstructs a buffer from a fused.rdatablob (VirtualAllocrandautoseedsweepWaiters...), slices it viamain.uciyqnpcdd/main.qhybyctuf, and passes the result tomain.sncqxmmgztyymeiwhich resolves the API throughsyscall._LazyProc_.Call. ^[/intel/analyses/2914975816372d0dc79b777915f66955d312213ea036b84ff16ad5ab0bcfdd66.html]