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:

  1. Calculates a value based on Project Euler problem #3
  2. Encodes it using a custom algorithm
  3. 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:

  • flag is a single 8-byte long variable on the stack
  • The code treats these 8 bytes as an array by using pointer arithmetic
  • &flag + index accesses 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, moves index bytes
  • (char *)((long)&flag + index) → integer arithmetic, adds index to 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-0xb8 or x/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

  1. Stack variables can be treated as byte arrays using pointer arithmetic
  2. Type casting order matters: (char *)((long)ptr + offset) does integer arithmetic
  3. GDB’s * operator is crucial for breaking at memory addresses vs symbols
  4. ASLR requires setting breakpoints after program loads or using relative offsets
  5. Dynamic analysis (debugging) often reveals values that are hard to calculate manually
  6. 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