TFC CTF 2024 - vspm
Table of Contents
0x00. Introduction
Structure
This structure can store up to 10 entries in the code_base + 0x4060 region.
0x01. Vulnerability
unsigned __int64
In the save() function for storing passwords, the user inputs the size len for credential, then reads len + 1 bytes. However, since the fixed-length(0x20) name also reads len + 1 bytes, we can overwrite the next password structure’s credential.
While we can write len + 1 bytes in credential to overwrite the first byte of the next chunk’s header, I couldn’t find an exploitation path by modifying prev_size.
0x02. Exploit
Before proceeding with exploitation, checking the protections reveals that all regions (code, stack, libc) are randomized. With the current credential overwrite vulnerability, I needed to leak at least one memory region.
Libc Leak
One commonly used heap memory leak technique involves leaking main_arena through the unsorted bin. Since main_arena is in the libc region, we can obtain the libc base by calculating the offset.
The problem is that chunks need to be at least 0x80 bytes to be sent to the unsorted bin when freed, but the maximum input len is 0x79. Since heap chunks are allocated sequentially and only the 0xXXXXXXXXXXXXX000 portion of the address varies, I can construct a fake chunk and use the credential overwrite vulnerability to make the next password structure’s credential point to it:
= # fake chunk -> prev_size
+= # fake chunk -> size
First, arrange the chunks considering the offset differences between 0000, 4444, and the top chunk.
Initially, I thought I only needed to consider the offset between 0000 and 4444, but the offset with the top chunk also needs to match for successful fake chunk free:
# first password structure
# first fake chunk header
# second fake chunk header
# top chunk
Now to make 2222’s credential point to the fake chunk:
# free "1111"
# alloc "1111" and overwrite next pointer
# free fake chunk -> unsorted bin
Using 1111 to make 2222’s credential point to 0x55555555d020 makes the value at 0x55555555d010 act as the chunk header. When we free 2222, it’s treated as freeing a 0x100-sized chunk and moves to the unsorted bin:
| | )
During this process, the freed chunk’s fd and bk get written with main_arena addresses. Since memory isn’t cleared when the region is returned via malloc, we can leak it through check().
Fortunately, requesting a chunk smaller than 0x100 still allocates from the unsorted bin by splitting it, so I requested a 0x30-sized chunk:
# alloc from unsorted bin
=
= 0x3b4cc0
= -
The \xc0 I input is the first byte of main_arena from the debugger. While we don’t need to match it since we calculate the offset anyway, we need to provide some input, so I matched it.
Stack Leak
With the libc leak successful, I can now print any libc region using credential overwrite and check(). The environ variable in the libc region stores a stack address, so I used this for stack leak:
= 0x3b75d8
= b * 0x20
+=
First, free 0000 and overwrite so 1111’s credential points to environ:
=
= - 0x110
Since 1111 points to environ and check() prints credential information, we can obtain the stack address stored in environ.
Fastbin Dup into Stack
Similar to using the credential overwrite vulnerability to point to a fake chunk, we can trigger a double free vulnerability by pointing to another credential:
After executing this payload, the password structures contain:
To trigger the double free vulnerability, we need to overwrite 7777’s 0x55555555d240 with 5555’s 0x55555555d1d0.
The reason for using 0x60-sized credential will be explained later. Since we allocate three 0x60-sized chunks, the second byte of the chunk address differs. In 0xd1d0, the 0x1d0 is fixed and only the 0xd000 portion varies, creating a 1/16 probability for successful exploitation.
While it might be possible to manipulate fastbin carefully or perform heap leak, the probability wasn’t too low, so I proceeded as is:
As in the payload above, matching 7777’s credential with 5555’s and freeing in order 5555, 6666, 7777 results in:
0x55555555d1d0->0x55555555d160->0x55555555d1d0
Now requesting the 0x60-sized chunk via malloc returns 0x55555555d1d0. If we write a stack address here and can write a fake chunk header at that address, it gets added to the fastbin list.
So I searched for a stack location where I could construct a fake chunk header. Initially, I tried using save() function’s stack:
unsigned __int64
Constructing a fake chunk header using len and i gives 0x70 bytes to the return address, which is less than the maximum allocation size of 0x78, so it seemed feasible.
However, there’s a contradiction: to allocate a 0x70-sized chunk, we need to input 0x70 for len, but when constructing the fake chunk header, we need to input 0x80 for the size to match.
So I needed to find another area. Since there’s no function that terminates by returning, I realized that when allocating and reading in save(), overwriting read()’s return address might work:
unsigned __int64
The problem again is the fake chunk header. Checking the area that can overwrite read()’s return address just before malloc():
While 0x60 is stored at 0x7fffffffdbf8, this value was stored during internal logic execution after being passed as an argument to malloc(), creating the same contradiction.
Here’s the trick: chunk headers don’t need proper alignment, so considering that the highest byte of stack addresses is 0x7f, I examined memory again:
There are two areas that can be used as fake chunk headers: 0x7fffffffdc15 and 0x7fffffffdc25. Setting fd to 0x7fffffffdc15 fails malloc(), while 0x7fffffffdc25 succeeds.
The supposed reason is that this area is just above the current function’s stack, overlapping with malloc()’s stack region, and values stored at 0x7fffffffdc15 get overwritten as malloc() uses its stack internally.
Anyway, to set 0x7fffffffdc25 as fd, after calculating the offset with the acquired stack address and allocating 0x55555555d1d0, the fastbin is configured as:
0x55555555d160->0x55555555d1d0->0x7fffffffdc35
Therefore, the third malloc() returns the stack address, and we can calculate the offset to overwrite read()’s return address with a one-shot gadget:
=
= 0xe1fa1
= b * 0x13
+=
0x03. Payload
=
= 0x0000555555554000
=
= f
=
return
return
return
return
=
=
=
= # fake chunk -> prev_size
+= # fake chunk -> size
# libc leak
# free "1111"
# alloc "1111" and overwrite next pointer
# free fake chunk -> unsorted bin
# alloc from unsorted bin
=
= 0x3b4cc0
= -
# flush unsorted bin
# stack leak
= 0x3b75d8
= b * 0x20
+=
=
= - 0x110
# fastbin dup
=
= 0xe1fa1
= b * 0x13
+=
=
=