Diagon Alley write-up (FAUST CTF 2018)
Overview
This task is a service which is running on team’s vulnbox (server). It can be seen in traffic that check system frequently connects to service and performs some actions but this traffic is almost unreadable (actually encrypted as we will see later). While performing initial overview of the service we noticed that it is working as systemd network socket and each connection is spawning a new instance of /srv/diagon_alley/diagon_alley
which receives client socket as its stdin and stdout (inetd-style server).
There are two binaries inside /srv/diagon_alley/
folder: main binary named diagon_alley
and another one named shops
. diagon_alley
is statically linked library without symbols acting as a frontend, which uses dynamically linked binary shops
as a backend to database. By analyzing disassembly code we can figure out that both binaries are “sandboxed” using seccomp which explains why such architecture has been chosen instead of performing database query from the main binary. diagon_alley
uses pipe-fork-exec routine to start shops
as a child process and communicate with it later on. We can communicate only with diagon_alley
binary, there is no direct access to shops
backend.
Service supports following actions:
- registration
- login
- creating, entering and listing shops
- creating, adding, buying and listing items
To login and enter the shop you need to provide a password. Flags are stored inside items so you need to enter the corresponding shop and list items in it in order to get flags.
Communication protocol reverse engineering
No client is provided to communicate with server so let’s open diagon_alley
binary in disassembler and dig our way to complete understanding of used communication protocol.
One of the first functions called in main
is a constructor of IO_wrapper
class. This class provides two methods, IO_read
and IO_puts
as can be understood from their inner logic. We need to note that these functions not only perform data sending and receiving but are also doing some crypto stuff with that data. They heavily use IO_wrapper
object variables so let’s see what are these variables initialized with.
In IO_wrapper
constructor (0x402750
) two function pointers are set to IO_read
and IO_puts
functions, input and output FILE*
variables are set to stdin
and stdout
respectively. One DWORD value is set to zero and another is set to rand()
. Additionally current time is saved for unknown purpose, it is updated in IO_read
so we called it last_op_time
.
After constructing IO
object it is immediately used in function at address 0x402890
. A lot of things are going on there so we start to randomly inspect called functions. In function 0x408580
we can see calls that look like asserts:
...
sub_406290("in != NULL", "src/pk/rsa/rsa_import.c", 32LL);
...
sub_406290("key != NULL", "src/pk/rsa/rsa_import.c", 33LL);
...
Using these messages we can quickly find used crypto library: libtomcrypt. We can identify function at 0x402450
by assert that it contains: perror("fread", a2, v7);
. Using this approach we can find most standard function names and obtain the following “beautified” form of function at address 0x402890
:
_BOOL8 __fastcall set_check_rsa(IO_wrapper *IO)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v13 = __readfsqword(0x28u);
sub_402A00();
stdin = IO->stdin;
packet_size = 0LL;
fread(&packet_size, 1LL, 8uLL, stdin);
v2 = fread(&packet, 1LL, packet_size, IO->stdin);
user_rsa_key = (rsa_key *)calloc(1LL, 72LL);
if ( !user_rsa_key )
goto LABEL_12;
user_rsa_key& = user_rsa_key;
if ( (unsigned int)rsa_import(&packet, v2, user_rsa_key) )
return 1LL;
IO->user_rsa_key = user_rsa_key&;
server_rsa_key = (rsa_key *)calloc(1LL, 72LL);
server_rsa_key& = server_rsa_key;
if ( server_rsa_key )
{
LOBYTE(v9) = rsa_generate(server_rsa_key, 2048);
if ( v9 )
return 1LL;
IO->server_rsa_key = server_rsa_key&;
a3 = 2048LL;
if ( rsa_export((unsigned __int8 *)&packet, &a3, 0, server_rsa_key&) )
return 1LL;
fwrite(&a3, 1LL, 8uLL, IO->stdout);
fwrite(&packet, 1LL, a3, IO->stdout);
result = sub_402810(IO);
}
else
{
LABEL_12:
perror("calloc", 72LL, v4);
result = 1LL;
}
return result;
}
We identified two additional variables in IO_wrapper
object used in this function: user_rsa_key
and server_rsa_key
. Full IO_wrapper
structure:
struct __attribute__((aligned(16))) IO_wrapper
{
QWORD init_random;
QWORD cnt;
FILE *stdin;
FILE *stdout;
rsa_key *user_rsa_key;
rsa_key *server_rsa_key;
QWORD (__cdecl *write)(IO_wrapper *, char *);
QWORD (__cdecl *read)(IO_wrapper *, _BYTE *);
time_t last_op_time;
};
Now we look back at IO_puts
and see that it uses IO->user_rsa_key
. Similarly, IO_read
uses IO->server_rsa_key
. Looks like we need to generate our RSA key, send it to server, receive server’s RSA key and after that our messages to server are encrypted using server’s key and incoming messages are encrypted with ours key.
After that we reverse engineer regular packet structure from both IO_puts
and IO_read
functions. It looks like this:
struct __attribute__((aligned(8))) in_buffer
{
QWORD chunk_length; // including itself
QWORD init_random; // as determined in IO_wrapper::ctr
QWORD cnt; // increments in IO_puts
QWORD data_length; // size of data array
BYTE data[]; // actual data
};
At first cnt
is zero. You may think that we don’t know server’s random but after exchanging RSA keys servers sends us main menu and since it is encoded in this format, it contains this random number. Overall, here is the process of establishing and maintaining connection:
- Generate and send our RSA key
- Receive server’s RSA key
- Receive first encrypted packet with main menu, extract
init_random
from it - Send and receive data encoded as
in_buffer
structure, do not forget to update localcnt
copy on each server’sIO_puts
(i.e., our send).
Side note: an interesting question is how exactly to use RSA keys. What encryption&decryption scheme is used? Of course you can reverse engineer
diagon_alley
binary further or google what schemelibtomcrypt
is using by default but who needs these smart approaches in 2018? In our case we imported ours and server’s keys asCrypto.PublicKey.RSA
objects and wondered why their methodsencrypt
anddecrypt
are raisingNotImplemented
exception so we googled it and first answer suggested to usePKCS1_OAEP
scheme with these keys (good_key = PKCS1_OAEP.new(our_key)
). It worked. After the end of the game player from some other team in IRC said that in their version of library raw RSA key’sencrypt
anddecrypt
methods were implemented but not as inPKCS1_OAEP
scheme so they were stuck with fully understood but not working protocol. Bad luck.
Script that handles connection and makes encryption layer transparent for pwntools
can be found here.
Exploitation
After busting out our static analysis tools (strings
from binutils
) we can find vulnerability in shops
binary. Queries that are meant to check password during login and shop entering use LIKE operator instead of normal comparison. That means, by simply entering %%
as a password you can enter any shop from the list and get the items. As you see importing server-generated private keys and establishing communication between client and the server is way harder than further exploitation.
Full exploit can be found here.
It is also possible to achieve code execution at this service which we didn’t manage to do due to the lack of time, but it can still be your homework. It is possible to enter item name large enough to trigger the stack overflow in add_item
method. It happenes due to the “typo” which copies 0x20 bytes of the item name to the buffer with 20 bytes size.
IRC logs
Here is what orgs said about this challenge after the end of CTF (saved for history):
23:07 <+mightymo> so in the frontend there was a simple buffer overflow, when you create an item
23:08 <+mightymo> it would check that the item name is less than 50 by using strlen but then copy using the length of the message which is not based on c-strings
23:14 <+mightymo> to get code execution in the backend then, you could trigger a SIGUSR, which would lead to a uaf or craft a malicious createShopReq that would lead to an overflow