Debugger Flag

Download Challenge Files

Download the GDB challenge binary here: gdb_challenge.bin

Download the GDB challenge elf here: gdb_challenge.elf

Once downloaded, update the firmware running on one of your boards using the flash tool.

uvx ectf hw <DEVICE_PORT> flash <PATH_TO_CHALLENGE_IMG> -n GDBIMG

Follow the instructions to get starting debugging with OpenOCD and GDB here: OpenOCD.

Finally, the GDB challenge firmware will print a flag to the UART0 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.

After starting an OpenOCD session and connecting to GDB, you should see an output similar to:

(gdb) target remote localhost:3333
Remote debugging using localhost:3333
0x00006f26 in DL_Common_delayCycles (cycles=4000000)
at bazel-out/k8-opt/bin/source/ti/driverlib/dl_common.c:49
warning: 49     bazel-out/k8-opt/bin/source/ti/driverlib/dl_common.c: No such file or directory
(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 hardware breakpoint using hbreak or hb for shorthand:

To get to the start of the challenge function, set a breakpoint at the gdb_challenge use *gdb_challenge to get to the start of the function (because * tells gdb get this address and if you pass it a function it jumps right to the code the user made) function and run to it using continue or c:

(gdb) hb *gdb_challenge
Breakpoint 1 at 0x6750: file src/HSM.c, line 95.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) c
Continuing.
Thread 2 "max32xxx.cpu" hit Breakpoint 1, gdb_challenge () at src/debugger_challenge.c:102
102 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 registers
r0             0xdeadbeef          -559038737
r1             0xfeedface          -17958194
r2             0xcafecafe          -889271554
r3             0xc0ff3311          -1057017071
r4             0x3d0900            4000000
r5             0x6fdb              28635
r6             0x5a5a5a5a          1515870810
r7             0xffffffff          -1
r8             0xffffffff          -1
r9             0xffffffff          -1
r10            0xffffffff          -1
r11            0xffffffff          -1
r12            0xffffffff          -1
sp             0x20207fd8          0x20207fd8
lr             0x654b              25931
pc             0x6750              0x6750 <gdb_challenge+12>
xpsr           0x81000000          -2130706432
msp            0x20207fd8          0x20207fd8
psp            0xfffffffc          0xfffffffc
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
0x6744 <gdb_challenge>: 0xb082b510
(gdb) x 0x6744
0x6744 <gdb_challenge>: 0xb082b510

Write down the raw 4-byte value of the instruction(s) in memory at `to_hex` 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 0xb0.

Stepping Through Code

With execution paused at the breakpoint for do_some_math, let’s inspect the disassembly for the function using disass

(gdb) disass
Dump of assembler code for function do_some_math:
=> 0x00006c9e <+0>:     push    {r4, r5, r6, r7, lr}
   0x00006ca0 <+2>:     sub     sp, #20
   0x00006ca2 <+4>:     str     r0, [sp, #16]
   0x00006ca4 <+6>:     str     r1, [sp, #12]
   0x00006ca6 <+8>:     str     r2, [sp, #8]
   0x00006ca8 <+10>:    str     r3, [sp, #4]
   0x00006caa <+12>:    ldr     r4, [sp, #16]
   0x00006cac <+14>:    ldr     r5, [sp, #12]
   0x00006cae <+16>:    adds    r7, r4, r5
   0x00006cb0 <+18>:    ldr     r1, [sp, #8]
   0x00006cb2 <+20>:    mov     r0, r5
   0x00006cb4 <+22>:    bl      0x69d8 <__aeabi_idivmod>
   0x00006cb8 <+26>:    mov     r6, r0
   0x00006cba <+28>:    muls    r6, r7
   0x00006cbc <+30>:    ldr     r7, [sp, #4]
   0x00006cbe <+32>:    mov     r0, r7
   0x00006cc0 <+34>:    mov     r1, r4
   0x00006cc2 <+36>:    bl      0x69d8 <__aeabi_idivmod>
   0x00006cc6 <+40>:    mov     r0, r1
   0x00006cc8 <+42>:    muls    r0, r6
   0x00006cca <+44>:    eors    r4, r5
   0x00006ccc <+46>:    adds    r1, r7, r4
   0x00006cce <+48>:    bl      0x69d8 <__aeabi_idivmod>
   0x00006cd2 <+52>:    mov     r0, r1
   0x00006cd4 <+54>:    add     sp, #20
   0x00006cd6 <+56>:    pop     {r4, r5, r6, r7, pc}
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
0x00006cac      26      in src/HSM.c
(gdb) si
0x00006cae      26      in src/HSM.c
(gdb) si
0x00006cb0      26      in src/HSM.c

You can see that we are stepping through the instructions of the function. If you run disass again, you will see our position has changed:

(gdb)disass
Dump of assembler code for function do_some_math:
   0x00006c9e <+0>:     push    {r4, r5, r6, r7, lr}
   0x00006ca0 <+2>:     sub     sp, #20
   0x00006ca2 <+4>:     str     r0, [sp, #16]
=> 0x00006ca4 <+6>:     str     r1, [sp, #12]
   0x00006ca6 <+8>:     str     r2, [sp, #8]
   0x00006ca8 <+10>:    str     r3, [sp, #4]
   0x00006caa <+12>:    ldr     r4, [sp, #16]
   0x00006cac <+14>:    ldr     r5, [sp, #12]
   0x00006cae <+16>:    adds    r7, r4, r5
   0x00006cb0 <+18>:    ldr     r1, [sp, #8]
   0x00006cb2 <+20>:    mov     r0, r5
   0x00006cb4 <+22>:    bl      0x69d8 <__aeabi_idivmod>
   0x00006cb8 <+26>:    mov     r6, r0
   0x00006cba <+28>:    muls    r6, r7
   0x00006cbc <+30>:    ldr     r7, [sp, #4]
   0x00006cbe <+32>:    mov     r0, r7
   0x00006cc0 <+34>:    mov     r1, r4
   0x00006cc2 <+36>:    bl      0x69d8 <__aeabi_idivmod>
   0x00006cc6 <+40>:    mov     r0, r1
   0x00006cc8 <+42>:    muls    r0, r6
   0x00006cca <+44>:    eors    r4, r5
   0x00006ccc <+46>:    adds    r1, r7, r4
   0x00006cce <+48>:    bl      0x69d8 <__aeabi_idivmod>
   0x00006cd2 <+52>:    mov     r0, r1
   0x00006cd4 <+54>:    add     sp, #20
   0x00006cd6 <+56>:    pop     {r4, r5, r6, r7, pc}
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 r2 and continue running:

(gdb) watch $r2
Watchpoint 3: $r2

By default, GDB will just print the old and new value in signed integer format, so let’s tell GDB to inspect register r2 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 r2
>end

Now, we can continue running until r2 changes:

(gdb) c
Continuing.

Watchpoint 3: $r2

Old value = -889271554
New value = -8979097
__aeabi_idivmod ()
    at /scratch/build_jenkins/workspace/BuildAndValidate_Worker/llvm_cgt/llvm-project/compiler-rt/lib/builtins/arm/aeabi_idivmod.S:38
warning: 38     /scratch/build_jenkins/workspace/BuildAndValidate_Worker/llvm_cgt/llvm-project/compiler-rt/lib/builtins/arm/aeabi_idivmod.S: No such file or directory
r2             0xff76fd67          -8979097

Continue running through the `do_some_math` function until the value of r2 starts with 0xca and record that value as value3

When we’re done watching r2, we can delete the watchpoint. First, view the existing breakpoints and watchpoints with info break or i b for shorthand:

(gdb) i b
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x00006750 in gdb_challenge at src/HSM.c:95
        breakpoint already hit 1 time
2       breakpoint     keep y   0x00006caa in do_some_math at src/HSM.c:26
        breakpoint already hit 1 time
3       watchpoint     keep y              $r2
        breakpoint already hit 6 times
        info registers r2

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) i b
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x00006750 in gdb_challenge at src/HSM.c:95
        breakpoint already hit 1 time
2       breakpoint     keep y   0x00006caa in do_some_math at src/HSM.c:26
        breakpoint already hit 1 time

Writing to Registers and Memory

Set a breakpoint at 0x00006cd6 (the end of do_some_math) and continue there:

(gdb) hb *0x00006cd6
Breakpoint 4 at 0x6cd6: file src/HSM.c, line 26.
(gdb) c
Continuing.

Breakpoint 4, 0x00006cd6 in do_some_math (a=26453, b=-1, c=1515870810, d=28635) at src/HSM.c:26
warning: 26     src/HSM.c: No such file or directory

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. Make sure the breakpoint is at the first instruction in the function and not deeper down.

check_flag has five arguments; let’s check them out. The ARM calling convention is to place the first four arguments in registers (r0-r3) 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             0x6ffa              28666
r5             0x6fdb              28635
r6             0x5a5a5a5a          1515870810
r7             0xffffffff          -1
r8             0xffffffff          -1
r9             0xffffffff          -1
r10            0xffffffff          -1
r11            0xffffffff          -1
r12            0xffffffff          -1
sp             0x20207fd8          0x20207fd8
lr             0x677b              26491
pc             0x6230              0x6230 <check_flag>
xpsr           0x1000000           16777216
msp            0x20207fd8          0x20207fd8
psp            0xfffffffc          0xfffffffc
primask        0x0                 0
basepri        0x0                 0
faultmask      0x0                 0
control        0x0                 0
(gdb) x $sp
0x20207fd8:     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]