Perfectroot CTF 2024
Table of Contents

- 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.

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:

- The file was an
ELF 64-bit LSB pie executable
,dynamically linked
andnot 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
- 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.
- Among the results, I found the strings
flag
andprintFlag
. 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.
- From the Symbol Tree in Ghidra, 2 functions of interest were
main
(program entry point) andprintFlag
(function to give the flag supposedly)
Decompiling the main()
function: #

- The function
main()
appeared to handle the program’s flow, but it didn’t callprintFlag()
directly. I needed to find how printFlag() was being triggered.
Decompiling printFlag()
function #

- Reversing the decompiled code for
printFlag
proved to be quite challenging.
Disassembling the main()
function #

-
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. -
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. -
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. -
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 callprintFlag
, we can manually invoke it to execute it.
- Load the Binary into GDB
- We start by loading the binary into GDB for analysis.
- Set a Breakpoint at main
- Next, we set a breakpoint at the main function, which is where the program’s execution will pause.
- 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: - 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:
- 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.
- 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.
- 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:

- 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.
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 #

- 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 #

- 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.
- And we get the flag!
Approach 3: Modifying Variable Values in GDB During Runtime #

Control Flow Graph in Ghidra #

- To get the flag, we need to modify the value at [rbp-0x4] and set it to 1 during runtime.
Variable Modification During Runtime #
-
Run the Program in GDB: Launch the program in GDB to set a breakpoint at
main
and inspect the execution: -
Step through the instructions until [rbp-0x4] is set to 0.
-
Modify [rbp-0x4] and set its value to 1 using
set *(int*)($rbp-4)=1
-
Finally, continue the execution of the program
- And we got the flag :)
Challenge 2. Adversary Within - Part 1 #

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