Debugger Flag¶
This tutorial will show you how to use the GDB debugger to walk through and interact with running hardware.
Download Challenge Files¶
Download the GDB challenge binary here: gdb_challenge.img.
Download the GDB challenge elf here: gdb_challenge.elf.
Once downloaded, update the firmware running on one of your boards using the ectf_update tool.
ectf_update --infile <PATH_TO_CHALLENGE_IMG> --port <DEVICE_PORT>
Connecting the Debugger¶
Follow the instructions to get starting debugging with OpenOCD and GDB here: OpenOCD
Finally, the GDB challenge firmware will print a flag to the UART port if you succeed, so you’ll need a way to read from the serial debug port to get the flag.
Getting Your Bearings¶
When the device finishes starting up, you should see output along the lines of: “Welcome to the GDB challenge” printed to the serial console. The LED will cycle for 3 seconds before running the GDB challenge function and repeating.
After starting an OpenOCD session and connecting to GDB, you should see an output similar to:
(gdb) tar ext :3333
Remote debugging using :3333
0x10010ce4 in MXC_Delay (us=<optimized out>)
(gdb)
You are now using GDB. The CPU should have halted once the debugger was connected and we’re now ready to start debugging!
Setting a Breakpoint¶
Now that we have control of the system, let’s continue to main. To do that, we must first set a breakpoint at main using break or b for shorthand:
To get to the start of the challenge function, set a breakpoint at the
gdb_challenge function and run to it using continue
or c
:
(gdb) b gdb_challenge
Breakpoint 1 at 0x10010634: file src/application_processor.c, line 99.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) c
Continuing.
[New Thread 1]
[Switching to Thread 1]
Thread 2 "max32xxx.cpu" hit Breakpoint 1, gdb_challenge () at src/application_processor.c:99
99 void __attribute__((optimize("O0"))) gdb_challenge() {
Now you are stopped at the beginning of the gdb_challenge function.
Let’s view the current register values with info registers
:
(gdb) info register
r0 0x0 0
r1 0x0 0
r2 0xe000e000 -536813568
r3 0x4 4
r4 0x0 0
r5 0x0 0
r6 0x0 0
r7 0x2001fff8 537001976
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0xf4240000 -198967296
sp 0x2001fff8 0x2001fff8
lr 0x1001071f 268502815
pc 0x10010634 0x10010634 <gdb_challenge>
xPSR 0x61010000 1627455488
fpscr 0x0 0
msp 0x2001fff8 0x2001fff8
psp 0x0 0x0
primask 0x0 0
basepri 0x0 0
faultmask 0x0 0
control 0x0 0
We can also view values in memory with x
specified by address or symbol:
(gdb) x gdb_challenge
0x10010634 <gdb_challenge>: 0xb084b580
(gdb) x 0x10010634
0x10010634 <gdb_challenge>: 0xb084b580
Write down the raw value of the instruction in memory at `printf` for later as value1
Use what you’ve learned so far to set a breakpoint at the start of the do_some_math function.
Write down the raw value of the stack pointer (SP) after hitting this new breakpoint as value2. Hint: It should end with 0xE0.
Stepping Through Code¶
With execution paused at the breakpoint for do_some_math, let’s inspect the disassembly
for the function using disas
(gdb) disas
Dump of assembler code for function do_some_math:
=> 0x10010294 <+0>: push {r7}
0x10010296 <+2>: sub sp, #20
0x10010298 <+4>: add r7, sp, #0
0x1001029a <+6>: str r0, [r7, #12]
0x1001029c <+8>: str r1, [r7, #8]
0x1001029e <+10>: str r2, [r7, #4]
0x100102a0 <+12>: str r3, [r7, #0]
0x100102a2 <+14>: ldr r2, [r7, #12]
0x100102a4 <+16>: ldr r3, [r7, #8]
0x100102a6 <+18>: add r3, r2
0x100102a8 <+20>: ldr r1, [r7, #8]
0x100102aa <+22>: ldr r2, [r7, #4]
0x100102ac <+24>: sdiv r2, r1, r2
0x100102b0 <+28>: mul.w r2, r3, r2
0x100102b4 <+32>: ldr r3, [r7, #0]
0x100102b6 <+34>: ldr r1, [r7, #12]
0x100102b8 <+36>: sdiv r1, r3, r1
0x100102bc <+40>: ldr r0, [r7, #12]
0x100102be <+42>: mul.w r1, r0, r1
0x100102c2 <+46>: subs r3, r3, r1
0x100102c4 <+48>: mul.w r3, r2, r3
0x100102c8 <+52>: ldr r1, [r7, #12]
0x100102ca <+54>: ldr r2, [r7, #8]
0x100102cc <+56>: eors r1, r2
0x100102ce <+58>: ldr r2, [r7, #0]
0x100102d0 <+60>: add r2, r1
0x100102d2 <+62>: sdiv r1, r3, r2
0x100102d6 <+66>: mul.w r2, r1, r2
0x100102da <+70>: subs r3, r3, r2
0x100102dc <+72>: mov r0, r3
0x100102de <+74>: adds r7, #20
0x100102e0 <+76>: mov sp, r7
0x100102e2 <+78>: pop {r7}
0x100102e4 <+80>: bx lr
End of assembler dump.
Instead of using breakpoints, we can instead step through this function instruction by instruction using si for step instruction:
(gdb) si
0x10010296 39 int __attribute__((optimize("O0"))) do_some_math(int a, int b, int c, int d) {
(gdb) si
0x10010298 39 int __attribute__((optimize("O0"))) do_some_math(int a, int b, int c, int d) {
(gdb) si
0x1001029a 39 int __attribute__((optimize("O0"))) do_some_math(int a, int b, int c, int d) {
You can see that we are stepping through the instructions of the function. If you run disas again, you will see our position has changed:
(gdb) disas
Dump of assembler code for function do_some_math:
0x10010294 <+0>: push {r7}
0x10010296 <+2>: sub sp, #20
0x10010298 <+4>: add r7, sp, #0
=> 0x1001029a <+6>: str r0, [r7, #12]
0x1001029c <+8>: str r1, [r7, #8]
0x1001029e <+10>: str r2, [r7, #4]
0x100102a0 <+12>: str r3, [r7, #0]
0x100102a2 <+14>: ldr r2, [r7, #12]
0x100102a4 <+16>: ldr r3, [r7, #8]
0x100102a6 <+18>: add r3, r2
0x100102a8 <+20>: ldr r1, [r7, #8]
0x100102aa <+22>: ldr r2, [r7, #4]
0x100102ac <+24>: sdiv r2, r1, r2
0x100102b0 <+28>: mul.w r2, r3, r2
0x100102b4 <+32>: ldr r3, [r7, #0]
0x100102b6 <+34>: ldr r1, [r7, #12]
0x100102b8 <+36>: sdiv r1, r3, r1
0x100102bc <+40>: ldr r0, [r7, #12]
0x100102be <+42>: mul.w r1, r0, r1
0x100102c2 <+46>: subs r3, r3, r1
0x100102c4 <+48>: mul.w r3, r2, r3
0x100102c8 <+52>: ldr r1, [r7, #12]
0x100102ca <+54>: ldr r2, [r7, #8]
0x100102cc <+56>: eors r1, r2
0x100102ce <+58>: ldr r2, [r7, #0]
0x100102d0 <+60>: add r2, r1
0x100102d2 <+62>: sdiv r1, r3, r2
0x100102d6 <+66>: mul.w r2, r1, r2
0x100102da <+70>: subs r3, r3, r2
0x100102dc <+72>: mov r0, r3
0x100102de <+74>: adds r7, #20
0x100102e0 <+76>: mov sp, r7
0x100102e2 <+78>: pop {r7}
0x100102e4 <+80>: bx lr
End of assembler dump.
Setting a Watchpoint¶
Instead of manually stepping through individual instructions, we can also
automatically run through code and break when a variable, register, or value in
memory changes by setting a watchpoint. Let’s set a watchpoint on register r3
and continue running:
(gdb) watch $r3
Watchpoint 3: $r3
By default, GDB will just print the old and new value in signed integer format,
so let’s tell GDB to inspect register r3
when it breaks on the
watchpoint in order to automatically see the hexadecimal representation:
(gdb) commands
Type commands for breakpoint(s) 3, one per line.
End with a line saying just "end".
>info registers r3
>end
Now, we can continue running until r3
changes:
(gdb) c
Continuing.
Thread 2 "max32xxx.cpu" hit Watchpoint 3: $r3
Old value = -1057017071
New value = -17958194
0x100102a6 in do_some_math (a=-559038737, b=-17958194, c=-889271554, d=-1057017071)
at src/application_processor.c:40
40 return ((a + b) * (b / c) * (d % a)) % (d + (a ^ b));
r3 0xfeedface -17958194
Continue running through the `do_some_math` function until the value of r3 starts with 0xe and record that value as value3
When we’re done watching r3
, we can delete the watchpoint. First, view the
existing breakpoints and watchpoints with info break
or i b
for shorthand:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x10010634 in gdb_challenge at src/application_processor.c:99
breakpoint already hit 1 time
2 breakpoint keep y 0x10010294 in do_some_math at src/application_processor.c:39
breakpoint already hit 1 time
3 watchpoint keep y $r3
breakpoint already hit 1 time
info registers r3
The break number of the r3
watchpoint is 3
, so we will delete that break
number, and check the break numbers again to make sure we successfully removed
the watchpoint:
(gdb) delete 3
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x10010634 in gdb_challenge at src/application_processor.c:99
breakpoint already hit 1 time
2 breakpoint keep y 0x10010294 in do_some_math at src/application_processor.c:39
breakpoint already hit 1 time
Writing to Registers and Memory¶
Set a breakpoint at 0x100102e4 (the end of do_some_math and continue there:
(gdb) b *0x100102e4
Breakpoint 4 at 0x100102e4: file src/application_processor.c, line 41.
(gdb) c
Continuing.
Thread 2 "max32xxx.cpu" hit Breakpoint 4, 0x100102e4 in do_some_math (a=-559038737,
b=-17958194, c=-889271554, d=-1057017071) at src/application_processor.c:41
41 }
With the set command we can now modify registers (make sure to reset them):
(gdb) info registers r0
r0 0x0 0
(gdb) set $r0=111
(gdb) info registers r0
r0 0x6f 111
(gdb) set $r0=0
And memory:
(gdb) x 0x2001fff8
0x2001fff8: 0x00000000
(gdb) set *0x2001fff8=0x111
(gdb) x 0x2001fff8
0x2001fff8: 0x00000111
(gdb) set *0x2001fff8=0
Capturing the flag¶
With what you’ve learned, set a breakpoint at the first instruction of the check_flag function and continue up to there.
check_flag has five arguments; let’s check them out. The ARM calling convention is to place the first four arguments in registers and further arguments are pushed to the stack.
Print the registers and then the top value on the stack to view the arguments:
(gdb) info registers
r0 0x11111111 286331153
r1 0x22222222 572662306
r2 0x33333333 858993459
r3 0x44444444 1145324612
r4 0x0 0
r5 0x0 0
r6 0x0 0
r7 0x2001ffe8 537001960
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x1 1
sp 0x2001ffe0 0x2001ffe0
lr 0x10010683 268502659
pc 0x100104b0 0x100104b0 <check_flag>
xPSR 0x1010000 16842752
fpscr 0x0 0
msp 0x2001ffe0 0x2001ffe0
psp 0x0 0x0
primask 0x0 0
basepri 0x0 0
faultmask 0x0 0
control 0x0 0
(gdb) x $sp
0x2001ffe0: 0x55555555
We can see that arguments 1-4 (0x11111111, 0x22222222, 0x33333333, and 0x44444444) are in registers r0 through r3, and the top value of the stack hold the fifth argument (0x55555555). Now, using what you have learned, change the values of the function arguments so that the first argument is set to value1, the third argument is set to value2, and the fifth argument is set to value3.
Next, continue program execution and check the serial console output. If you did everything correctly, you should see a flag, and if not, you should see an explanation of which argument was incorrect. If done correctly, When you’re done, type q to quit GDB.
(gdb) q
A debugging session is active.
Inferior 1 [Remote target] will be detached.
Quit anyway? (y or n) y
[Inferior 1 (Remote target) detached]