Introduction to analysing Go binaries

Published: 2019-03-22
Last Updated: 2019-03-22 16:13:46 UTC
by Remco Verhoef (Version: 1)
0 comment(s)

Golang is gaining popularity by malware authors, and more golang based malware is being found in the wild. It is also one of my favourite programming languages, especially for all network related applications, for the reasons of:

  • the language syntax itself is simple, but you can do everything with it
  • binaries are being compiled statically, no need to install libraries
  • support for many operating systems and architectures
  • statically typed
  • goroutines (light weight threads)
  • channels (pipes using which goroutines communicate)

And my most important reason is that code I've created years ago, still compiles without the need to update or refactor the code to the newer compilers and language versions.  

Due the popularity of Golang by malware authors, we need more often to analyse these binaries and they are a bit different. Let me give you some primers.

Let's start first with a Hello World Golang application first, to give you an idea about the language and the structure.

package main

import "fmt"

func main() {
        fmt.Println("Hello World")

It starts with a package name, all files in the same folder will have the same package name. In this case main. Next we'll configure the imports, in this case the fmt library. Go is very struct and only allows imports that are being used. The main entry point is always called main. Now the code can be compiled for different architectures using the following command:

$ GOOS=linux GOARCH=amd64  go build -o hello ./hello.go 


Go compiled binaries are large, the Hello World example in this case is 1.9M. Now we know the basics, we can start analysing a real malware named sshd, which someone delivered kindly to our honeypots. Because of the large binaries, upx is being used often to suppress the file size. By default upx compress files can be decompressed using:

$ upx -d ./sshd 


When loading the binary into your favourite disassembler, you'll find many functions and strings. Most of those functions and strings are from default Go packages. If you have an address of a function start, the golang tool addr2line will come in handy. Addr2line uses the gopclntab section to retrieve data about the original function name, source file and locations. You'll probably encounter cases where the location of the source file contains the username in its path. 

$ echo "0x0062cd40" | go tool addr2line ./sshd


There is also objdump which will output symbols and source locations together with assembly code if possible:

$ go tool objdump -s main ./vv 

  vv.go:321             0x6128e5                e8b6e9ffff              CALL
  vv.go:321             0x6128ea                488b442418              MOVQ 0x18(SP), AX
  vv.go:321             0x6128ef                488b4c2420              MOVQ 0x20(SP), CX
  vv.go:322             0x6128f4                48890424                MOVQ AX, 0(SP)
  vv.go:322             0x6128f8                48894c2408              MOVQ CX, 0x8(SP)
  vv.go:322             0x6128fd                e86ee7ffff              CALL main.rsaDecrypt(SB)
  vv.go:322             0x612902                488b442410              MOVQ 0x10(SP), AX
  vv.go:322             0x612907                488b4c2418              MOVQ 0x18(SP), CX
  vv.go:323             0x61290c                48890424                MOVQ AX, 0(SP)
  vv.go:323             0x612910                48894c2408              MOVQ CX, 0x8(SP)
  vv.go:323             0x612915                e896feffff              CALL main.jsonload(SB)
  vv.go:323             0x61291a                488d742410              LEAQ 0x10(SP), SI
  vv.go:323             0x61291f                488d7c2440              LEAQ 0x40(SP), DI


Strings in the binary are not null terminated, but instead all concatenated to long strings. When strings are being referenced to, a following instruction will load the length. The instruction lea rax, ... loads the string reference, where the following instructions defines the length of the string to load, in this case 7 and makes it the string /bin/sh. In later Golang version this has been changed, as I believe, which I'll get back to in a later post.


Objects are being instantiated using runtime_newobject, taking a structure pointer as argument. The definition of the struct is being defined in the .typelink section of the binary. 


Version information in the binary can be found by searching for go1, which function runtime_schedinit references to. 


The call to sym.runtime.morestack_noctxt will resize the stack, as Go makes use of resizable stacks, then it will return to the start of the function, this pattern marks also the end and start of the function. 


This was just a quick overview of Golang binaries, more to follow. Please share your ideas, comments and/or insights, with me via social media, @remco_verhoef or email, remco.verhoef at dutchsec dot com. Have a great day!

Remco Verhoef (@remco_verhoef)
ISC Handler - Founder of DutchSec

0 comment(s)


Diary Archives