Tackle Inline Go Functions

The Problem

Within the last few releases, the Go compiler has gotten better at inlining smaller functions. This can sometimes make it harder when analyzing an unknown Go binary with a disassembler. Instead of seeing a call to a documented library function, the library function’s code has been “merged” with the caller and it is easy to waste time trying to decipher the library code when just reading the documentation would have been faster. The code snippet below shows a standard Hello World example in Go.

package main

import (
	"fmt"
)

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

If we look at the assembly code generated, the snippet below, we can see that there is no call to fmt.Println. The string to be printed is loaded into rax and moved to the stack location var_40h. Instead of seeing a preparation for a call to fmt.Println, a preparation for fmt.Fprintln is seen.

cmp rsp, qword [rcx + 0x10]
sub rsp, 0x58
lea rbp, [var_50h]
movups xmmword [var_40h], xmm0
mov qword [var_40h], rax
lea rax, [0x010eaa80] ; "Hello, World"
mov qword [var_48h], rax
mov rax, qword [sym._os.Stdout]
lea rcx, sym._go.itab._os.File_io.Writer
mov qword [rsp], rcx
mov qword [var_8h], rax
lea rax, [var_40h]
mov qword [var_10h], rax
mov qword [var_18h], 1
mov qword [var_20h], 1
call sym._fmt.Fprintln
mov rbp, qword [var_50h]
add rsp, 0x58
ret
nop
call sym._runtime.morestack_noctxt
jmp sym._main.main

In the Hello World scenario, it may not be too hard to identify where the code-inlining has happened. When the inlined code doesn’t perform any other calls and just bit operations, it can be a bit harder to work with.

The code snippet below is taken from a suspicious binary I analyzed a few months back. I have annotated the function calls with their function signature. On the first line, we can see a call to the function Now() in the time package. This function returns an exported structure called Time. On the last line, we can see a call to the method Seed and that it takes an int64. I don’t think it would be too much of a stretch to guess that the bit operations between the two calls are converting the data stored in the Time struct to an int64.

call fcn.time.Now ; func Now() Time
mov rax, qword [var_8h]
mov rcx, qword [rsp]
bt rcx, 0x3f
jae 0x4f6216
mov rax, rcx
shl rcx, 1
shr rcx, 0x1f
movabs rdx, 0xdd7b17f80
add rcx, rdx
mov rdx, qword [0x00673338]
mov qword [rsp], rdx
imul rcx, rcx, 0x3b9aca00
and rax, 0x3fffffff
movsxd rax, eax
add rax, rcx
movabs rcx, 0xa1b203eb3d1a0000
add rax, rcx
mov qword [var_8h], rax
call fcn.math_rand___Rand_.Seed ; func (r *Rand) Seed(seed int64)

If we take a look at the documentation for the time package, we can see that there are two methods for the Time struct that returns an int64.

% go doc time.Time | grep int64
func Unix(sec int64, nsec int64) Time
func (t Time) Unix() int64
func (t Time) UnixNano() int64

The Solution

Go binaries contain a data table called PCLNTab. This table was described in an earlier post but in short, it provides a translation between source code line numbers and process counters. GoRE provides access to this table via the method PCLNTab. The returned value is a pointer to the gosym.Table type, as can be seen in the function signature below.

func (f *GoFile) PCLNTab() (*gosym.Table, error)

This type has a set of methods that takes an instruction offset and returns information from it. The snippet shown below is part of the documentation for two of these methods. From the offset of the instruction, we can get the function, filename, and line number.

func (t *Table) PCToLine(pc uint64) (file string, line int, fn *Func)
    PCToLine looks up line number information for a program counter. If there is
    no information, it returns fn == nil.

func (t *Table) PCToFunc(pc uint64) *Func
    PCToFunc returns the function containing the program counter pc, or nil if
    there is no such function.

Using this information, it’s trivial to implement some logic that annotates the filename and line number within radare2. The code snippet below is one way of implementing it. The function takes a handler to an active session of radare2 and a handler to the file that is being analyzed. The current function and the PCLNTab are extracted. Next, all the instructions within the function are iterated through. For each instruction, the filename and line number are looked up. If both of these values are the same as the previous instruction, the instruction is skipped. If either of these values is different, a comment is created that annotates the filename and line number.

func srcLineInfo(r2 *r2g2.Client, file *gore.GoFile) {
	fn, err := r2.GetCurrentFunction()
	if err != nil {
		fmt.Printf("Failed to get current function: %s.\n", err)
		return
	}

	tbl, err := file.PCLNTab()
	if err != nil {
		fmt.Printf("Failed to get lookup table: %s.\n", err)
		return
	}

	var curFile string
	var curLine int
	for _, pc := range fn.Ops {
		fileStr, line, _ := tbl.PCToLine(pc.Offset)

		// Check if on the same source line.
		if line == curLine && fileStr == curFile {
			continue
		}
		curLine = line
		curFile = fileStr

		// Add line as multiline comment.
		comment := fmt.Sprintf("%s:%d", fileStr, line)
		encodedComment := base64.StdEncoding.EncodeToString([]byte(comment))

		// Execute command.
		cmd := fmt.Sprintf("CCu base64:%s @ 0x%x", encodedComment, pc.Offset)
		_, err := r2.Run(cmd)
		if err != nil {
			fmt.Println("Error when adding comment:", err)
			return
		}
	}
}

If we look back to the code snippet from earlier where we were trying to figure out which function was inlined and add the line numbers, it becomes easier to figure this out. Except for the first and last instructions, the rest of this code is from the standard library.

call fcn.time.Now ; src/Lock/internal/payload/payloadutil/payloadutil.go:16
mov rax, qword [var_8h]
mov rcx, qword [rsp]
bt rcx, 0x3f ; /usr/local/go/src/time/time.go:169
jae 0x4f6216
mov rax, rcx ; /usr/local/go/src/time/time.go:170
shl rcx, 1
shr rcx, 0x1f
movabs rdx, 0xdd7b17f80
add rcx, rdx
mov rdx, qword [0x00673338] ; /usr/local/go/src/math/rand/rand.go:303
mov qword [rsp], rdx
imul rcx, rcx, 0x3b9aca00 ; /usr/local/go/src/time/time.go:1165
and rax, 0x3fffffff ; /usr/local/go/src/time/time.go:164
movsxd rax, eax ; /usr/local/go/src/time/time.go:1165
add rax, rcx
movabs rcx, 0xa1b203eb3d1a0000
add rax, rcx
mov qword [var_8h], rax ; /usr/local/go/src/math/rand/rand.go:303
call fcn.math_rand___Rand_.Seed
lea rax, sym.type.uint8 ; src/Lock/internal/payload/payloadutil/payloadutil.go:17

This allows us to look at the source code for the times package to figure out which function was called. Lines 167 to 173 in the time.go file is the following code:

// sec returns the time's seconds since Jan 1 year 1.
func (t *Time) sec() int64 {
	if t.wall&hasMonotonic != 0 {
		return wallToInternal + int64(t.wall<<1>>(nsecShift+1))
	}
	return t.ext
}

Lines 162 to 165 in the same file has this code:

// nsec returns the time's nanoseconds.
func (t *Time) nsec() int32 {
	return int32(t.wall & nsecMask)
}

Finally, lines 1164 to 1166 has this:

func (t Time) UnixNano() int64 {
	return (t.unixSec())*1e9 + int64(t.nsec())
}

With this information, it’s straightforward to conclude that the inlined code is the method UnixNano.

This functionality has been added as part of redress version 0.7.0, just execute #!pipe redress -line from within radare2.