Minut IoT exploit

Cybersecurity · English · IoT · Minut

17 minutes

Back in late 2019 I saw an ad on Facebook for a very interesting IoT device from a Swedish company local to me - Minut. I’ve been a very happy user of the four devices (named Point at the time, known as Minut M2 now) since then. This is the story of how I spent quite some time investigating what made them tick, and if I could make them tick differently. Being a few years old, this is not their latest device. However, they are still fully supported by their service.

My current profession is in cybersec consultancy, where I have various assignments within areas I’m usually not able to talk about. This one is different though, and I’m very happy to share a very interesting journey into hardware and firmware reverse engineering.

(If you have a strong dislike for reading, this tale also exists in video format)

The Hardware

After teardown we can immediately see that the device is using an STM32F412ZGT6 MCU, a wireless controller and a 4MB SPI flash. Now, if you’ve watched other IoT reverse engineers on Youtube you might think that all that’s needed is to read out the SPI flash and then hunt down admin passwords therein. Well. That’s not the case here.

The MCU is also known as a Cortex M4, a pretty capable CPU with 256Kb SRAM and 1Mb of internal flash. We can also see that there’s an 8 pin and 10 pin connector broken out on the board but unused in the commercial version. I don’t know for certain what these are, but I’ve speculated in that they are for SIM cards and additional storage.

Finally, beneath the battery, we find a breakout PCB containing the USB connector, the externally reachable reset button as well as a non-externally reachable boot-button that will select the system bootloader from STM rather than Minut’s own.

Somewhat confused by not finding either JTAG or a likely UART, I tried connecting the device to a computer via the USB-C “charging port” - and up popped USB serial with standard parameters. This also verified that we got DFU access when booting with the STM bootloader - good to know but not something I needed to make use of.

The SPI flash

This is of course what I started by extracting. Subsequently running binwalk on the image resulted in … nothing. Strings did however tell me that there were identifyable information here, in the form of what seemed to be filenames. As you can see, I added uniq to this grep since the same filenames appeared over and over in the output.

$ strings minut_spi.bin | grep ".fw" | sort | uniq
15142.fw.manifest
28063b7.appl.fw
2be22e4.beata_ble_a.fw
331065a.ok_sbc.fw
8094071.appl.fw
8e9f781.boot.fw
9e2fae5.bt.fw
b0e7a1b.wlan_nvram.fw
be146b6.notify_wav.fw
c6b0ee3.wlan_clm.fw
c79f1e5.boot.fw
cc7a58b.wlan.fw
cc99613.error_sbc.fw
d28aeac.smoke.fw

This looks very interesting! Esp. boot.fw and appl.fw - the other files would seem to be more involved in the wireless chipset (Broadcom).

The Commands

Connecting to the device over USB gets us into a menu with a plethora of commands:

       acotest                  adc                  als            app_event                  arp
            at                atdev                bcast              bq25050                   bt
        btnmfg            btnstatus                  cat              cellcfg               chgled
crashdump_send            debuglock                   df     diagnostics_send             digipyro
          fota                 fsck             fulldump                   gc                  get
          hall                  hap              hdc1080                 heap                 help
           hex                   hf          hl78_update                 http                  i2c
           i2s             ifconfig               ifdown                 ifup                  led
   led_animate             lis2dh12              lps22hb           lps22hbmfg                   ls
     ltr308als                  mcu                modem               motion                mount
        netcfg                nvram               osccal                  otp                  pdm
          ping                  pir             playbeep              playwav                 plot
      poweroff                 quit               reboot             reformat                   rm
         rmobj                  rng                  rsp                  rtc                 rtos
            rx                sdpcm               sensor                  set                setup
        sflash               sfpart                sgpc3              sha2sum                sleep
         smoke                swi2c              trigger                  tss                   tx
       unmount                  usb                vault                 vbat            verbosity
       version                 wlan              wlanmfg           wlanregmfg

Some of the output:

> version

*** Minut, Inc. Point 2 ***
6258a4 (master release) #5142 2021-12-14 15:18:15 UTC build@45feb9c0fdc9
Board: P2 (R3B)
Locked
> debuglock please
Flash RDP enabled
> vault list
[7a7893be] APP_SFLASH_KEY (32 bytes)
[11936359] SECFLASH_KEY (32 bytes)
[128df42a] FOTA_SIGN_PUBKEY (452 bytes)
[feff45ec] SETUP_KEY (889 bytes)
[807a6004] SECBOOT_KEY (16 bytes)
[6a99ed02] DEVICE_TOKEN (36 bytes)
Allocated vault space left: 267 bytes
> sflash
usage: sflash {id|stats}
More operations available in p2boot.

Alright, so from this we get that the device is in a locked state. The manufacturer understands the concept of STM’s RDP (readout protection) functionality and they make use of both symmetric (keysizes 16-32 bytes) and public key (452 and 889 bytes) encryption. And finally, there’s something called “p2boot”.

After a few tries I see that if I connect over serial while the device is rebooting I can get into another menu, this time in their bootloader (boot.fw) rather than the main application (appl.fw).

      adc                bcast                 boot              bq25050               btnmfg
btnstatus                cause               chgled            cryptfile            debuglock
       df                flash                 fsck                 help                  i2c
      led                   ls                  mcu                mount                nvram
      otp                 quit               reboot             reformat                   rm
    rmobj                  rng                  rtc                   rx             secflash
   sflash               sfpart              sha2sum                sleep                smoke
    swi2c                   tx              unmount                  usb                vault

Some of these are the same, some are not. I of course investigated what capabilities these had and could be used for but there’s nothing here that by itself helps in getting around the device security.

The command “get” lists a bunch of interesting properties, where I immediately noticed that ap_hostname and ap_port were likely something Minut would not like if you changed - the communication endpoint for the device. Minut devices need a paid cloud subscription to be of any use.

> set ap_hostname my.own.domain
var 'ap_hostname' is read only

One thing I did learn via the menus though, something was missing from the filelist the ls command gave me compared to my own strings-analysis of the SPI flash:

> ls
Directory /:
  [00cc]   4820   spool-ts-0
  [00e9]   1142   settings.cfg
  [01b4]   1142   settings.cfg.bkp
  [00af]      0   fs_clean
  [0140]  15834   cc99613.error_sbc.fw
  [017a]  15792   331065a.ok_sbc.fw
  [0197]     20   fw_version
  [0106]   6672   d28aeac.smoke.fw
  [003b] 383110   cc7a58b.wlan.fw
  [0058]    598   b0e7a1b.wlan_nvram.fw
  [0075]   7222   c6b0ee3.wlan_clm.fw
  [0092]  34496   9e2fae5.bt.fw
  [015d]  14616   2be22e4.beata_ble_a.fw
  [001e]    968   15142.fw.manifest

There’s no boot.fw or appl.fw here. At first I assumed this was because the firmware was simply hiding them to not make it easy to just extract them via USB - but this rabbit hole was a bit deeper.

The SPI filesystem

Back to the image I had extracted. After painstakingly assuming there was some sort of proprietary boot sector, things started looking up at what seemed to be similar to FAT entries. After documenting the way the filesystem was split up into pages where directory entries pointed to where file contents could be found I was finally able to deduce that Minut uses the SPIFFS filesystem here.

Sidenote: I had some help from the menu command cat here - excluding it would only have made things take longer but it’s a good idea to never include commands not needed in production of course. We’ll get back to this.

I dug up two open source projects, spiffsimg and mkspiffs, but neither was able to work with the image even after I had removed the proprietary header/bootsector.

After even more analysis I figured out what the differences were and could in the end successfully patch both projects so that they were fully working with the image. And they did not show the appl.fw and boot.fw files either.

After even further analysis I deduced that those files had been on the flash at some point but had since been deleted. I could recover a few pages (one page is 256 bytes) of content though and, as I expected at this time, it was clear that these files were encrypted.

SWD - Single Wire Debugging

At this point I was very excited. What could have been just any other haphazardly secured IoT device seemed to be the real deal and I would likely have a nice challenge on my hand. So I brought out my ST-Link SWD debugger tool and prepared to connect it to the device. While my soldering skills would allow me to connect to the MCU directly, I did spend a few minutes on finding suitable test points for the needed signals and built up a little breakout board.

Success.

TODO: Insert st-probe output.

Remember the earlier comment about the manufacturer seemingly knowing about STM RDP? Of course that was enabled. When trying to read out anything from the device it immediately shuts down communications and has to be hard rebooted to come back. Almost always. See, there are three different RDP levels, where 0 is unprotected, 1 is well protected and 2 is “don’t screw up or you’ll brick your devices out in the field”. It’s not uncommon to see manufacturers stay on RDP 1 in products, and that’s the case here.

That’s also the first instance where I got a bit lucky. See, only access to the internal flash is prohibited in RDP 1, not access to SRAM. And while the application only seemed to use SRAM for its stack & heap, the bootloader seemed to load itself into SRAM and execute completely from there. (This is to be able to reflash itself on the internal flash, I now know).

Would this give us everything we need? After all, this is the code that launches the application and the application is what stops us from modifying the interesting properties in the device.

Ghidra #1

Here begins the firmware reverse engineering. I spent an enormous amount of time figuring out what code did what. And no, there are no keys in memory. They use what they call “vault” which is on the internal flash, and since the application is decrypted when loaded onto the device it doesn’t need decryption when launched from the bootloader. Being ARM, the internal flash can also be referenced just as any other memory address from code and so there’s no need to “load” keys into RAM.

Realizing that reading the SRAM was the only real way of getting information off the device I started looking into what codepaths would be interesting to execute and then pull the stack from. This lead me to …

Acquiring the official firmware #1

Getting the firmware for a device is easy. You just go to the manufacturer website and … No. Minut does not keep links to firmware binaries on their web site. They do (did …) link to a Minut Update Tool (beta) for Windows, Mac and Linux which seemed to be able to flash new firmware to the device over USB. I extracted the app and looked through the binary for the firmware URL. Which isn’t there - there’s an endpoint taking parameters including a device id and some information about the host though.

Ghidra #2

Alright, let’s attach to the process and break just before they use the collected information and access the URL.

Acquiring the official firmware #2

Huh, even though I know the User-Agent, the parameters, I’m fully certain I have the right information and yet … nothing. So I decide to allow reflashing one of my devices and just execute the updater as is.

That doesn’t work either - it just stops at the downloading step at 0%.

I spent some time here wondering if it could be my IP, my host, my VPN - everything. And then decided to just ask support. I am an owner of these devices and have been for many years, and I’m a paying subscriber to the cloud service - which they indeed verify before answering.

Long-ish story short, the updater tool isn’t supported and they thanked me for pointing out that it was available on the web which it shouldn’t be. And no, I cannot get any help with my “non-working devices” (social engineering is a thing … and they passed) because they’re quite old and out of warranty.

Alright, but just unpublishing the information page on the web site help section doesn’t actually remove the web site endpoint answering on the URL from the updater program, so I decided to take a look at what that did. I already knew the updater itself was a wrapped web program and it seemed to just execute pages from that server.

A little while later, I had deobfuscated a main.js which turned out to be the updater program, using USB access to run the commands we had seen in the menus on the device to load and flash the firmware. And finally, by analysing this code, I got the actual URL for the firmware and could download it. Actually I saw that there were both “firmware” and “Modem firmware” but the latter wasn’t of interest.

Yet again, this was in a format I had no tools that could understand. Looking through the bundle with strings and ImHex I could see that besides boot.fw and appl.fw it did contain a few more files from the SPI flash. Making use of already knowing their content, I could reverse also this file format enough for me to successfully extract the two files I wanted.

I also learnt from the javascript that the menu command used to flash these files onto the device was “secflash” - which simply takes a filename.

Getting there

At some point I had figured out that the tx and rx commands available in both their bootloader and application were simple Xmodem send and receive. While this made things much easier, I would’ve been able to transfer files to the device via the SPI flash anyway. So, I uploaded appl.fw to the device and ran “secflash appl.fw”.

> secflash appl.fw
secflash : verifying file
flash : Erasing sector at 08020000
flash : Erasing sector at 08040000
flash : Erasing sector at 08060000
flash : Erasing sector at 08080000
flash : Erasing sector at 080a0000
flash : Erasing sector at 080c0000
flash : Erasing sector at 080e0000

… and I could then boot up normally. So, now I knew we were executing a codepath that would verify and decrypt the application binary.

Ghidra #3

I’m lucky in that I started programming in assembler at 12 years of age back in 1986. Ghidra’s decompiling is awesome, but not perfect. Here I spent quite some time reversing the secflash code, especially trying to figure out if there were any key leaks when the decryption took place. After a while, I had most of the code in a legible state, and knew that the “verifying file” was a simple CRC32 over the encrypted content, placed in a header in the beginning of the .fw file called “P2XF”. Another part of the header included the identity of the key from the vault to be used - not surprisingly the SECFLASH_KEY. Being 32 bytes, seeing the decryption code and also finding the sboxes I deduced that AES256 was used for the encryption. I also assumed from the looks of the code that Minut were using some existing crypto library meaning less chance for any simple implementation mistakes to be exploited.

If you’ve read this far, congratulations, we’ve now reached the point where I managed to break through the security. I’d like to take this time to point something out: This is an extremely well secured IoT device. While I did succeed in the end, I don’t want anyone’s takeway here to be that this was easy, that Minut had bad security (they don’t!) or that anyone could’ve easily gone down the same path as I did without somewhat extensive experience in reverse engineering and cryptography.

Because this turned out to be a cryptography break of sorts. What I know, and people who work in cryptography do, is that when using AES it’s not only the key that needs to be kept secret. During encryption and decryption, the key scheduling algorithm produces what’s known as roundkeys, used in turn for each successful round of the *-cryption. Now if you know that you will only seldomly encrypt and decrypt, like in Minut’s case, you could keep these buffers (4KB) on the internal flash. However, in this case they’re placed on the heap.

And I can read the heap, remember, we’re in RDP 1.

Using these I was able to reverse the original AES256 SECFLASH_KEY - which meant I was now able to decrypt boot.fw and appl.fw. Change them, encrypt, calculcate the CRC32 and flash them back.

appl.fw

While I had reversed a lot of the bootloader before, since it executed from SRAM, the application was completely new. It lives in the internal flash at 0x08020000 and I could now add it to the Ghidra project, as well as placing the decrypted boot.fw at 0x08000000. I decided that the first thing I wanted to do was to unlock the ability to set the ap_hostname and ap_port options, since I believed that to be a suitable PoC to show to Minut.

I found the code, nop:ed out a few instructions, encrypted the file, calculated the checksum, put together a hack.fw, uploaded to the device, ran secflash without problems and then tried booting it.

> boot
724.823          secboot : Verifying application
725.914           spiffs : Filesystem mounted
725.915          secboot : Failed to boot: 8

Did I mention that I consider Minut to have properly implemented security?

Alright, secboot. That’s not secflash - which did accept the file without issues.

Ghidra #4

More time was now spent on reversing how secboot verified the application when booting. And again there were no obvious exploits here. After a while I could deduce that they used SHA256-HMAC over the unencrypted contents.

Sidenote: This finally allowed me to understand that what I had called “KeyId”, the four bytes shown when listing the vault, were the SHA256sums of the keys. I could now write a script that would traverse any SRAM dump to see if it happened to include any of the keys. I haven’t seen this happen though, and I don’t think they leak from having looked at how they’re used by the code. In any case, this is an unnecessary information leak by the vault command.

The decrypted file has a header in itself, pointing to a block at the end of the code identified as “P2SB” (“Secure Boot”). This header contains the four byte SHA256sum of the key used, and the 32 byte HMAC. At first I thought I had found an exploitable mistake in the implementation here. While the SECBOOT_KEY according to vault is 16 bytes (not a serious issue, but it should be 32 for SHA256), the code seemed to be able to handle dynamic keylength (again, I believe those routines to be a third party lib) and I wondered if maybe they used these 4 bytes to select which key from the vault to use for the digest. That is, could I perhaps just use the SECFLASH_KEY identifier instead and calculate the HMAC using the key I had?

Well, no. This resulted in the error code 7 instead of 8 which means the key wasn’t found in the vault. There are different types of keys and the code selects the correct type for the current task, even though the SHA256sum indeed selects the key. We’ll need to be a bit more … brutal.

secboot is a Minut program, living in their bootloader. There’s no secboot involved in executing the bootloader itself though - that’s done by the STM bootloader, in ROM, and on a Cortex M4 there’s no ability for it to know of any Minut specifics. Also, the exact place in the decrypted application file where the P2SB header was pointed to is null in the decrypted bootloader.

So I simply patched the bootloader to not care about any SHA256-HMAC failures in secboot. Flashed this new bootloader to the device and rebooted.

Complete success. Not only was my patched bootloader accepted, it could now readily launch my patched application too.

Conclusion

This is what’s called a complete pwn. These keys are shared between devices. This means that with the use of USB and an SWD tool, someone could replicate my findings here and be able to create a hackboot.fw and hackappl.fw. With those, the attacker can attach to the USB of any Minut M2 and replace the official firmware with their own version. With Minuts nowadays mostly being targeted for the short term rental market, to keep track of noise levels and other things guests might not be allowed to do, this means a guest could in theory reset that configuration for their stay and avoid the renter knowing about their parties.

Even worse, an attacker could leave code behind that would spy on the next guest.

I wrote up a report and made a responsible disclosure to Minut. They acted impressively fast and sent out new firmware shortly thereafter. A CVE was published through Mitre as well.

While Minut made their own decision on how to best handle this, the suggestion I sent was this:

  1. Enable RDP 2. This will remove the ability to read out the SRAM.
  2. Rewrite the AES code to keep the roundkeys on the internal flash instead of in SRAM. These would only be used when secflash is performed and these device are already no longer getting firmware updates so this should not cause fatal flash wear. This might not be possible due to NAND needing full page rewriting, and if so change crypto algorithm instead.

Now, you might ask, why #2 when #1 would be sufficient?

Glitch exploits. The STM32F412 is glitchable and it’s possible to cause a downgrade from RDP 2 to RDP 1 this way - at which point we would be back to the roundkeys exploit. While it has been documented how to read out the internal flash through glitching this process is very inexact in its timing and the internal flash is highly likely to get erased before getting very far. I have actually explored this as well and documented here

I think this is the most well-implemented IoT device I have reverse engineered so far. Kudos to Minut for their excellent security, this was a very very fun challenge!