Looking inside Go - Reverse Engineering
July 18, 2020
Google hosts an abundance of technical events where avid programmers and engineers can get hands-on experience solving problems and battling it out against some of the best engineers in the world. One event in particular is the Google CTF where cyber-security experts can try their hand at hacking at various levels of security challenges.
Google also host a beginner’s quest (the “qualifiers”) for those looking for a challenge but are just getting started in the industry. While it is called the “beginner’s quest” the challenges are usually by no means simple for those just starting out. However, they require less man power and breadth of knowledge compared to the main event.
Beginner’s Quest 2019
One of the challenges from the 2019 quest was simply named “Satellite” and labelled as a networking challenge.
The linked attachment is a compressed file which contains a pdf and an unknown file named
We can use the
file command to get a better idea of what the
init_sat file is.
$ file init_sat init_sat: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=YhfyV09rKV_0ewkLiNr1/6ZJO5J8awFQSRgZDzlnA/zvyuoO7Qu3ralSU_Aheb/QK0rATh0jzljJY8j2313, not stripped
Here we can see that it is actually a 64-bit executable. We can also see that the binary is not stripped, indicating that the symbols table is still intact. This is useful as it means the binary contains the function names as well as other useful debug information.
Running, Running, Running
To get an idea of what we are dealing with we should first execute the binary. After allowing execution (
chmod +x) we see that the program outputs a prompt asking for the name of the satellite.
Practise what you preach
It’s always good practice to try what we know before digging deep. When presented with user input we can test for the usual pitfalls; buffer overflows, printf formatting bugs and so on. Unfortunately these trivial tests proved futile in revealing any of the typical bugs.
To dig further, we need to dump the assembly code and understand how the program functions.
objdump -D init_sat > init_sat.asm
On inspection of the dump we notice there is a lot of assembly code. In fact the assembly dump is a total of 85MB of text!
This is a bit unexpected, especially since we noted before that the executable was dynamically linked. The point of a dynamically linked executable is that it uses shared libraries already installed on your operating system. In doing so it does not have to include bundled library code, greatly reducing its size.
Skimming through a small section of the dumped assembly, we notice a few functions that sound like standard library functions. For example,
x_cgo_sys_thread_create. Searching for this function reveals that this binary was actually written in Go.
When a Go binary is compiled, the standard libaries/Go runtime are statically linked. This has the consequence of ballooning the overall file size, resulting in even basic CLI programs being megabytes in size.
Fortunately Go also includes a powerful toolkit that we can use to debug and investigate further. For example, it includes an
objdump command which we can use to dump the main function.
go tool objdump -s "main.main" init_sat
This command outputs way more debug information, with Go-specific symbols being included in the output. The dump also includes the Go file and associated line number that the assembly implements. Here is a snippet to illustrate what that looks like.
init_sat.go:12 MOVQ CX, 0xb0(SP) init_sat.go:12 NOPL print.go:243 MOVQ os.Stdout(SB), CX print.go:243 LEAQ go.itab.*os.File,io.Writer(SB), DX print.go:243 MOVQ DX, 0(SP) print.go:243 MOVQ CX, 0x8(SP) print.go:243 LEAQ 0xa8(SP), CX print.go:243 MOVQ CX, 0x10(SP) print.go:243 MOVQ $0x1, 0x18(SP) print.go:243 MOVQ $0x1, 0x20(SP) print.go:243 CALL fmt.Fprint(SB) init_sat.go:16 NOPL init_sat.go:16 MOVQ os.Stdin(SB), AX init_sat.go:16 MOVQ AX, 0x70(SP)
NB: Memory addresses have been ommitted for ease of reading
Here we can see assembly code for the main file (
init_sat.go) and one of the standard library functions (
print.go). This clear distinction gives us more power to glance over the assembly and get to what we really care about. Since we aren’t dissecting the standard library, we can mostly ignore the assembly code from anything other than
How that we have a clear focus we can start to cross-reference the behaviour of the program with the assembly in front of us.
Recalling the functionality, we note that the program first prints a message to the user. This takes form in the assembly snippet above with the call to
The user is then prompted to enter the name of the satellite to begin the connection. It achieves this via the
bufio.go library code which makes a call to
print.go:243 CALL fmt.Fprint(SB) init_sat.go:18 NOPL bufio.go:474 LEAQ 0x110(SP), AX bufio.go:474 MOVQ AX, 0(SP) bufio.go:474 MOVB $0xa, 0x8(SP) bufio.go:474 CALL bufio.(*Reader).ReadBytes(SB)
We can continue to skim over this library code until we find assembly from
bufio.go:475 0x4f8b87 MOVQ CX, 0x68(SP) init_sat.go:20 0x4f8b8c MOVQ CX, 0(SP) init_sat.go:20 0x4f8b90 MOVQ AX, 0x8(SP) init_sat.go:20 0x4f8b95 CALL strings.ToLower(SB) init_sat.go:20 0x4f8b9a MOVQ 0x10(SP), AX init_sat.go:20 0x4f8b9f MOVQ 0x18(SP), CX
strings.ToLower function is part of the Go standard library. As the name suggests, it converts a string to lowercase and returns the resulting string.
We can assume that one of the
MOVQ instructions is moving the lowercase string into a register. We can continue to follow the program flow to figure out whether it is the
init_sat.go:24 0x4f8ba4 CMPQ $0x5, CX init_sat.go:24 0x4f8ba8 JNE 0x4f8bbc
This code block compares
0x5 to the value in the
CX register and then jumps to a memory address if it is not equal. For now we can assume that
CX == 5 and follow that branch of execution. In this case the program continues directly after the
init_sat.go:24 0x4f8baa CMPL $0x74697865, 0(AX) init_sat.go:24 0x4f8bb0 JNE 0x4f8bbc init_sat.go:24 0x4f8bb2 CMPB $0xa, 0x4(AX) init_sat.go:24 0x4f8bb6 JE 0x4f8ca6
The instructions above compare multiple values with the contents of the
AX register. The first instruction compares
AX followed by a comparison of
AX offset by 0x4 bytes.
Converting the literal values (represented in hexadecimal) to ASCII, we note that they translate to ‘tixe’ and the newline character. Reversing the text (due to LSB format) reveals the string ‘exit\n’.
Obviously these instructions are checking if the user entered ‘exit’ to quit. We can then conclude that the
JE instruction will jump to a memory address which prints “Exiting, goodbye” before terminating the program.
This is interesting, but not exactly what we are after. We need to know what name for the satellite the program is expecting. Let’s take a look where the execution jumps to when
CX != 5.
init_sat.go:21 0x4f8bbc CMPQ $0x7, CX init_sat.go:21 0x4f8bc0 JNE 0x4f8bdc init_sat.go:21 0x4f8bc2 CMPL $0x696d736f, 0(AX) init_sat.go:21 0x4f8bc8 JNE 0x4f8bdc init_sat.go:21 0x4f8bca CMPW $0x6d75, 0x4(AX) init_sat.go:21 0x4f8bd0 JNE 0x4f8bdc init_sat.go:21 0x4f8bd2 CMPB $0xa, 0x6(AX) init_sat.go:21 0x4f8bd6 JE 0x4f8c89
At this point we can probably infer that the
CX register is storing the length of the entered string. Previously the
CMPQ instruction checked for
0x5 and the instructions that followed were looking for ‘exit\n’ - a five byte string. The first instruction in this code block is comparing
0x7 to the
CX register, implying that the satellite name is seven characters in length.
Again, the instructions execute a string comparison. The first
CMPL compares 0x696d736f, or ‘imso’ with
AX. If this passes, it then compares 0x6d75 (‘mu’) with
0x4 bytes. The final comparison checks for the familiar newline character. Reversing and putting it altogether we get
osmium\n - a seven character string!
Sniffing out the flag
After connecting to the satellite we are prompted with several options. The first option is of particular interest.
Here we can see the
password printed to the screen. Unfortunately the
password has been redacted. The config data response also includes a link to a Google doc which has a single base64 encoded string.
echo "VXNlcm5hbWU6IHdpcmVzaGFyay1yb2NrcwpQYXNzd29yZDogc3RhcnQtc25pZmZpbmchCg==" | base64 -d Username: wireshark-rocks Password: start-sniffing!
As suggested let’s fire up wireshark and start sniffing! Once we setup wireshark to sniff the correct interface, we need to reconnect to the satellite to capture the packets that send the config data.
To filter the noise, we can search the packet bytes for the “Username” string, as shown in the screenshot above. We can then follow the TCP stream to reveal the plaintext password.
That’s it! We have successfully solved this CTF challenge by first reversing the Go binary to find the satellite name, connecting to the satellite and finally sniffing for the password which was returned in plaintext over the wire.