Skip to main content
  1. ctf-writeups/

Perfectroot CTF 2024

·7 mins
Perfectroot CTF
  • Welcome to my write-up for the Perfectroot CTF 2024! In this post, I’ll walk through the challenges I tackled, sharing insights into my approach, analysis, and how I cracked the flags. Let’s dive straight into the action!

Challenge 1: Pores #

Category: Binary Exploitation

Objective: Reverse engineer the binary to uncover the hidden flag.

Perfectroot CTF

Approach 1:Runtime Analysis and Manipulation using GDB (Invoking Arbitrary Functions) #

1. Initial Exploration: #

  • The first thing I did was inspect the binary using the file command:
Perfectroot CTF
  • The file was an ELF 64-bit LSB pie executable, dynamically linked and not stripped, meaning it contained debugging symbols for easier analysis

2. Running the Program #

  • Before diving deeper into the analysis, I decided to execute the program to observe its behavior. I first made it executable using chmod +x poresssss then ran it using ./poresssss
    Perfectroot CTF
  • The process immediately exited with no output.

3. Running strings: #

  • I ran strings to look for any readable strings that might give clues about the flag.
    Perfectroot CTF
  • Among the results, I found the strings flag and printFlag. These stood out as potential indicators of where the flag might be stored or how to trigger its retrieval.

4. Using Ghidra for Disassembly and Decompilation #

  • I loaded the binary into Ghidra for further analysis.
    Perfectroot CTF
  • From the Symbol Tree in Ghidra, 2 functions of interest were main (program entry point) and printFlag (function to give the flag supposedly)

Decompiling the main() function: #

Perfectroot CTF
  • The function main() appeared to handle the program’s flow, but it didn’t call printFlag() directly. I needed to find how printFlag() was being triggered.

Decompiling printFlag() function #

Perfectroot CTF
  • Reversing the decompiled code for printFlag proved to be quite challenging.

Disassembling the main() function #

Perfectroot CTF
Control Flow in the main Function:

  1. mov ESI, 0x08: This instruction loads the value 0x08 (which is 8 in decimal) into the ESI register. This value is likely used to represent the size or number of chunks that the program will work with when printing the flag.

  2. LEA RAX, [flag]: The LEA (Load Effective Address) instruction loads the memory address of the flag variable (or data) into the RAX register. This is a pointer to the actual flag data in memory, and it is used as the first argument for the printFlag function.

  3. mov RDI=>flag, RAX: This instruction moves the value from RAX (which holds the address of the flag) into the RDI register, which is used for the first argument in a function call (according to the x86-64 calling convention). Essentially, it is preparing the flag’s address as the first parameter for the printFlag function.

  4. call printFlag: Finally, the call printFlag instruction transfers control to the printFlag function, passing the prepared arguments in RDI (the address of the flag) and ESI (the size, 8). This function, as per the analysis, is responsible for printing or revealing the flag.

5. Analysis Using GDB #

  • Since main does not call printFlag, we can manually invoke it to execute it.
    Perfectroot CTF
    Step-by-Step Debugging Process:
  1. Load the Binary into GDB
  • We start by loading the binary into GDB for analysis.
  1. Set a Breakpoint at main
  • Next, we set a breakpoint at the main function, which is where the program’s execution will pause.
  1. Run the Program and Hit the Breakpoint
  • When we run the program, GDB pauses at the main function, allowing us to inspect the program’s state at this point.

Now that the program is paused, we have full control over its execution. This allows us to: View and modify memory, modify registers, continue execution, jump to different parts of the code, and much more!

Disassembling the main function #

  • When we disassemble main we got the following control flow:
    Perfectroot CTF
  • We can see that the flag is stored at address 0x555555558040.
  • I tried examining the string at that address using x/s 0x555555558040 but it gave me some non-printable ascii characters.

Modifying the program control flow by invoking the printFlag function #

  • Here’s how we do that:
  1. Resuming Execution at printFlag:
  • We will modify the program’s execution by using GDB’s call command to jump directly to the printFlag function. This effectively simulates what would have happened if main had called printFlag normally.
  1. Handling the void Return Type in GDB:
  • Since printFlag has a void return type, we need to ensure GDB recognizes it properly when invoking it manually. GDB does not automatically handle functions that return void, so we explicitly cast the function call to void to avoid errors. This ensures GDB knows we don’t expect a return value and prevents issues when invoking the function.
  1. Passing the Right Arguments:
  • The printFlag function requires two arguments:

a. The address of the flag, 0x555555558040

b. The number of blocks (each 8 bytes) that the function needs to process, 8

In GDB, we call the function as follows:

Perfectroot CTF

  • And we get the flag :) r00t{p4tch_th3_bin_and_h4ve_fun}

Approach 2: Static Patching Using Ghidra #

Main Function Breakdown in Ghidra #

  • The main function contains key instructions that set up the flag printing mechanism.
    Perfectroot CTF

1. Function Prologue #

00101298 55              PUSH       RBP
00101299 48 89 e5        MOV        RBP,RSP
0010129c 48 83 ec 10     SUB        RSP,0x10
  • This is the standard function prologue. The base pointer (RBP) is pushed to the stack, the stack pointer (RSP) is moved to RBP, and the stack is adjusted to allocate space for local variables.

2. Initialize Local Variable (local_c): #

001012a0 c7 45 fc        MOV        dword ptr [RBP + local_c],0x0
         00 00 00 00
  • A local variable (local_c) is initialized to 0. This is where the program will store the value that determines whether the flag is printed.

3. Comparison #

001012a7 83 7d fc 01     CMP        dword ptr [RBP + local_c],0x1
  • The program compares the value of local_c (at RBP + local_c) with 1.

4. Conditional Jump #

001012ab 75 14           JNZ        LAB_001012c1
  • If local_c is not equal to 1 (JNZ = Jump if Not Zero), it jumps to LAB_001012c1, skipping the flag printing instructions.

5. LAB_001012c1 #

                                                             
        001012c1 b8 00 00        MOV        EAX,0x0
                 00 00
        001012c6 c9              LEAVE
        001012c7 c3              RET
  • The program exits by setting EAX to 0 and executing RET (return from the function).

Patching the Binary #

Perfectroot CTF
  • Patch the instruction that sets local_c to 0 and change it to set local_c to 1
  • This modification ensures that the comparison at CMP dword ptr [RBP + local_c], 0x1 will always succeed.
  • You will notice that the raw machine code bytes for this instruction also change:

Original bytes: c7 45 fc 00 00 00 00

Patched bytes: c7 45 fc 01 00 00 00

1. Save the Patched Binary #

  • Save and export the modified binary to your local system.

2. Make a Hex dump of the Original and Patched Files #

xxd pores > pores.hex
xxd patched_pores > patched_pores.hex

3. Diffing the Hex Dump Original and Patched Files #

Perfectroot CTF
  • This gives us the differences in the 2 files which means we successfully patched the binary.

4. Run the Patched Binary #

  • Set the file to be executable then run it.
    Perfectroot CTF
  • And we get the flag!

Approach 3: Modifying Variable Values in GDB During Runtime #

Perfectroot CTF

Control Flow Graph in Ghidra #

Perfectroot CTF
  • To get the flag, we need to modify the value at [rbp-0x4] and set it to 1 during runtime.

Variable Modification During Runtime #

  1. Run the Program in GDB: Launch the program in GDB to set a breakpoint at main and inspect the execution:

  2. Step through the instructions until [rbp-0x4] is set to 0.

  3. Modify [rbp-0x4] and set its value to 1 using set *(int*)($rbp-4)=1

  4. Finally, continue the execution of the program

    Perfectroot CTF

  • And we got the flag :)

Challenge 2. Adversary Within - Part 1 #

Perfectroot CTF

Walkthrough: #

  • Breaking down the key elements of the challenge:

1. “…roasts”: This was the most telling clue. The term “roasts” is a subtle reference to Kerberos, the authentication protocol used by Active Directory (AD). In the context of AD, the term “Kerberoasting” refers to an attack technique where attackers request service tickets for service accounts in AD and attempt to crack them offline. The “roasts” here were a direct hint toward this attack method.

2. “backbone connecting users and resources in every environment”: This phrase clearly pointed to a system responsible for managing users and their access to resources in an environment. Active Directory (AD) is the most ubiquitous service for managing identities, permissions, and resource access in a network, especially in enterprise environments.

3. “I’m everywhere, supporting every interaction”: Active Directory is indeed the backbone of user authentication and resource management in virtually every corporate network, supporting all interactions that require authentication and authorization.

4. “What are my rules called?”: The reference to “rules” is crucial. In Active Directory, the schema defines the structure of the directory, including object types, attributes, and how those objects interact. The schema essentially sets the “rules” for how the data is organized and used in AD.

  • And so the flag was schema :)

  • Until next time