Introduction
cshell2 is a heap challenge I did during the corCTF 2022 event. It was pretty classic so I will not describe a lot.
If you begin with heap challenges, I advice you to read previous heap writeup.
TL; DR
- Fill tcache.
- Heap overflow in editon thebiofield which allows to leak the address of the unsortedbin.
- Leak heap and defeat safe-linking to get an arbitrary write through tcache poisoning.
- Hiijack GOT entry of freetosystem.
- Call free("/bin/sh").
- PROFIT
Reverse Engineering
Let’s take a look at the provided binary and libc:
| 1 | $ ./libc.so.6 | 
A very recent libc plus a non PIE-based binary without FULL RELRO. Thus we could think to some GOT hiijacking stuff directly on the binary. Let’s take a look at the add function:
| 1 | unsigned __int64 add() | 
It creates a chunk by asking several fields but nothing actually interesting there. Let’s take a look at the show function:
| 1 | unsigned __int64 show() | 
It prints a chunk only if it’s allocated (size entry initialized in the size array) and if the index is right.
Then the delete function:
| 1 | unsigned __int64 delete() | 
Quite common delete handler, it prevents double free.
The vulnerability is in the edit function:
| 1 | unsigned __int64 edit() | 
It reads size_array[2 * idx] - 32LL bytes into a 0x100-sized buffer which leads to a heap overflow.
Exploitation
There is no actual issue, we can allocate whatever chunk bigger than 0x407, the only fancy thing we have to do would be to defeat safe-linking to get an arbitrary write with a tcache poisoning attack on the 0x410 tcache bin. Here is the attack I led against the challenge but that’s not the most optimized.
The plan is to:
- Allocate two 0x408-sized chunks : pivot and victim, in order to easily get later libc leak.
- Allocate 9 more chunks and then fill the 0x410tcachebin with them (with only 7 of them).
- Delete victimand overflow pivot up to the next free pointer ofvictimto get a libc leak.
- Allocate a 0x408-sized chunk to get the8-th chunk (withinchunk_array) which is on the top of the bin.
- Leak the heap same way as for libc, but we have to defeat safe-linking.
- Delete the 9-th chunk to put it in the tcachebin at the first position.
- Then we can simply editchunk8and overflow over chunk9to poison its nextfpto hiijack it toward the GOT entry offree.
- Pop chunk 9from the freelist and then request another the target memory area : the GOT entry offree.
- Write systeminto the GOT entry offree.
- Free whatever chunk for which //bin/shis written at the right begin.
- PROFIT.
To understand the attack process I’ll show the heap state at certain part of the attack.
Libc / heap leak
First we have to fill the tcache. We allocate a chunk right after chunk0 we do not put into the tcache to be able to put it in the unsortedbin to make appear unsortedbin’s address:
| 1 | add(0, 1032, b"//bin/sh\x00", b"", b"", 1337, b"") # pivot | 
Then let’s get a heap leak, we request back from the tcache the 8-th chunk, we free the 9-th chunk that is allocated right after the 8-th to be able to leak its next free pointer same way as for the libc previously. Plus we have to defeat safe-linking. To understand the defeat of safe-linking I advice you to read this. It ends up to the decrypt_pointer function that makes use of known parts of the encrypted fp to decrypt the whole pointer. I didn’t code the function by myself, too lazy for that, code comes from the AeroCTF heap-2022 writeup.
| 1 | def decrypt_pointer(leak: int) -> int: | 
Then here we are, we leaked both libc and heap base addresses. We just have to to tcache poisoning on free.
Tcache poisoning + PROFIT
We overflow the 8-th chunk to overwrite the next freepointer of chunk9 that is stored at the HEAD of the 0x410 tcachebin. Then we got an arbitrary write.
We craft a nice header to be able to request it back from the tcache, and we encrypt the next with the location of the chunk9 to pass safe-linking checks.
Given we hiijack GOT we initialized properly some pointers around to avoid segfaults. We do not get a write into the GOT entry of free cause it is unaliagned and malloc needs 16 bytes aligned next free pointer.
| 1 | edit(11, b"", b"", b"", 1337, b"X"*(1032 - 64) + pwn.p64(0x411) + pwn.p64(((heap + 0x2730) >> 12) ^ (exe.got.free - 0x8))) | 
Here we are:
| 1 | nasm@off:~/Documents/pwn/corCTF/cshell2$ python3 exploit.py REMOTE HOST=be.ax PORT=31667 | 
Appendices
Final exploit:
| 1 | #!/usr/bin/env python | 
