First: the Context
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 cybersec 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 init_sat
.
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.
And Dumping...
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 init_sat.go
.
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 fmt.Fprint
.
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 bufio.(*Reader).ReadBytes
.
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 init_sat.go
again.
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
The 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 AX
or CX
register.
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 JNE
instruction.
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 0x74697865
to AX
followed by a comparison of 0xa
with 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 AX
offset 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 username
and 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.
Top comments (0)