Introduction to Tcache Poisoning
Thilan Dissanayaka Exploit Development Oct 29, 2026

Introduction to Tcache Poisoning

This going to be our first tutorial on heap exploitation. We have studied the heap internals in depth now. We need to move into the actual exploitation stuff.

First of all lets setup the environment. In this tutorial we are going to see the TCache posinoning based attacks. For this we need to find a LibC version where Tcache is enabled. I'm using libc 2.27 to demonstrate this. This is the first version which TCache is introduced. In this version there are less number of protections. Which makes it easier to study.

apt update
apt install -y gcc gdb python3 python3-pip git vim
pip3 install pwntools

git clone https://github.com/pwndbg/pwndbg ~/pwndbg
cd ~/pwndbg && ./setup.sh

The pwandbg is an extension to GDB. Of course we can do tis without the plain GDB. Just like we did in stack overflow tutorials. But here I'm going to introduce pawndbg.

Confirm you got the right libc:

thilan@ubuntu:~$ ldd --version | head -1
ldd (Ubuntu GLIBC 2.27-3ubuntu1.5) 2.27

In over stack buffer overflow tutorials, we turned off ASLR in the first most tutorial. You already know that Address Layout Randomization is a protection layer for exploitations. Since we are going to learn, we first disable it. In a later part we will enable it and study bypasses.

thilan@ubuntu:~$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
0

thilan@ubuntu:~$ cat /proc/sys/kernel/randomize_va_space
0

Tcache in action

Lets write a simple C Program to see the actual behavior of the Tcache.

#include<stdio.h>
#include<stdlib.h>

int main(){
    char* a = malloc(10);
    printf("malloc returned addr for a: %p\n", a);
    free(a);

    char* b = malloc(10);
    printf("malloc returned addr for b: %p\n", b);
    free(b);

    return 0;
}

What does it do? First it allocates a ten bytes memory. Then that memory is freed and another ten byte memory is allocated.

If we forget bins and Tcache, we might get some random memory addresses. However here the TCache layer is active.

Theoretically the Chunk a goes to the Tcache once it is free. Then we should get same chunk when we request memory for chunk b.

I use gcc to compile it.

thilan@ubuntu:~$ gcc simple_tcache.c -o simple_tcache -m32 -no-pie -fno-stack-protector -g

You can see that I'm using a several flags with gcc.

  • -g enable debugging symbols. This allow us to see the source code in GDB.

  • -no-pie fixes the binary at 0x400000, which puts the heap at a readable 0x602000 instead of a PIE address, and makes global symbols constant.

  • -fno-stack-protector is not strictly needed here as we never touch the stack. But it keeps the disassembly clean.

  • -m32 is used to compile the binary in 32 bit mode. I prefer short memory addresses for the tutorial. That's the whole reason.

Notice what is not in that list. No -z execstack. We are never executing heap data, so NX is irrelevant and stays on.

Lets run it and see what happens.

thilan@ubuntu:~$ ./simple_tcache
malloc returned addr for a: 0x804b160
malloc returned addr for b: 0x804b160

Hmm.. Same pointer. free() pushed the chunk onto a tcache bin, and the next malloc() of the same size popped it straight back off. No randomness, no delay, no quarantine.

Now we are going to analyze it in low level. Lets load it in to GDB.

thilan@ubuntu:~$ gdb -q ./simple_tcache

pwndbg> set disassembly-flavor intel
pwndbg> disass main
Dump of assembler code for function main:
   0x08048486 <+0>: lea    ecx,[esp+0x4]
   0x0804848a <+4>: and    esp,0xfffffff0
   0x0804848d <+7>: push   DWORD PTR [ecx-0x4]
   0x08048490 <+10>:    push   ebp
   0x08048491 <+11>:    mov    ebp,esp
   0x08048493 <+13>:    push   ebx
   0x08048494 <+14>:    push   ecx
   0x08048495 <+15>:    sub    esp,0x10
   0x08048498 <+18>:    call   0x80483c0 <__x86.get_pc_thunk.bx>
   0x0804849d <+23>:    add    ebx,0x1b63
   0x080484a3 <+29>:    sub    esp,0xc
   0x080484a6 <+32>:    push   0xa
   0x080484a8 <+34>:    call   0x8048340 <malloc@plt>
   0x080484ad <+39>:    add    esp,0x10
   0x080484b0 <+42>:    mov    DWORD PTR [ebp-0xc],eax
   0x080484b3 <+45>:    sub    esp,0x8
   0x080484b6 <+48>:    push   DWORD PTR [ebp-0xc]
   0x080484b9 <+51>:    lea    eax,[ebx-0x1a60]
   0x080484bf <+57>:    push   eax
   0x080484c0 <+58>:    call   0x8048320 <printf@plt>
   0x080484c5 <+63>:    add    esp,0x10
   0x080484c8 <+66>:    sub    esp,0xc
   0x080484cb <+69>:    push   DWORD PTR [ebp-0xc]
   0x080484ce <+72>:    call   0x8048330 <free@plt>
   0x080484d3 <+77>:    add    esp,0x10
   0x080484d6 <+80>:    sub    esp,0xc
   0x080484d9 <+83>:    push   0xa
   0x080484db <+85>:    call   0x8048340 <malloc@plt>
   0x080484e0 <+90>:    add    esp,0x10
   0x080484e3 <+93>:    mov    DWORD PTR [ebp-0x10],eax
   0x080484e6 <+96>:    sub    esp,0x8
   0x080484e9 <+99>:    push   DWORD PTR [ebp-0x10]
   0x080484ec <+102>:   lea    eax,[ebx-0x1a40]
   0x080484f2 <+108>:   push   eax
   0x080484f3 <+109>:   call   0x8048320 <printf@plt>
   0x080484f8 <+114>:   add    esp,0x10
   0x080484fb <+117>:   sub    esp,0xc
   0x080484fe <+120>:   push   DWORD PTR [ebp-0x10]
   0x08048501 <+123>:   call   0x8048330 <free@plt>
   0x08048506 <+128>:   add    esp,0x10
   0x08048509 <+131>:   mov    eax,0x0
   0x0804850e <+136>:   lea    esp,[ebp-0x8]
   0x08048511 <+139>:   pop    ecx
   0x08048512 <+140>:   pop    ebx
   0x08048513 <+141>:   pop    ebp
   0x08048514 <+142>:   lea    esp,[ecx-0x4]
   0x08048517 <+145>:   ret
End of assembler dump.

I know. It is bit lengthy when I put the whole disassemble here. But I want to keep the whole context in this article. I don't care about SEO. :p

Lets set breakpoints.

pwndbg> b *main+72
Breakpoint 1 at 0x80484ce: file simple_tcache.c, line 9.

pwndbg> b *main+77
Breakpoint 2 at 0x80484d3: file simple_tcache.c, line 9.

pwndbg> b *main+90
Breakpoint 3 at 0x80484e0: file simple_tcache.c, line 11.

The first BP is after the malloc(a). The second one is just after free(a). The third BP is after malloc(b).

We run the program and it will hit at first BP.

pwndbg> r
Starting program: /home/thilan/simple_tcache
malloc returned addr for a: 0x804b160

Breakpoint 1, 0x080484ce in main () at simple_tcache.c:7

Since we are using pawndbg, we have some nice command set to examine memory.

pwndbg> tcache

tcache is pointing to: 0x804b010 for thread 1
{
  counts = '\000' <repeats 63 times>,
  entries = {0x0 <repeats 64 times>},
}

We used the tcache command to examine the Tcache area. As we expected it is empty. Because there is nothing freed yet.

There is another useful command called vis which help us to visualize the heap layout.

pwndbg> vis

0x804b008   0x00000000  0x00000151  ....Q...
0x804b010   0x00000000  0x00000000  ........
0x804b018   0x00000000  0x00000000  ........
0x804b020   0x00000000  0x00000000  ........
0x804b028   0x00000000  0x00000000  ........
0x804b030   0x00000000  0x00000000  ........
0x804b038   0x00000000  0x00000000  ........
0x804b040   0x00000000  0x00000000  ........
0x804b048   0x00000000  0x00000000  ........
0x804b050   0x00000000  0x00000000  ........
0x804b058   0x00000000  0x00000000  ........
.
.
.
0x804b150   0x00000000  0x00000000  ........
0x804b158   0x00000000  0x00000011  ........
0x804b160   0x00000000  0x00000000  ........
0x804b168   0x00000000  0x00000411  ........
0x804b170   0x6c6c616d  0x7220636f  malloc r
0x804b178   0x72757465  0x2064656e  eturned
0x804b180   0x72646461  0x726f6620  addr for
0x804b188   0x203a6120  0x30387830   a: 0x80
0x804b190   0x36316234  0x00000a30  4b160...
0x804b198   0x00000000  0x00000000  ........
0x804b1a0   0x00000000  0x00000000  ........
.
.
.
0x804b570   0x00000000  0x00000000  ........
0x804b578   0x00000000  0x00021a89  ........     <-- Top chunk

As we can see the heap is starts from 0x804b008 and goes until 0x804b578.

To get the allocated chunk information, we have another command called heap. Lets see the available chunks at the moment.

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x804b008
Size: 0x151

Allocated chunk | PREV_INUSE
Addr: 0x804b158
Size: 0x11

Allocated chunk | PREV_INUSE
Addr: 0x804b168
Size: 0x411

Top chunk | PREV_INUSE
Addr: 0x804b578
Size: 0x21a89

Hmm.. Interesting.

There are four chunks. But we only allocated the chunk a at the moment. Lets deep in to the basics and figure out how this happens.

As we studied in the last tutorial, the Tcache is itself a chunk. It s the chunk we can see at the starting of the heap. Starts from 0x804b008 and goes till 0x804b158.

We can see its size flag is 0x00000151. We already know how to get the actual size from this. So I'm not going to to the math again here. The chunk size is 150 Bytes.

This space is used to store the Tcache related information. That means the counts and entries.

  • counts[64] occupies 0x804b010 to 0x804b04f
  • entries[64] occupies 0x804b050 to 0x804b14f

The next chunk is at 0x804b158. It's size flag is 0x00000011. It is the allocated memory for the chunk a.

There is another chunk with size flag 0x00000411 at 0x804b168. This is created by printf() function. That space is used to store the data belongs to that.

At last, the top chunk is there starting from 0x804b578.

Lets summarize these details.

Chunk addr Size field User data What it is
0x804b008 0x151 0x804b010 tcache_perthread_struct
0x804b158 0x11 0x804b160 Chunk a
0x804b168 0x411 0x804b170 stdout buffer from printf
0x804b578 0x21a89 top chunk

Each chunk address plus its size gives the next one: 0x804b008 + 0x150 = 0x804b158, 0x804b158 + 0x10 = 0x804b168, 0x804b168 + 0x410 = 0x804b578. The top chunk then runs to 0x806d000, the end of the initial 132 KB heap.

An additional point to be noted.

The heap starts at 0x804b000, but the first chunk is at 0x804b008. On i386 the chunk header is 8 bytes, and user data must be 16-byte aligned, so malloc shifts the first chunk by 8. That’s why every chunk address ends in 8 and every user pointer ends in 0.

Lets continue the execution till it hits the next BP which is just after calling free(a).

pwndbg> c
Continuing.

Breakpoint 2, 0x080484d3 in main () at simple_tcache.c:9
9       free(a);

pwndbg> tcache
tcache is pointing to: 0x804b010 for thread 1
{
  counts = "\001", '\000' <repeats 62 times>,
  entries = {0x804b160, 0x0 <repeats 63 times>},
}

As you can see the Tcache counts and entries got changed. Our chunk is 0x10 in size. Lets see quickly the breakdown.

We requested 10 bytes of user data. Malloc gave a chunk sized 0x10 which is 16 Bytes. However 8 bytes are allocated to meta data. As we already know we can use the remaining 8 bytes + some of next chunks meta data space to get the required 10 Bytes. If you forgot this just refer the heap internals article. There we discussed this concept deeply.

Anyhow we have the chunk with size 0x10. We know that chunks in the size range 1–12 bytes are fitting into the Tcache bin 0. In above we can see the counts and entries got filled.

  • counts[0] = 1
  • entries[0] = 0x804b160

If we say it more clearly, the counts array's first item hold the count of chunks in the Tcache bin 0. Here it is one. Also the entries Array's first element is a memory address. It is actually a pointer to the chunk a which is freed. In above case that is 0x804b160.

I think now you got the idea how it works actually. For a moment think what happen if we free two chunks which the size is 0x10? We will see this in a later part of this article.

Lets use the vis command to visually see the heap.

pwndbg> vis

0x804b008   0x00000000  0x00000151  ....Q...
0x804b010   0x00000001  0x00000000  ........
0x804b018   0x00000000  0x00000000  ........
0x804b020   0x00000000  0x00000000  ........
0x804b028   0x00000000  0x00000000  ........
0x804b030   0x00000000  0x00000000  ........
0x804b038   0x00000000  0x00000000  ........
0x804b040   0x00000000  0x00000000  ........
0x804b048   0x00000000  0x00000000  ........
0x804b050   0x0804b160  0x00000000  `.......
0x804b058   0x00000000  0x00000000  ........
.
.
.
0x804b150   0x00000000  0x00000000  ........
0x804b158   0x00000000  0x00000011  ........
0x804b160   0x00000000  0x0804b010  ........     <-- tcachebins[0x10][0/1]
0x804b168   0x00000000  0x00000411  ........
0x804b170   0x6c6c616d  0x7220636f  malloc r
0x804b178   0x72757465  0x2064656e  eturned
0x804b180   0x72646461  0x726f6620  addr for
0x804b188   0x203a6120  0x30387830   a: 0x80
0x804b190   0x36316234  0x00000a30  4b160...
0x804b198   0x00000000  0x00000000  ........
.
.
.
0x804b570   0x00000000  0x00000000  ........
0x804b578   0x00000000  0x00021a89  ........     <-- Top chunk

Nice! This visualization gives us a good opportunity to understand how tcache is represented in the actual heap.

The tcache structure starts at 0x804b010.

The address 0x804b010 contains the value 0x00000001. That is counts[0] meaning bin 0 contains one chunk.

Note that The address 0x804b050 contains the value 0x0804b160 This is the pointer to the first chunk in bin 0.

The freed chunk is at 0x0804b160. Let's break it down. The general chunk layout is something like following:

<layout scale="linear">
<label pos="top-left">Low memory</label>
<label pos="top-right">High memory</label>

<block size=4>...</block>
<block size=4>Prev size</block>
<block size=4>Size + Flags</block>
<block size=16>User Data</block>
<block size=4>...</block>

<ptr index=1>ptr - 8</ptr>
<ptr index=2>ptr - 4</ptr>
<ptr index=3>malloc() pointer</ptr>
<layout scale="linear">
<block size=4>...</block>
<block size=4>0x0</block>
<block size=4>0x11</block>

<block size=4>0x0</block>
<block size=4>0x0804b010</block>
<block size=8></block>
<block size=4>...</block>

<ptr index=1>0x804b158</ptr>
<ptr index=2>0x804b15c</ptr>
<ptr index=3>0x804b160</ptr>
<ptr index=4>0x804b164</ptr>

The former user-data area, now holding tcache metadata.

A few important observations:

  • 0x11 means chunk size 0x10 plus the PREV_INUSE flag (1).
  • 0x804b160 is the user pointer.
  • The first word of the freed chunk stores the next pointer. (In this case it is NULL)
  • The second word stores the tcache key. In glibc 2.27, the key is a pointer to the thread's tcache structure.

Just to confirm above lets use the heap command.

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x804b008
Size: 0x151

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804b158
Size: 0x11
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x804b168
Size: 0x411

Top chunk | PREV_INUSE
Addr: 0x804b578
Size: 0x21a89

Now we have observed the Tcache behavior. We can continue until the next BP and see what happens when we call malloc(10) again.

pwndbg> tcache

tcache is pointing to: 0x804b010 for thread 1
{
  counts = '\000' <repeats 63 times>,
  entries = {0x0 <repeats 64 times>},
}

We can see now tcache is empty. You may use vis and heap commands too to see what happened in the memory level. Here I'm not going to list out that.

Tcache Exploitation

Now we are moving to the next part. Actual exploitation.

#include <stdio.h>
#include <stdlib.h>

int target[4];

int main(){
    char *a = malloc(10);
    char *b = malloc(10);

    free(a);
    free(b);

    *(void **)b = target;

    char *c = malloc(10);
    char *d = malloc(10);

    printf("target: %p\n", (void *)target);
    printf("d:      %p\n", (void *)d);

    return 0;
}

Its clear what this code does except *(void **)a = target;. Let me explain that part later. First of all lets compile and run the program.

thilan@ubuntu:~$ gcc pois_tcache.c -o pois_tcache -m32 -no-pie -fno-stack-protector -g
thilan@ubuntu:~$ ./pois_tcache
target: 0x804a028
y:      0x804a028

We can see target's memory address is given to the chunk d.

The following is the virtual memory layout.

<label pos="top-left">High memory</label>
<label pos="top-right">Low memory</label>

<block size=4>...</block>
<block size=4>Kernel Space</block>
<block size=4>Stack -></block>
<block size=4>Memory Mapping</block>
<block size=4><- Heap</block>
<block size=4>Bss</block>
<block size=4>Data</block>
<block size=4>Txt</block>
<block size=4>...</block>

We selected a memory block in .bss segment as the target. We could assign the memory address of this .bss code block into another malloc() returned pointer. Now have an arbitrary-address allocation primitive, and anything we write through d lands in target. How this is possible?

To understand the exploit, lets go though gdb and see the program at low level.

thilan@ubuntu:~$ gdb -q ./pois_tcache

pwndbg> disass main
Dump of assembler code for function main:
   0x08048486 <+0>: lea    ecx,[esp+0x4]
   0x0804848a <+4>: and    esp,0xfffffff0
   0x0804848d <+7>: push   DWORD PTR [ecx-0x4]
   0x08048490 <+10>:    push   ebp
   0x08048491 <+11>:    mov    ebp,esp
   0x08048493 <+13>:    push   ebx
   0x08048494 <+14>:    push   ecx
   0x08048495 <+15>:    sub    esp,0x10
   0x08048498 <+18>:    call   0x80483c0 <__x86.get_pc_thunk.bx>
   0x0804849d <+23>:    add    ebx,0x1b63
   0x080484a3 <+29>:    sub    esp,0xc
   0x080484a6 <+32>:    push   0xa
   0x080484a8 <+34>:    call   0x8048340 <malloc@plt>
   0x080484ad <+39>:    add    esp,0x10
   0x080484b0 <+42>:    mov    DWORD PTR [ebp-0xc],eax
   0x080484b3 <+45>:    sub    esp,0xc
   0x080484b6 <+48>:    push   0xa
   0x080484b8 <+50>:    call   0x8048340 <malloc@plt>
   0x080484bd <+55>:    add    esp,0x10
   0x080484c0 <+58>:    mov    DWORD PTR [ebp-0x10],eax
   0x080484c3 <+61>:    sub    esp,0xc
   0x080484c6 <+64>:    push   DWORD PTR [ebp-0xc]
   0x080484c9 <+67>:    call   0x8048330 <free@plt>
   0x080484ce <+72>:    add    esp,0x10
   0x080484d1 <+75>:    sub    esp,0xc
   0x080484d4 <+78>:    push   DWORD PTR [ebp-0x10]
   0x080484d7 <+81>:    call   0x8048330 <free@plt>
   0x080484dc <+86>:    add    esp,0x10
   0x080484df <+89>:    mov    eax,DWORD PTR [ebp-0x10]
   0x080484e2 <+92>:    mov    edx,0x804a028
   0x080484e8 <+98>:    mov    DWORD PTR [eax],edx
   0x080484ea <+100>:   sub    esp,0xc
   0x080484ed <+103>:   push   0xa
   0x080484ef <+105>:   call   0x8048340 <malloc@plt>
   0x080484f4 <+110>:   add    esp,0x10
   0x080484f7 <+113>:   mov    DWORD PTR [ebp-0x14],eax
   0x080484fa <+116>:   sub    esp,0xc
   0x080484fd <+119>:   push   0xa
   0x080484ff <+121>:   call   0x8048340 <malloc@plt>
   0x08048504 <+126>:   add    esp,0x10
   0x08048507 <+129>:   mov    DWORD PTR [ebp-0x18],eax
   0x0804850a <+132>:   sub    esp,0x8
   0x0804850d <+135>:   mov    eax,0x804a028
   0x08048513 <+141>:   push   eax
   0x08048514 <+142>:   lea    eax,[ebx-0x1a30]
   0x0804851a <+148>:   push   eax
   0x0804851b <+149>:   call   0x8048320 <printf@plt>
   0x08048520 <+154>:   add    esp,0x10
   0x08048523 <+157>:   sub    esp,0x8
   0x08048526 <+160>:   push   DWORD PTR [ebp-0x18]
   0x08048529 <+163>:   lea    eax,[ebx-0x1a24]
   0x0804852f <+169>:   push   eax
   0x08048530 <+170>:   call   0x8048320 <printf@plt>
   0x08048535 <+175>:   add    esp,0x10
   0x08048538 <+178>:   mov    eax,0x0
   0x0804853d <+183>:   lea    esp,[ebp-0x8]
   0x08048540 <+186>:   pop    ecx
   0x08048541 <+187>:   pop    ebx
   0x08048542 <+188>:   pop    ebp
   0x08048543 <+189>:   lea    esp,[ecx-0x4]
   0x08048546 <+192>:   ret
End of assembler dump.

As usual, I did not remove any Assembly instruction from the output. Incase you are following the tutorial itself, you can clearly see each instruction.

Lets set the break points.

pwndbg> b *main+64                <- After two malloc()s
Breakpoint 1 at 0x80484c6: ...

pwndbg> b *main+72                <- After free(a)
Breakpoint 2 at 0x80484ce: ...

pwndbg> b *main+86                <- After free(b)
Breakpoint 3 at 0x80484dc: ...

pwndbg> b *main+100               <- After *(void **)a = target;
Breakpoint 4 at 0x80484ea: ...

pwndbg> b *main+110               <- After malloc(c)
Breakpoint 5 at 0x80484f4: ...

pwndbg> b *main+126               <- After malloc(d)
Breakpoint 6 at 0x8048504: ...

Lets see the heap layout just after allocating a and b. I'm only showing the necessary part of the vis command output.

pwndbg> vis
...
0x804b158   0x00000000  0x00000011  ........
0x804b160   0x00000000  0x00000000  ........
0x804b168   0x00000000  0x00000011  ........
0x804b170   0x00000000  0x00000000  ........
0x804b178   0x00000000  0x00021e89  ........     <-- Top chunk

Now we know the starting addresses of chunk a and chunk b. The following is the memory layout of those two.

<label pos="top-left">Low memory</label>
<label pos="top-right">High memory</label>

<block size=1>...</block>
<block size=4>Prev size</block>
<block size=4>Size + Flags</block>
<block size=24>User Data</block>

<block size=4>Prev size</block>
<block size=4>Size + Flags</block>
<block size=24>User Data</block>
<block size=12>Top Chunk</block>
<block size=1>...</block>

<ptr index=1>Chunk a starts here (0x804b158)</ptr>
<ptr index=4>Chunk a ends & Chunk b starts here (0x804b168)</ptr>
<ptr index=7>Chunk b ends here (0x804b178)</ptr>

Now continue the execution till it hits the second breakpoint. At this point we have called free(a).

pwndbg> tcache
tcache is pointing to: 0x804b010 for thread 1
{
  counts = "\001", '\000' <repeats 62 times>,
  entries = {0x804b160, 0x0 <repeats 63 times>},
}

This is something we know. In the first simple_tcache example, we saw this behavior.

We need to get the vis output and compare with pervious one.

pwndbg> vis
...
0x804b158   0x00000000  0x00000011  ........
0x804b160   0x00000000  0x0804b010  ........     <-- tcachebins[0x10][0/1]
0x804b168   0x00000000  0x00000011  ........
0x804b170   0x00000000  0x00000000  ........
0x804b178   0x00000000  0x00021e89  ........     <-- Top chunk

As we expected the chunk a is moved to tcache. We can see the tcache next pointer and key is in the chunk a's user data section.

Also note that chunk b is the adjacent chunk of b. But we have not change anything in there. The prev_size of chunk b is not still changed.

<label pos="top-left">Low memory</label>
<label pos="top-right">High memory</label>

<block size=1>...</block>
<block size=4>Prev size</block>
<block size=4>Size + Flags</block>
<block size=24>User Data</block>

<block size=4>Prev size</block>
<block size=4>Size + Flags</block>
<block size=24>User Data</block>
<block size=12>Top Chunk</block>
<block size=1>...</block>

<ptr index=1>Chunk a starts here (0x804b158)</ptr>
<ptr index=4>Chunk a ends & Chunk b starts here (0x804b168)</ptr>
<ptr index=7>Chunk b ends here (0x804b178)</ptr>

Lets hit the third breakpoint. At this point we have called free(b).

pwndbg> tcache
tcache is pointing to: 0x804b010 for thread 1
{
  counts = "\002", '\000' <repeats 62 times>,
  entries = {0x804b170, 0x0 <repeats 63 times>},
}
pwndbg> vis
...
0x804b158   0x00000000  0x00000011  ........
0x804b160   0x00000000  0x0804b010  ........     <-- tcachebins[0x10][1/2]
0x804b168   0x00000000  0x00000011  ........
0x804b170   0x0804b160  0x0804b010  `.......     <-- tcachebins[0x10][0/2]
0x804b178   0x00000000  0x00021e89  ........     <-- Top chunk
<label pos="top-left">Low memory</label>
<label pos="top-right">High memory</label>

<block size=1>...</block>
<block size=4>Prev size</block>
<block size=4>Size + Flags</block>
<block size=24>User Data</block>

<block size=4>Prev size</block>
<block size=4>Size + Flags</block>
<block size=24>User Data</block>
<block size=12>Top Chunk</block>
<block size=1>...</block>

<ptr index=1>Chunk a starts here (0x804b158)</ptr>
<ptr index=4>Chunk a ends & Chunk b starts here (0x804b168)</ptr>
<ptr index=7>Chunk b ends here (0x804b178)</ptr>

next BP. After *(void **)a = target;

pwndbg> tcache
tcache is pointing to: 0x804b010 for thread 1
{
  counts = "\002", '\000' <repeats 62 times>,
  entries = {0x804b170, 0x0 <repeats 63 times>},
}
0x804b158   0x00000000  0x00000011  ........
0x804b160   0x00000000  0x0804b010  ........
0x804b168   0x00000000  0x00000011  ........
0x804b170   0x0804a028  0x0804b010  (.......     <-- tcachebins[0x10][0/2]
0x804b178   0x00000000  0x00021e89  ........     <-- Top chunk

next BP. After malloc(c);

pwndbg> tcache
tcache is pointing to: 0x804b010 for thread 1
{
  counts = "\001", '\000' <repeats 62 times>,
  entries = {0x804a028 <target>, 0x0 <repeats 63 times>},
}
0x804b158   0x00000000  0x00000011  ........
0x804b160   0x00000000  0x0804b010  ........
0x804b168   0x00000000  0x00000011  ........
0x804b170   0x0804a028  0x00000000  (.......
0x804b178   0x00000000  0x00021e89  ........     <-- Top chunk

next BP. After malloc(d);

pwndbg> tcache
tcache is pointing to: 0x804b010 for thread 1
{
  counts = '\000' <repeats 63 times>,
  entries = {0x0 <repeats 64 times>},
}
0x804b158   0x00000000  0x00000011  ........
0x804b160   0x00000000  0x0804b010  ........
0x804b168   0x00000000  0x00000011  ........
0x804b170   0x0804a028  0x00000000  (.......
0x804b178   0x00000000  0x00021e89  ........     <-- Top chunk

Two details in this result are worth noting:

tcache_get never validated the target. There’s no chunk at 0x804a028, no size field, nothing heap-like. Tcache simply followed the pointer. Fastbins, by contrast, check that the fake chunk’s size field matches the bin, so fastbin attacks need you to find or forge a plausible size first. That’s why tcache poisoning became the go-to technique after 2.26.

The returned address isn’t 16-byte aligned. 0x804a028 ends in 8, but every legitimate chunk on your system returns addresses ending in 0. malloc handed it over anyway, which confirms there’s no alignment check (that arrived in 2.32 alongside safe-linking). On newer glibc this exact program would abort with malloc(): unaligned tcache chunk detected.

ALSO READ
Exploiting a Stack Buffer Overflow on Windows
Apr 12 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Cryptography 0x100 - Basic concepts
Mar 01 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

DSA 0x202 - Sorting Algorithms
Sep 01 DSA

Hmm.., It is going to be a lengthy article. Sorting is an important topic in Data structures and Algorithms. ## Why Sorting Matters **It unlocks binary search.** Finding a value in an unsorted...

Common Web Application Attacks
Feb 05 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

DSA 0x102 - Tree Data structure
Aug 30 DSA

Tree is one of most important data structure. We all know that in Linux, the file system is starting from the directory.The root directory contains many sub directories such as etc, var, home......