├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── heap_explorer.c ├── heap_explorer.h └── playground.c /.gitignore: -------------------------------------------------------------------------------- 1 | .gdb_history 2 | *.o 3 | *.so 4 | playground 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux/archlinux:base-devel-20250209.0.306557 2 | 3 | RUN useradd --create-home user 4 | 5 | USER user 6 | 7 | WORKDIR /home/user 8 | 9 | COPY . /home/user 10 | 11 | RUN make 12 | 13 | CMD ["./playground"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 heap-exploitation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean fmt 2 | 3 | CC ?= gcc 4 | CFLAGS ?= -ggdb -O0 -Wall -Wextra -Wpedantic -Wvla -std=c23 -fPIC 5 | 6 | all: playground libheap_explorer.so 7 | 8 | heap_explorer.o: heap_explorer.c 9 | $(CC) -c $(CFLAGS) $^ -o $@ 10 | 11 | libheap_explorer.so: heap_explorer.o 12 | $(CC) -shared $(CFLAGS) $^ -o $@ 13 | 14 | playground: playground.c heap_explorer.o 15 | $(CC) $(CFLAGS) $^ -o $@ 16 | 17 | clean: 18 | rm -f *.so *.o playground 19 | 20 | fmt: 21 | clang-format --style='{IndentWidth: 4, AllowShortFunctionsOnASingleLine: false}' -i *.c 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heap Explorer 2 | 3 | Heap Explorer is an `LD_PRELOAD`able library that exports only 1 function: 4 | ```C 5 | void explore_heap(void); 6 | ``` 7 | 8 | `explore_heap` starts a REPL that allows the user to perform a few different actions, such as `free`ing chunks, `malloc`ing chunks, printing freelists, etc. 9 | 10 | `libheap_explorer.so` installs `explore_heap` as the `SIGINT` handler at load time, so you can do stuff like this: 11 | ``` 12 | $ LD_PRELOAD=/the/path/to/libheap_explorer.so python3 -c 'while True: print("Dave and Dale are cool cats")' 13 | Dave and Dale are cool cats 14 | Dave and Dale are cool cats 15 | Dave and Dale are cool cats 16 | ... 17 | ^C 18 | Welcome to Heap Explorer! 19 | You are TID 821537, viewing arena 0 (main_arena) 20 | 1. Allocate chunk(s). 21 | 2. Free a chunk. 22 | 3. Print this arena. 23 | 4. Print a tcache list. 24 | 5. Print a fastbin list. 25 | 6. Print a bin list. 26 | 7. Switch to next arena. 27 | 8. Switch to next thread. 28 | 9. Exit Heap Explorer. 29 | > 3 30 | [0]: 0x55e6d9316008, data size: 0x288 31 | [1]: 0x55e6d9316298, data size: 0x18 (arena 0, tcache 0) 32 | [2]: 0x55e6d93162b8, data size: 0x18 33 | [3]: 0x55e6d93162d8, data size: 0x78 34 | [4]: 0x55e6d9316358, data size: 0x318 35 | ... 36 | [1614]: 0x55e6d93c7a28, data size: 0x2e8 (arena 0, tcache 45) 37 | [1615]: 0x55e6d93c7d18, data size: 0x128 (arena 0, tcache 17) 38 | [1616]: 0x55e6d93c7e48, data size: 0x3d8 (arena 0, tcache 60) 39 | [1617]: 0x55e6d93c8228, data size: 0x5c8 40 | [1618]: 0x55e6d93c87f8, data size: 0xb68 (free 92) 41 | [1619]: 0x55e6d93c9368, data size: 0x288 (arena 0, tcache 39) 42 | [1620]: 0x55e6d93c95f8, data size: 0x878 (free 0) 43 | [1621]: 0x55e6d93c9e78, data size: 0x2008 44 | [1622]: 0x55e6d93cbe88, data size: 0x308 45 | [1623]: 0x55e6d93cc198, data size: 0x1de68 (top chunk) 46 | 47 | You are TID 821551, viewing arena 0 (main_arena) 48 | 1. Allocate chunk(s). 49 | 2. Free a chunk. 50 | 3. Print all chunks. 51 | 4. Print a tcache list. 52 | 5. Print a fastbin list. 53 | 6. Print a bin list. 54 | 7. Switch to next arena. 55 | 8. Switch to next thread. 56 | 9. Exit Heap Explorer. 57 | > 9 58 | Dave and Dale are cool cats 59 | Dave and Dale are cool cats 60 | Dave and Dale are cool cats 61 | ... 62 | ``` 63 | 64 | ## Compatibility 65 | 66 | This library has been tested only on programs using Arch's glibc package, version `2.41+r6+gcf88351b685d-1`. 67 | It's pretty easy to port to other modern versions of glibc; just change the `*_OFFSET` constants in `heap_explorer.c`. 68 | 69 | ## Why should I use this over $OTHER_TOOL? 70 | 71 | You shouldn't :) 72 | -------------------------------------------------------------------------------- /heap_explorer.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a glibc heap exploration program. 3 | * It allows you to allocate and free chunks, 4 | * and observe how various heap data structures 5 | * change. 6 | */ 7 | 8 | #define _GNU_SOURCE 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "heap_explorer.h" 21 | 22 | static char const GREEN[] = "\x1b[0;32m"; 23 | static char const YELLOW[] = "\x1b[0;33m"; 24 | static char const BLUE[] = "\x1b[0;34m"; 25 | static char const PURPLE[] = "\x1b[0;35m"; 26 | static char const CLEAR_COLOR[] = "\x1b[0m"; 27 | 28 | // Takes a pointer to chunk's data, 29 | // returns a pointer to its size 30 | static void *data2chunk(void const *const data) { 31 | return (uint8_t *)data - sizeof(size_t); 32 | } 33 | 34 | // Takes a pointer to a chunk's size, 35 | // returns a pointer to its data 36 | static void *chunk2data(void const *const chunk) { 37 | return (uint8_t *)chunk + sizeof(size_t); 38 | } 39 | 40 | // Takes a pointer to a chunk's prev_size, 41 | // returns a pointer to its size. 42 | static void *glibc_chunk2chunk(void const *const glibc_chunk) { 43 | return (uint8_t *)glibc_chunk + sizeof(size_t); 44 | } 45 | 46 | // Takes a pointer to a chunk's size, 47 | // returns a pointer to its prev_size. 48 | static void *chunk2glibc_chunk(void const *const chunk) { 49 | return (uint8_t *)chunk - sizeof(size_t); 50 | } 51 | 52 | // Takes a pointer to a chunk's size, 53 | // returns a pointer to its data. 54 | static void *glibc_chunk2data(void const *const glibc_chunk) { 55 | return (uint8_t *)glibc_chunk + 2 * sizeof(size_t); 56 | } 57 | 58 | static bool is_in_skip_list(void const *const chunk) { 59 | return ((void const* const *)chunk)[3] != NULL || ((void const* const *)chunk)[4] != NULL; 60 | } 61 | 62 | // Converts an int to a hex string. 63 | // Returns a pointer to static memory. 64 | static char *itoa_hex(uint64_t n) { 65 | static char const UINT64_MAX_STR_HEX[] = "0xffffffffffffffff"; 66 | static char result_data[sizeof(UINT64_MAX_STR_HEX)]; 67 | char *result = result_data; 68 | memset(result, 0, sizeof(result_data)); 69 | 70 | static char PREFIX[] = "0x"; 71 | strcpy(result, PREFIX); 72 | for (size_t i = 0; i < strlen(PREFIX); i++) { 73 | result++; 74 | } 75 | 76 | if (n == 0) { 77 | result[0] = '0'; 78 | return result; 79 | } 80 | 81 | char const HEXDIGS[] = "0123456789abcdef"; 82 | 83 | for (int i = 0; n > 0; i++) { 84 | result[i] = *(HEXDIGS + n % 16); 85 | n /= 16; 86 | } 87 | 88 | for (uint64_t i = 0; i < strlen(result) / 2; i++) { 89 | char tmp = result[i]; 90 | result[i] = result[strlen(result) - i - 1]; 91 | result[strlen(result) - i - 1] = tmp; 92 | } 93 | 94 | return result - strlen(PREFIX); 95 | } 96 | 97 | static char *ptoa(void const *const p) { 98 | return itoa_hex((intptr_t)p); 99 | } 100 | 101 | static char const UINT64_MAX_STR_DEC[] = "18446744073709551615"; 102 | 103 | // Converts an int to a decimal string 104 | // Returns a pointer to static memory. 105 | static char *itoa(uint64_t n) { 106 | static char result[sizeof(UINT64_MAX_STR_DEC)]; 107 | memset(result, 0, sizeof(result)); 108 | 109 | if (n == 0) { 110 | result[0] = '0'; 111 | return result; 112 | } 113 | 114 | for (int i = 0; n > 0; i++) { 115 | result[i] = '0' + n % 10; 116 | n /= 10; 117 | } 118 | 119 | for (uint64_t i = 0; i < strlen(result) / 2; i++) { 120 | char tmp = result[i]; 121 | result[i] = result[strlen(result) - i - 1]; 122 | result[strlen(result) - i - 1] = tmp; 123 | } 124 | 125 | return result; 126 | } 127 | 128 | static int32_t atoi32(uint8_t const *s) { 129 | int32_t result = 0; 130 | while ('0' <= *s && *s <= '9') { 131 | result *= 10; 132 | result += *s - '0'; 133 | s++; 134 | } 135 | if (*s != '\0') { 136 | _exit(EXIT_FAILURE); 137 | } 138 | return result; 139 | } 140 | 141 | // Writes a null-terminated string to stdout 142 | static void print(char const *const s) { 143 | write(STDOUT_FILENO, s, strlen(s)); 144 | } 145 | 146 | // Writes a null-terminated string to stdout, 147 | // followed by a newline 148 | static void println(char const *const s) { 149 | static char const NL[] = "\n"; 150 | print(s); 151 | print(NL); 152 | } 153 | 154 | // The glibc malloc_state struct, from glibc's malloc/malloc.c, 155 | // with some slight simplifications. Should still be binary-compatible. 156 | struct malloc_state { 157 | uint32_t mutex; 158 | uint32_t flags; 159 | uint32_t have_fastchunks; 160 | void *fastbinsY[10]; 161 | void *top; 162 | void *last_remainder; 163 | void *bins[254]; 164 | uint32_t binmap[4]; 165 | void *next; 166 | void *next_free; 167 | uint64_t attached_threads; 168 | uint64_t system_mem; 169 | uint64_t max_system_mem; 170 | }; 171 | 172 | #define ARRAY_LEN(A) (sizeof(A) / sizeof(A[0])) 173 | 174 | // The offset of malloc within the glibc mapping 175 | static intptr_t const MALLOC_OFFSET = 0xa9190; 176 | 177 | // The base address of glibc. 178 | // Note that this is not the base of the first entry from `info proc mappings` 179 | // that came from libc.so.6. Instead, this is the base of the entry before that, 180 | // because that mapping (which I believe is the libc .bss) is randomized 181 | // contiguously with glibc. 182 | static intptr_t const LIBC_BASE = (intptr_t)malloc - MALLOC_OFFSET; 183 | 184 | // The offset of main_arena within glibc. 185 | static intptr_t const MAIN_ARENA_OFFSET = 0x1ebac0; 186 | 187 | // A pointer to main_arena in glibc. This is the 188 | // struct that stores most of the heap state. 189 | static struct malloc_state const *const the_main_arena = 190 | (struct malloc_state *)(LIBC_BASE + MAIN_ARENA_OFFSET); 191 | #define NFASTBINS (ARRAY_LEN(the_main_arena->fastbinsY)) 192 | 193 | // Gets a chunk's data size. 194 | // Note that this is 8 less than its size field, 195 | // because the size itself is 8 bytes wide. 196 | static uint64_t get_chunk_data_size(void const *const chunk) { 197 | // We mask off the low 3 bits because they store metadata. 198 | return (*(uint64_t const *)chunk & ~7ull) - sizeof(size_t); 199 | } 200 | 201 | // Prints a chunk's data size. 202 | static void print_chunk_data_size(void const *const chunk) { 203 | uint64_t const size = get_chunk_data_size(chunk); 204 | print(", "); 205 | print(PURPLE); 206 | print("data size: "); 207 | print(CLEAR_COLOR); 208 | print(itoa_hex(size)); 209 | } 210 | 211 | // Takes a pointer to a chunk's size (not prev_size), 212 | // and dumps information about that chunk. 213 | static void print_chunk(void const *const chunk, char const *const msg, 214 | int64_t const arena_index, int64_t const bin_index, 215 | char const *const color) { 216 | print(ptoa(chunk)); 217 | print_chunk_data_size(chunk); 218 | print(" "); 219 | if (color != NULL) { 220 | print(color); 221 | } 222 | 223 | if (msg != NULL) { 224 | print("\t("); 225 | if (arena_index != -1) { 226 | print("arena "); 227 | print(itoa(arena_index)); 228 | print(", "); 229 | } 230 | print(msg); 231 | } 232 | 233 | if (bin_index != -1) { 234 | print(" "); 235 | print(itoa(bin_index)); 236 | } 237 | 238 | if (msg != NULL) { 239 | print(")"); 240 | } 241 | 242 | if (color != NULL) { 243 | print(CLEAR_COLOR); 244 | } 245 | println(""); 246 | } 247 | 248 | #define TCACHE_SIZE (64) 249 | struct tcache_perthread_struct { 250 | uint16_t counts[TCACHE_SIZE]; 251 | void *entries[TCACHE_SIZE]; 252 | }; 253 | 254 | // Takes a pointer to chunk's size, and returns a pointer 255 | // to the next chunk's size. 256 | static void *get_next_chunk(void const *const chunk) { 257 | return (uint8_t *)chunk2data(chunk) + get_chunk_data_size(chunk); 258 | } 259 | 260 | // Gets the address of the given arena's tcache struct. 261 | static struct tcache_perthread_struct * 262 | get_the_tcache(struct malloc_state const *const arena) { 263 | if (arena == the_main_arena) { 264 | // Just get the first chunk on this heap 265 | return (struct tcache_perthread_struct *)glibc_chunk2data( 266 | ((uint8_t *)chunk2glibc_chunk( 267 | get_next_chunk(glibc_chunk2chunk(arena->top))) - 268 | arena->system_mem)); 269 | } else { 270 | // Get the chunk after the chunk containing the arena 271 | return (struct tcache_perthread_struct *)((uint64_t *)(arena + 1) + 3); 272 | } 273 | } 274 | 275 | // Gets the first chunk on the heap. 276 | // If the heap is uninitialized, this will likely segfault. 277 | static void *get_first_chunk(struct malloc_state const *const arena) { 278 | return data2chunk(get_the_tcache(arena)); 279 | } 280 | 281 | // Gets the last chunk on the heap. 282 | // If the heap is uninitialized, this will likely segfault. 283 | static void *get_last_chunk(struct malloc_state const *const arena) { 284 | return glibc_chunk2chunk(arena->top); 285 | } 286 | 287 | // Returns whether chunk is in use (according to the following chunk's 288 | // PREV_INUSE bit). 289 | static bool is_in_use(void const *const chunk) { 290 | return (*(uint64_t *)get_next_chunk(chunk)) & 1; 291 | } 292 | 293 | // Takes the address of a list link from tcache or fastbin, 294 | // and deobfuscates it. Equivalent to REVEAL_PTR from glibc. 295 | static void *deobfuscate_next_link(void const *const p) { 296 | return (void *)((((intptr_t)p) >> 12) ^ *(intptr_t const *)p); 297 | } 298 | 299 | struct lookup_result { 300 | int64_t idx; 301 | int64_t arena; 302 | }; 303 | 304 | static struct lookup_result const LOOKUP_FAILED = {.idx = -1, .arena = -1}; 305 | 306 | static bool lookup_failed(struct lookup_result lookup) { 307 | return memcmp(&lookup, &LOOKUP_FAILED, sizeof(struct lookup_result)) == 0; 308 | } 309 | 310 | static bool lookup_succeeded(struct lookup_result lookup) { 311 | return !lookup_failed(lookup); 312 | } 313 | 314 | // If `chunk` is in a fastbin, returns which one. 315 | // Otherwise, returns -1 316 | static int64_t arena_fastbin_lookup(struct malloc_state const *const arena, 317 | void const *const chunk) { 318 | for (int64_t i = 0; i < (int64_t)NFASTBINS; i++) { 319 | if (arena->fastbinsY[i] != NULL) { 320 | void const *curr = arena->fastbinsY[i]; 321 | while (curr != NULL) { 322 | if (glibc_chunk2chunk(curr) == chunk) { 323 | return i; 324 | } 325 | curr = deobfuscate_next_link(glibc_chunk2data(curr)); 326 | } 327 | } 328 | } 329 | return -1; 330 | } 331 | 332 | // If `chunk` is in a tcache bin, returns which one. 333 | // Otherwise, returns -1 334 | static int64_t arena_tcache_lookup(struct malloc_state const *const arena, 335 | void const *const chunk) { 336 | struct tcache_perthread_struct const *const tcache = get_the_tcache(arena); 337 | for (int64_t i = 0; i < TCACHE_SIZE; i++) { 338 | if (tcache->entries[i] != NULL) { 339 | void const *curr = tcache->entries[i]; 340 | while (curr != NULL) { 341 | if (chunk == data2chunk(curr)) { 342 | return i; 343 | } 344 | curr = deobfuscate_next_link(curr); 345 | } 346 | } 347 | } 348 | return -1; 349 | } 350 | 351 | static struct lookup_result tcache_lookup(void const *const chunk) { 352 | struct malloc_state const *arena = the_main_arena; 353 | int64_t arena_idx = 0; 354 | do { 355 | int64_t i = arena_tcache_lookup(arena, chunk); 356 | if (i != -1) { 357 | return (struct lookup_result){.idx = i, .arena = arena_idx}; 358 | } 359 | arena = arena->next; 360 | arena_idx++; 361 | } while (arena != the_main_arena); 362 | return LOOKUP_FAILED; 363 | } 364 | 365 | // If `chunk` is in a normal (small/large/unsorted) bin, returns which one. 366 | // Otherwise, returns -1 367 | static int64_t const NBINS = ARRAY_LEN(the_main_arena->bins) / 2; 368 | static int64_t arena_bin_lookup(struct malloc_state const *const arena, 369 | void const *const chunk) { 370 | for (int64_t i = 0; i < NBINS; i++) { 371 | void const *const head = data2chunk(arena->bins + i * 2); 372 | void const *const head_link = *(void **)chunk2data(head); 373 | if (head_link == NULL) { 374 | continue; 375 | } 376 | 377 | void const *curr = glibc_chunk2chunk(head_link); 378 | while (curr != head) { 379 | if (curr == chunk) { 380 | return i; 381 | } 382 | curr = glibc_chunk2chunk(*(void **)chunk2data(curr)); 383 | } 384 | } 385 | return -1; 386 | } 387 | 388 | // Prints all the chunks in the heap. 389 | static void print_arena(struct malloc_state const *const arena) { 390 | if (arena->top == NULL) { 391 | println("This arena is empty."); 392 | return; 393 | } 394 | void const *const last_chunk = get_last_chunk(arena); 395 | void const *curr_chunk = get_first_chunk(arena); 396 | 397 | uint64_t i = 0; 398 | while (curr_chunk < last_chunk) { 399 | print(YELLOW); 400 | print("["); 401 | print(itoa(i)); 402 | print("]:\t"); 403 | print(CLEAR_COLOR); 404 | char const *msg = NULL; 405 | char const *color = NULL; 406 | int64_t bin_idx = -1; 407 | int64_t arena_idx = -1; 408 | struct lookup_result const tcache_lookup_result = 409 | tcache_lookup(curr_chunk); 410 | int64_t fastbin_idx = arena_fastbin_lookup(arena, curr_chunk); 411 | if (!is_in_use(curr_chunk)) { 412 | msg = "free"; 413 | color = GREEN; 414 | bin_idx = arena_bin_lookup(arena, curr_chunk); 415 | if (bin_idx == 0) { 416 | msg = "unsorted bin"; 417 | bin_idx = -1; 418 | } else if (bin_idx < 63) { 419 | msg = "smallbin"; 420 | } else { 421 | msg = "largebin"; 422 | } 423 | } else if (lookup_succeeded(tcache_lookup_result)) { 424 | msg = "tcache"; 425 | color = GREEN; 426 | bin_idx = tcache_lookup_result.idx; 427 | arena_idx = tcache_lookup_result.arena; 428 | } else if (fastbin_idx != -1) { 429 | msg = "fastbin"; 430 | color = GREEN; 431 | bin_idx = fastbin_idx; 432 | } 433 | print_chunk(curr_chunk, msg, arena_idx, bin_idx, color); 434 | void const *const next_chunk = get_next_chunk(curr_chunk); 435 | curr_chunk = next_chunk; 436 | i++; 437 | } 438 | 439 | if (curr_chunk == last_chunk) { 440 | print(YELLOW); 441 | print("["); 442 | print(itoa(i)); 443 | print("]:\t"); 444 | print(CLEAR_COLOR); 445 | print_chunk(last_chunk, "top chunk", -1, -1, BLUE); 446 | } else { 447 | print("Heap corrupted!"); 448 | } 449 | } 450 | 451 | static void *get_chunk_by_index(struct malloc_state const *const arena, 452 | uint64_t const n) { 453 | if (arena->top == NULL) { 454 | println("The heap is empty."); 455 | return NULL; 456 | } 457 | 458 | void const *const last_chunk = get_last_chunk(arena); 459 | void *curr_chunk = get_first_chunk(arena); 460 | 461 | uint64_t i = 0; 462 | while (curr_chunk != last_chunk && i < n) { 463 | curr_chunk = get_next_chunk(curr_chunk); 464 | i++; 465 | } 466 | 467 | if (i != n) { 468 | print("Couldn't find chunk "); 469 | print(itoa(n)); 470 | println("."); 471 | return NULL; 472 | } else { 473 | return curr_chunk; 474 | } 475 | } 476 | 477 | // Frees the `n`th chunk on the heap. 478 | static void free_chunk(void *const chunk) { 479 | free(chunk2data(chunk)); 480 | } 481 | 482 | // Returns the index of `chunk` in the heap. 483 | // i.e., if `chunk` is the first thing allocated, returns 1 484 | // (because of the bottom chunk), and if `chunk` is the top chunk, 485 | // returns (num_chunks-1). 486 | static int64_t arena_chunk_lookup(struct malloc_state const *const arena, 487 | void const *const target_chunk) { 488 | void const *const last_chunk = get_last_chunk(arena); 489 | void *curr_chunk = get_first_chunk(arena); 490 | 491 | int64_t i = 0; 492 | while (curr_chunk != last_chunk && curr_chunk != target_chunk) { 493 | curr_chunk = get_next_chunk(curr_chunk); 494 | i++; 495 | } 496 | 497 | if (curr_chunk == target_chunk) { 498 | return i; 499 | } else { 500 | return -1; 501 | } 502 | } 503 | 504 | static struct lookup_result chunk_lookup(void const *const chunk) { 505 | struct malloc_state const *arena = the_main_arena; 506 | int64_t arena_idx = 0; 507 | do { 508 | int64_t i = arena_chunk_lookup(arena, chunk); 509 | if (i != -1) { 510 | return (struct lookup_result){.idx = i, .arena = arena_idx}; 511 | } 512 | arena = arena->next; 513 | arena_idx++; 514 | } while (arena != the_main_arena); 515 | return LOOKUP_FAILED; 516 | } 517 | 518 | static void print_bin_list(struct malloc_state const *const arena, 519 | int64_t const bin_idx) { 520 | if (bin_idx >= NBINS) { 521 | println("Index out of bounds."); 522 | return; 523 | } 524 | 525 | void const *const head = data2chunk(arena->bins + bin_idx * 2); 526 | void const *const head_link = *(void **)chunk2data(head); 527 | if (head_link == NULL) { 528 | println("The bins are uninitialized."); 529 | return; 530 | } 531 | 532 | void const *curr = glibc_chunk2chunk(head_link); 533 | uint64_t i = 0; 534 | print("{ "); 535 | while (curr != head) { 536 | if (i != 0) { 537 | print(" -> "); 538 | } 539 | if (is_in_skip_list(curr)) { 540 | print(GREEN); 541 | } 542 | print(ptoa(curr)); 543 | if (is_in_skip_list(curr)) { 544 | print(CLEAR_COLOR); 545 | } 546 | curr = glibc_chunk2chunk(*(void **)chunk2data(curr)); 547 | i++; 548 | } 549 | println(" }"); 550 | } 551 | 552 | static void print_fastbin_list(struct malloc_state const *const arena, 553 | uint64_t const fastbin_idx) { 554 | if (fastbin_idx >= NFASTBINS) { 555 | println("Index out of bounds."); 556 | return; 557 | } 558 | void const *const head = arena->fastbinsY[fastbin_idx]; 559 | void const *curr = head; 560 | uint64_t i = 0; 561 | print("{ "); 562 | while (curr != NULL) { 563 | if (i != 0) { 564 | print(" -> "); 565 | } 566 | print(ptoa(glibc_chunk2chunk(curr))); 567 | curr = deobfuscate_next_link(glibc_chunk2data(curr)); 568 | i++; 569 | } 570 | println(" }"); 571 | } 572 | 573 | static void print_tcache_list(struct malloc_state const *const arena, 574 | uint64_t const tcache_idx) { 575 | if (tcache_idx >= TCACHE_SIZE) { 576 | println("Index out of bounds."); 577 | return; 578 | } 579 | 580 | struct tcache_perthread_struct const *const the_tcache = 581 | get_the_tcache(arena); 582 | void const *const head = the_tcache->entries[tcache_idx]; 583 | void const *curr = head; 584 | uint64_t i = 0; 585 | print("{ "); 586 | while (curr != NULL) { 587 | if (i != 0) { 588 | print(" -> "); 589 | } 590 | print(ptoa(data2chunk(curr))); 591 | curr = deobfuscate_next_link(curr); 592 | i++; 593 | } 594 | println(" }"); 595 | } 596 | 597 | // Parses a decimal int 598 | static uint64_t parse_base10(char const *s) { 599 | uint64_t result = 0; 600 | while ('0' <= *s && *s <= '9') { 601 | result *= 10; 602 | result += *s - '0'; 603 | s++; 604 | } 605 | 606 | return result; 607 | } 608 | 609 | // Parses a hex int 610 | static uint64_t parse_base16(char const *s) { 611 | uint64_t result = 0; 612 | while (('0' <= *s && *s <= '9') || ('a' <= *s && *s <= 'f') || 613 | ('A' <= *s && *s <= 'F')) { 614 | result *= 16; 615 | if ('0' <= *s && *s <= '9') { 616 | result += *s - '0'; 617 | } else if ('a' <= *s && *s <= 'f') { 618 | result += *s - 'a' + 10; 619 | } else if ('A' <= *s && *s <= 'F') { 620 | result += *s - 'A' + 10; 621 | } 622 | s++; 623 | } 624 | 625 | return result; 626 | } 627 | 628 | // Reads a decimal or hex int from stdin 629 | static uint64_t get_number(void) { 630 | char num[sizeof(UINT64_MAX_STR_DEC)] = 631 | {}; // we use UINT64_MAX_STR_DEC because it's longer than 632 | // UINT64_MAX_STR_HEX 633 | for (uint64_t i = 0; i < sizeof(num) - 1; i++) { 634 | int const rc = read(STDIN_FILENO, num + i, 1); 635 | if (rc <= 0) { 636 | _exit(rc); 637 | } 638 | if (num[i] == '\n') { 639 | num[i] = '\0'; 640 | break; 641 | } 642 | } 643 | return num[0] == '0' && (num[1] == 'x' || num[1] == 'X') 644 | ? parse_base16(num + 2) 645 | : parse_base10(num); 646 | } 647 | 648 | static bool is_mmapped(void const *const chunk) { 649 | return (*(uint64_t *)chunk) & 2; 650 | } 651 | 652 | static uint64_t arena_lookup(struct malloc_state const *const arena) { 653 | struct malloc_state const *curr = the_main_arena; 654 | int64_t arena_idx = 0; 655 | do { 656 | if (curr == arena) { 657 | return arena_idx; 658 | } 659 | curr = curr->next; 660 | arena_idx++; 661 | } while (curr != the_main_arena); 662 | _exit(EXIT_FAILURE); 663 | } 664 | 665 | static pid_t get_next_tid(void) { 666 | pid_t const my_tid = syscall(SYS_gettid); 667 | uint8_t dirents[4096]; 668 | int const procfs_fd = open("/proc/self/task", O_DIRECTORY); 669 | uint64_t const bytes_read = 670 | syscall(SYS_getdents64, procfs_fd, dirents, sizeof(dirents)); 671 | if (bytes_read == 0) { 672 | _exit(EXIT_FAILURE); 673 | } 674 | 675 | uint64_t offset = 0; 676 | bool found_a_thread = false; 677 | uint64_t first_valid_offset = 0; 678 | while (offset < bytes_read) { 679 | uint16_t const d_reclen = *( 680 | uint16_t *)(dirents + offset + sizeof(uint64_t) + sizeof(uint64_t)); 681 | uint8_t *const filename = dirents + offset + sizeof(uint64_t) + 682 | sizeof(uint64_t) + sizeof(uint16_t) + 683 | sizeof(uint8_t); 684 | int32_t received_tid = 0; 685 | if (strcmp((char *)filename, ".") != 0 && 686 | strcmp((char *)filename, "..") != 0) { 687 | received_tid = atoi32(filename); 688 | if (!found_a_thread) { 689 | first_valid_offset = offset; 690 | found_a_thread = true; 691 | } 692 | } 693 | offset += d_reclen; 694 | if (received_tid == my_tid) { 695 | break; 696 | } 697 | } 698 | if (offset > bytes_read) { 699 | _exit(EXIT_FAILURE); 700 | } 701 | if (offset == bytes_read) { // Wrap back around to the beginning 702 | offset = first_valid_offset; 703 | } 704 | offset += sizeof(uint64_t) + sizeof(uint64_t) + sizeof(uint16_t) + 705 | sizeof(uint8_t); 706 | pid_t result = atoi32(dirents + offset); 707 | close(procfs_fd); 708 | return result; 709 | } 710 | 711 | static void *get_fs_base(void) { 712 | void *const fs_base; 713 | syscall(SYS_arch_prctl, ARCH_GET_FS, &fs_base); 714 | return fs_base; 715 | } 716 | 717 | static int const TRIGGER_SIGNAL = SIGINT; 718 | 719 | void explore_heap(void) { 720 | static char const PS1[] = "> "; 721 | static char const PS2[] = ">> "; 722 | 723 | println("\nWelcome to Heap Explorer!"); 724 | 725 | // This was determined by experiment in GDB. 726 | // No idea about how portable this is. 727 | struct malloc_state const *const my_arena = 728 | *(struct malloc_state const **)((uint8_t *)get_fs_base() - 0x30); 729 | 730 | struct malloc_state const *arena = my_arena; 731 | if (arena == NULL) { 732 | arena = the_main_arena; 733 | } 734 | 735 | while (true) { 736 | print("You are TID "); 737 | print(itoa(syscall(SYS_gettid))); 738 | print(", viewing arena "); 739 | uint64_t arena_idx = arena_lookup(arena); 740 | print(itoa(arena_idx)); 741 | if (arena_idx == 0) { 742 | print(" (main_arena)"); 743 | } 744 | if (arena == my_arena) { 745 | print(" (this thread's arena)"); 746 | } 747 | println(""); 748 | 749 | static char const *OPTIONS[] = { 750 | "Allocate chunk(s).", 751 | "Free a chunk.", 752 | "Reallocate a chunk.", 753 | "Print this arena.", 754 | "Print a tcache list.", 755 | "Print a fastbin list.", 756 | "Print a bin list.", 757 | "Switch to next arena.", 758 | "Switch to next thread.", 759 | "Exit Heap Explorer." 760 | }; 761 | for (size_t i = 0; i < ARRAY_LEN(OPTIONS); i++) { 762 | print(BLUE); 763 | print(itoa(i + 1)); 764 | print(". "); 765 | print(CLEAR_COLOR); 766 | println(OPTIONS[i]); 767 | } 768 | print(PS1); 769 | switch (get_number()) { 770 | case 0: { 771 | println("Command not recognized."); 772 | break; 773 | } 774 | case 1: { 775 | println("How many?"); 776 | print(PS2); 777 | uint64_t count = get_number(); 778 | println("How big?"); 779 | print(PS2); 780 | uint64_t size = get_number(); 781 | for (uint64_t i = 0; i < count; i++) { 782 | void const *const chunk = data2chunk(malloc(size)); 783 | if (is_mmapped(chunk)) { 784 | print("-> (mmapped)"); 785 | } else { 786 | struct lookup_result const lookup_result = 787 | chunk_lookup(chunk); 788 | if (lookup_failed(lookup_result)) { 789 | println("Couldn't find the chunk we requested. " 790 | "Possibly, the allocation failed."); 791 | _exit(EXIT_FAILURE); 792 | } 793 | print("-> [arena "); 794 | print(itoa(lookup_result.arena)); 795 | print(", chunk "); 796 | print(itoa(lookup_result.idx)); 797 | println("]"); 798 | } 799 | } 800 | break; 801 | } 802 | case 2: { 803 | println("Free which chunk?"); 804 | print(PS2); 805 | uint64_t const chunk_idx = get_number(); 806 | void *const chunk = get_chunk_by_index(arena, chunk_idx); 807 | if (chunk != NULL) { 808 | free_chunk(chunk); 809 | } 810 | break; 811 | } 812 | case 3: { 813 | println("Reallocate which chunk?"); 814 | print(PS2); 815 | uint64_t const chunk_idx = get_number(); 816 | void *const chunk = get_chunk_by_index(arena, chunk_idx); 817 | if (chunk == NULL) { 818 | break; 819 | } 820 | println("How big?"); 821 | print(PS2); 822 | uint64_t size = get_number(); 823 | void *const result = realloc(chunk2data(chunk), size); 824 | void const *const result_chunk = data2chunk(result); 825 | if (is_mmapped(result_chunk)) { 826 | print("-> (mmapped)"); 827 | } else { 828 | struct lookup_result const lookup_result = chunk_lookup(result_chunk); 829 | if (lookup_failed(lookup_result)) { 830 | println("Couldn't find the chunk we requested. " 831 | "Possibly, the reallocation failed."); 832 | _exit(EXIT_FAILURE); 833 | } 834 | print("-> [arena "); 835 | print(itoa(lookup_result.arena)); 836 | print(", chunk "); 837 | print(itoa(lookup_result.idx)); 838 | println("]"); 839 | } 840 | break; 841 | } 842 | case 4: { 843 | print_arena(arena); 844 | break; 845 | } 846 | case 5: { 847 | println("Print which tcache list?"); 848 | print(PS2); 849 | print_tcache_list(arena, get_number()); 850 | break; 851 | } 852 | case 6: { 853 | println("Print which fastbin list?"); 854 | print(PS2); 855 | print_fastbin_list(arena, get_number()); 856 | break; 857 | } 858 | case 7: { 859 | println("Print which bin list?"); 860 | print(PS2); 861 | print_bin_list(arena, get_number()); 862 | break; 863 | } 864 | case 8: { 865 | arena = arena->next; 866 | break; 867 | } 868 | case 9: { 869 | pid_t const next_tid = get_next_tid(); 870 | syscall(SYS_tkill, next_tid, TRIGGER_SIGNAL); 871 | return; 872 | } 873 | case 10: { 874 | println("Bye!"); 875 | return; 876 | } 877 | default: { 878 | println("Unrecognized command."); 879 | break; 880 | } 881 | } 882 | println(""); 883 | } 884 | } 885 | 886 | static void explore_heap_sighandler(int) { 887 | explore_heap(); 888 | } 889 | 890 | static void __attribute__((constructor)) install_signal_handler(void) { 891 | struct sigaction sa; 892 | sa.sa_handler = explore_heap_sighandler; 893 | sigemptyset(&sa.sa_mask); 894 | sa.sa_flags = SA_RESTART; 895 | if (sigaction(TRIGGER_SIGNAL, &sa, NULL) == -1) { 896 | println("libheap_explorer: Couldn't install signal handler!"); 897 | _exit(EXIT_FAILURE); 898 | } 899 | } 900 | -------------------------------------------------------------------------------- /heap_explorer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void explore_heap(void); 4 | -------------------------------------------------------------------------------- /playground.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "heap_explorer.h" 10 | 11 | void *wait_forever(void *) { 12 | while (1) { 13 | sleep(UINT_MAX); 14 | } 15 | return NULL; 16 | } 17 | 18 | int main(int argc, char **argv) { 19 | int const threads_to_spawn = argc >= 2 ? atoi(argv[1]) : 0; 20 | 21 | for (int i = 0; i < threads_to_spawn; i++) { 22 | pthread_t thread; 23 | pthread_create(&thread, NULL, wait_forever, NULL); 24 | } 25 | 26 | kill(getpid(), SIGINT); 27 | 28 | wait_forever(NULL); 29 | } 30 | --------------------------------------------------------------------------------