GDB Guides Series:

  1. GDB Guide Part 1 - Basics
  2. GDB Guide Part 2 - Breakpoints & Linux Calling Conventions
  3. GDB Guide Part 3 - Process Maps
  4. GDB Guide Part 4 - Examining Memory
  5. GDB Guide Part 5 - Stepping
  6. GDB Guide Part 6 - Automation
  7. GDB Guide Part 7 - Custom Commands
  8. GDB Guide Part 8 - Invoking Function Calls

1. Introduction

This post is a continuation from the previous post in this series and will cover more fundamentals on GDB and dynamic analysis.

To ensure this post is not too long-winded, I have decided to only go through the following topics today:

  1. Breakpoints
  2. Linux x86 vs x86_64 Calling Convention

2. Breakpoints

A breakpoint is a special marker that you can set in your program to tell GDB to pause (or “break”) the program’s execution when it reaches a certain point.

When you set a breakpoint at a specific line of code or address, GDB will make the program stop executing by inserting a int 3 instruction just before that point is reached.

2.1 Setting Breakpoints

Tip
You can use the shorthand ‘b’ instead of “break”
You can set a breakpoint with the following syntax:

(gdb) break [Function]
(gdb) break *[Address]
Setting Breakpoints - Syntax

2.2 Regex Breakpoints

Apart from setting breakpoint at specific functions or addresses, there’s another cool trick to break on functions that match a regular expression.

# Break on every function available
(gdb) rbreak .

# Break on function that starts with "func_"
(gdb) rbreak ^func_.$

# Break on function that ends with "nc_1"
(gdb) rbreak .nc_1$
Setting Breakpoints via Regex

2.3 Viewing Breakpoints

When you have too many breakpoints and things start to get a little messy, you can list down your current breakpoints by doing the following command:

(gdb) info breakpoints

# Example Output
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000062086fb611d1 <func_1+8>
2       breakpoint     keep y   0x000062086fb61203 <func_2+8>
Listing Breakpoints

2.4 Deleting Breakpoints

If you have finished debugging at one of the breakpoints, you can delete them via the following command:

(gdb) delete breakpoints [Breakpoint Number]
Deleting Breakpoints - Syntax

If you need to delete more than one breakpoint, here are some other examples:

# If no number is specified, it will delete all breakpoints
(gdb) delete breakpoints 

# Delete breakpoints 2, 4 and 6
(gdb) delete breakpoints 2 4 6

# Delete breakpoints 1 to 6
(gdb) delete breakpoints 1-6
Deleting Breakpoints - Examples

2.5 Disabling / Enabling Breakpoints

Depending on the situation, you may also want to consider disabling and re-enabling certain breakpoints.

(gdb) disable breakpoints [Breakpoint Number]
(gdb) enable breakpoints [Breakpoint Number]
Disabling/Enabling Breakpoints - Syntax

If you need to disable / enable more than one breakpoint, you can follow the syntax in Section 2.4

2.6 Conditional Breakpoints

At times you will find that your breakpoint keeps getting triggered by random sources and is generating too much noise. In that case, you may consider adding a condition clause to the breakpoint.

(gdb) condition [Breakpoint Number] [....]
Setting Conditions to Breakpoints - Syntax

The example below demonstrates how to set a conditional breakpoint to func_1(), such that this breakpoint only triggers if valueAt(EBP + 0xC) == 0xD

(gdb) b func_1
(gdb) condition 1 {unsigned long}($ebp+0xc) == 0xd

# Alternatively
(gdb) condition 1 *(unsigned long *)($ebp+0xc) == 0xd
Setting Conditional Breakpoint - Example

3. Linux Calling Conventions

Depending on the architecture, the way registers are “loaded” in memory will be different. Bear in mind that this section will not cover ARM. Perhaps in the future, I will do a separate post about it. For now, we will keep things simple and stick to x86(32 bit) and x86_64(64 bit) architectures.

3.1 32 Bit - x86

You can read the full details here, but if you just want the gist of it, the following diagram summarizes the x86 calling convention.

epb-structure
x86 Stack Frame

Following the above diagram as an example, if we have a function that takes in 3 parameters, the 3rd parameter will get pushed onto the stack first, followed by the 2nd then the 1st. 4 bytes after the address of EBP, it contains the return address after the function call.

Unlike 64 bit binaries, addresses are only stored as a 4-byte value.

We can actually test this out by compiling our test.c program shown previously in part 1 of the tutorial, as a 32 bit ELF binary.

$ gcc -m32 test.c -o test32
Compiling 32-Bit Binary

To practice what we have learned earlier, let us set a breakpoint on func_2() and print out its parameters and return address.

# Set the GDB configuration
(gdb) set pagination off
(gdb) set confirm off
(gdb) set output-radix 16
(gdb) set disassembly-flavor intel

# Set breakpoint at func_2 and continue
(gdb) b func_2
(gdb) continue

# In the process, select option 2 to trigger the breakpoint
## We know that param 1 is a char *.
## and param_2 is an unsigned long.
(gdb) set $param_1 = {char *}($ebp + 0x8)
(gdb) set $param_2 = {unsigned long}($ebp + 0xc)
(gdb) set $return_addr = {unsigned long}($ebp + 0x4)
(gdb) p (char *)$param_1
(gdb) p (unsigned long)$param_2
(gdb) p (void *)$return_addr
Printing Parameters of func_2() - x86

Demonstrating x86 Calling Convention

3.2 64 Bit - x86_64

The MIT documentation on 64-bit Linux does get straight to the point but lack concrete examples. Instead, I found out this other blog post talking about it and found it a lot more comprehensible. You could give it a read if you have the time.

For those of you who just want the TLDR, refer to the diagram below.

epb-structure
x86_64 Stack Frame

At the start of the function, the following parameters will be stored in its respective registers:

  1. RDI — Param 1
  2. RSI — Param 2
  3. RDX — Param 3
  4. RCX — Param 4
  5. R8 — Param 5
  6. R9 — Param 6

This is followed by a set of instructions to push the values stored in RDI, RSI, RDX, RCX, R8 and R9 onto the stack sequentially.

The example below shows a possible set of instructions near the start of test_func() that takes in 6 parameters.

(gdb) disass test_func
Dump of assembler code for function test_func:
   0x00005988a0e0c1c9 <+0>:     endbr64 
   0x00005988a0e0c1cd <+4>:     push   rbp
   0x00005988a0e0c1ce <+5>:     mov    rbp,rsp
   0x00005988a0e0c1d1 <+8>:     sub    rsp,0x20
   0x00005988a0e0c1d5 <+12>:    mov    QWORD PTR [rbp-0x8],rdi
   0x00005988a0e0c1d9 <+16>:    mov    QWORD PTR [rbp-0x10],rsi
   0x00005988a0e0c1dd <+20>:    mov    DWORD PTR [rbp-0x14],edx
   0x00005988a0e0c1e0 <+23>:    mov    DWORD PTR [rbp-0x18],ecx
   0x00005988a0e0c1e3 <+26>:    mov    DWORD PTR [rbp-0x1c],r8d
   0x00005988a0e0c1e7 <+30>:    mov    DWORD PTR [rbp-0x20],r9d
   0x00005988a0e0c1eb <+34>:    mov    rax,QWORD PTR [rbp-0x8]
x86_64 Instruction Set at Start of test_func()

Upon entry into test_func(), RIP is at <test_func+0>. At this point, all the parameter values are still stored in the 6 registers. However, when the RIP is at <test_func+34>, the Stack Frame will be similar to the one shown in Figure 2.

Enough talking and let’s get to work!

Similar to the GDB exercise shown in Section 3.1, we will do the same here for a 64-bit binary. In case you haven’t compiled already, please run the following command to compile a 64-bit ELF binary:

$ gcc test.c -o test
Compiling a x86_64 Test Binary

Again, we shall do the same to set a breakpoint on func_2() and print out its parameters and return address.

# Set the GDB configuration
(gdb) set pagination off
(gdb) set confirm off
(gdb) set output-radix 16
(gdb) set disassembly-flavor intel

# Set breakpoint at func_2 and continue
(gdb) b func_2
(gdb) continue

# In the process, select option 2 to trigger the breakpoint
## We know that param 1 is a char *.
## and param_2 is an unsigned long.
(gdb) set $param_1 = $rdi
(gdb) set $param_2 = $rsi
(gdb) set $return_addr = {unsigned long long}($rbp + 0x8)
(gdb) p (char *)$param_1
(gdb) p (unsigned long)$param_2
(gdb) p (void *)$return_addr
Printing Parameters of func_2() - x86_64

Demonstrating x86_64 Calling Convention

4. Conclusion

Good job making it this far! There won’t be any more new posts for a while from now as Chinese New Year is around the corner.

Until then, keep practicing and stay awesome!

5. References

  1. x86 Calling Convention
  2. MIT x86-64 Architecture Guide
  3. IRed Team Blog - Linux x64 Calling Convention Stack Frame

6. Resources

  1. test.c

GDB Guides Series:

  1. GDB Guide Part 1 - Basics
  2. GDB Guide Part 2 - Breakpoints & Linux Calling Conventions
  3. GDB Guide Part 3 - Process Maps
  4. GDB Guide Part 4 - Examining Memory
  5. GDB Guide Part 5 - Stepping
  6. GDB Guide Part 6 - Automation
  7. GDB Guide Part 7 - Custom Commands
  8. GDB Guide Part 8 - Invoking Function Calls