Contents
Introduction
Debugging is a practical skill, so the best way for you to become proficient is by getting plenty of hands-on practice within realistic reverse-engineering contexts (i.e. where the source code is not available to you). This week, we continue to focus on dynamic analysis using the GNU Debugger (GDB) whilst solving eight more challenges.
Advanced GDB Techniques
Before diving into the advanced CTF challenges, let’s cover some essential GDB techniques that you’ll need for the more complex debugging scenarios ahead.
Backtrace and Stack Analysis
When a program crashes or you want to understand the call chain, use the backtrace command:
bt
This shows you the sequence of function calls that led to the current point in execution.
To examine the values pushed to the stack before a function call:
x/10x $rsp
This shows the top 10 values on the stack, which often include function parameters.
Function Call Analysis
Understanding how function parameters are passed is crucial for advanced debugging:
Note: In x86-64 Linux, function parameters are passed in registers:
- 1st parameter: RDI
- 2nd parameter: RSI
- 3rd parameter: RDX
- 4th parameter: RCX
To examine the first parameter to a function:
x/s $rdi
To examine the second parameter:
x/s $rsi
XOR Operations and Encryption
XOR is commonly used in encryption and obfuscation. In assembly, you’ll see instructions like:
xor eax, 0x12345678
Tip: When you see XOR operations, one operand is often the data and the other is the encryption key.
To examine XOR operations, look for the xor instruction in the disassembly and note the values being XORed together.
Advanced Memory Examination
Here are different format specifiers for the x command:
x/s $rdi # Read as string
x/x $rax # Read as hexadecimal
x/d $rax # Read as decimal
x/c $rax # Read as character
x/10i $rip # Read as 10 instructions
To examine strings in memory, use:
x/s $rdi
The /s
format specifier reads the data as a null-terminated string.
Setting Breakpoints in Function Calls
To set a breakpoint just before a function call:
break *main+0x123
This allows you to examine the parameters being passed to the function.
Use the fin
command to step out of a function and return to the caller:
fin
Tip: The
fin
command is particularly useful when you’ve stepped into a function and want to return to the calling function.
CTF Challenges
There are eight challenges this week and, as you would expect, there is more than one way to solve them - feel free to explore different approaches.
General tips
Tip: Don’t forget to change your GDB configuration settings so programs are disassembled using the Intel notation (GDB defaults to AT&T).
==action: Run:==
vi ~/.gdbinit
==action: Add the following line:==
set disassembly-flavor intel
==action: Save and close the file.==
Flag: For all the challenges, once you have found the password, run the program again outside of GDB to get your flag.
Tip: If at any point your console layout looks jumbled, with the text you type overwriting the text on the screen, hit Ctrl + L to fix it.
Tip: Remember to use these essential GDB commands:
Command | Description |
---|---|
run |
execute the program from within GDB |
si |
“step into” the next assembly instruction |
ni |
“step over” the next assembly instruction (don’t step into functions) |
fin |
“step out” of the function |
c |
continue, or “play” |
XorLong
Hint: There are different ways to solve this challenge. One of them is to find the cmp in main() that controls whether printflag() will be called. Put a breakpoint there. Print the values of the registers being compared (as hexadecimal numbers). One of them will be the password.
XorStr
Hint: One of the ways to solve this challenge is to put a breakpoint before the call to strlen(), then examine the value of rdi (as a string) when the execution stops there.
Hint: Next, look for the XOR line in the disassembled code. See if you can find the mask.
Hint: Write a simple C program that goes through every character of the string (char array) and XORs it against the mask.
SegvBacktrace
Hint: Run the program in gdb. When the execution fails following a wrong password, run “bt”.
Hint: Put a break in each of the functions, just before they call the next function.
Hint: When execution stops, examine the value of the rdi register (as a string) in each of them.
2DArrays {#2darrays}
Hint: Using GDB to obtain this flag could almost be considered cheating… but since our focus this week is to get as much practice with GDB as possible, you are allowed (and indeed encouraged) to use it.
Hint: Disassemble main() and find the cmp instruction that compares a register with a hardcoded value.
InputFormat
Hint: The password is made up of three three-digit values, separated by a space: an integer, a character and a hexadecimal number. When testing, enter something like 111 aaa 222. For this challenge, you will need to work on one value at a time.
Hint: First, set the breakpoints:
Hint: 1. Find the call to scanf() that takes user input. Then find the cmp instruction that follows it. Put a breakpoint there.
Hint: 2. Find the call to strcmp() a little further down and put a breakpoint there.
Hint: 3. Finally, find the next cmp instruction and put a breakpoint there.
Hint: Run the program in GDB. When it stops at the first cmp instruction, examine the values being compared. One of them is your input, the other is the answer.
Hint: Run the program again, this time replacing the integer/character/decimal part of the answer with the value you found. When the execution breaks, examine the values being pushed onto the stack before strcmp() is called. You should be able to find your second value.
Hint: Run the program one more time, replacing the two parts of the answer you now know with the correct values. When the execution stops, once again, examine the values being compared to find your third and final value.
StaticInt
Hint: Disassemble main(). See that EAX needs to be set to not zero for it to print the flag, and the code that sets the value of EAX is inside the check_code() function.
Hint: Use breakpoints in check_code() to read the value of a local variable just before the cmp instruction is executed. Remember to examine it as a double-word decimal.
StaticRE
Hint: This one is fairly straightforward. Disassemble main(). Put a breakpoint on the cmp based on which the flag will be printed.
Hint: Run the program and check the values compared. Remember to examine the local variable as a double-word decimal.
StaticStrcmp
Hint: Find the call to strcmp (it doesn’t show the name, just the function address). Put a breakpoint just before the function is called and examine the values pushed to the stack.
Conclusion
At this point you have:
- Become more confident in using GDB to step through a program’s execution at assembly level, without having access to the source code;
- Solved practical challenges using MetaCTF and found 8 more flags!
Well done!
By now, you will have had plenty of exposure to GDB and should have started to feel confident performing advanced dynamic analysis on malware for which the source code is not available.