Debugging Golang based ELF malware
Golang is simple and has a cross-platform compiler which gives malware authors a chance to develop malware without putting too much effort into the code.
Golang is a compiled language just like C/C++ which requires no VM.Go compiled binaries include a runtime package which takes care of language features like Garbage collection, scheduling & concurrency.This blog discusses golang routines, some details about the go scheduler which comes with runtime and later debugging of a golang based fileless ELF malware.
Before debugging let’s discuss a bit about some functionalities of golang:
- Goroutines in golang is just a function call which includes the ‘go’ keyword before the call. For example, In go hello(), hello is the goroutine.Goroutines are mainly used for multithreading in which it provides lightweight threads that are managed by its runtime package.
- Goroutines include the runtime package, the runtime package comes with its own scheduler known as the go scheduler. The go scheduler takes responsibility for the execution of the goroutines.
- This go scheduler by-default works with one CPU core on the machine, so even if we are using dual-core, the scheduler would use one core.
- Above diagram illustrates the execution of the goroutines via go scheduler. At any given time only one goroutine gets executed in the core.
- Now if a programmer keeps the value of environment variable GOMAXPROCS > 1, the goroutines would get divided into multiple cores which means multiple goroutines would be running at same time, as shown below:
- Goroutines internally use channels to communicate with each other(other goroutines).Channels are just like pipes which connect concurrent goroutines.We could create a new channel with make(chan val-type).
Let’s now try to understand some of the functions which are their in go runtime package, below images show the graphical view of instructions and runtime package functions commonly seen in a go compiled file:
When we define a goroutine, we do something like go hello(), go scheduler later launches the goroutine executing hello(). The compiler turns this go statement into a runtime.newproc function. In other words, go hello(args) can be seen as runtime.newproc(size,hello,args).
This is generally used for dynamic memory allocation, internally it implements mallocgc.
Go programs use resizable stack.This function in golang collects space for the stack and is repeatedly called until enough space has been allocated for new instructions.
- Write barrier
The runtime package in go also manages heap memory by garbage collection. It does this in a way by freeing memory from the objects which are not referenced on the stack and thereby making heap available for the new objects.When the garbage collector starts, it turns on the write barriers to maintain the data integrity.
Now as we are aware of golang routine calls, let’s start debugging of a golang based ELF malware. The sample is not very recent, Bitdefender and Microsoft have already talked about this(and similar) sample.
The sample could be downloaded from malwarebazaar.
Analysis of the golang elf malware binary
We start analyzing the sample by loading it in radare2. Radare2 could be installed by running sudo apt-get install radare2.
Command used to load the malware: r2 <malware_binary> :
After loading we check for functions in the binary using afl command as shown:
The ELF malware binary was stripped, which means no function names were visible while disassembling the binary.
For function renaming, we have an opensource tool called redress which is generally used during analysis of Go binaries. We could also detect the compiler version, source tree of the binary using redress. Command: redress -src <binary>
Redress can also be piped with radare as explained in redress documentation, so for functions renaming we would use pipe, command: #!pipe redress :
Now, as we got the function names, we would debug this malware binary using gdb .
NOTE:For debugging, radare could also be used but we will explore that in later blogs. For this blog, let’s continue with gdb.
Function listed above tells a lot about the behaviour of the malware binary like InstallStartUp must be related to persistence, Execshell might execute shell script.
Command to load binary in gdb: sudo gdb -q <binary> :
We start by putting breakpoints on above notable listed functions we got in radare2:
We also set the debugger to continue in case the malware binary spawns child process, for that command would be set follow-fork-mode child and then we just run the binary using r command:
The breakpoint gets hit on a certain function, which is main.run , we could see this in radare2:
Now to look what instructions and functions main.run includes, we use seek in radare2 and we view it visually through graph, command for which is s <addr> and VV for graph view:
Digging into the graph of main.run, the functions which looked interesting were main.IsDir, call to mkdir, chmod, copytofile and exec as shown below(fig a, fig b, fig c):
Now, in malware calls like mkdir and Copytofile indicates that the malware must be creating a directory to a certain location where it could store malicious files or from where it could run another malware(in-memory execution). Let’s explore this behavior in the gdb.
In previous step, we had a breakpoint hit at main.run, so we continue(c in gdb), as expected we get a hit at IsDir function:
Disassembling IsDir function in gdb requires x command(examine) , we disassemble first 15 instructions using command x/15i $eip :
We could see a jump to 0x823a5c7(in image above), so we follow that by putting bp at 0x823a580 address (b *0x823a580) and then stepping each instruction by using si command:
Now we are at the location where the jump directed, looking into assembly instructions, the interesting part above is at 0x823a589, so we put bp at 0x823a58c to look what eax carries:
To examine eax, we use the command: x/s $eax.
We can see that in-memory, the malware creates a hidden directory at /usr/bin location by the name .esd-644.
We continue(c) in gdb and bp hits on copyToFile:
Let’s see the graph of copyTofile to find out what malware does in here:
Looking at the graph above, the malware first opens a file using OpenFile call and copies contents of the opened file(the malware itself) to another malicious file. Let’s debug this in gdb to find out the destination file.
We put BP at OpenFile where it gets called(b *0x08238885) and another bp at the location where arguments of OpenFile gets stored to stack for later inspection of register values:
And there we go, we finally get the file where the malicious content of the malware gets copied. The auditd is the file where the malicious content gets written. Lets now try to find the malicious content which gets written to auditd.
After the OpenFile, the malware calls copyBuffer to copy the contents and chmod to run the auditd file.
For contents, we can either put BP on write syscall while debugging in gdb or we can use strace utility. Running strace with malware we can see the data, the malware copies to auditd from itself.
Once the malicious content gets written to the auditd, the auditd thread executes a shell script for persistence.
As we can see above the script ensures the persistence for auditd file for different *nix based systems like centos fedora debian etc using cron. In the script, main_program is the variable assigned to auditd.