Recently I’ve gotten hung-up attempting to analyze some malware samples that were written in Go. Looking at stripped Go binaries statically is a nightmare and I’ve had to over-rely on dynamic analysis and have gotten lucky enough to get the samples to fully execute without a hitch. But then I thought about the prospect of running into a Golang sample that had extreme anti-analysis measures, and it made me decide to perform a deep-dive on Go malware now so that I wouldn’t have to do it later.
Initial Look
Starting off with the basics I wanted to write a simple “Hello, World!” program in Golang and take a look at its unstripped version as a binary in Ghidra and a few other tools. Here’s the code for the initial program:
//HelloWorld.go
//version 1.21.5 windows/amd64
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
The results can be seen with this command:
go run HelloWorld.go
Output: Hello, World!
Now let’s build it into a binary:
go build
Which will create an executable file that looks like “HelloWorld.exe”. Just to verify, execute it on the command line like this:
.\HelloWorld.exe
Output: Hello, World!
Super easy so far! The binary it produced is 1.8MB so it’s pretty large for such a simple program. Luckily Detect-It-Easy recognizes that this is a Go binary. Checking the strings for “Hello, World!” is interesting because it does identify it, but it is concatenated with a ton of other strings:
It also shows the entropy as “6.86” and says it’s packed:
Opening it up in Ghidra there are 1147 functions (😅), and one of them is named main.main:
The function call can be identified here:
The decompiled view is pretty telling as well:
I want to also highlight that objdump is an option here:
Command: go tool objdump -s <func> <binary>
Example: go tool objump -s main HelloWorld.exe
It’s also worthwhile to point out that it contains information regarding the exact line numbers from the code:
Another useful option is “nm”:
Command: go tool nm <Binary>
Example: go tool nm .\HelloWorld.exe
Then checking for main and looking through the strings can be as simple as this:
At this point lets build the stripped version as well.
go build -ldflags "-s" -o HelloWorldStripped.exe
The binary size is 1.21MB now instead of the previous 1.8MB. Using the “nm” tool again lets see what it can pull up:
Running “objdump” yields similar results:
Running GoReSym from Madiant yields some results with the stripped binary though, showing the our main function and the call to the “fprintln” function:
.\GoReSym_win.exe -t -d -p .\HelloWorldStripped.exe | Select-String -pattern 'println'
The output from it can be fully parsed for clues on the functionality of the program and Mandiant also created a script to import these results into IDA.
Jumping into x64dbg I had to go several layers down just to see our targeted string pop-up:
Debugging Golang has definitely proved A LOT harder than I thought it would be. Jumping back into the HelloWorld.exe (not stripped) I found some more manipulations of the string:
I found this section by using the Go “nm” tool and looking for “println”. From there I took the address and set it as a breakpoint in x64dbg.
That led to a call that if stepped over would execute the print, but is that THE function call? Or is there one I need to dive deeper to grab? I think this will be a clearer distinction to make if this was an if/else test instead of a printing-out one.
Weirdly enough I’ve landed in an area at this point where char by char the string is being moved. Some more research into this (for my sake) is definitely called for 😂:
At this point I’m going to realign my objectives though. I could keep “wasting” time delving deeper into how exactly the print function works within this, but I’m going to take a look and see how I would go about patching the “Hello, World!” string to have another term within it.
Lucky for me, I didn’t obfuscate anything. Throwing this in a hex editor I’m able to navigate to the offset Detect-It-Easy shows:
Now, time to test it!
I’m going to declare this a [moderate] success, but there are still a lot of questions I want to answer for myself. The eventual goal is to have the skills needed to patch a malicious Golang binary that has anti-analysis capabilities and it will be quite the journey to get there!
In the next post I’d like to create my own CTF challenge surrounding if/else statements in Go. From there I can start implementing internet & VM checks, and then add an obfuscator into the mix and see if I can still reach the flag planted in the binaries. If anyone has any great learning resources surrounding patching Golang feel free to leave a comment!