Post

Off-by-one overflow

Explanation on how off-by-one vulnerability on the heap can lead to RCE

Off-by-one overflow

off-by-one vulnerability

Programming mistakes that cause off-by-one

Incorrect Bounds Checking.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <string.h>
#define SIZE 1024
int main(void) { 
	
	char *a = (char*)malloc(SIZE);
	if (!a) exit(0);
	
	for (int i = 0; i <= SIZE; i++) {
		char c = fgetc(stdin);
		if (c == EOF) a[i] = '\0';
		else a[i] = c;
	}
}

If i reaches SIZE, then the loop continues, but a[SIZE] is out of bounds. The valid index ranges from 0 to SIZE - 1.

String Operations.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <string.h>
#define SIZE 1024
int main(int argc, char **argv) {
	if (argc == 2 && strlen(argv[1] > SIZE)) exit(0); 
	
	char *chunk1 = malloc(SIZE);
	if (!chunk1) exit(0);
	
	if (strlen(argv[1] == SIZE))
	strcpy(chunk1, argv[1]);
}

This is caused by the functionality of strlen() because when the function calculates the string length, it does not count \x00, which then causes strcpy() to copy SIZE+1 bytes into the chunk1.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>
#define SIZE 1024
int main(int argc, char **argv) {
	if (argc == 2 && strlen(argv[1] > SIZE)) exit(0); 
	
	char *a = malloc(SIZE);
	if (!a) exit(0);
	int bytesRead = read(0, a, SIZE);
	a[bytesRead] = '\0';
}

Types of off-by-one

There are 2 types of off by one vulnerabilities:

  1. Arbitrary byte: Allows to overwrite with any value.
  2. Poison null byte (off-by-null): Allows to overwrite with only null byte (0x00).

Heap Review

In 64 bit systems, the memory is aligned with 16 bytes ( 8 bytes for 32 bit systems). For some other allocations whose size isn’t aligned with 16 bytes (e.g. 0x38) the last 8 bytes of a chunk will overlap with the prev_size field of the next chunk, and because of this a off-by-one vulnerability can overwrite the size field.

Exploitation

Off-by-one / poison null bytes on the heap can lead to code execution by corrupting heap metadata, particularly the prev_inuse flag and the previous size field, to create overlapping chunks by forcing the malloc() to consolidate memory blocks incorrectly .So that we can change the contents of other chunks to then further use other heap exploitation techniques like tcache poisoning.

General off-by-one attack. (arbitrary byte)

  1. Allocate 4 chunks.
    • Chunk A, B, and Cof any size.
    • Chunk D to prevent consolidation.
  2. Free chunk C
    • It will go to one of the bins based on the size.
  3. Use chunk A to overflow to the size field of chunk B
    • Modify the size field of chunk B and make sure the size overlaps with chunk C
  4. Now chunk B will contain the free chunk C or its metadata.
  5. Free chunk B and allocate the same size again.
    • Lets say chunk B was 0x20 and we modified it to be 0x40.
    • Now free chunk B and allocate a chunk of size 0x40. pass 0x30 to malloc()
  6. Now the fd/next pointer of chunk C can be modified or viewed.
    • Can modify the fd/next pointer to perform tcache poisoning or any other bin attack.
    • If chunk C is in unsorted bin, you can view the fd/bk and get libc leak.

General poison null byte.

The poison null byte is used to clear the prev_inuse flag for chunks that are greater than `0x100.

Here is why.

  • Lets say that we have a chunk of size 0x20.
  • when we try to clear the prev_inuse flag we will overwrite the size field from 0x21 to 0x00.
  • Now the chunk will have size 0x00 which is not a valid size field, and the

Chunk C is our target.

Steps

  1. Allocate 4 chunks.
    • Chunk A’s size must not be aligned with 16 bytes and must be large enough to contain libc addresses (unsorted bin).
    • Chunk B is used to trigger the overflow.
    • Chunk C is our target.
    • Chunk D to prevent consolidation.
  2. Corrupt metadata.
    • Free chunk A first to make it a valid free chunk.
    • Use off-by-null vulnerability in chunk B to set a fake prev_size and clear the prev_inuse flag in chunk C.
    • Fake prev_size combined size of A + B e.g. Lets say the size of chunk A = 0x40 and size of chunk B = 0x20, then set prev_size = 0x60.
  3. Force Consolidation.
    • Free chunk C. because the prev_size field is set to fake size (A + B) and the prev_inuse flag is not set, libc will start the consolidation process.
    • A overlapping chunk will be created: All the chunks will be combined (A + B + C ) into a single large free chunk.
    • The large chunk now overlaps with chunk B, which is NOT free (currently in use).
  4. Allocate new chunk.
    • Lets call it chunk E.
    • This chunk will be carved out from the large consolidated free chunk.
    • chunk E is allocated to adjust the fd and bk pointers.
    • The size must be the same as chunk A

how2heap

  1. allocate a large padding.
    • so that the fake chunk’s addresses lowest 2nd byte is 0x00
  2. allocate 3 chunks
    • chunk 3 to prevent consolidation.
  3. link chunk 1 into largebin.
    • fd_nextsize and bk_nextsize = fd and bk of the fake chunk.
    • Allocate new chunk (a) with a little bit smaller size then chunk1, and then a small chunik to prevent cosolidation
    • allocate another new chunk (b) with a little bit larger size than chunk1 and a small chunk for colidation.
    • free chunk a, b, and then 1.
    • allocate a huge chunk to enable sorting. e.g. 0x1000
    • now a, b, and 1 will be in largebin.
  4. allocate new chunk with size 1.

Double free

Requirements

  • Use-after-free.
  • Poison null byte.

    1. Allocate chunk A and then free it.
    2. Allocate chunk B of different size than chunk A then free it.
    3. Allocate chunk C of the same size as chunk A.
    • This will return the same chunk as chunk A
    • Use off-by-one to overwrite the prev_inuse flag of chunk B
      1. Free chunk B
    • Now you have a double free.

You can now use double free for other attacks like tcache poisoning or fastbin dup.


  1. Allocate 3 adjacent chunks. a, b, c
    • poison null byte at b
  2. Allocate another chunk to prevent consolidation
  3. Free c .
    • b and a stays in use.
  4. Set fake metadata in chunk a.
    • prev_size = 0
    • size = a + b - 0x10
  5. use off-by-one metadata
    • Set prev_size of b to a + b - 0x10
    • perform off-by-null byte to unset the prev_inuse in chunk b
  6. free b
    • This will force backward consolidation A + B. - Allocate overlapping chunks with of size A + B

Resources

This post is licensed under CC BY 4.0 by the author.