Crackme 15: ELF x64 - Golang basic
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:
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.