DEV Community

Cover image for Looking inside Go - Reverse Engineering
Brayden V
Brayden V

Posted on

Looking inside Go - Reverse Engineering

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.

alt text

The linked attachment is a compressed file which contains a pdf and an unknown file named init_sat.

alt text

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.

alt text

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!

alt 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.

Alt Text

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'.

alt text

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.

Alt Text

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!

Alt Text

Sniffing out the flag

After connecting to the satellite we are prompted with several options. The first option is of particular interest.

Alt Text

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.

Alt Text

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.

Alt Text

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)