Cancelled
Description:
1879ptsSolvers 26
We should cancel all pwners. by jitterbug
pwnable
2377bb9cec90614f4ba5c4c213a48709
libc-2.27.so
50390b2ae8aaa73c47745040f54e602fnc binary.utctf.live 9050
Solution
- Allocate 4 chunks A[0x18], B[0x18], C[0x70], D[0x21].
- Free chunk A[0x18].
- Allocate a new chunk A[0x18] and use off by one overflow to change size of B to 0x91.
- Free chunk B, this won’t return any errors because we created some fake chunks in C and D.
- B[0x90] is on unsortedbin now.
- Free chunk C.
- Next allocations will reuse space from chunk B if they fit.
- Allocate a new chunk of size 0x10 to put a libc address at the FD of chunk C.
- Malloc(0x20) and do a 4 bit brute force at the libc address present in FD to get stdout.
- Stdout is now present in the tcache[0x80] linked list.
- Second malloc of that size will write into the stdout struct.
- Modify _IO_2_1_stdout to make puts leak a libc address (Angelboy leak).
- Reuse the same technique to modify some tcache linked list pointer into free_hook.
- Write system into free_hook.
- Free a chunk that has /bin/sh\x00 as content to get a shell.
Architecture and protections
The binary is 64-bit and libc is dynamically linked.
1 | $ file pwnable |
Besides fortify everything is enabled:
1 | $ checksec pwnable |
Binary
The binary has two options, in the “add person” option we can specify the index to store the persons name and a description, for the description we can also control its size.
The cancel person option we can remove it from the list by specifying the respective index.
Vulnerability
We have a controllable off by one at the add option:
Angelboy leak
Not sure if this technique was first used by angelboy but the first time I saw it being used was at Hitcon 2018, in a challenge created by himself which he later published his solution at github.
This technique resolves on corrupting the stdout IO_FILE
struct to make puts leak a libc address, I’m not explaining in detail the internals of printf you can find some explanations in my older write up plane market or at babytcache writeup.
To write into the stdout IO_FILE struct we kinda need to do a 4 bit brute-force in an unsorted bin libc address, but to achieve this we need to first use the off by one overflow vulnerability.
The main idea here is to use off by one to increase the size of a chunk in the unsorted bin to get some chunk overlaps via shrinking of the freed chunk and also overlapping new allocated chunks.
We can start by creating 4 chunks (A,B,C,D).
1 | add(0x0, 'A'*8, 0x18, 'A'*0x8) |
The next thing to do is to change chunk B size into 0x91, but the libc version is 2.27 which uses tcache, so any chunk bellow 0x410 will go into their respective tcache bin. To prevent this we can fill tcache[0x90] with 7 frees which is the limit of a tcache bin:
1 | for x in range(7): |
Now that tcache[0x90] is full we have to overflow chunks B size, there isn’t an edit function so we need to free chunk A first and allocate a new one there. The chunk A is now placed at tcache[0x20] if the new allocation is in same range that memory space is reused, and the new chunk will be placed at the same place as the old A. Now that we can control chunks A description we can finally modify chunks B size to 0x91.
1 | free(0) # Insert chunk A into tcache[0x20] |
The chunks created inside C and D are to prevent two security checks “prevent double-free or corruption” and “corrupted vs. prev_size” when freeing chunk B, you can check my write up penpal_world to understand more about this security checks.
Now we want to use tcache[0x90] again, we filled it before by freeing 7 times , to use it again we need to malloc the same numbers:
1 | for x in range(7): |
tcache[0x90] is now reusable again, we can now send chunk C into tcache[0x90] , chunk C is located right after chunk B which size just got increased, because of this it can be used to overlap the fd pointer of chunk C by shrinking chunk B using malloc:
1 | add(0x11, 'A'*8, 0x10, 'A'*0x2) # put a libc address at next pointer from tcache[0x80] |
The view of the chunks before the shrink:
The view after the shrink:
It’s time to update the FD of C into stdout, we can do this by allocating a 0x20 chunk to shrink B again and overlap C:
1 | add(0x12,'B'*8,0x20, '\x60\xa7') # STDOUT, trying a 4bit bruteforce |
Failed attempt to get stdout:
To check if we succeeded to get it we can preform this checks:
1 |
|
Update free_hook to system
To update free_hook we can do a similar strategy we used before to edit stdout, we can start by freeing a chunk after the old chunk B located in the unsorted bin and then allocate it again to create a fake chunk inside of it(to prevent a security check error):
1 | free(0xa+6, True) # free chunk after old chunk B |
Next we allocate the chunk before chunk B and tamper the size to 0xa1:
1 | add(0x0,'B'*8,0x28, 'A'*0x28+'\xa1', True) # change size of chunk B to 0xa1 |
Now that chunk B overlaps the next, we can allocate a chunk that covers the entire freed chunk and edit the FD of the next chunk to free_hook:
1 | add(0x0, 'L'*8, 0x90, b'L'*0x70+p64(0)+p64(0x91)+p64(FREE_HOOK), True) # Overlapping chunk |
Now is a matter of doing two mallocs and change the hook to system and freeing a chunk with “/bin/sh\x00” in its data:
1 | add(0x7, b'/bin/sh\x00', 0x80, b'/bin/sh\x00', True) # prepare the first argument of system |
The full exploit:
1 | from pwn import * |
Running it:
1 | $ python3 cancelled.py REMOTE |
References
- https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/off_by_one/
- https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/chunk_extend_overlapping/
- https://github.com/scwuaptx/CTF/blob/master/2018-writeup/hitcon/baby_tcache.py
- https://vigneshsrao.github.io/babytcache/
- https://teamrocketist.github.io/2020/03/01/Pwn-Aero-2020-Plane-Market/
- https://teamrocketist.github.io/2019/08/17/Pwn-RedpwnCTF-penpal-world/