CTF Challenge: Good Kitty - Writeup
Challenge Overview
This is a reverse engineering CTF challenge where we need to find the correct password by analyzing a binary that:
- Calculates a value based on Project Euler problem #3
- Encodes it using a custom algorithm
- Compares user input against the encoded value
Initial Analysis
Decompiled Code Structure
undefined8 main(void)
{
byte bVar1;
ssize_t bytes_read;
long input_len;
int iVar2;
long in_FS_OFFSET;
double dVar3;
undefined1 local_be;
byte is_correct;
uint index;
long flag;
undefined8 local_b0;
undefined8 local_a8 [4];
undefined8 local_88;
undefined8 uStack_80;
undefined8 local_78;
char user_input [72];
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
flag = ppeuler_3();
dVar3 = cbrt((double)flag);
flag = (long)dVar3;
flag = factorial(flag);
input_len = 0;
do {
bVar1 = *(byte *)((long)&flag + input_len);
if ((0x19 < (byte)((bVar1 & 0xdf) + 0xbf)) && (9 < (byte)(bVar1 - 0x30))) {
bVar1 = bVar1 % 0x3e;
if ((byte)(bVar1 - 10) < 0x1a) {
*(byte *)((long)&flag + input_len) = bVar1 + 0x37;
}
else if ((byte)(bVar1 + 0x30) < 0x54) {
*(byte *)((long)&flag + input_len) = bVar1 + 0x30;
}
else {
*(byte *)((long)&flag + input_len) = bVar1 + 0x3d;
}
}
input_len = input_len + 1;
} while (input_len != 8);
// ... rest of code validates input
}
Key Concepts Learned
1. Understanding Pointer Arithmetic on Stack Variables
Question: flag is declared as long flag; (not an array), so what does &flag + index mean?
Answer:
flagis a single 8-bytelongvariable on the stack- The code treats these 8 bytes as an array by using pointer arithmetic
&flag + indexaccesses individual bytes within the 8-byte variable
Memory Layout (Little-Endian):
Address: &flag &flag+1 &flag+2 ... &flag+7
+--------+--------+--------+-----+--------+
flag: | byte 0 | byte 1 | byte 2 | ... | byte 7 |
+--------+--------+--------+-----+--------+
(LSB) (MSB)
2. Type Casting and Pointer Arithmetic
Question: In *(char *)((long)&flag + (long)(int)index), why cast to long first?
Answer: The expression does integer arithmetic, not pointer arithmetic:
// Step-by-step evaluation:
1. &flag // Get address (type: long *)
2. (long)&flag // Cast pointer to integer
3. (long)(int)index // Ensure index is long
4. (long)&flag + index // INTEGER addition (not pointer arithmetic)
5. (char *)(...) // Cast result back to char pointer
6. *(...) // Dereference to get the byte
Is it equivalent to (char *)&flag + index?
Yes, for char * specifically, because sizeof(char) == 1:
(char *)&flag + index→ pointer arithmetic, movesindexbytes(char *)((long)&flag + index)→ integer arithmetic, addsindexto address
Both give the same result for char, but would differ for other types like int *.
3. Finding Stack Offsets in Ghidra
In Ghidra’s function variable list:
undefined8 Stack[-0xb8]:8 flag
This means:
- Variable is at
rbp - 0xb8 - Size is 8 bytes (
:8) - In GDB:
x/gx $rbp-0xb8orx/8bx $rbp-0xb8
GDB Debugging Techniques
Setting Breakpoints
Problem: Why did break 0x5555555564de fail but break *0x5555555564de work?
Answer: The * operator tells GDB to interpret the value as a memory address:
# WITHOUT * - GDB looks for a SYMBOL named "0x5555555564de"
break 0x5555555564de # ❌ Looks for function/symbol name
# WITH * - GDB treats it as a MEMORY ADDRESS
break *0x5555555564de # ✅ Breaks at instruction at this address
Other breakpoint methods:
break main # Break at function (no * needed)
break main+194 # Break at offset from function
break *0x1234 # Break at address
break file.c:42 # Break at source line
Handling PIE Executables
For PIE executables, a constant offset will be added for each section, so we need to get a section’s address at runtime.
Solution:
# Method 1: Use relative addressing
(gdb) start # Start and break at main
(gdb) break *main+194 # Offset from function start
(gdb) continue
# Method 2: Set breakpoint after program loads
(gdb) start
(gdb) info proc mappings # Check actual base address
(gdb) break *0x555555555000+0x136e
(gdb) continue
# Method 3: Use PIE-independent addresses
(gdb) break *0x136e # GDB calculates base automatically
Solution Process
The main highlight of this puzzle is that we can just read the flag from stack if its directly compared to our input at any point in the program. This will save us having to go through inverse of all the transformations that are applied. Of course this does not hold true if the comaparision is done after applying some trasnformations on input itself.
# Start the program
gdb ./crack
# Break after encoding loop completes
(gdb) break *main+194
Breakpoint 1 at 0x136e
# Run the program
(gdb) run
# Program hits breakpoint after encoding
Step 2: Extracting the Flag
# Examine the encoded flag (at rsp+0x10)
(gdb) x/8bx $rsp+0x10
0x7fffffffdeb0: 0x30 0x30 0x73 0x47 0x6f 0x34 0x4d 0x30
(gdb) x/8c $rsp+0x10
0x7fffffffdeb0: 48 '0' 48 '0' 115 's' 71 'G' 111 'o' 52 '4' 77 'M' 48 '0'
Final Answer
Password: 00sGo4M0
Verification
# Test the password
echo "00sGo4M0" | ./crack
# Output: good kitty!
Key Takeaways
- Stack variables can be treated as byte arrays using pointer arithmetic
- Type casting order matters:
(char *)((long)ptr + offset)does integer arithmetic - GDB’s
*operator is crucial for breaking at memory addresses vs symbols - ASLR requires setting breakpoints after program loads or using relative offsets
- Dynamic analysis (debugging) often reveals values that are hard to calculate manually
- Stack offsets in Ghidra directly translate to GDB commands like
$rbp-0xb8
Commands Reference Card
# Essential GDB workflow for CTFs
gdb ./binary
(gdb) start # Break at main
(gdb) break *main+OFFSET # Set breakpoint at offset
(gdb) continue # Run to breakpoint
(gdb) x/8bx $rsp+0x10 # Examine memory
(gdb) x/8c $rsp+0x10 # View as characters
(gdb) info registers # Check register values
(gdb) disassemble main # View assembly