A simple writeup about a CTF from Rootme
28/12/2021
I would like to write something about this CTF because it makes me take a deeper look at how a simple Go binary is formed. When just looking over a binary formed from Go it’s impossible not to think how weird this binary is, more because of the large size for small instructions written, easily a “Hello World” compiled gets more than one megabyte.
The main reason is because all of those functions called “runtime” (light weight threads) have the ability to improve things in executing time, automating management that in turn of other languages like C would demand a lot from the developer even if building programs with simple goals. Go as a language is aiming to be simple but empowered for high scaling, performance and security.
Go includes in its binary a more powerful runtime support which provides a garbage collection process to manage those objects that are no longer in use allocated in heap memory. Garbage collection will usually affect performance, in fact, Go spends a disproportionate amount of time collecting garbage and this can be seen in any benchmark, however this feature probably will help a lot by automating explicit deallocation and avoiding its problems, but that’s not the focus at the moment.
Another look at the importance of knowing how to analyse binaries from the Golang compiler it is because a lot of malware is being created in this way, with perhaps the most interesting example, the formidable Mirai Botnet, the one that made CDNs suck in 2016, what a good story.
The syntax is simple, powerful and the libraries are already compiled along with the code, another important point for the file size. And it’s fast, so simpler programs use fewer resources. Want a better way to create malware?
In reviewing this reverse engineering and cracking CTF I will not be using any functionality to recreate a pseudo code from the instructions that some tools provide because assembly is cooler and a pain at the same time, but alright.
When running the binary on the machine, it asks for user input, after that it prints the message “wrong flag” on the screen and terminates the process, so it needs the correct password.
To debug the binary I usually like to use the GDB (GNU Debugger) with a plugin named Gef, which extends the GDB a lot in the matter of possibilities and also makes the interface more intuitive.
After running gdb the first thing to do is to list all functions and try to take some good information from this enumeration, however, the number of functions is easily bigger than one thousand, so it’s not helpful to analyze it, because it would take a long time.
The first step is to set a breakpoint at entry to function “main” which initializes the program.
Disassembly Main
gef➤ disas main
Dump of assembler code for function main:
0x0000000000452310 <+0>: lea rax,[rip+0xffffffffffffc7a9] # 0x44eac0 <runtime.rt0_go>
0x0000000000452317 <+7>: jmp rax
What appears to be a “runtime” (function), there is nothing useful in the main() function. If we dig in a bit further we can see that the actual code we are interested in is inside of main.main that really has the main functions inside this.
disas *main.main
!The full code is at the end!
The main function is extensive which makes me think that all the code worth analyzing is in here, the rest are imported libraries. There are some interesting functions to mention right at the beginning.
This part is not important for resolution but it is how a new object (structure) is created. Objects are instantiated using runtime.newobject, taking a structure pointer as an argument. The structure definition is being defined in the .typelink section of the binary.
<+47>: lea rax,[rip+0x1053a] # 0x4a33e0
<+54>: mov QWORD PTR [rsp],rax
<+58>: call 0x40ec00 <runtime.newobject>
Inside main +150 there is a function that waits for user input:
<+150>: call 0x48d970 <fmt.Scanln>
What was entered is sent to memory and its address pointer is stored in the register r10.
$r10 : 0x000000c4200180d0 → 0x0000000000616161 ("aaa"?)
…
gef➤ print $r10
$1 = 0xc4200180d0
gef➤ x/1xs $r10
0xc4200180d0: "aaa")
Then there is a loop passing the string into a xor function, which uses “rootme” as a key to encode the input from the user.
Another thing to pay attention to when analyzing Go binaries is that strings do not end in null bytes, and are usually concatenated with other strings, so it is always important to pay attention to the length of the string that is passed along with it.
Using the tool radare2 makes it more friendly to see the memory exchanges with strings ascii format.
0488d053a1503. lea rax, [0x004c446d] ; "rootmesignalstatusstringstructsw…
4889442408 mov qword [var_8h], rax
48c744241006. mov qword [var_10h], 6
The fragment above shows an address being loaded into rax, the address refers to a string which contains the key. As mentioned Go binaries does not separate strings with null bytes, the line below is moving the length in bytes that will be loaded.
$r8 : 0x000000c42003df10 → 0x0000656d746f6f72 ("rootme"?)
…
0x492fed <main.main+381> movzx edx, BYTE PTR [r8+rdx*1]
→ 0x492ff2 <main.main+386> xor r10d, edx
The xor function will be called in the loop the number of times it relates to the length of the string entered, and then register r9 will be incremented each time it passes through the loop. which defines the position of the array that will index the character.
the value representing the position in the key array is also incremented, in this way the string character is XORed with the character that matches with it in the key.
If the current character of the entered string is not the last character, the loop continues performing a jump to main.
<+318>: inc r9
….
<+381>: movzx edx, BYTE PTR [r8+rdx*1]
<+386>: xor r10d, edx
<+389>: cmp r9, rcx
<+392>: jb 0x492fa7 <main.main+311>
r10d The current caractere from the entered string. rcx The length of the entered string. edx Current string from the key. jb Jump if below.
Once the string has been encoded it will be sent as the argument to a function that compares bytes to validate if the entered password is the correct one. then there is the password already encoded stored in memory, this password just needs to be found.
<+404>: mov QWORD PTR [rsp],rbx
<+408>: mov QWORD PTR [rsp+0x8],0xe
<+417>: mov QWORD PTR [rsp+0x10],0xe
<+426>: mov QWORD PTR [rsp+0x18],rdx
<+431>: mov QWORD PTR [rsp+0x20],rcx
<+436>: mov QWORD PTR [rsp+0x28],rax
<+441>: call 0x4510d0 <bytes.Compare>
I set a breakpoint at the calling of the “bytes.Compare” function and ran the program. In the code above it is possible to see that the content from the register rbx has been moved to rsp. Then the value 0xe in hex is moved as well, which will be passed as an argument being the length of the string.
In this way it is possible to extract the contents of rbx in a range of 14 (0xe) bytes, as the length of the string is seen being passed to rsp+0x10.
gef➤ x/14xb $rbx
0x3b 0x02 0x23 0x1b 0x1b 0x0c 0x1c 0x08
0x28 0x1b 0x21 0x04 0x1c 0x0b
Decoding this byte string with the key “rootme” using the xor function the result is: ImLoving******
$ ./ch32.bin
ImLoving******
u can validate with this flag
I learnt from this CTF that analysing Golang binaries is a pain at the first look, but understanding how it works and how the functions are handled, it is very intuitive. The assembly formed can look like a mess when comparing with C, but it is unfair, considering that Golango’s own team cites it as the new C and that, the attribution of easier to use, in thi case other features are lost such as the simplicity of instructions, which gives the quality of speed and lightness.
Dump of assembler code for function main.main:
0x0000000000492e70 <+0>: mov rcx,QWORD PTR fs:0xfffffffffffffff8
0x0000000000492e79 <+9>: lea rax,[rsp-0x38]
0x0000000000492e7e <+14>: cmp rax,QWORD PTR [rcx+0x10]
0x0000000000492e82 <+18>: jbe 0x49310a
0x0000000000492e88 <+24>: sub rsp,0xb8
0x0000000000492e8f <+31>: mov QWORD PTR [rsp+0xb0],rbp
0x0000000000492e97 <+39>: lea rbp,[rsp+0xb0]
0x0000000000492e9f <+47>: lea rax,[rip+0x1053a] # 0x4a33e0
0x0000000000492ea6 <+54>: mov QWORD PTR [rsp],rax
0x0000000000492eaa <+58>: call 0x40ec00
0x0000000000492eaf <+63>: mov rax,QWORD PTR [rsp+0x8]
0x0000000000492eb4 <+68>: mov QWORD PTR [rsp+0x78],rax
0x0000000000492eb9 <+73>: mov QWORD PTR [rsp+0x80],0x0
0x0000000000492ec5 <+85>: mov QWORD PTR [rsp+0x88],0x0
0x0000000000492ed1 <+97>: lea rcx,[rip+0xbea8] # 0x49ed80
0x0000000000492ed8 <+104>: mov QWORD PTR [rsp+0x80],rcx
0x0000000000492ee0 <+112>: mov QWORD PTR [rsp+0x88],rax
0x0000000000492ee8 <+120>: lea rcx,[rsp+0x80]
0x0000000000492ef0 <+128>: mov QWORD PTR [rsp],rcx
0x0000000000492ef4 <+132>: mov QWORD PTR [rsp+0x8],0x1
0x0000000000492efd <+141>: mov QWORD PTR [rsp+0x10],0x1
0x0000000000492f06 <+150>: call 0x48d970
0x0000000000492f0b <+155>: mov rax,QWORD PTR [rip+0x4312e] # 0x4d6040
0x0000000000492f12 <+162>: mov rcx,QWORD PTR [rip+0x4312d] # 0x4d6046
0x0000000000492f19 <+169>: mov QWORD PTR [rsp+0x42],rax
0x0000000000492f1e <+174>: mov QWORD PTR [rsp+0x48],rcx
0x0000000000492f23 <+179>: lea rax,[rsp+0x50]
0x0000000000492f28 <+184>: mov QWORD PTR [rsp],rax
0x0000000000492f2c <+188>: lea rax,[rip+0x3153a] # 0x4c446d
0x0000000000492f33 <+195>: mov QWORD PTR [rsp+0x8],rax
0x0000000000492f38 <+200>: mov QWORD PTR [rsp+0x10],0x6
0x0000000000492f41 <+209>: call 0x43ed90
0x0000000000492f46 <+214>: mov rax,QWORD PTR [rsp+0x18]
0x0000000000492f4b <+219>: mov QWORD PTR [rsp+0x70],rax
0x0000000000492f50 <+224>: mov rcx,QWORD PTR [rsp+0x20]
0x0000000000492f55 <+229>: mov QWORD PTR [rsp+0x38],rcx
0x0000000000492f5a <+234>: mov rdx,QWORD PTR [rsp+0x78]
0x0000000000492f5f <+239>: mov rbx,QWORD PTR [rdx+0x8]
0x0000000000492f63 <+243>: lea rsi,[rip+0x11756] # 0x4a46c0
0x0000000000492f6a <+250>: mov QWORD PTR [rsp],rsi
0x0000000000492f6e <+254>: mov QWORD PTR [rsp+0x8],rbx
0x0000000000492f73 <+259>: mov QWORD PTR [rsp+0x10],rbx
0x0000000000492f78 <+264>: call 0x43b1a0
0x0000000000492f7d <+269>: mov rax,QWORD PTR [rsp+0x28]
0x0000000000492f82 <+274>: mov rcx,QWORD PTR [rsp+0x20]
0x0000000000492f87 <+279>: mov rdx,QWORD PTR [rsp+0x18]
0x0000000000492f8c <+284>: mov rbx,QWORD PTR [rsp+0x78]
0x0000000000492f91 <+289>: mov rsi,QWORD PTR [rbx+0x8]
0x0000000000492f95 <+293>: mov rbx,QWORD PTR [rbx]
0x0000000000492f98 <+296>: mov rdi,QWORD PTR [rsp+0x38]
0x0000000000492f9d <+301>: mov r8,QWORD PTR [rsp+0x70]
0x0000000000492fa2 <+306>: xor r9d,r9d
0x0000000000492fa5 <+309>: jmp 0x492fb7
0x0000000000492fa7 <+311>: mov BYTE PTR [r12+r9*1],r10b
0x0000000000492fab <+315>: inc rbx
0x0000000000492fae <+318>: inc r9
0x0000000000492fb1 <+321>: mov rax,r11
0x0000000000492fb4 <+324>: mov rdx,r12
0x0000000000492fb7 <+327>: cmp r9,rsi
0x0000000000492fba <+330>: jge 0x492fff
0x0000000000492fbc <+332>: movzx r10d,BYTE PTR [rbx]
0x0000000000492fc0 <+336>: test rdi,rdi
0x0000000000492fc3 <+339>: je 0x493103
0x0000000000492fc9 <+345>: mov r11,rax
0x0000000000492fcc <+348>: mov rax,r9
0x0000000000492fcf <+351>: mov r12,rdx
0x0000000000492fd2 <+354>: cmp rdi,0xffffffffffffffff
0x0000000000492fd6 <+358>: je 0x492fdf
0x0000000000492fd8 <+360>: cqo
0x0000000000492fda <+362>: idiv rdi
0x0000000000492fdd <+365>: jmp 0x492fe4
0x0000000000492fdf <+367>: neg rax
0x0000000000492fe2 <+370>: xor edx,edx
0x0000000000492fe4 <+372>: cmp rdx,rdi
0x0000000000492fe7 <+375>: jae 0x4930fc
0x0000000000492fed <+381>: movzx edx,BYTE PTR [r8+rdx*1]
0x0000000000492ff2 <+386>: xor r10d,edx
0x0000000000492ff5 <+389>: cmp r9,rcx
0x0000000000492ff8 <+392>: jb 0x492fa7
0x0000000000492ffa <+394>: jmp 0x4930fc
0x0000000000492fff <+399>: lea rbx,[rsp+0x42]
0x0000000000493004 <+404>: mov QWORD PTR [rsp],rbx
0x0000000000493008 <+408>: mov QWORD PTR [rsp+0x8],0xe
0x0000000000493011 <+417>: mov QWORD PTR [rsp+0x10],0xe
0x000000000049301a <+426>: mov QWORD PTR [rsp+0x18],rdx
0x000000000049301f <+431>: mov QWORD PTR [rsp+0x20],rcx
0x0000000000493024 <+436>: mov QWORD PTR [rsp+0x28],rax
0x0000000000493029 <+441>: call 0x4510d0
0x000000000049302e <+446>: mov rax,QWORD PTR [rsp+0x30]
0x0000000000493033 <+451>: test rax,rax
0x0000000000493036 <+454>: jne 0x4930a1
0x0000000000493038 <+456>: mov QWORD PTR [rsp+0xa0],0x0
0x0000000000493044 <+468>: mov QWORD PTR [rsp+0xa8],0x0
0x0000000000493050 <+480>: lea rax,[rip+0x11529] # 0x4a4580
0x0000000000493057 <+487>: mov QWORD PTR [rsp+0xa0],rax
0x000000000049305f <+495>: lea rax,[rip+0x4306a] # 0x4d60d0
0x0000000000493066 <+502>: mov QWORD PTR [rsp+0xa8],rax
0x000000000049306e <+510>: lea rax,[rsp+0xa0]
0x0000000000493076 <+518>: mov QWORD PTR [rsp],rax
0x000000000049307a <+522>: mov QWORD PTR [rsp+0x8],0x1
0x0000000000493083 <+531>: mov QWORD PTR [rsp+0x10],0x1
0x000000000049308c <+540>: call 0x486bc0
0x0000000000493091 <+545>: mov rbp,QWORD PTR [rsp+0xb0]
0x0000000000493099 <+553>: add rsp,0xb8
0x00000000004930a0 <+560>: ret
0x00000000004930a0 <+560>: ret
0x00000000004930a1 <+561>: mov QWORD PTR [rsp+0x90],0x0
0x00000000004930ad <+573>: mov QWORD PTR [rsp+0x98],0x0
0x00000000004930b9 <+585>: lea rax,[rip+0x114c0] # 0x4a4580
0x00000000004930c0 <+592>: mov QWORD PTR [rsp+0x90],rax
0x00000000004930c8 <+600>: lea rax,[rip+0x43011] # 0x4d60e0
0x00000000004930cf <+607>: mov QWORD PTR [rsp+0x98],rax
0x00000000004930d7 <+615>: lea rax,[rsp+0x90]
0x00000000004930df <+623>: mov QWORD PTR [rsp],rax
0x00000000004930e3 <+627>: mov QWORD PTR [rsp+0x8],0x1
0x00000000004930ec <+636>: mov QWORD PTR [rsp+0x10],0x1
0x00000000004930f5 <+645>: call 0x486bc0
0x00000000004930fa <+650>: jmp 0x493091
0x00000000004930fc <+652>: call 0x425a40
0x0000000000493101 <+657>: ud2
0x0000000000493103 <+659>: call 0x425b20
0x0000000000493108 <+664>: ud2
0x000000000049310a <+666>: call 0x44ef70
0x000000000049310f <+671>: jmp 0x492e70