Go Malware: How Far Can Memory Analysis Take Us? — A Paper Review
Go Malware: How Far Can Memory Analysis Take Us? — A Paper Review
When a server address or file path turns up in memory, I want to know how it is used. An address in a connection configuration needs to be interpreted differently from a path used as an argument to a file API. The question I had while reading this paper was “Can we tell what role a string we found plays in the program?”
The paper I read for this review is Memory Forensics Techniques for Automated Detection and Analysis of Go Malware. It uses Go runtime structures to analyze strings, function information, and goroutine stacks in memory. The authors implemented their techniques as three Volatility 3 plugins: go_strings, go_functions, and go_goroutines. Public implementation
| Item |
Details |
| Authors |
Hala Ali, Andrew Case, Irfan Ahmed |
| Publication |
Forensic Science International: Digital Investigation, 57 Supplement, 302118, June 2026 |
| Paper |
DOI · DFRWS public PDF |
I will first look at how a string's pointer and length are read. Then, through the Screenshotter, BRICKSTORM, and Pantegana cases, I will discuss how function arguments and HTTP connection objects are interpreted.
1. Where does a Go string begin and end?
A Go string here means a value of Go's string type. For example, s := "hello" stores the string "hello" in the variable s.
In Go, a string is a sequence of bytes. Its length is the number of bytes, not the number of characters, and the ABI represents it as a pointer to the data and a length. In the x86-64 environments covered by this paper, each occupies 8 bytes. The part containing these two values is the string header, while the actual data is stored at the address the pointer refers to. Go language specification · Go 1.25.5 ABI documentation
The following hypothetical memory layout illustrates this. Suppose the 11 bytes of adminupload are stored consecutively starting at 0x5000.
| Header address |
Data pointer |
Length |
Referenced value |
0x7000 |
0x5000 |
5 |
admin |
0x7020 |
0x5005 |
6 |
upload |
A conventional string search may show adminupload as a single value. However, the first header refers to the first 5 bytes, and the second refers to the following 6 bytes. Even without a separator at the end of each string, the pointers and lengths identify the boundaries of admin and upload.
A Go string is not always valid UTF-8. For example, string([]byte{0x41, 0x00, 0xff}) is also a string of length 3. Strings can contain null bytes or bytes that cannot be interpreted as UTF-8, so an extractor that primarily selects candidates based on printable characters cannot be assumed to enumerate every Go string. Go's explanation of strings and bytes
2. Starting with runtime metadata
Finding two values that look like a pointer and a length is not enough to interpret them as a string. We also need to know which structure to use when reading that location and which memory region the referenced address belongs to. Analyzing functions and stacks likewise requires a way to associate code addresses with function information.
The Go runtime metadata used in the analysis is summarized below. It contains information about memory regions, functions, and types. The exact fields and layouts vary between Go versions. Go 1.25.5 runtime source
| Structure |
Information used in the analysis |
pclntab and related tables |
Associations between code addresses and functions, function names, source files and line numbers, and information needed to interpret stacks |
moduledata |
Memory regions of the loaded executable and the locations of function and type tables |
typelinks, itablinks |
References used to follow type information and interface implementation information |
Type information is used to interpret object fields, while function information identifies the function associated with a particular code address.
GoReSym is another tool for recovering Go metadata, including function names and types. Official GoReSym repository
The paper also describes using an executable remaining in the page cache to supplement function names and source paths. Paper §3.1.2
3. go_strings reads both string values and their locations
go_strings follows Go's memory management structures to locate candidate heap objects. A span is a memory region the Go runtime uses to manage the heap, and a slot is a fixed-size position within it where an object can reside.
The plugin uses allspans and mspan to locate spans and slots. It then checks candidate types and reads pointers and lengths from fields that can be interpreted as strings. Objects of the same size can have different types, so it also checks referenced addresses, lengths, and actual contents. Public string analysis code
The size of an allocation slot does not always match the size of the type itself. Go uses size classes for small object allocations, which can leave unused space in a slot. Size should therefore be treated as a clue for narrowing down candidate types. Go 1.25.5 allocator documentation
The paper describes object recovery using allocation bitmaps, but the version of the public code referenced here scans slots without restricting the scan to slots that are currently allocated. Its output should therefore not be interpreted as a list of currently allocated objects. Paper §3.2.1 · Code examined for this review
What to look for in the Screenshotter output
Figure 1. Go string extraction results for Screenshotter. Source: Original paper · Figure 4(b) · PDF p. 7.
When reading Figure 4(b), I looked at both the value user1password and the locations of its header and data. Its length is 13 bytes, and the header is marked as heap, while the data is marked as rodata.
The header of this string is on the heap, but its actual bytes are in a read-only region. Finding a string on the heap does not, on its own, establish that its contents were newly created at runtime.
If a string created at runtime is assigned to a global string variable, its actual bytes may be on the heap even though the header is in the global data region. Go 1.25.5 string conversion source
4. go_functions analyzes arguments at call sites
Once the boundaries of a string are known, the next question is which code uses that value. go_functions reconstructs function metadata, classifies functions by origin, and interprets argument types and layouts. It uses method type information, function signature data, and argument layout information left by the compiler. Public function analysis code
Classifying functions by origin helps narrow the analysis to application code among runtime and standard library functions. Classification as application code is not, by itself, a determination that the code is malicious.
Reading arguments also requires accounting for the calling convention, or ABI. Go 1.17 introduced a new calling convention that uses registers on amd64 platforms, including Linux and Windows. Where arguments should be read from therefore depends on the target Go version. Go 1.17 release notes
The plugin works backward through the instructions preceding a CALL to analyze which values are placed in argument locations. It follows instructions that load constants, copy values between registers, and calculate data addresses to recover values where possible. Call-site analysis implementation
File-related calls in BRICKSTORM
Figure 2. Argument setup before file API calls in BRICKSTORM. Source: Original paper · Figure 5 · PDF p. 9.
This BRICKSTORM sample was compiled with Go 1.16.3 and uses a calling convention that places arguments on the stack. In the figure, the lea instructions before calls to os.ReadFile, os.Remove, and os.WriteFile calculate addresses. The subsequent mov instructions place pointers and lengths on the stack.
The pointers and lengths used earlier to identify string boundaries represent paths to be passed to file APIs in this figure.
What has been recovered here is the code that sets those paths as arguments. Whether a call actually executed and a file write succeeded must be checked against the file system or other execution records.
5. go_goroutines follows objects referenced by the stack
A goroutine is a unit of execution managed by the Go runtime. The runtime's g structure contains information about its state and stack, and allgs maintains references to goroutine structures. Go 1.25.5 runtime structures · Code managing allgs
go_goroutines walks stack frames using these structures, saved PC and SP values, and stack information for each function. PC indicates a code location, while SP indicates a stack location. It then interprets frame arguments and reads fields from objects referenced by pointers. Public goroutine analysis code
The analysis here reads values linked to preserved stack frames. Checking whether a server address appears in a connection object's configuration or in a request buffer helps interpret its purpose.
Pantegana's HTTP connection state
In the Pantegana case, the researchers compiled the source code and ran it in a controlled local network. They recovered HTTP connection object settings and buffers through standard library stack frames in waiting goroutines. Paper §4.4
Figure 3. Information recovered from Pantegana's HTTP/TLS-related goroutines. Source: Original paper · Table 5 · PDF p. 10. The addresses and token are values from the experimental environment.
The table summarizes connection destinations, TLS state, and request and response buffers. persistConn is a structure in Go's HTTP transport that manages a connection, its read and write buffers, and its TLS state. Go 1.25.5 HTTP transport source
I read this table in the order goroutine → function frame → object field → value. Distinguishing bw.buf from br.buf shows which buffers contain the requests and responses. What stood out to me was that the analysis could identify both the string contents and the object and field they came from.
Even with HTTPS, applications need data in a readable form when constructing requests and processing responses. Reading HTTP buffers in memory must be distinguished from decrypting TLS ciphertext on the network.
The presence of request contents in a buffer also does not establish that the server received the request. The valid portion of the buffer and the correspondence between requests and responses need to be checked together.
Interpreting reconstructed stacks
Reading a stack requires considering both the goroutine's state and the ABI. The saved PC and SP in g.sched do not necessarily match the CPU state at capture time for every goroutine. Stack space reserved for register arguments is not guaranteed to contain valid values either. Go 1.25.5 stack tracing implementation · Argument storage in the ABI
6. What do the experimental results show?
The authors evaluated the following four cases using memory images collected in controlled environments. The table summarizes the experimental setup for each case. The target architecture was x86-64. Paper §4 and §5
| Case |
OS |
Go version |
| Screenshotter |
Linux |
1.24.10 |
| Obscura |
Windows |
1.15 |
| BRICKSTORM |
Linux |
1.16.3 |
| Pantegana |
Linux |
1.25.5 |
In the Obscura case included in the table, the authors recovered a Curve25519 public key. Recovering a public key does not mean that the victim's files were successfully decrypted. Paper Table 1
For Pantegana, the authors identified 31 goroutines, marked 23 of them as valid, and associated 13 with HTTP/TLS communication. Paper §4.4
Here, validation refers to checking whether candidate structures are plausible using properties such as stack bounds, state values, and goroutine identifiers. Paper §3.4
These numbers describe the scope of what was analyzed in this case. Assessing a recovery success rate would require comparison with what was actually present, checking both omissions and incorrect identifications.
The authors explain that the current analysis focuses on recovering arguments at call sites and in goroutine frames. Tracking data flow through local variables and return values remains future work. Paper §5
Linux analysis with the public implementation requires a memory image and kernel symbols. These kernel symbols are separate from debug symbols in a Go binary. Plugin-specific additional inputs, such as function signature data, and the preparation order are described in the public repository.
One question I would like to explore further is how much can be analyzed when only part of the memory is available. I would like to examine separately how much of the string data can be read and how much of it can be linked to objects and stacks.
Conclusion
Reading this paper made me want to look more closely at where a string value was located and what code or objects it was connected to after finding the string itself. In Screenshotter, I looked at header and data locations; in BRICKSTORM, at argument setup code; and in Pantegana, at buffer fields. When reading similar analysis results in the future, I would like to check the string contents together with the evidence that explains those values.
References