Happy Birthday: Messing Around With Gdb and Golang
Some time ago (my last birthday), my nephew gave me a really nice little game as a present :}
It’s a simple and fun question-and-answer kind of game. He wrote it in golang with my brother’s help (he’s father).
Point being, that in all this time, I had been able to solve all but the last question, and /me being who I am, decided to hack it for the fun of it \o/
First , I decided to use the linux command strings
to see if anything would
pop up easily, and well, yes it did :), though it showed way way more stuff than
what I wanted :/, and well, seemed not too elegant:
dcaro@urcuchillay$ strings --data --encoding=S elprograma | wc
41757 46655 507317
So my second approach was using gdb
, but I’m quite unfamiliar with it (though
I used it some 20+ years ago for some university exercises), so I had to brush
up.
I also don’t have the source code, so that complicates things a little bit, as I had to brush up on some assembler too. Anyhow, attaching to the running program was easy:
dcaro@urcuchillay$ gdb elprograma
...
(gdb)
Then you have to run the program:
(gdb) run
Another option is starting the program by itself, and attaching to the running one:
dcaro@urcuchillay$ gdb elprograma
...
(gdb) attach <pid_of_program>
Once started and when it’s asking for an answer, you can drop to the gdb prompt
by hitting Ctrl+Z
, or sending SIGSTOP
using kill or similar:
^Z
Thread 1 "elprograma" received signal SIGTSTP, Stopped (user).
runtime/internal/syscall.Syscall6 () at /home/ruben/.asdf/installs/golang/1.21.4/go/src/runtime/internal/syscall/asm_linux_amd64.s:36
warning: 36 /home/ruben/.asdf/installs/golang/1.21.4/go/src/runtime/internal/syscall/asm_linux_amd64.s: No such file or directory
As you can see it’s trying to get to the source code, but not finding it as that was built in my brother’s computer xd
Now you can check where you are in the stack, from here, you can extract the file name and the line the code is running from (note though that as I don’t have the source code, it will not show the source code of the line):
(gdb) where
#0 runtime/internal/syscall.Syscall6 () at /home/ruben/.asdf/installs/golang/1.21.4/go/src/runtime/internal/syscall/asm_linux_amd64.s:36
#1 0x000000000040314d in syscall.RawSyscall6 (num=18446744073709551104, a1=0, a2=4206958, a3=0, a4=824634863616, a5=0, a6=0, r1=<optimized out>, r2=<optimized out>, errno=<optimized out>)
at /home/ruben/.asdf/installs/golang/1.21.4/go/src/runtime/internal/syscall/syscall_linux.go:38
#2 0x0000000000471e46 in syscall.Syscall (trap=0, a1=0, a2=824634863616, a3=4096, r1=<optimized out>, r2=<optimized out>, err=<optimized out>) at /home/ruben/.asdf/installs/golang/1.21.4/go/src/syscall/syscall_linux.go:82
#3 0x0000000000471858 in syscall.read (fd=<optimized out>, p=..., n=<optimized out>, err=...) at /home/ruben/.asdf/installs/golang/1.21.4/go/src/syscall/zsyscall_linux_amd64.go:721
#4 0x000000000047320e in syscall.Read (fd=0, n=<optimized out>, err=..., p=...) at /home/ruben/.asdf/installs/golang/1.21.4/go/src/syscall/syscall_unix.go:181
#5 internal/poll.ignoringEINTRIO (fd=0, fn=<optimized out>, p=...) at /home/ruben/.asdf/installs/golang/1.21.4/go/src/internal/poll/fd_unix.go:736
#6 internal/poll.(*FD).Read (fd=0xc00018c000, p=..., ~r0=<optimized out>, ~r0=<optimized out>, ~r1=..., ~r1=...) at /home/ruben/.asdf/installs/golang/1.21.4/go/src/internal/poll/fd_unix.go:160
#7 0x0000000000473a12 in os.(*File).read (f=0xc00018a000, b=..., n=<optimized out>, err=...) at /home/ruben/.asdf/installs/golang/1.21.4/go/src/os/file_posix.go:29
#8 os.(*File).Read (f=0xc00018a000, b=..., n=<optimized out>, err=...) at /home/ruben/.asdf/installs/golang/1.21.4/go/src/os/file.go:118
#9 0x00000000004633fb in bufio.(*Scanner).Scan (s=0xc0001a2e40, ~r0=<optimized out>) at /home/ruben/.asdf/installs/golang/1.21.4/go/src/bufio/scan.go:214
#10 0x000000000047d76f in main.presentarAdivinanza (enunciado=..., ~r0=...) at /home/ruben/Documents/elprograma/main.go:30
#11 0x000000000047d645 in main.main () at /home/ruben/Documents/elprograma/main.go:15
Ok, that’s good, so the original file was
/home/ruben/Documents/elprograma/main.go
, and I can see also that the method
I’m in is main.presentarAdivinanza
, nice :), but I want to see the code it’s
running, even if it’s only the assembler code itself, so I can figure out how
it’s working.
To do so, you can use the x
command from gdb, that will show data, passing to
it the $pc
registry (current instruction’s memory address):
(gdb) x/10i $pc
=> 0x40316e <runtime/internal/syscall.Syscall6+14>: cmp $0xfffffffffffff001,%rax
0x403174 <runtime/internal/syscall.Syscall6+20>: jbe 0x40318b <runtime/internal/syscall.Syscall6+43>
0x403176 <runtime/internal/syscall.Syscall6+22>: neg %rax
0x403179 <runtime/internal/syscall.Syscall6+25>: mov %rax,%rcx
0x40317c <runtime/internal/syscall.Syscall6+28>: mov $0xffffffffffffffff,%rax
0x403183 <runtime/internal/syscall.Syscall6+35>: mov $0x0,%rbx
0x40318a <runtime/internal/syscall.Syscall6+42>: ret
0x40318b <runtime/internal/syscall.Syscall6+43>: mov %rdx,%rbx
0x40318e <runtime/internal/syscall.Syscall6+46>: mov $0x0,%rcx
0x403195 <runtime/internal/syscall.Syscall6+53>: ret
Hmm, that’s the syscall internal code, let me go instead to the source code stack almost at the top, and check there.
(gdb) up
... as many times as needed
#10 0x000000000047d76f in main.presentarAdivinanza (enunciado=..., ~r0=...) at /home/ruben/Documents/elprograma/main.go:30
warning: 30 /home/ruben/Documents/elprograma/main.go: No such file or directory
(gdb) x/10i $pc
=> 0x47d76f <main.presentarAdivinanza+207>: mov 0x68(%rsp),%rbx
0x47d774 <main.presentarAdivinanza+212>: mov 0x70(%rsp),%rcx
0x47d779 <main.presentarAdivinanza+217>: xor %eax,%eax
0x47d77b <main.presentarAdivinanza+219>: nopl 0x0(%rax,%rax,1)
0x47d780 <main.presentarAdivinanza+224>: call 0x44a060 <runtime.slicebytetostring>
0x47d785 <main.presentarAdivinanza+229>: add $0xc8,%rsp
0x47d78c <main.presentarAdivinanza+236>: pop %rbp
0x47d78d <main.presentarAdivinanza+237>: ret
0x47d78e <main.presentarAdivinanza+238>: mov %rax,0x8(%rsp)
0x47d793 <main.presentarAdivinanza+243>: mov %rbx,0x10(%rsp)
That is more interesting :), I can see there also that there’s a return little
after, but I’m missing the current executed function too, let’s use $pc - 0x8
,
see what we get (the 0x8 is a guess, note that the size of the assembler
instruction is variable):
(gdb) x/10i $pc - 0x8
0x47d767 <main.presentarAdivinanza+199>: rex.R and $0x48,%al
0x47d76a <main.presentarAdivinanza+202>: call 0x462be0 <bufio.(*Scanner).Scan>
=> 0x47d76f <main.presentarAdivinanza+207>: mov 0x68(%rsp),%rbx
0x47d774 <main.presentarAdivinanza+212>: mov 0x70(%rsp),%rcx
0x47d779 <main.presentarAdivinanza+217>: xor %eax,%eax
0x47d77b <main.presentarAdivinanza+219>: nopl 0x0(%rax,%rax,1)
0x47d780 <main.presentarAdivinanza+224>: call 0x44a060 <runtime.slicebytetostring>
0x47d785 <main.presentarAdivinanza+229>: add $0xc8,%rsp
0x47d78c <main.presentarAdivinanza+236>: pop %rbp
0x47d78d <main.presentarAdivinanza+237>: ret
Oooh, so we are in bufio.(*Scanner).Scan
, that’s the one that reads the
screen, makes sense.
gdb
has a nice command to add an expression that will be run on every prompt
refresh, let’s add the one showing the current assembly code to it, so we don’t
need to manually enter it every time:
(gdb) display /10i $pc - 0x8
Neat, now, let’s try to add a breakpoint on the next instructions, so we can follow after we enter our wrong solution, as I don’t have the source code, let’s try putting it on the next line in the file, so first let’s get the current frame (stack level):
(gdb) frame
#10 0x000000000047d76f in main.presentarAdivinanza (enunciado=..., ~r0=...) at /home/ruben/Documents/elprograma/main.go:30
30 in /home/ruben/Documents/elprograma/main.go
And just breakpoint at line+1:
(gdb) b /home/ruben/Documents/elprograma/main.go:31
Breakpoint 1 at 0x47d785: file /home/ruben/Documents/elprograma/main.go, line 31.
And let’s continue the execution :)
Here comes a first weird thing, it turns out that continue
is not enough!
(gdb) continue
Continuing.
Thread 1 "elprograma" received signal SIGTSTP, Stopped (user).
runtime/internal/syscall.Syscall6 () at /home/ruben/.asdf/installs/golang/1.21.4/go/src/runtime/internal/syscall/asm_linux_amd64.s:36
36 in /home/ruben/.asdf/installs/golang/1.21.4/go/src/runtime/internal/syscall/asm_linux_amd64.s
1: x/10i $pc - 0x8
0x403166 <runtime/internal/syscall.Syscall6+6>: mov %rcx,%rsi
0x403169 <runtime/internal/syscall.Syscall6+9>: mov %rbx,%rdi
0x40316c <runtime/internal/syscall.Syscall6+12>: syscall
=> 0x40316e <runtime/internal/syscall.Syscall6+14>: cmp $0xfffffffffffff001,%rax
0x403174 <runtime/internal/syscall.Syscall6+20>: jbe 0x40318b <runtime/internal/syscall.Syscall6+43>
0x403176 <runtime/internal/syscall.Syscall6+22>: neg %rax
0x403179 <runtime/internal/syscall.Syscall6+25>: mov %rax,%rcx
0x40317c <runtime/internal/syscall.Syscall6+28>: mov $0xffffffffffffffff,%rax
0x403183 <runtime/internal/syscall.Syscall6+35>: mov $0x0,%rbx
0x40318a <runtime/internal/syscall.Syscall6+42>: ret
It turns out, that you need to hit continue
for each thread so they get
the SIGCONT properly, so let’s do that, eventually you get back to the program:
...
Continuing.
Now when we enter any answer, it gets to the breakpoint:
Bad answer.
[Switching to LWP 2256393]
Thread 1 "elprograma" hit Breakpoint 1, main.presentarAdivinanza (enunciado=..., ~r0=...) at /home/ruben/Documents/elprograma/main.go:31
warning: 31 /home/ruben/Documents/elprograma/main.go: No such file or directory
1: x/10i $pc - 0x8
0x47d77d <main.presentarAdivinanza+221>: add %r8b,(%rax)
0x47d780 <main.presentarAdivinanza+224>: call 0x44a060 <runtime.slicebytetostring>
=> 0x47d785 <main.presentarAdivinanza+229>: add $0xc8,%rsp
0x47d78c <main.presentarAdivinanza+236>: pop %rbp
0x47d78d <main.presentarAdivinanza+237>: ret
0x47d78e <main.presentarAdivinanza+238>: mov %rax,0x8(%rsp)
0x47d793 <main.presentarAdivinanza+243>: mov %rbx,0x10(%rsp)
0x47d798 <main.presentarAdivinanza+248>: call 0x45aa20 <runtime.morestack_noctxt>
0x47d79d <main.presentarAdivinanza+253>: mov 0x8(%rsp),%rax
0x47d7a2 <main.presentarAdivinanza+258>: mov 0x10(%rsp),%rbx
You can see now that the process state is in t
, that means stopped for
debugger:
dcaro@urcuchillay$ ps aux | grep elprograma
dcaro 2256393 0.0 0.0 1226320 1604 pts/4 tl 16:52 0:00 /home/dcaro/elprograma
Awesome, so now let’s start following the code :), note that next/step
will
work for “source lines of code”, but we don’t have source code, so it’s better
to move around using nexti/stepi
, that will go to the next instruction, or
step into a call instruction instead. After a few ni
we get to return from
that function, back to the main:
17 in /home/ruben/Documents/elprograma/main.go
1: x/10i $pc - 0x8
0x47d63d <main.main+157>: add %r8b,(%rax)
0x47d640 <main.main+160>: call 0x47d6a0 <main.presentarAdivinanza>
=> 0x47d645 <main.main+165>: mov 0x40(%rsp),%rcx
0x47d64a <main.main+170>: cmp %rcx,%rbx
0x47d64d <main.main+173>: jne 0x47d664 <main.main+196>
0x47d64f <main.main+175>: mov %rbx,%rcx
0x47d652 <main.main+178>: mov 0x48(%rsp),%rbx
0x47d657 <main.main+183>: call 0x402f00 <runtime.memequal>
0x47d65c <main.main+188>: nopl 0x0(%rax)
0x47d660 <main.main+192>: test %al,%al
Hmm, there’s an interesting jump jne
there but what would that be?
(gdb) ni
0x000000000047d64a 17 in /home/ruben/Documents/elprograma/main.go
1: x/10i $pc - 0x8
0x47d642 <main.main+162>: add %al,(%rax)
0x47d644 <main.main+164>: add %cl,-0x75(%rax)
0x47d647 <main.main+167>: rex.WR and $0x40,%al
=> 0x47d64a <main.main+170>: cmp %rcx,%rbx
0x47d64d <main.main+173>: jne 0x47d664 <main.main+196>
0x47d64f <main.main+175>: mov %rbx,%rcx
0x47d652 <main.main+178>: mov 0x48(%rsp),%rbx
0x47d657 <main.main+183>: call 0x402f00 <runtime.memequal>
0x47d65c <main.main+188>: nopl 0x0(%rax)
0x47d660 <main.main+192>: test %al,%al
(gdb) info registers rcx rbx
rcx 0x5 5
rbx 0xb 11
Hmm, after some trial and error, I found that those are the string lengths of the input string, and the correct answer!
It turns out that golang will do a first check for the lengths to be the same. Awesome, first clue :)
Okok, let’s then send an answer with that same length:
Tu respuesta: 12345678901
Thread 1 "elprograma" hit Breakpoint 1, main.presentarAdivinanza (enunciado=..., ~r0=...) at /home/ruben/Documents/elprograma/main.go:31
31 in /home/ruben/Documents/elprograma/main.go
1: x/10i $pc - 0x8
0x47d77d <main.presentarAdivinanza+221>: add %r8b,(%rax)
0x47d780 <main.presentarAdivinanza+224>: call 0x44a060 <runtime.slicebytetostring>
=> 0x47d785 <main.presentarAdivinanza+229>: add $0xc8,%rsp
0x47d78c <main.presentarAdivinanza+236>: pop %rbp
0x47d78d <main.presentarAdivinanza+237>: ret
...
Let’s continue for a bit…
0x000000000047d64a 17 in /home/ruben/Documents/elprograma/main.go
1: x/10i $pc - 0x8
0x47d642 <main.main+162>: add %al,(%rax)
0x47d644 <main.main+164>: add %cl,-0x75(%rax)
0x47d647 <main.main+167>: rex.WR and $0x40,%al
=> 0x47d64a <main.main+170>: cmp %rcx,%rbx
0x47d64d <main.main+173>: jne 0x47d664 <main.main+196>
0x47d64f <main.main+175>: mov %rbx,%rcx
0x47d652 <main.main+178>: mov 0x48(%rsp),%rbx
0x47d657 <main.main+183>: call 0x402f00 <runtime.memequal>
0x47d65c <main.main+188>: nopl 0x0(%rax)
0x47d660 <main.main+192>: test %al,%al
(gdb) info registers rcx rbx
rcx 0x5 5
rbx 0x5 5
Awesome, let’s continue…
1: x/10i $pc - 0x8
0x47d64f <main.main+175>: mov %rbx,%rcx
0x47d652 <main.main+178>: mov 0x48(%rsp),%rbx
=> 0x47d657 <main.main+183>: call 0x402f00 <runtime.memequal>
0x47d65c <main.main+188>: nopl 0x0(%rax)
0x47d660 <main.main+192>: test %al,%al
0x47d662 <main.main+194>: jne 0x47d673 <main.main+211>
0x47d664 <main.main+196>: call 0x47d7c0 <main.defeat>
0x47d669 <main.main+201>: mov 0x38(%rsp),%rax
0x47d66e <main.main+206>: jmp 0x47d5b4 <main.main+20>
0x47d673 <main.main+211>: call 0x47d880 <main.victory>
Hmm, it seems that is calling that memequal
, that’s promising, a quick search
reveals that that function is
internal to golang too,
but it’s just a facade for the actual architecture specific function, if we step
into it (using stepi
) we see the actual file and line:
(gdb) si
runtime.memequal () at /home/ruben/.asdf/installs/golang/1.21.4/go/src/internal/bytealg/equal_amd64.s:14
warning: 14 /home/ruben/.asdf/installs/golang/1.21.4/go/src/internal/bytealg/equal_amd64.s: No such file or directory
1: x/10i $pc - 0x8
0x402ef8 <memeqbody+312>: shl %cl,%edi
0x402efa <memeqbody+314>: sete %al
0x402efd <memeqbody+317>: ret
0x402efe: int3
0x402eff: int3
=> 0x402f00 <runtime.memequal>: cmp %rbx,%rax
0x402f03 <runtime.memequal+3>: jne 0x402f0d <runtime.memequal+13>
0x402f05 <runtime.memequal+5>: mov $0x1,%rax
0x402f0c <runtime.memequal+12>: ret
0x402f0d <runtime.memequal+13>: mov %rax,%rsi
Yep, we have it here:
// memequal(a, b unsafe.Pointer, size uintptr) bool
TEXT runtime·memequal<ABIInternal>(SB),NOSPLIT,$0-25
// AX = a (want in SI)
// BX = b (want in DI)
// CX = size (want in BX)
CMPQ AX, BX
JNE neq
MOVQ $1, AX // return 1
RET
neq:
MOVQ AX, SI
MOVQ BX, DI
MOVQ CX, BX
JMP memeqbody<>(SB)
There’s some nice helper comments there, main ones being that it has the strings
in AX
and BX
registries, that was the key :), now all I have to do is to
print out the contents of those registries \o/, okok, so I’m going to the last
question, and then stopping at the breakpoint we put before, I see I got the
right string length:
(gdb) ni
0x000000000047d64a 17 in /home/ruben/Documents/elprograma/main.go
1: x/10i $pc - 0x8
0x47d642 <main.main+162>: add %al,(%rax)
0x47d644 <main.main+164>: add %cl,-0x75(%rax)
0x47d647 <main.main+167>: rex.WR and $0x40,%al
=> 0x47d64a <main.main+170>: cmp %rcx,%rbx
0x47d64d <main.main+173>: jne 0x47d664 <main.main+196>
0x47d64f <main.main+175>: mov %rbx,%rcx
0x47d652 <main.main+178>: mov 0x48(%rsp),%rbx
0x47d657 <main.main+183>: call 0x402f00 <runtime.memequal>
0x47d65c <main.main+188>: nopl 0x0(%rax)
0x47d660 <main.main+192>: test %al,%al
(gdb) info registers rbx rcx
rbx 0x1e 30
rcx 0x1e 30
Continuing…
runtime.memequal () at /home/ruben/.asdf/installs/golang/1.21.4/go/src/internal/bytealg/equal_amd64.s:14
warning: 14 /home/ruben/.asdf/installs/golang/1.21.4/go/src/internal/bytealg/equal_amd64.s: No such file or directory
1: x/10i $pc - 0x8
0x402ef8 <memeqbody+312>: shl %cl,%edi
0x402efa <memeqbody+314>: sete %al
0x402efd <memeqbody+317>: ret
0x402efe: int3
0x402eff: int3
=> 0x402f00 <runtime.memequal>: cmp %rbx,%rax
0x402f03 <runtime.memequal+3>: jne 0x402f0d <runtime.memequal+13>
0x402f05 <runtime.memequal+5>: mov $0x1,%rax
0x402f0c <runtime.memequal+12>: ret
0x402f0d <runtime.memequal+13>: mov %rax,%rsi
(gdb) x/30s $rax
0xc0001be000: "123456789012345678901234567890"
That’s my answer, and finally:
(gdb) x/s $rbx
0x49a78a: "Escribe la respuesta correcta.227373675443232059478759765625reflect: Elem of invalid type MapIter.Key called before Nextsync: inconsistent mutex statesync: unlock of unlocked mutexSIGUSR1: user-define"...
Yay!! That’s what we want! Just the first 30 chars of it:
(gdb) x/30c $rbx
0x49a78a: 69 'E' 115 's' 99 'c' 114 'r' 105 'i' 98 'b' 101 'e' 32 ' '
0x49a792: 108 'l' 97 'a' 32 ' ' 114 'r' 101 'e' 115 's' 112 'p' 117 'u'
0x49a79a: 101 'e' 115 's' 116 't' 97 'a' 32 ' ' 99 'c' 111 'o' 114 'r'
0x49a7a2: 114 'r' 101 'e' 99 'c' 116 't' 97 'a' 46 '.'
-> Nivel 12 <-
Cómo hacer que este programa explote?
Tu respuesta: Escribe la respuesta correcta.
🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂....
Perfect!! ᕙ(`▽´)ᕗ
-> Nivel 13 <-panic: runtime error: index out of range [13] with length 13
goroutine 1 [running]:
main.main()
/home/ruben/Documents/elprograma/main.go:14 +0xef
That was an awesome present, that made me have a good time for a loooong time xd
And a nice reference to a very nice movie “The thirteenth floor”
Thanks nephew! (and brother)