CC Blog Design Solutions Research & Design Hub

Keeping Your Memories Secret

Written by Colin O'Flynn

SRAM Read-Back Attacks

In this article, I demonstrate attacks which rely on reading back SRAM from a microcontroller (MCU). These attacks occur because many debug lock or security features in MCUs allow read-back of SRAM, even when code memory is secure. Taking advantage of the known structure of certain features such as the Advanced Encryption Standard (AES) key schedule allows an attacker to easily detect where these sensitive keys are stored. We can complicate these attacks by clearing or obfuscating memory, and this article gives some practical demonstrations of both attacks and countermeasures.

  • How are SRAM read-back attacks conducted?
  • What are AES vulnerabilities?
  • What are some SRAM read-back attack countermeasures?
  • STM32F0
  • ChipWhisperer

AES is the most likely symmetric security algorithm you’ll be using on your embedded system. It’s fast, secure, and code-efficient. Even if your application doesn’t use it, you might also find it used in a bootloader or other part of your system.

In this article I’m going to show you how an attacker could recover your AES keys by taking advantage of the fact that many microcontrollers (MCUs) don’t correctly protect SRAM access, even when the MCUs do protect FLASH access.

In practice, this means an attacker may be able to attach a debugger, dump a snapshot of your device SRAM, and then run a simple script to detect AES keys. If the attacker has more knowledge of your device internals they may be able to extract even more secrets.

It’s also something you can easily test yourself—it only requires access to a debugger, which you’re likely already using as part of your embedded development. This makes it a very accessible test you can do on your own products. There are also simple countermeasures you can apply, which mostly concentrate around zeroing out critical data, and storing the critical data in an obfuscated format.

In previous articles, I’ve shown you how to break AES with power analysis and fault injection. I’ve also talked about both hardware AES (such as that implemented by an MCU peripheral) and software AES (implemented in your firmware or a library). Power analysis and fault injection are much more advanced attacks than what I’m going to show you here, but I recently had a chance to use this “classic” attack, and it was a good reminder that it’s important to cover the basics as well as the more advanced attacks.

This attack has two parts: the first is that an attacker can read SRAM from an otherwise secure device using a couple simple techniques. The second is that the structure of how most AES implementations store the key allows it to be easily detected in memory. I’ll summarize those two points first before we get into the real-life examples and work.

— ADVERTISMENT—

Advertise Here

READING MEMORY FROM LOCKED DEVICES

If you’re using a secret, such as an encryption key, in your device firmware, you almost certainly know that at minimum you should enable the vendor-provided firmware protection. This is typically called something like “code read protection” or “debug lock.”

There are two related problems with the typical debug lock. The first is that in some devices the debug lock only prevents read access from the FLASH memory which holds the program code. Even with the device locked, an attached debugger can read the SRAM memory. An example of that is the STM32F1 series of devices—these older devices have only a single “level” of code protection, as described in ST Programming Manual PM0075.

More recent devices often have a way of disabling SRAM access as an option. For the ST devices, application note AN5156 provides an overview of which devices offer specific security features. Here we can see one of the RAM banks can be protected on a more recent part such as the STM32F4, so if we are storing sensitive secrets in that RAM bank an attacker cannot read them out like they could with a STM32F1.

In this case we rely on a more destructive attack: we disable the code read protection, and then try to read the SRAM. On many devices the code read protection can be disabled with a device erase. The attack works because sometimes the device erases only the flash memory, and not the SRAM. While this “kills” our target, it allows an attacker the ability to get a copy of the SRAM at a specific moment in time.

Note that since the attacker needs to kill a device to get a copy of the SRAM, if they aren’t sure when the sensitive data is stored in memory, it becomes more complex and expensive to automatically try multiple times.

Some devices have features that help you counteract this attack. The two main things to check for are seeing if the device will erase SRAM at the same time it erases FLASH memory, and seeing if the device clears SRAM at boot. When devices support this feature it may only be a specific portion of the SRAM that it applies to.

For example, the ST describes in reference manual RM0090 how the STM32F415 treats the special “backup SRAM” memory. This backup SRAM memory is protected against read-out when the flash read protection is enabled, and this memory section is erased when you disable code read-out protection. While the backup SRAM is designed to be used with a battery for storage when power is off, you could use it like any other SRAM section even without the backup battery feature. A smart choice would be to store all secrets to this SRAM, by defining a memory section and ensuring your linker script puts all of the AES state (along with similar sensitive data) into this memory section.

DOWNGRADE ATTACKS ON CODE READ PROTECTION

While you’re investigating the manufacture claims for SRAM read protection, you should also consider if there might be attacks that reduce the claimed security levels. Devices from different manufactures have proven to be vulnerable to various types of “downgrade attacks.” As an example, some of the STM32F devices I mentioned can be glitched from the debug level which has the SRAM access disabled (RDP2 in the ST world), to a lower level (RDP1 in the ST world).

While the ST documentation claims such change is impossible, this attack has been widely demonstrated in practical scenarios. One detailed example is given in the blog post “Kraken Identifies Critical Flaw in Trezor Hardware Wallets,” which uses an STM32F215. In fact, this also shows how you can further read flash memory once in RDP1 level, which I similarly demonstrated in my article “Revisiting Code Readout Protection Claims” in Circuit Cellar July 2022 Issue #384.

— ADVERTISMENT—

Advertise Here

Such attacks are more difficult than those using just a debugger, but may still be in scope for your threat model, so I want to make sure you’re aware of the broader picture. But for now, let’s look at how you can evaluate devices with only a debugger attached.

TESTING YOUR OWN DEVICES

One thing I promised at the start of this article is how easily you can test your own devices for the sort of simple SRAM read-out. To demonstrate this, I’ll use an STM32F0 “target” device that is part of my ChipWhisperer platform. You could use any sort of development board or other board you have with your target device.

To do this, I compiled the simple code from Listing 1. This code places some known byte sequence (FE ED FA CE CA FE BE EF) into SRAM, which we can confirm by looking at the MAP file, as shown in Listing 2. Here it shows this object was placed at address 0x20000000 (the start of SRAM). Watch to ensure the toolchain doesn’t remove the object once it detects that it’s never used—simply declaring it as volatile may be enough to keep it in the object file during compilation, but it may still be removed by the linker. In this case I’ve purposely accessed one element of the array, which caused the entire array to stick around.

Listing 1
This basic C code uses the ChipWhisperer Hardware Abstraction Layer (HAL) to compile this simple code of a variety of target MCUs.

#include “hal.h”
#include <stdint.h>
#include <stdlib.h>

volatile uint8_t test_value[] = {0xFE,0xED,0xFA,0xCE,0xCA,0xFE,0xBE,0xEF};

int main(void)
{
    platform_init();
    init_uart();

    while(1) {
        test_value[0] = test_value[0];
    }
}
Listing 2
 
A MAP file created by our toolchain lets us see where the symbol was placed in RAM.

 *(.data)
 .data          0x0000000020000000        0x8 objdir-CW308_STM32F0/simpleserial-base.o
                0x0000000020000000                test_value

To test the device, I programmed this binary file into my target device. I then connected with a debugger and confirmed that, as expected, my byte sequence is shown in SRAM memory at the expected location. This step confirms that, without a doubt, the data has been placed into memory at the correct location.

Finally, we use our device programmer to “secure” the device. I was using Segger J-Flash, and it has a secure option that programs the device to RDP2. For the STM32 device you’ll need to power cycle it for the setting to take effect. At this point, the device is running our code and placed our test value in SRAM at address 0x20000000. But the JTAG access is locked, preventing us from reading the flash memory.

Next, we unlock the device, which will trigger a flash erase. Without power cycling it, we connect the debugger and read the SRAM memory. With any luck, it looks something like Figure 1. Here you can see I’m using Segger Ozone as a debugger, and the memory window shows the value FE ED FA CE CA FE BE EF at address 20000000. Sharp-eyed readers might notice that the window showing “Disassembly” is pointing to invalid code, since the code has been erased and the device has an invalid boot address.

Figure 1
The Ozone Debugger shows the SRAM memory in the top window, with SRAM starting at address 0x20000000, and the secret value clearly visible.
Figure 1
The Ozone Debugger shows the SRAM memory in the top window, with SRAM starting at address 0x20000000, and the secret value clearly visible.

But the important finding here is that by erasing the device and connecting a debugger, the general-purpose SRAM has not been cleared. Any sensitive data left in SRAM will be readable by an attacker.

Of course, there is no structure to this data. If an attacker has reverse engineered your firmware or has some inside knowledge they may know exact addresses that store your secrets, but let’s look at the more generic case where an attacker has no such knowledge and needs to exploit the structure of the secret keys.

FINDING AES KEYS

We don’t need to dive into the details of AES to understand how you can find its keys in memory. But you need to understand that AES takes 16 bytes of input and uses a secret key to generate 16 bytes of output. Depending on the type of AES, the key could be 16 or 32 bytes long (AES-192 could also be in use, which means a 24-byte key, but is almost never found in practice).

Depending on how AES is used, we use different “modes” of AES. The most basic form of AES is called Electronic Code Book (ECB), referred to as AES-ECB. In this mode encrypting the same input (plaintext) will always map to the same output (ciphertext). This means it’s often insecure for many applications, since an attacker can see patterns in the encrypted ciphertext, even if they don’t know the exact ciphertext-to-plaintext mapping.

The other aspect of AES-ECB is that we can decrypt a random chunk of 16-byte data, without decrypting the entire file. While there are better modes of encryption to use when random access is required (such as AES-XTS used for disk drive encryption), you still often find AES-ECB used in embedded systems. If your system uses AES-ECB, this also means you can perform a brute-force attempt to decrypt a piece of ciphertext using every possible 16-byte key from memory.

A simple example of doing this is shown in Listing 3 using Python. You’ll need some way to identify a successful decryption—this will depend on your system. If it’s a firmware file for example, you may expect to find some specific strings in the decrypted file. So, you could look for a string that you know exists in the file, such as a debug message that says “System booted.” I’ve done exactly that in my example from Listing 3.

One caveat here is that how the key is stored in memory will vary depending on the AES implementation. For this reason, I also do a brute-force test of the most common variations and their permutations: the key could be stored Most Significant Byte (MSB) first, or Least Significant Byte (LSB) first. Each 4-byte word (assuming a 32-bit system) could itself also be mirrored. This more complex search is shown in Listing 4.

Listing 3

An example Python script to test all possible keys for decrypting a file

from Crypto.Cipher import AES

def chunks(l, n):
    return [l[i:i+n] for i in range(0, len(l), n)]

sram = open(“sram_dump.bin”, “rb”).read()
ciphertext = open(“fw_update_file.bin”, “rb”).read()

keys = chunks(sram, 16)

for offset,k in enumerate(keys):
    cipher = AES.new(bytearray(k), AES.MODE_ECB)
    dec = cipher.decrypt(bytearray(ciphertext))

    if b’System’ in dec:
        print(offset*16)
        print(k)
        break
Listing 4
We don’t know how the AES key is stored in memory, so try to capture several likely permutations.

from Crypto.Cipher import AES

def chunks(l, n):
    return [l[i:i+n] for i in range(0, len(l), n)]

def word_reverse(k):
    return k[0:4][::-1] + k[4:8][::-1] + k[8:12][::-1] + k[12:16][::-1]

def byte_reverse(k):
    return k[::-1]

def test_result(ciphertext, k):
    cipher = AES.new(bytearray(k), AES.MODE_ECB)
    dec = cipher.decrypt(bytearray(ciphertext))
    if b”System” in dec:
        print(k)
        return True
    return False

sram = open(“sram_dump.bin”, “rb”).read()
ct = open(“fw_update_file.bin”, “rb”).read()
keys = chunks(sram, 16)

for offset,k in enumerate(keys):
    if test_result(ct, k):
        break
    if test_result(ct, byte_reverse(k)):
        break
    if test_result(ct, word_reverse(k)):
        break
Listing 5

We can search for the AES key schedule in memory without having any knowledge of the AES mode or data being used.

def xor(s1, s2):
  return tuple(a^b for a,b in zip(s1, s2))

class AES(object):
  class __metaclass__(type):
    def __init__(cls, name, bases, classdict):
      cls.Gmul = {}
      for f in (0x02, 0x03, 0x0e, 0x0b, 0x0d, 0x09):
        cls.Gmul[f] = tuple(cls.gmul(f, x) for x in range(0,0x100))

  Rcon = ( 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a )
  Sbox = (
      0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
      0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
      0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
      0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
      0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
      0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
      0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
      0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
      0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
      0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
      0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
      0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
      0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
      0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
      0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
      0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
      )

  @staticmethod
  def rot_word(word):
    return word[1:] + word[:1]

  @staticmethod
  def sub_word(word):
    return (AES.Sbox[b] for b in word)

  def key_schedule(self):
    expanded = []
    expanded.extend(self.key)
    for i in range(self.nk, self.nb * (self.nr + 1)):
      t = expanded[(i-1)*4:i*4]
      if i % self.nk == 0:
        t = xor( AES.sub_word( AES.rot_word(t) ), (AES.Rcon[i // self.nk],0,0,0) )
      elif self.nk > 6 and i % self.nk == 4:
        t = AES.sub_word(t)
      expanded.extend( xor(t, expanded[(i-self.nk)*4:(i-self.nk+1)*4]))
    return expanded

  def add_round_key(self, rkey):
    for i, b in enumerate(rkey):
      self.state[i] ^= b

  def sub_bytes(self):
    for i, b in enumerate(self.state):
      self.state[i] = AES.Sbox[b]


class AES_128(AES):
  def __init__(self):
    self.nb = 4
    self.nr = 10
    self.nk = 4

class AES_256(AES):
  def __init__(self):
    self.nb = 4
    self.nr = 14
    self.nk = 8

def word_reverse(k):
  return k[0:4][::-1] + k[4:8][::-1] + k[8:12][::-1] + k[12:16][::-1]

def byte_reverse(k):
  return k[::-1]

def test_key_schedule(aes, key, r1sched):

  aes.key = key
  test_round_1 = aes.key_schedule()[16:32]
  r1sched = list(r1sched)

  if test_round_1 == r1sched:
    return True, key
  return False

if __name__==”__main__”:
  #Here is the main search program itself

  aes = AES_128()

  sram = open(“sram_dump.bin”, “rb”).read()

  for o in range(0, len(sram)-31):

    #Round-1 key is +16 bytes from initial key
    kin = sram[o+0:o+16]
    r1schedin = sram[o+16:o+32]

    k = word_reverse(kin)
    r1sched = word_reverse(r1schedin)
    if test_key_schedule(aes, k, r1sched):
      print(“Key FOUND == {}”.format(k.hex()))

    k = kin
    r1sched = r1schedin
    if test_key_schedule(aes, k, r1sched):
      print(“Key FOUND == {}”.format(k.hex()))
def xor(s1, s2):
  return tuple(a^b for a,b in zip(s1, s2))

class AES(object):
  class __metaclass__(type):
    def __init__(cls, name, bases, classdict):
      cls.Gmul = {}
      for f in (0x02, 0x03, 0x0e, 0x0b, 0x0d, 0x09):
        cls.Gmul[f] = tuple(cls.gmul(f, x) for x in range(0,0x100))

  Rcon = ( 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a )
  Sbox = (
      0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
      0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
      0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
      0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
      0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
      0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
      0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
      0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
      0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
      0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
      0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
      0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
      0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
      0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
      0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
      0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
      )

  @staticmethod
  def rot_word(word):
    return word[1:] + word[:1]

  @staticmethod
  def sub_word(word):
    return (AES.Sbox[b] for b in word)

  def key_schedule(self):
    expanded = []
    expanded.extend(self.key)
    for i in range(self.nk, self.nb * (self.nr + 1)):
      t = expanded[(i-1)*4:i*4]
      if i % self.nk == 0:
        t = xor( AES.sub_word( AES.rot_word(t) ), (AES.Rcon[i // self.nk],0,0,0) )
      elif self.nk > 6 and i % self.nk == 4:
        t = AES.sub_word(t)
      expanded.extend( xor(t, expanded[(i-self.nk)*4:(i-self.nk+1)*4]))
    return expanded

  def add_round_key(self, rkey):
    for i, b in enumerate(rkey):
      self.state[i] ^= b

  def sub_bytes(self):
    for i, b in enumerate(self.state):
      self.state[i] = AES.Sbox[b]


class AES_128(AES):
  def __init__(self):
    self.nb = 4
    self.nr = 10
    self.nk = 4

class AES_256(AES):
  def __init__(self):
    self.nb = 4
    self.nr = 14
    self.nk = 8

def word_reverse(k):
  return k[0:4][::-1] + k[4:8][::-1] + k[8:12][::-1] + k[12:16][::-1]

def byte_reverse(k):
  return k[::-1]

def test_key_schedule(aes, key, r1sched):

  aes.key = key
  test_round_1 = aes.key_schedule()[16:32]
  r1sched = list(r1sched)

  if test_round_1 == r1sched:
    return True, key
  return False

if __name__==”__main__”:
  #Here is the main search program itself

  aes = AES_128()

  sram = open(“sram_dump.bin”, “rb”).read()

  for o in range(0, len(sram)-31):

    #Round-1 key is +16 bytes from initial key
    kin = sram[o+0:o+16]
    r1schedin = sram[o+16:o+32]

    k = word_reverse(kin)
    r1sched = word_reverse(r1schedin)
    if test_key_schedule(aes, k, r1sched):
      print(“Key FOUND == {}”.format(k.hex()))

    k = kin
    r1sched = r1schedin
    if test_key_schedule(aes, k, r1sched):
      print(“Key FOUND == {}”.format(k.hex()))
Listing 6
A very simple obfuscation XOR’s the data with both a constant and a changing byte.

static uint8_t[176] aes_state;

/* XOR means the same function used for hiding or unhiding the AES state */
void hide_or_unhide(void){
    for(int i = 0; i < 176; i++){
        aes_state[i] = aes_state[i] ^ 0xAB ^ i;
    }
}

Note both Listing 3 and Listing 4 assume a 16-byte key, meaning AES-128 is in use. You could switch this to use 32-byte keys and AES-256. Another change you might consider is searching with a 1-byte step size instead of the 16-byte step size. The 16-byte search step size is faster, but might miss the start of an array.

You can also eliminate unlikely keys—for example, in practice, large blocks of the SRAM will be 00, which you could skip.

— ADVERTISMENT—

Advertise Here

If you know something about what the device is doing, such as AES for a standard communications interface, you can also feed details of that to build your own model into which you feed the brute force key search. Remember to include the various permutations of how the key could be stored in memory!

But if the device is running AES in software, you can also take advantage of the AES “key schedule.” The key schedule is how the AES algorithm transforms the 16-byte key into a longer 176-byte sequence used by the full AES-128 algorithm (or converts a 32-byte key into a 240-byte sequence for AES-256).

To take advantage of that, we simply perform a search in memory by taking the 16-byte key, calculating the next 16 bytes that would be stored in memory, and seeing if we find that data in our device SRAM. An example of how you can implement that is shown in Listing 5.

This attack is powerful since we don’t need to know anything about what the device does—if we can find an AES key schedule in memory, we know exactly what the AES key is, and can then try to figure out what it’s used for.

HIDING YOUR SECRETS

What happens if your device is vulnerable to this type of attack? Obviously, the best solution would be to use a more secure device, but you likely have devices in the field, or are constrained by the ongoing supply chain problems and have no other stock.

But even with such a device, we can make improvements to our implementation. The first choice is to scrub data from memory when it’s not needed. You might currently calculate the AES key schedule once and keep that in memory for example—a reasonable choice for speed reasons. But you’ve just made an attacker’s life very easy, since they don’t need to decide on a specific time to read SRAM. They can do it at any point in time. By clearing data immediately after you use it, you leave a much smaller window an attacker can exploit.

Of course you may not leave the key schedule around in memory, but a library you are relying on might. You can use the code from Listing 4 to see if you find AES key schedules in your own memory, and if you have source code for the library you can likely inspect it to see if it clears the key schedule after use.

An additional countermeasure is to obfuscate data in memory. This can be relatively simple yet effective—for example you could XOR the data read and written to an array with the index of the array element along with a constant. An example of this is shown in Listing 6. This would completely break my brute force attack in Listing 5.

You can also store data in unusual orders to make brute-force searches more complicated. Again, these countermeasures can be relatively easily undone if an attacker gets access to your code, but with only SRAM access it will take more effort to discover your specific byte ordering.

Hopefully this article has given you some additional topics to consider when attempting to secure your system. 

RESOURCES
ST Programming Manual PM0075. https://www.st.com/resource/en/programming_manual/pm0075-stm32f10xxx-flash-memory-microcontrollers-stmicroelectronics.pdf
ST Application Note AN5156. https://www.st.com/resource/en/application_note/dm00493651-introduction-to-stm32-microcontrollers-security-stmicroelectronics.pdf
STM32F415 Reference Manual RM0090. https://www.st.com/resource/en/reference_manual/dm00031020-stm32f405-415-stm32f407-417-stm32f427-437-and-stm32f429-439-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf
Kraken blog post on STM32F215 Security: https://blog.kraken.com/post/3662/kraken-identifies-critical-flaw-in-trezor-hardware-wallets/

Code and Supporting Files

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JANUARY 2023 #390 – Get a PDF of the issue

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the May 2020 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
Website | + posts

Colin O’Flynn has been building and breaking electronic devices for many years. He is an assistant professor at Dalhousie University, and also CTO of NewAE Technology both based in Halifax, NS, Canada. Some of his work is posted on his website (see link above).

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2023 KCK Media Corp.

Keeping Your Memories Secret

by Colin O'Flynn time to read: 18 min