GDB: Zero to Hero
After analyzing the binaries using tools like objdump, strace, and nm as explored in our Linux Binary Analysis series, it’s time to delve into debugging with GDB.
What is GDB?
GDB is a command-line debugger that allows developers to inspect and manipulate the execution of programs. It provides a range of features for analyzing the state of a program, examining variables, setting breakpoints, and stepping through code. GDB, short for the GNU Debugger, is a versatile and portable debugging tool designed to run on a wide range of Unix-like systems. It offers support for various programming languages, including Ada, Assembly, C, C++, D, Fortran, Haskell, Go, Objective-C, OpenCL C, Modula-2, Pascal, Rust, and more. This broad language support makes GDB an indispensable companion for developers working across diverse software projects and languages. GDB was first written by Richard Stallman in 1986 as part of his GNU system, after his GNU Emacs was “reasonably stable”.
For quick reference, you can download our GDB Cheat Sheet to have key GDB commands and shortcuts at your fingertips.
Let’s explore GDB in a step-by-step fashion, covering essential tasks from running your program to examining variables and memory. These steps will give you a solid foundation for debugging with GDB.
For basic debugging with GDB demonstration, I’ve used simple debugMe binary here:
Source Code:
// debugMe.c
#include <stdio.h>
int main() {
int a=1337;
printf("%d\n",a);
return 20;
}
$ gcc -m32 -g debugMe.c -o debugMe
Now we will compile the above source code. -m32 specifies that the target architecture for the compilation should be 32-bit. -g is used to enable debugging symbols while debugging.
Let’s keep this handy:
Basic Commands
- run: Start the program from the beginning.
- break: Set a breakpoint at a specific line of code or function.
Example: break main or break filename.c:line_number
- continue: Continue execution after hitting a breakpoint.
- next: Execute the next line of code (step over function calls).
- step: Execute the next line of code (step into function calls).
- print: Print the value of a variable.
Example: print my_variable
- quit: Exit GDB.
1. Running Your Program
To start debugging with GDB, run your program with the following command:
$ gdb ./debugMe
Reading symbols from ./debugMe...
(No debugging symbols found in ./debugMe)
(gdb)
If you didn’t specify a program to debug, you’ll have to load it in now:
$ gdb
(gdb) file debugMe
To facilitate the debugging of a running process with a known Process ID (PID), developers can utilize the -p or --pid options with GDB (GNU Debugger).
$ gdb --pid PID
If you’re ever confused about a command or just want more information, use the “help” command, with or without an argument:
(gdb) help [command]
2. Setting Breakpoints
Breakpoints are markers that tell GDB to pause the program’s execution at a specific point. We can set breakpoints at functions or specific lines of code.
Often, we want to start debugging from the main function. We can set a breakpoint at main using:
(gdb) break main
When debugging symbols are enabled during compilation, we can set breakpoints using specific lines of code from the source file. To enable symbols during compilation use -g flag with gcc.
The list command in GDB is used to display the source code around the current point of execution. This is helpful for seeing the context of the code where the program is currently paused. list command also provides the flexibility to specify a range of lines for a more focused inspection. We can set how many lines to show in list
by using set listsize <count>
command.
(gdb) list
1 // debugMe.c
2
3 #include <stdio.h>
4
5 int main() {
6 int a=1337;
7 printf("%d\n",a);
8 return 20;
9 }
(gdb) break debugMe.c:5
Breakpoint 1 at 0x11a9: file debugMe.c, line 6.
(gdb) list 1,3
1 // debugMe.c
2
3 #include <stdio.h>
By setting a breakpoint at line 6 of debugMe.c, we establish a designated pause point within our program. If the program execution reaches this line during runtime, it will halt, providing an opportunity to inspect variables, analyze the program’s state, and issue further commands within the debugging session. This strategic breakpoint empowers us to precisely control the flow of execution and gain deeper insights into our code’s behavior.
3. Listing Breakpoints
To list all active breakpoints, we can use the info breakpoints
command:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x000011a9 in main at debugMe.c:6
Disabling, Enabling, Deleting and Clearing Breakpoints
We can disable or enable breakpoints without removing them.
(gdb) enable 1
(gdb) disable 1
(gdb) delete 1
(gdb) clear # This will remove all breakpoints
4. Running the Program
Once a breakpoint is set, you can use the run
command to start the program. Upon reaching the breakpoint, the program will pause, allowing you to inspect variables and analyze the program’s state. You can then proceed to the next breakpoint by entering the continue
command. It’s important to note that re-entering run
would restart the program from the beginning, which is typically not desired during a debugging session.
(gdb) run
Starting program: /home/kali/Desktop/Bl0gs/Resources/gdb/debugMe
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main () at debugMe.c:6
6 int a=1337;
The program execution has paused at the main() function.
Now, we can single-step through the program by typing next
.
The next
command is used to execute the next line of code in the program, without stepping into any function calls. This means that if the current line of code contains a function call, the next
command will execute that entire function and pause at the line immediately following the function call.
This grants precise control over the program’s execution, allowing us to proceed one line of code at a time.
(gdb) next
7 printf("%d\n",a);
We have now paused at line no. 8.
We can proceed onto the next breakpoint by typing continue
(Typing run
again would restart the program from the beginning, which isn’t very useful.) If no further breakpoints are encountered, the program will continue its execution until it exits.
(gdb) continue
Continuing.
1337
[Inferior 1 (process 45522) exited with code 024]
The final line, “[Inferior 1 (process 45522) exited with code 024],” informs us that the program has exited with an exit code of 024. Now refer the source code, you can see the return status code for incorrect input which is 1.
If you just press ENTER, gdb will repeat the same command you just gave it. You can do this a bunch of times.
Up to this point, you’ve mastered the art of interrupting program flow at specific breakpoints and navigating line-by-line. However, as your debugging journey progresses, you’ll inevitably want to inspect variables, view memory contents, and delve deeper into the program’s state.
5. Inspecting Variables
In the debugging realm, gaining insights into variable values is crucial for understanding program behavior. GDB provides the print command, allowing developers to examine variable values during a debugging session. Additionally, the print/x command is handy for displaying values in hexadecimal format.
(gdb) break main
(gdb) run
Breakpoint 1, main () at debugMe.c:6
6 int a=1337;
(gdb) next
7 printf("%d\n",a);
(gdb) print a
$1 = 1337
(gdb) print/x a
$2 = 0x539
The hexadecimal value 0x539 corresponds to the decimal value 1337.
6. Setting Watchpoints
While breakpoints allow us to interrupt the program flow at specific lines or functions, watchpoints provide a targeted approach by focusing on variables. These powerful tools pause the program whenever a watched variable’s value is modified.
Let’s examine the command using the same binary named ‘debugMe’:
(gdb) break main
(gdb) run
(gdb) watch a # Setting a watchpoint on variable 'a'
Watchpoint 2: a
(gdb) continue
Continuing.
Watchpoint 2: a
Old value = -134470656
New value = 1337
main () at debugMe.c:7
7 printf("%d\n",a);
In the given C program, an integer variable a is initialized with the value 1337 in the main function. However, upon debugging with GDB, setting a watchpoint on a revealed an initial value of -134470656, indicating uninitialized memory. As the program executed and a was assigned the value 1337, the watchpoint triggered, displaying the transition from the old value to the new value. This showcases how watchpoints in GDB can be invaluable for tracking variable modifications, particularly in cases where variables are initialized or modified unexpectedly during program execution.
7. Kill
The kill command in GDB is used to terminate the current debugging session. This can be useful when you no longer need to continue debugging or if you want to exit GDB entirely.
(gdb) kill
Kill the program being debugged? (y or n) y
[Inferior 1 (process 46063) killed]
After entering kill, GDB confirms if you want to terminate the program being debugged. Entering ‘y’ and pressing Enter will exit the program and return you to the GDB prompt.
8. Examining Memory
In the world of debugging, gaining insight into memory contents can be invaluable for understanding program behavior and diagnosing issues. GDB offers several commands to examine memory locations and data structures.
x/nfu <address>
The x command allows you to examine memory at a given address.
x - Command for examining memory.
/n - Specifies how many units to print (default is 1).
f - Format character, similar to the print command's format specifiers.
u - Unit size:
b: Byte
h: Half-word (two bytes)
w: Word (four bytes)
g: Giant word (eight bytes)
Let’s consider a scenario where we have an array of integers:
// array.c
#include <stdio.h>
int main() {
int array[] = {10, 20, 30, 40};
return 0;
}
To load GDB silently, use the -q option. This option suppresses unnecessary messages and prompts, allowing for a more streamlined and efficient debugging experience. We can examine the memory location of the array variable using the x command in GDB:
$ gdb -q ./array
Reading symbols from ./array...
(gdb) list
1 // array.c
2
3 #include <stdio.h>
4
5 int main() {
6 int array[] = {10, 20, 30, 40};
7
8 return 0;
9 }
(gdb) break array.c:7
(gdb) run
(gdb) print array
$1 = {10, 20, 30, 40}
(gdb) x/4xb array
0xffffd5f8: 0x0a 0x00 0x00 0x00
(gdb) x/4*4xb array
0xffffd5f8: 0x0a 0x00 0x00 0x00
(gdb) x/16xb array
0xffffd5f8: 0x0a 0x00 0x00 0x00 0x14 0x00 0x00 0x00
0xffffd600: 0x1e 0x00 0x00 0x00 0x28 0x00 0x00 0x00
The output will display the memory contents of the array variable in hexadecimal format. Since the size of an int is typically 4 bytes, when examining memory with the x command, we can see the contents of array[1] represented by 4 bytes. We use array in place of &array, it is because the array name decomposes to a pointer to the first element.
Here is a table showing the size of common data types in C for a 64-bit system:
Data Type | Size (bytes) |
---|---|
char |
1 |
short |
2 |
int |
4 |
long |
8 |
long long |
8 |
float |
4 |
double |
8 |
long double |
16 |
To calculate the total size of an array in bytes, you multiply the number of elements by the size of each element.
Total size of array (in bytes) = Number of elements * Size of each element (in bytes)
For example, if we have an array arr with 5 integers (int type, which is 4 bytes on a 64-bit system), the total size would be: Total size of arr = 5 * 4 bytes = 20 bytes
In memory inspection, we can specify unit sizes using the following formats:
b: Byte
h: Half-word (two bytes)
w: Word (four bytes)
g: Giant word (eight bytes)"
For instance, in memory inspection, 4 bytes are considered as 1 word.
(gdb) x/4xw array # Hexadecimal Format
0xffffd5f8: 0x0000000a 0x00000014 0x0000001e 0x00000028
(gdb) x/4dw array # Decimal Format
0xffffd5f8: 10 20 30 40
Here are the formats for various data types in GDB:
a: Pointer.
c: Read as integer, print as character.
d: Integer, signed decimal.
f: Floating point number.
o: Integer, print as octal.
s: Try to treat as C string.
t: Integer, print as binary (t = "two").
u: Integer, unsigned decimal.
x: Integer, print as hexadecimal.
9. Manipulating the Program
In addition to examining memory and variables, GDB allows for the manipulation of the program’s execution.
The set var
command in GDB is used to modify the value of a variable during program execution. This can be incredibly useful for debugging and testing purposes, allowing you to manipulate variables to observe how the program behaves under different conditions.
$ gdb -q ./debugMe
Reading symbols from ./debugMe...
(gdb) break debugMe.c:7
Breakpoint 1 at 0x11b0: file debugMe.c, line 7.
(gdb) run
Breakpoint 1, main () at debugMe.c:7
7 printf("%d\n",a);
(gdb) set var a = 10
(gdb) print a # Verify
$1 = 10
(gdb) continue
Continuing.
10
[Inferior 1 (process 47674) exited with code 024]
Now, the value of a has been changed to 10. This can be particularly handy when you want to test how your program behaves with different variable values without having to recompile or modify the source code.
The set
command in GDB allows you to modify memory locations directly. When combined with memory addresses, you can change the value of a variable without needing to refer to its name.
In GDB, to print the memory address of a variable, you can use the print command followed by the ‘&’ (address-of) operator and the variable name. This combination allows for the precise display of the memory location of the variable during debugging sessions.
(gdb) run
Breakpoint 1, main () at debugMe.c:7
(gdb) print a
$1 = 1337
(gdb) print &a
$2 = (int *) 0xffffd5ec
(gdb) set *0xffffd5ec = 10
(gdb) continue
Continuing.
10
[Inferior 1 (process 47674) exited with code 024]
In GDB, the * symbol is used as a shorthand to indicate that you are working with the value stored at a specific memory address, rather than with the address itself. So, in essence, * in GDB acts as a dereference operator, allowing you to manipulate the value stored at a particular memory location. Additionally, in GDB, you have the flexibility to typecast the memory address to a different data type using C-style casting.
(gdb) run
Breakpoint 1, main () at debugMe.c:7
(gdb) print a
$1 = 1337
(gdb) set var a=0x41
(gdb) print (char)a
$2 = 65 'A'
(gdb) continue
Continuing.
65
[Inferior 1 (process 47940) exited with code 024]
You can also use set {char}0xffffd5ec = 'A'
. The {char} specifies that you are working with a character.
print *(char*)0xffffd5ec
This prints the character value stored at 0xffffd5ec. The (char*) is a typecast indicating that you are treating the memory contents as a character.
10. Gathering Insights
Under this section, we’ll explore important GDB commands that provide valuable insights into the program’s state and structure during debugging.
1. disassemble
Usage: disassemble or disassemble <where>
Description: Disassembles the current function or a specific location in the program, showing the assembly instructions.
$ gdb -q ./debugMe
Reading symbols from ./debugMe...
(gdb) break debugMe.c:7
Breakpoint 1 at 0x11b0: file debugMe.c, line 7.
(gdb) run
(gdb) disassemble
Dump of assembler code for function main:
0x5655618d <+0>: lea 0x4(%esp),%ecx
0x56556191 <+4>: and $0xfffffff0,%esp
0x56556194 <+7>: push -0x4(%ecx)
0x56556197 <+10>: push %ebp
0x56556198 <+11>: mov %esp,%ebp
0x5655619a <+13>: push %ebx
0x5655619b <+14>: push %ecx
0x5655619c <+15>: sub $0x10,%esp
0x5655619f <+18>: call 0x565561d6 <__x86.get_pc_thunk.ax>
0x565561a4 <+23>: add $0x2e50,%eax
0x565561a9 <+28>: movl $0x539,-0xc(%ebp)
=> 0x565561b0 <+35>: sub $0x8,%esp
0x565561b3 <+38>: push -0xc(%ebp)
0x565561b6 <+41>: lea -0x1fec(%eax),%edx
0x565561bc <+47>: push %edx
0x565561bd <+48>: mov %eax,%ebx
0x565561bf <+50>: call 0x56556040 <printf@plt>
0x565561c4 <+55>: add $0x10,%esp
0x565561c7 <+58>: mov $0x14,%eax
0x565561cc <+63>: lea -0x8(%ebp),%esp
0x565561cf <+66>: pop %ecx
0x565561d0 <+67>: pop %ebx
By default, disassemble shows the assembly instructions in AT&T syntax style.
To change the syntax style to Intel, use the command:
set disassembly-flavor intel
To disassemble main
:
(gdb) disassemble main
info
is a very useful command. To get a list of info subcommands, type info
.
2. info args
Usage: info args
Description: Prints the arguments passed to the function of the current stack frame.
3. info breakpoints
Usage: info breakpoints
Description: Provides information about the set breakpoints and watchpoints in the program.
5. info locals
Usage: info locals
Description: Prints the local variables in the currently selected stack frame.
6. info sharedlibrary
Usage: info sharedlibrary
Description: Lists the shared libraries that are loaded into the program.
7. whatis variable_name
Usage: whatis variable_name
Description: Prints the type of the named variable.
8. info registers
Usage: info registers
Description: Displays the contents of all general-purpose registers and, depending on the CPU architecture, may also include other registers like floating-point or SIMD (Single Instruction, Multiple Data) registers.
9. info functions
Usage: info functions
Description: Displays a list of all functions defined in the program’s code.
11. Quit
And of course, when you’re done with GDB, you’ll want to gracefully exit. The quit command does just that:
(gdb) quit
In our journey thus far, we’ve delved into essential GDB commands that serve as indispensable tools for understanding our code’s execution, inspecting memory, setting breakpoints, and analyzing variables. These commands offer invaluable insights into the state and structure of our programs during the intricate process of debugging.
Now equipped with a solid grasp of these information-gathering tools in GDB, we are poised to venture into the captivating realm of x86 assembly language. Here, we will embark on a comprehensive exploration of fundamental concepts and syntax in assembly programming, where we will craft code that operates at the raw, elemental level of the CPU. Before we plunge into the captivating realm of x86 assembly language, let’s introduce a powerful tool that eases the debugging process - pwndbg.
pwndbg is a Python wrapper built around GDB, tailored specifically for exploit development and reverse engineering. It provides a plethora of additional features and enhancements over plain GDB, making the debugging experience more efficient and intuitive.
Installation
$ git clone https://github.com/pwndbg/pwndbg
$ cd pwndbg
$ ./setup.sh
Type gdb in your terminal to launch GDB. Upon launching GDB with pwndbg installed, you should see a message indicating that pwndbg has been successfully loaded:
$ gdb
pwndbg: loaded 147 pwndbg commands and 48 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $ida GDB functions (can be used with print/break)
------- tip of the day (disable with set show-tips off) -------
Want to NOP some instructions? Use patch <address> 'nop; nop; nop'
pwndbg>
Now you are equipped with numerous pwndbg commands and features, significantly enhancing your debugging capabilities.
An Introduction to x86 Assembly Language
x86 is a family of backward-compatible instruction set architectures originally developed by Intel. It has been the dominant architecture for personal computers since the 1980s and remains widely used today. x86 assembly language is the low-level programming language specific to the x86 family of processors. In this guide, we’ll explore the basics of x86 assembly language, a widely-used instruction set architecture found in many modern computers.
Basic Concepts
Registers
Registers are small storage locations within the CPU. x86 architecture has several general-purpose registers:
eax, ebx, ecx, edx: General-purpose data registers used for arithmetic and data manipulation.
esi, edi: Index registers often used for pointer arithmetic and accessing arrays.
esp: Stack pointer, points to the top of the stack.
ebp: Base pointer, used as a reference point for accessing function arguments and local variables.
eip: Instruction pointer, points to the next instruction to be executed.
Instructions
x86 assembly instructions are low-level commands that perform operations like arithmetic, logic, or control flow. Some common instructions include:
mov: Move data between registers or memory.
add, sub: Perform addition or subtraction.
cmp: Compare two values.
jmp: Unconditional jump to another part of the code.
je, jne, jl, jg: Conditional jumps based on comparison results.
Memory Access
Memory can be accessed using the mov instruction. Syntax: [address] or [register].
Practical Time
Let’s put our newfound knowledge to use by demonstrating the debugging process on a simple binary called My first crackme by ss1ned downloaded from crackmes.one. For this example, we’ll use a basic crackme binary that prompts the user for a password.
Let’s load the binary crackme into pwndbg and let’s display a list of all functions defined in the binary.
$ gdb ./crackme
pwndbg> info functions
All defined functions:
Non-debugging symbols:
0x0000000000001000 _init
0x0000000000001080 __cxa_finalize@plt
0x0000000000001090 strncpy@plt
0x00000000000010a0 puts@plt
0x00000000000010b0 __stack_chk_fail@plt
0x00000000000010c0 fgets@plt
0x00000000000010d0 strcmp@plt
0x00000000000010e0 _start
0x0000000000001110 deregister_tm_clones
0x0000000000001140 register_tm_clones
0x0000000000001180 __do_global_dtors_aux
0x00000000000011c0 frame_dummy
0x00000000000011c9 main
0x00000000000013f0 __libc_csu_init
0x0000000000001460 __libc_csu_fini
0x0000000000001468 _fini
Now as we don’t see any user defined function, we will set a breakpoint at main function and run the program.
First, we set a breakpoint at the main function:
pwndbg> b main
Breakpoint 1 at 0x11d1
Next, we run the program:
pwndbg> run
# As the program executes, pwndbg offers a wealth of information. Don't feel overwhelmed—I'll simplify it for you step by step.
# 1. You will see the current values of all registers.
# 2. Next, you'll see the disassembled code at the current instruction pointer (EIP).
# 3. Then, the stack contents.
# 4. Finally, the backtrace showing the call stack.
While cracking binaries, especially where password checks are implemented, one common technique is to inspect string comparison functions such as strcmp(). These functions are pivotal as they are often used to check if a provided password matches the expected one within the program.
Let’s examine the disassembly of the main function.
pwndbg> disassemble # As we are already in main, disassemble will be same as disassemble main
...
0x0000555555555368 <+415>: call 0x5555555550a0 <puts@plt>
...
0x0000555555555380 <+439>: call 0x5555555550c0 <fgets@plt>
...
0x0000555555555393 <+458>: call 0x5555555550d0 <strcmp@plt>
...
0x00005555555553a8 <+479>: call 0x5555555550a0 <puts@plt>
...
0x00005555555553b6 <+493>: call 0x5555555550a0 <puts@plt>
...
0x00005555555553e1 <+536>: ret
End of assembler dump.
From this disassembly, we can infer the program flow:
- It starts by displaying a message using
puts
. - Then it reads user input using
fgets
. - Next, it likely compares the user input with a predefined value using
strcmp
. - Based on the result of the comparison, it displays different messages using
puts
. - Finally, it reaches the end of the main function and returns.
So we will set a breakpoint at puts@plt
and strcmp@plt
pwndbg> break *main+415 # break *0x5555555550a0 should also work
Breakpoint 2 at 0x555555555368
pwndbg> break *main+458 # break *0x5555555550d0 should also work
Breakpoint 3 at 0x555555555393
Now, we will continue the execution.
pwndbg> continue
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
► 0x555555555368 <main+415> call puts@plt <puts@plt>
s: 0x55555555600d ◂— 'Enter password:'
...
As seen in the DISASM section, a call to puts@plt
has been made with the argument ‘Enter password:’. Let’s proceed step by step using the next
command.
pwndbg> next
Enter password:
0x000055555555536d in main ()
...
Hence, we verified that the argument passed was indeed ‘Enter password:’. Now, it’s time to hit our next breakpoint. Let’s proceed!
pwndbg> continue
Continuing.
AAAA
Breakpoint 3, 0x0000555555555393 in main ()
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
...
*RDX 0x7fffffffe3a0 ◂— 'Password'
*RDI 0x7fffffffe3b0 ◂— 0xa41414141 /* 'AAAA\n' */
*RSI 0x7fffffffe3a0 ◂— 'Password'
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
...
► 0x555555555393 <main+458> call strcmp@plt <strcmp@plt>
s1: 0x7fffffffe3b0 ◂— 0xa41414141 /* 'AAAA\n' */
s2: 0x7fffffffe3a0 ◂— 'Password'
...
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rdx rsi rsp 0x7fffffffe3a0 ◂— 'Password'
01:0008│ 0x7fffffffe3a8 ◂— 0x0
02:0010│ rax rdi 0x7fffffffe3b0 ◂— 0xa41414141 /* 'AAAA\n' */
...
After entering ‘AAAA’ as the password, we noticed that it was received as ‘AAAA\n’. As we analyzed the disassembled code, we observed that our program indeed reached the strcmp@plt
function. pwndbg beautifully displays the arguments passed to the function: s1 was 0xa41414141, which corresponds to the hex value of ‘AAAA\n’, and s2 was 0x7fffffffe3a0, a pointer to the buffer containing the string ‘Password’.
$ echo AAAA | xxd -p # Display output in continuous hex dump style
414141410a
Additionally, in the STACK Frame, you can observe that the first argument is actually the last argument, and the last argument is the first. This is because arguments are pushed from right to left on the stack. Therefore, the first argument will be at the top of the stack, which in this case is ‘Password’.
To display the string at memory location 0x7fffffffe3a0, we can use the x
command in pwndbg.
pwndbg> x/s 0x7fffffffe3a0
0x7fffffffe3a0: "Password"
Now, let’s start the program by using the run
or r
command. We will keep the breakpoints for verification.
pwndbg> run
pwndbg> continue
Continuing.
Enter password:
Password
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
...
► 0x555555555393 <main+458> call strcmp@plt <strcmp@plt>
s1: 0x7fffffffe3b0 ◂— 'Password'
s2: 0x7fffffffe3a0 ◂— 'Password'
...
pwndbg> continue
Continuing.
Welcome!
[Inferior 1 (process 53980) exited normally]
BINGO!
We’ve cracked it!
Now, let’s explore a sneaky method to crack the above binary by entering the wrong password. I call it ‘WRONG PASS, RIGHT RETURN’.
If you’ve noticed the disassembly of the main function, you may have observed some JE
and JMP
instructions.
# main disassembly
...
0x0000555555555393 <+458>: call 0x5555555550d0 <strcmp@plt>
0x0000555555555398 <+463>: mov DWORD PTR [rbp-0x4c],eax
0x000055555555539b <+466>: cmp DWORD PTR [rbp-0x4c],0x0
0x000055555555539f <+470>: je 0x5555555553af <main+486>
0x00005555555553a1 <+472>: lea rdi,[rip+0xc75] # 0x55555555601d
0x00005555555553a8 <+479>: call 0x5555555550a0 <puts@plt>
0x00005555555553ad <+484>: jmp 0x5555555553bb <main+498>
0x00005555555553af <+486>: lea rdi,[rip+0xc70] # 0x555555556026
0x00005555555553b6 <+493>: call 0x5555555550a0 <puts@plt>
0x00005555555553bb <+498>: mov eax,0x0
0x00005555555553c0 <+503>: mov rsp,rbx
0x00005555555553c3 <+506>: mov rbx,QWORD PTR [rbp-0x28]
0x00005555555553c7 <+510>: xor rbx,QWORD PTR fs:0x28
0x00005555555553d0 <+519>: je 0x5555555553d7 <main+526>
...
NOTE:
In x86-64 assembly language, RAX
is indeed used as the register to store the return value of a function. When a function finishes executing and is ready to return a value to its caller, it places that value in the RAX
register.
This convention is used for functions that return a single value, such as integers or pointers. When a function finishes, the value in RAX
is typically what will be used by the calling code.
So the return value of function strcmp@plt
which is an integer will be stored in RAX
.
strcmp is a C library function used to compare two strings. It returns an integer value that indicates the result of the comparison.
Here’s what strcmp returns in short:
- If the two strings are equal, strcmp returns 0.
- If the first string is lexicographically less than the second string, strcmp returns a negative value.
- If the first string is lexicographically greater than the second string, strcmp returns a positive value.
So, if the user-entered password is correct, in that case, 0 will be returned… Hmm… What if I just change the returned value? HuiHui…
Let’s set up a breakpoint at *main+463. We cannot directly use the address 0x0000555555555398, even though they refer to the same thing. This is because PIE (Position Independent Executable) is enabled, so we need to use the symbol name main+463. In my upcoming blog, we will dive into various Binary Protections such as DEP/NX, RELRO, Stack Protection, PIE, FORTIFY, and more.
pwndbg> b *main+463
Breakpoint 1 at 0x1398
pwndbg> run
Starting program: /home/kali/Desktop/Bl0gs/Resources/gdb/crackme
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter password:
AAAA
...
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX 0xfffffff1
...
As you can see, RAX
contains a non-zero value. Let’s disassemble once more and see what happens next.
pwndbg> disassemble
...
=> 0x0000555555555398 <+463>: mov DWORD PTR [rbp-0x4c],eax
0x000055555555539b <+466>: cmp DWORD PTR [rbp-0x4c],0x0
0x000055555555539f <+470>: je 0x5555555553af <main+486>
...
0x00005555555553ad <+484>: jmp 0x5555555553bb <main+498>
0x00005555555553af <+486>: lea rdi,[rip+0xc70] # 0x555555556026
0x00005555555553b6 <+493>: call 0x5555555550a0 <puts@plt>
...
The value in RAX
will be copied into [rbp-0x4c], and this value will then be compared to 0x0. As we know, this comparison will fail, resulting in the ZERO flag not being set. Therefore, the JUMP will not be taken at address 0x5555555553af <main+486>. Instead, the JUMP will be taken at 0x5555555553bb <main+498>.
Now we have an idea of how to manipulate the flow of the program.
Let’s once again run the program, but this time we’ll set the RAX
value to 0x0 and observe what happens.
pwndbg> b *main+463
Breakpoint 1 at 0x1398
pwndbg> run
Starting program: /home/kali/Desktop/Bl0gs/Resources/gdb/crackme
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter password:
AAAA
...
pwndbg> set $rax=0x0
pwndbg> info registers rax
rax 0x0 0
pwndbg> continue
Continuing.
Welcome!
[Inferior 1 (process 54930) exited normally]
YEAH!! We’ve successfully fooled the program!
We will now learn another technique which I call “WRONG PASS, RIGHT FLAG”. Here, we will set the Zero Flag to 1. On X86/X64: setflag ZF 1 – set zero flag setflag CF 0 – unset carry flag On ARM: setflag Z 0 – unset the Z cpsr/xpsr flag
WARNING!
Change flag after CMP
instruction.
pwndbg> b *main+463
Breakpoint 1 at 0x1398
pwndbg> run
Starting program: /home/kali/Desktop/Bl0gs/Resources/gdb/crackme
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter password:
AAAA
...
pwndbg> next
pwndbg> next
pwndbg> info registers eflags
eflags 0x282 [ SF IF ]
pwndbg> setflag ZF 1
Set flag ZF=1 in flag register eflags (old val=0x283, new val=0x2c3)
pwndbg> info registers eflags
eflags 0x2c2 [ ZF SF IF ]
pwndbg> continue
Continuing.
Welcome!
[Inferior 1 (process 55171) exited normally]
Extra Tips
- To execute Linux commands directly within the GDB prompt, use
!
as a prefix. For example,!ls
to list files. - GDB provides shorthand commands for convenience like
r
forrun
,c
forcontinue
,i r
forinfo registers
… - Refer pwndbg docs.
Refer
In this journey through the world of GDB, we’ve covered a wide range of topics, from the basics of setting breakpoints and inspecting memory to the thrilling experience of cracking a program. We started with understanding how GDB can be a powerful ally in debugging, allowing us to step through our code, examine variables, and understand the flow of our programs. I hope this guide has been informative and has sparked your curiosity to dive deeper into the realm of debugging and beyond. Keep exploring, keep learning, and let GDB be your trusted companion on your coding adventures.
Happy debugging!