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]