This one is a pretty interesting challenge. I’ve used IDA Free only.
But before jumping into main, I’ll be analyzing the opendoor namespace
So, the class Buffer has two members; a vector of bytes and a offset indicating where to start the next read
Now lets analyze the Buffer::read<T> functions. This is important as it tells how the server unmarshalls the data.
Buffer::read<bool> is a wrapper to Buffer::read<uchar>.
Int32 and Int64 are being read in BigEndian
i.e., var_20[7 - var_14] = Buffer::read<uchar>()
read<shared_ptr<vector<uchar>>> and read<string> work the same. The first call read<uint> to read the no. of. bytes and read<uchar> to read that many bytes i.e., a vector is a string of bytes prefixed by its length
The constructor of AESCrypter calls the superclass constructor, before initializing the members. AESCrypter::decrypt and AESCrypter::encrypt perform decryption and encryption using AES 256 CBC.
There is another class that implements Crypter. Its the PlainCrypter. Well you have guessed it right. Its a dummy class which neither encrypts nor decrypts. It has another parameter which if set to TRUE, prints debug logs.
Let’s move to opendoor::State which encapsulates a lock for the magic door.
The methods of State are straightforward. Here’s the representation of State
The Messaging Protocol
The Message class consists of six methods - parse_message, execute, to_string, serialize, ptr, and get_id out of which parse_message, to_string and get_id are pure virtual, i.e., they have to be implemented in the classes implementing Message.
The subclasses of Message are of:
- Messages that have a request and response - UnlockMessage, DebugMessage, PingMessage
Message::serialize performs the common serialization.
It writes the message_id followed by the timestamp returned by time().
Now let’s go to Message::ParseMessage
It reads two Int32 words i.e., the message_id and timestamp and checks if the recieved timestamp bounded by 5 seconds of the current timestamp. Otherwise it responds with an INVALID_TIMESTAMP ErrorMessage. I’ll discuss later how I got error constant names.
The generic parsing routine
So, the timestamp must be within 5 seconds.
Message also defines 7 lambdas that creates an instance each of the concrete message classes and encapsulates within a shared_ptr.
UnlockMessage::parse_message reads two Int64 words and stores them in its member variables.
Clearly, the first member variable must be non zero and the second member variable must equate to door_number. The _good branch continues at
which unlocks the door and creates an UnlockResponse. While the _bad branch, locks the door instead and returns an ACCESS_DENIED ErrorMessage.
Now we can represent Message as
It reads an Int32 which can be either 1 or 2. If the value read is 1, then it reads a boolean. If the value is 2, it reads a string. These are stored in member variables at offsets +8, +12, +16
If the member at offset +8 is 1 then DebugRequestMessage::handle_debug_message_ is called. If the value is not 1 and the lock is not debuggable, an ACCESS_DENIED Error is returned. Whereas if the value is 2, and the lock is debuggable, DebugRequestMessage::handle_readfile is called.
Thus the member at offset +8, denotes the debug_type
Yay ! This looks promising !
So, to execute handle_readfile_, we must have the lock’s DEBUG flag turned on. But the lock’s debug flag is initially 0.
If the member at offset +12 is 1, the routine turns on the door’s DEBUG flag if the door is unlocked. If the value at offset +12 is not 1, then the door’s debug flag is turned off.
The member at offset +12 denotes the flag for turning on lock’s debug flag.
handle_readfile_ reads 4K bytes from the file whose path is stored in the member variable at offset +16 and returns the contents.
- Send UnlockMessage to set the lock’s status to UNLOCKED
- Send DebugMessage of type 1 to set the lock’s DEBUG flag
- Send DebugMessage of type 2 to read any file !!
The ConnectionPool class uses non-blocking IO. It maintains a map whose keys are the client socket descriptors and values are instances of ConnectionHandler. The do_read_ (do_write) methods read (write) a vector of bytes (from the socket) in the same format as Buffer reads (writes).
Here’s the vtable for ConnectionHandler
The members of ConnectionHandler are
Let’s visit ConnectionHandler::process_message_
The routine calls cryptr->decrypt() on buffer. If the decryption is successful, it proceeds to ParseMessage
If the message has been parsed successfully, the execute() method is invoked. If it succeeds, a positive response is returned by invoking serialize() followed by cryptr->encrypt()
Last but not the least, init
The second routine, sets up the maps as follows
main is also straightforward. It calls parse_flags to determine the default Crypter instance to be used. The default is AESCrypter. If -n is specified, PlainCrypter is used. The default port is 4848 which can be changed with -p option.
So, we have to write the encrypted Message prefixed by the size of the encrypted message to the server.
And the Output …
Solved after the CTF was over :(