Crackme 15: ELF x64 - Golang basic

Feb 10, 2021

Link: https://www.root-me.org/en/Challenges/Cracking/ELF-x64-Golang-basic (binary)

$ echo 'foo' | ./ch32.bin
wrong flag

The error message doesn’t appear immediately when we run the program interactively or with a command line argument, it seems instead to be waiting for data on standard input.

It’s easy to see that this is a Go program; the title mentions it here but the symbols leave no doubt:

$ objdump -t ./ch32.bin
./ch32.bin:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*   0000000000000000 go.go
0000000000401000 l     F .text   0000000000000000 runtime.text
0000000000493180 l     F .text   0000000000000000 runtime.etext
[...]
0000000000486ac0 g     F .text   00000000000000f2 fmt.Fprint
0000000000486bc0 g     F .text   0000000000000089 fmt.Print
0000000000486c50 g     F .text   0000000000000107 fmt.getField
[...]

These are clearly symbols for Go functions, so at least we have this much information and entry points for them.

If we run the program with GDB and put a breakpoint on fmt.Fprint, we might see where the error message is coming from:

$ echo 'foo' > /tmp/input.txt   # preparing input
$ gdb ./ch32.bin
gdb$ b fmt.Fprint
Breakpoint 1 at 0x486ac0: file /usr/lib/go/src/fmt/print.go, line 213.
gdb$ r < /tmp/input.txt
Starting program: ./ch32.bin < /tmp/input.txt
[New LWP 85]
[New LWP 86]
[New LWP 87]
[New LWP 88]
[New LWP 89]
Error while running hook_stop:

Thread 1 "ch32.bin" hit Breakpoint 1, fmt.Fprint (w=..., a=..., n=0x4a46c0, err=...) at /usr/lib/go/src/fmt/print.go:213
213   in /usr/lib/go/src/fmt/print.go
gdb$ fin
Run till exit from #0  fmt.Fprint (w=..., a=..., n=0x4a46c0, err=...) at /usr/lib/go/src/fmt/print.go:213
Error while running hook_stop:

Thread 1 "ch32.bin" hit Breakpoint 1, fmt.Fprint (w=..., a=..., n=0x4a46c0, err=...) at /usr/lib/go/src/fmt/print.go:213
213   in /usr/lib/go/src/fmt/print.go
gdb$ bt
#0  fmt.Fprint (w=..., a=..., n=0x4a46c0, err=...) at /usr/lib/go/src/fmt/print.go:213
#1  0x0000000000486c17 in fmt.Print (a=..., n=0xc42008c028, err=...) at /usr/lib/go/src/fmt/print.go:225
#2  0x00000000004930fa in main.main () at /home/jenaye/dev/C/CTF_perso/reverseGo.go:22
gdb$ fin
Run till exit from #0  fmt.Fprint (w=..., a=..., n=0x4a46c0, err=...) at /usr/lib/go/src/fmt/print.go:213
wrong flagError while running hook_stop:
0x0000000000486c17 in fmt.Print (a=..., n=0xc420080028, err=...) at /usr/lib/go/src/fmt/print.go:225
225   in /usr/lib/go/src/fmt/print.go

After the first breakpoint was hit, we use fin once to step out of the frame and it hits the breakpoint a second time without printing wrong flag. At this stage we dump the backtrace with bt which gives even more information about the binary (including the path to the source file under /home/jenaye) and the call to fmt.Print being made from main.main at only line 22. After a second fin the error message was printed, so we might be able to use this backtrace to find the logic that leads to it.

If we open ch32.bin in Hopper and jump to main.main, a few things are noticeable straight away. main.main starts at 00492e70 and we can easily follow the calls being made. Here it is with a few lines cut out:

0000000000492eaa   call runtime.newobject                  ; runtime.newobject
[...]
0000000000492ee8   lea  rcx, qword [rsp+0xb8+var_38]       ; argument #4 for method fmt.Scanln
0000000000492ef0   mov  qword [rsp+0xb8+var_B8], rcx       ; argument #7 for method fmt.Scanln
0000000000492ef4   mov  qword [rsp+0xb8+var_B0], 0x1       ; argument #8 for method fmt.Scanln
0000000000492efd   mov  qword [rsp+0xb8+var_A8], 0x1       ; argument #9 for method fmt.Scanln
0000000000492f06   call fmt.Scanln                         ; fmt.Scanln
0000000000492f0b   mov  rax, qword [main.statictmp_2]      ; main.statictmp_2
0000000000492f12   mov  rcx, qword [main.statictmp_2+6]    ; argument #4 for method runtime.stringtoslicebyte, 0x4d6046
[...]
0000000000492f28   mov  qword [rsp+0xb8+var_B8], rax       ; argument #7 for method runtime.stringtoslicebyte
0000000000492f2c   lea  rax, qword [go.string.*+1093]      ; 0x4c446d
0000000000492f33   mov  qword [rsp+0xb8+var_B0], rax       ; argument #8 for method runtime.stringtoslicebyte
0000000000492f38   mov  qword [rsp+0xb8+var_A8], 0x6       ; argument #9 for method runtime.stringtoslicebyte
0000000000492f41   call runtime.stringtoslicebyte          ; runtime.stringtoslicebyte
0000000000492f46   mov  rax, qword [rsp+0xb8+var_A0]
0000000000492f4b   mov  qword [rsp+0xb8+var_48], rax
0000000000492f50   mov  rcx, qword [rsp+0xb8+var_98]       ; argument #4 for method runtime.makeslice
0000000000492f55   mov  qword [rsp+0xb8+var_80], rcx
0000000000492f5a   mov  rdx, qword [rsp+0xb8+var_40]       ; argument #3 for method runtime.makeslice
0000000000492f5f   mov  rbx, qword [rdx+8]
0000000000492f63   lea  rsi, qword [type.*+67264]          ; argument #2 for method runtime.makeslice, 0x4a46c0
0000000000492f6a   mov  qword [rsp+0xb8+var_B8], rsi       ; argument #7 for method runtime.makeslice
0000000000492f6e   mov  qword [rsp+0xb8+var_B0], rbx       ; argument #8 for method runtime.makeslice
0000000000492f73   mov  qword [rsp+0xb8+var_A8], rbx       ; argument #9 for method runtime.makeslice
0000000000492f78   call runtime.makeslice                  ; runtime.makeslice

We can see an allocation with newobject, then a call to fmt.Scanln to read from stdin, then stringtoslicebyte, then makeslice. So far nothing unexpected. It continues further down with a call to bytes.Compare at 00493029, followed by a JNE 004930a1 that either jumps to a block containing calls to fmt.Print, or continues into code also containing calls to fmt.Print. And indeed if we look at the bt call from earlier we can see that our call to fmt.Print had been made from 004930fa which is in the “not equals” branch of the JNE call:

#1  0x0000000000486c17 in fmt.Print (a=..., n=0xc42008c028, err=...) at /usr/lib/go/src/fmt/print.go:225
#2  0x00000000004930fa in main.main () at /home/jenaye/dev/C/CTF_perso/reverseGo.go:22

Here’s the comparison code:

0000000000492fff   lea    rbx, qword [rsp+0xb8+var_76]
0000000000493004   mov    qword [rsp+0xb8+var_B8], rbx      ; argument #7 for method bytes.Compare
0000000000493008   mov    qword [rsp+0xb8+var_B0], 0xe      ; argument #8 for method bytes.Compare
0000000000493011   mov    qword [rsp+0xb8+var_A8], 0xe      ; argument #9 for method bytes.Compare
000000000049301a   mov    qword [rsp+0xb8+var_A0], rdx      ; argument #10 for method bytes.Compare
000000000049301f   mov    qword [rsp+0xb8+var_98], rcx      ; argument #11 for method bytes.Compare
0000000000493024   mov    qword [rsp+0xb8+var_90], rax      ; argument #12 for method bytes.Compare
0000000000493029   call   bytes.Compare                     ; bytes.Compare
000000000049302e   mov    rax, qword [rsp+0xb8+var_88]
0000000000493033   test   rax, rax
0000000000493036   jne    loc_4930a1                        ;         jumps over the following block
                                                            ;                                     │
0000000000493038   mov    qword [rsp+0xb8+var_18], 0x       ;                                     │
0000000000493044   mov    qword [rsp+0xb8+var_10], 0x       ;                                     │
0000000000493050   lea    rax, qword [type.*+66944]         ; 0x4a458                             │
0000000000493057   mov    qword [rsp+0xb8+var_18], rax      ;                                     │
000000000049305f   lea    rax, qword [main.statictmp_0]     ; main.statictmp_                     │
0000000000493066   mov    qword [rsp+0xb8+var_10], ra       ;                                     │
000000000049306e   lea    rax, qword [rsp+0xb8+var_18       ;                                     │
0000000000493076   mov    qword [rsp+0xb8+var_B8], rax      ; argument #7 for method fmt.Print    │
000000000049307a   mov    qword [rsp+0xb8+var_B0], 0x1      ; argument #8 for method fmt.Print    │
0000000000493083   mov    qword [rsp+0xb8+var_A8], 0x1      ; argument #9 for method fmt.Print    │
000000000049308c   call   fmt.Print                         ; fmt.Print                           │
                                                            ;                                     │
0000000000493091   mov    rbp, qword [rsp+0xb8+var_8]       ;                                     │
0000000000493099   add    rsp, 0xb                          ;                                     │
00000000004930a0   ret                                      ;                                     │
                    ; endp                                  ;                                     │
loc_4930a1:                                                 ; this is where the JNE land     <────┘
00000000004930a1   mov    qword [rsp+0xb8+var_28], 0x0
00000000004930ad   mov    qword [rsp+0xb8+var_20], 0x0
00000000004930b9   lea    rax, qword [type.*+66944]         ; 0x4a4580
00000000004930c0   mov    qword [rsp+0xb8+var_28], rax
00000000004930c8   lea    rax, qword [main.statictmp_1]     ; main.statictmp_1
00000000004930cf   mov    qword [rsp+0xb8+var_20], rax
00000000004930d7   lea    rax, qword [rsp+0xb8+var_28]
00000000004930df   mov    qword [rsp+0xb8+var_B8], rax      ; argument #7 for method fmt.Print
00000000004930e3   mov    qword [rsp+0xb8+var_B0], 0x1      ; argument #8 for method fmt.Print
00000000004930ec   mov    qword [rsp+0xb8+var_A8], 0x1      ; argument #9 for method fmt.Print
00000000004930f5   call   fmt.Print                         ; fmt.Print
00000000004930fa   jmp    loc_493091

The comparison followed by the two print calls is even clearer in CFG mode:

comparison

Let’s take a look at what is being compared. Shortly after the call to makeslice, we reset r9d and jump to code that includes a loop. It goes like this:

0000000000492fa2   xor      r9d, r9d                ; reset counter
0000000000492fa5   jmp      loc_492fb7

loc_492fa7:
0000000000492fa7   mov      byte [r12+r9], r10b
0000000000492fab   inc      rbx                     ; increment rbx counter
0000000000492fae   inc      r9                      ; increment r9 counter
0000000000492fb1   mov      rax, r11
0000000000492fb4   mov      rdx, r12

loc_492fb7:
0000000000492fb7   cmp      r9, rsi                 ; compare r9 to input length
0000000000492fba   jge      loc_492fff

0000000000492fbc   movzx    r10d, byte [rbx]        ; stores one byte of input in r10d
0000000000492fc0   test     rdi, rdi
0000000000492fc3   je       loc_493103

0000000000492fc9   mov      r11, rax
0000000000492fcc   mov      rax, r9
0000000000492fcf   mov      r12, rdx
0000000000492fd2   cmp      rdi, 0xffffffffffffffff
0000000000492fd6   je       loc_492fdf
    
0000000000492fd8   cqo 
0000000000492fda   idiv     rdi                     ; used to repeat the constant string
0000000000492fdd   jmp      loc_492fe4
    
loc_492fdf:
0000000000492fdf   neg      rax
0000000000492fe2   xor      edx, edx

loc_492fe4:
0000000000492fe4   cmp      rdx, rdi
0000000000492fe7   jae      loc_4930fc

0000000000492fed   movzx    edx, byte [r8+rdx]      ; stores one byte of the constant string into edx
0000000000492ff2   xor      r10d, edx               ; XORs input and the current byte of this string
0000000000492ff5   cmp      r9, rcx                 ; checks if we’ve reached the end
0000000000492ff8   jb       loc_492fa7

0000000000492ffa   jmp      loc_4930fc

loc_492fff:
0000000000492fff   lea      rbx, qword [rsp+0xb8+var_76]

If we break at the beginning of this code, we can see that rbx contains our input string, and rsi might be its length. We also have rdi containing the value 6, and r8 points to a string of length 6, rootme:

gdb$ printf "%s", $rbx
foo
gdb$ print $rsi
$1 = 0x3
gdb$ print $rdi
$2 = 0x3
gdb$ printf "%s", $r8
rootme

It seems from the cmp r9, rsi and the inc r9 that r9 is an increment that goes the length of our input. I don’t think we had seen a cqo instruction in previous posts; it doubles the size of rax and copies the sign of rax into every bit of rdx. The idiv rdi that follows divides edx:eax by rdi and stores the quotient in rax and the remainder in rdx – a division + mod in one instruction. After this operation we jump to 00492fe4 and do a cmp rdx, rdi (remember rdi was 6 and here rdx is 0) followed by jae or Jump-if-Above-or-Equal. At this point it’s worth looking at the next instruction, movzx edx, byte [r8+rdx]. This is storing one byte of this constant string into edx.

We had seen above that with movzx r10d, byte [rbx] we had stored a byte of our input into r10d, and the next instruction xor r10d, edx XORs them together storing the result in r10d. As we jump back to the top, this value is stored at the buffer pointed by r12, indexed by the increment r9: mov byte [r12+r9], r10b. We increment our counters with inc rbx and inc r9, and continue. The last step of this loop stores a value in rbx, the disassembled instruction lea rbx, qword [rsp+0xb8+var_76] corresponding to lea rbx,[rsp+0x42]. If we look at what’s in rsp+0x42, we can see a few pre-computed bytes:

gdb$ x /16b $rsp+0x42
0xc42003df02:   0x3b   0x2    0x23   0x1b  0x1b  0xc  0x1c  0x8
0xc42003df0a:   0x28   0x1b   0x21   0x4   0x1c  0xb  0x72  0x6f

This loop XORs together our input with the constant rootme, and after the loop we have a call to bytes.Compare. If we XOR rootme again with what’s now being stored in rbx (presumably for passing to bytes.Compare), we might be able to find something relevant. In Python:

>>> rbx = [0x3b, 0x2, 0x23, 0x1b, 0x1b, 0xc, 0x1c, 0x8, 0x28, 0x1b, 0x21, 0x4, 0x1c, 0xb, 0x72, 0x6f, 0x6f]
>>> ''.join(map(chr, map(lambda tup: tup[0] ^ tup[1], zip(map(ord, 'rootme'*3), rbx))))
'ImLovingGoLand\x1d\x1b\x02'

There seems to be a little bit more than we need, but this looks promising. A buffer XOR’d with the built-in constant string produces a value that is passed to bytes.Compare along with a pre-set array of bytes, and if we XOR these bytes with rootme we get ImLovingGoLand. Let’s try it:

$ echo -n 'ImLovingGoLand' | ./ch32.bin
u can validate with this flag

Another way to validate that this is correct would be to use GDB to jump to the equality branch in the comparison after the call to bytes.Compare. We had some fmt.Print code at 00493038 if the bytes matched, and some at 004930a1 if the JNE was taken – meaning the bytes didn’t match.

Jumping to the non-matching branch:

gdb$ b main.main
Breakpoint 1 at 0x492e70: file /home/jenaye/dev/C/CTF_perso/reverseGo.go, line 8.
gdb$ r < /tmp/input.txt
Starting program: ./ch32.bin < /tmp/input.txt

Thread 1 "ch32.bin" hit Breakpoint 1, main.main () at /home/jenaye/dev/C/CTF_perso/reverseGo.go:8
8	/home/jenaye/dev/C/CTF_perso/reverseGo.go: No such file or directory.
gdb$ j *0x004930a1
Continuing at 0x4930a1.
wrong flag
Thread 1 "ch32.bin" received signal SIGSEGV, Segmentation fault.

While if we had jumped to the other branch, we would have seen the success message (not that it would have helped us find the correct input):

gdb$ j *0x00493038
Continuing at 0x493038.
u can validate with this flag
Thread 1 "ch32.bin" received signal SIGSEGV, Segmentation fault.