Blog

Go Hacking - Part 1

Apr 1, 2019 | 6 minutes read
Share this:

Tags: go, reversing

Go is a relatively new programming language that has grown a lot in recent years. Not surprisingly it also started being used by malware authors, which somehow caught security researchers off guard mainly after the APT28 attacks last year.

Riding the trend there is now an abundance of tools, online materials and soon to be released books teaching people how to leverage Go’s simple yet powerful constructs for pentesting, forensics and reconnaissance. In this 2-part series I show how Go can also be used for binary analysis and reversing engineering. For the first part I’ve chosen a simple task: how to enumerate all code caves in a Windows executable?

Code Caves

So what is a code cave? You’re free to check the Wikipedia definition but let me please summarize it for you.

Code caves are areas inside a program’s file that are not used for code (instructions). They’re usually filled out with zeros and are added by the compiler as either padding or placeholders for future code changes.

For a security researcher the interesting part about code caves is that they can be used to hide malicious code or data. I refer you to this blog post for more details about how code caves can be used and abused.

Coding

Fortunately Go’s library already gives us some cool APIs for working with different file formats. As mentioned earlier, let’s focus on Windows binaries (PE format). That said our first task is to open an exe file in Go and display its basic structure (called sections).

I want to show you the actual code, so in case you’re not familiar with Go I strongly advise you taking an online tour.

Already back? OK, let’s code!

package main

import (
	"debug/pe"
	"fmt"
	"log"
	"os"
)

func main() {
	f, err := pe.Open(os.Args[1])
	if err != nil {
		log.Fatalln(err)
	}
	defer f.Close()

	// Loop through all PE sections.
	for _, s := range f.Sections {
		fmt.Println(s)
	}
}

Quite basic, uh? We open an exe file passed via command line (os.Args[1]) and quickly loop through all its sections. When run against the 64-bit version of calc.exe this is the output:

&{{.text 2960 4096 3072 1024 0 0 0 0 1610612768} [] 0xc000082270 0xc000082270}
&{{.rdata 3142 8192 3584 4096 0 0 0 0 1073741888} [] 0xc0000822a0 0xc0000822a0}
&{{.data 1592 12288 512 7680 0 0 0 0 3221225536} [] 0xc0000822d0 0xc0000822d0}
&{{.pdata 228 16384 512 8192 0 0 0 0 1073741888} [] 0xc000082300 0xc000082300}
&{{.rsrc 18192 20480 18432 8704 0 0 0 0 1073741888} [] 0xc000082330 0xc000082330}
&{{.reloc 44 40960 512 27136 0 0 0 0 1107296320} [] 0xc000082360 0xc000082360}

It’s a bit messy I must agree. For the moment just pay attention to the section names (.text for instance) and their size (4th column). We’re going to use this information later on. Now let’s clean up the code a little bit:

package main

import (
	"debug/pe"
	"fmt"
	"log"
	"os"
	"strings"
)

func main() {
	f, err := pe.Open(os.Args[1])
	if err != nil {
		log.Fatalln(err)
	}
	defer f.Close()

	// Loop through all PE sections.
	sep := strings.Repeat("-", 35)
	fmt.Println(sep)
	for _, s := range f.Sections {
		fmt.Printf("Section: %s\n", s.Name)
		fmt.Printf("Size: %d bytes\n", s.Size)
		fmt.Println(sep)
	}
}

Our program’s output looks way cleaner now:

-----------------------------------
Section: .text
Size: 3072 bytes
-----------------------------------
Section: .rdata
Size: 3584 bytes
-----------------------------------
Section: .data
Size: 512 bytes
-----------------------------------
Section: .pdata
Size: 512 bytes
-----------------------------------
Section: .rsrc
Size: 18432 bytes
-----------------------------------
Section: .reloc
Size: 512 bytes
-----------------------------------

Finally we need to write a function — let’s call it Dig() — that will take a PE section and search for a sequence of null bytes, i.e. bytes whose values are zero. This byte sequence is our code cave. Dig() should also take a minimum cave size as argument since we know upfront how much space is required by the code or data that we want to hide.

func Dig(s *pe.Section, n int) {
	data, _ := s.Data()
	data = append(data, 0xff) // Sentinel.

	// Start digging...
	var index, begin, end, count int
	for i, b := range data {
		switch {
		case b == 0:
			count++
		case count >= n:
			// Cave found!
			index++
			begin = i - count
			end = i
			fmt.Printf("# Cave %d\n", index)
			fmt.Printf("\tSize        : %d bytes\n", count)
			fmt.Printf("\tOffset Start: %xh\n", uint32(begin)+s.Offset)
			fmt.Printf("\tOffset End  : %xh\n", uint32(end)+s.Offset)
			fallthrough
		default:
			count = 0 // reset counter to keep digging.
		}
	}
	if index == 0 {
		fmt.Println("No caves found.")
	}
}

There is a lot going on here and if you don’t have a programming background you head may begin to spin but bear with me. All that Dig() does is count all sequences of null bytes and print their location every time the sequence’s size (count) is greater than a certain value.

The complete code, including error checking and command line handling, can be found in my Github account. Now let’s try it with our calc.exe binary from the beginning. As an example we will look for code caves that are at least 100 bytes in size.

$ ./caveminer calc.exe 100
-----------------------------------
Section .text (3072 bytes)
# Cave 1
        Size        : 112 bytes
        Offset Start: f90h
        Offset End  : 1000h
-----------------------------------
Section .rdata (3584 bytes)
# Cave 1
        Size        : 117 bytes
        Offset Start: 10a3h
        Offset End  : 1118h
# Cave 2
        Size        : 444 bytes
        Offset Start: 1c44h
        Offset End  : 1e00h
-----------------------------------
Section .data (512 bytes)
# Cave 1
        Size        : 431 bytes
        Offset Start: 1e51h
        Offset End  : 2000h
-----------------------------------
Section .pdata (512 bytes)
# Cave 1
        Size        : 286 bytes
        Offset Start: 20e2h
        Offset End  : 2200h
-----------------------------------
Section .rsrc (18432 bytes)
# Cave 1
        Size        : 128 bytes
        Offset Start: 42a8h
        Offset End  : 4328h
# Cave 2
        Size        : 386 bytes
        Offset Start: 6750h
        Offset End  : 68d2h
# Cave 3
        Size        : 243 bytes
        Offset Start: 690dh
        Offset End  : 6a00h
-----------------------------------
Section .reloc (512 bytes)
# Cave 1
        Size        : 470 bytes
        Offset Start: 6a2ah
        Offset End  : 6c00h
-----------------------------------

We did it! We found all code caves and it only required a little digging. That was indeed a great feat for a program comprising of less than 80 lines of code. Opening calc.exe in a hex editor we can easily locate the first code cave reported by our program:

Code cave in calc.exe

I hope this first part has whet your appetite because in Go Hacking - Part 2 I will show how to build a disassembler from scratch.

comments powered by Disqus