It's Time to Grow Up and Learn GDB
Archit ChoudharyBy the end of this blog post, you will know how to use GDB to debug not just any C program, but any binary executable in general. Even if you are a seasoned GDB user, stick around to the end for a CTF challenge.
Why GDB?
It's 2AM and your code refuses to work. Desperate, you litter all your files with print and log statements, struggling to find just where your program goes awry. The deadline is in 6 hours and all you have to show for yourself is dysfunctional code.
We have all been there. The way actual hackers and programmers debug, in the vast majority of cases, is by using a debugger. And when you live in the terminal, "debugger" is synonymous with GDB.
GDB is a command-line debugger that's been around for decades. It gives you precise control over program execution. You can pause the execution and inspect variables, and play around with memory as you please. Although this goes beyond the scope of this blog post, GDB is also scriptable, as well as being quite essential for things like reverse engineering, writing exploits, and hacking games --- all worthy pursuits.
How to Use GDB Like a Pro
Installation
The first step is obviously to install GDB. Here's how you do it:
sudo apt install gdb # Debian-based
sudo dnf install gdb # Fedora/RHEL
sudo pacman -S gdb # Arch Linux/Manjaro
sudo zypper install gdb # openSUSE
brew install gdb # macOS (via Homebrew)
pkg install gdb # FreeBSD
apk add gdb # Alpine Linux
You can probably make it work on Windows, but come on, just get Linux. You can easily spin up a virtual machine, or now, even more easily, use Windows Subsystem for Linux (WSL); look it up.
GDB: Basics
Download the following zip file: [gdb-tutorial.tar.gz]. Extract it. Inside you will find a file called gdb-tut-1.c
. Compile and run it like so:
tar -xvf gdb-tutorial.tar.xz
cd gdb-tutorial/
gcc -g -o gdb-tut-1 gdb-tut1.c
./gdb-tut-1
Here the -g
option tells GCC to produce extra information (read: debug symbols) that GDB can use while debugging. You get the following output:
You can open gdb-tut-1.c
to look at the code. It's a very simple program; as seen above, it computes the square of 5. Now let's fire up GDB:
gdb ./gdb-tut-1
The program has been loaded into memory; it's play time. Here's a list of the most commonly used commands. We're gonna go through each of them, but it's better if you have the list upfront:
break <func> – Set a breakpoint at function
break <file>:<line> – Break at a specific line
run – Run the program
next – Step over function calls
step – Step into function calls
continue – Resume execution
print <var> – Print a variable’s value
where / backtrace – Show call stack
list – View source
watch
quit – Exit GDB
The most basic way to debug a program is as follows: set a breakpoint at a significant line in your code's logic, run the program, and when it stops at the breakpoint, step through the program while keeping an eye on the variables. Let us first try these commands on the harmless gdb-tut-1
:
The run
command runs the program, naturally. If you write list
you can see the source code of the file that you are debugging. Now let's set some breakpoints and play around with them:
Here's what happened in the above image. First, we look at the source code using list
. Then we set two breakpoints: the first one at line 10 (the printf
call) and the second one at the square
function. When I then run
the program, the execution stops at the square()
call on line 9 and GDB jumps to line 4 where the square()
function execution starts. At this point, we print the value of the parameter value x
, which is 5. The second print statement reminds us that the variable b
defined on line 9 does not exist in the current scope.
The where
and backtrace
commands basically show you where the execution has been paused, and the current state of the stack (so the program counter is currently at line 4 inside the square()
function, which was called on line 9). The continue
command starts the execution again until the next breakpoint, which, in our case, was line 10. Then step
goes to the next line, which is the return statement.
Quick note about the step
command. Normally, when the program counter is on a function call (like on line 10 above where we call printf
), the step
goes into the function. This doesn't happen by default with printf
, because printf
is a special function from the glibc
library, which is not normally compiled with debug symbols (gcc -g ...
). Therefore, GDB just skips to the next line, and the result is the same as running next
instead of step
.
Let's try another example now. Compile gdb-tut-2.c
and open it in GDB.
Reading through the code, it looks alright. Just add up the numbers using a for-loop. Let's run it:
Looks great: 10 + 20 + 30 + 40 + 50 = 150. Right? Well...
We use the set
command here to set the address after the end of the numbers array to be the integer 1000. When we finish executing this time, the sum turns out to be 1150. This is obviously wrong. Looking back at the source code, we realise the error: the for-loop goes from i = 0 to i = 5, instead of ending at i = 4.
Training
Exercise: Debugging a multifile C project
Open the folder gdb-ex-1
and compile the project by running make
. Run it using ./math-engine
. It's a simple program that does a chosen operation on two given numbers. However, there is an error:
When you enter an unknown operation not from the list, instead of throwing an error it gives a wrong answer. Open the program in GDB using gdb ./math_engine
and figure out where the mistake is (using GDB). Some tips:
- Type
help list
and see all the ways you can use thelist
command to read the project files. Try usinglist
instead of opening the files in a text editor. - Set breakpoints, and
step
andnext
through until you see where the error is.
Capture the Flag
The tarball contains an executable gdb-ctf
. There is a flag hidden in the program that you have to find using GDB, and it is a string of the form "SHACK{XXXXXXXXXX}". You will need the following new commands: info
, disas
, x
. Learn how they work using the help
command and using the Internet.
(If you're scared of running an arbitrary executable from the Internet, do it in a VM or take other measures.
As a bonus, there is a GNU program that can give you the answer in a single shell command; which is it?