Vulnerabilities
20 Nov 2023

A Gap in the TrustZone Preset Settings for the LPC55S69

LA
Laura Abbott
Engineer

We’re very excited to have announced the general availability of our cloud computer. As part of this work, we continue to build on top of the LPC55S69 from NXP as our Root of Trust. We’ve discovered some gaps when using TrustZone preset settings on the LPC55S69 that can allow for unexpected behavior including enabling debug settings and exposure of the UDS (Unique Device Secret). These issues require a signed image or access at manufacturing time.

How to (safely, securely) configure a chip

The LPC55S69 uses the Armv8-m architecture which includes TrustZone-M. We’ve previously discussed some aspects of the Armv8-m architecture and presented on it in more detail. Fundamentally, setting up TrustZone-M is simply a matter of putting the right values in the right registers. The word "simply" is, of course, doing a lot of heavy lifting here. TrustZone-M must also be set up in conjunction with the Memory Protection Unit (MPU) and any other vendor specific security settings. Once the ideal settings have been decided upon, there’s still the matter of actually performing the register programming sequence. NXP offers a feature called TrustZone preset data to make this programming easier. Register data may optionally be appended to the end of an image for the LPC55S69, and the ROM will set the registers before jumping into the user image. Some of those registers may also be configured to prevent futher modification. This means the user image does not need to be concerned with the settings for those registers.

The structure used to configure the registers looks like the following:

typedef struct _tzm_secure_config
{
  uint32_t cm33_vtor_addr;  /*! CM33 Secure vector table address */
  uint32_t cm33_vtor_ns_addr; /*! CM33 Non-secure vector table address */
  uint32_t cm33_nvic_itns0; /*! CM33 Interrupt target non-secure register 0 */
  uint32_t cm33_nvic_itns1; /*! CM33 Interrupt target non-secure register 1 */
  uint32_t mcm33_vtor_addr; /*! MCM33 Secure vector table address */
  uint32_t cm33_mpu_ctrl; /*! MPU Control Register.*/
  uint32_t cm33_mpu_mair0; /*! MPU Memory Attribute Indirection Register 0 */
  uint32_t cm33_mpu_mair1; /*! MPU Memory Attribute Indirection Register 1 */
  uint32_t cm33_mpu_rbar0; /*! MPU Region 0 Base Address Register */
  uint32_t cm33_mpu_rlar0; /*! MPU Region 0 Limit Address Register */
  uint32_t cm33_mpu_rbar1; /*! MPU Region 1 Base Address Register */
  uint32_t cm33_mpu_rlar1; /*! MPU Region 1 Limit Address Register */
  uint32_t cm33_mpu_rbar2; /*! MPU Region 2 Base Address Register */
  uint32_t cm33_mpu_rlar2; /*! MPU Region 2 Limit Address Register */
  uint32_t cm33_mpu_rbar3; /*! MPU Region 3 Base Address Register */
  uint32_t cm33_mpu_rlar3; /*! MPU Region 3 Limit Address Register */
  uint32_t cm33_mpu_rbar4; /*! MPU Region 4 Base Address Register */
  uint32_t cm33_mpu_rlar4; /*! MPU Region 4 Limit Address Register */
  uint32_t cm33_mpu_rbar5; /*! MPU Region 5 Base Address Register */
  uint32_t cm33_mpu_rlar5; /*! MPU Region 5 Limit Address Register */
  uint32_t cm33_mpu_rbar6; /*! MPU Region 6 Base Address Register */
  uint32_t cm33_mpu_rlar6; /*! MPU Region 6 Limit Address Register */
  uint32_t cm33_mpu_rbar7; /*! MPU Region 7 Base Address Register */
  uint32_t cm33_mpu_rlar7; /*! MPU Region 7 Limit Address Register */
  uint32_t cm33_mpu_ctrl_ns; /*! Non-secure MPU Control Register.*/
  uint32_t cm33_mpu_mair0_ns; /*! Non-secure MPU Memory Attribute Register 0 */
  uint32_t cm33_mpu_mair1_ns; /*! Non-secure MPU Memory Attribute Register 1 */
  uint32_t cm33_mpu_rbar0_ns; /*! Non-secure MPU Region 0 Base Address Register */
  uint32_t cm33_mpu_rlar0_ns; /*! Non-secure MPU Region 0 Limit Address Register */
  uint32_t cm33_mpu_rbar1_ns; /*! Non-secure MPU Region 1 Base Address Register */
  uint32_t cm33_mpu_rlar1_ns; /*! Non-secure MPU Region 1 Limit Address Register */
  uint32_t cm33_mpu_rbar2_ns; /*! Non-secure MPU Region 2 Base Address Register */
  uint32_t cm33_mpu_rlar2_ns; /*! Non-secure MPU Region 2 Limit Address Register */
  uint32_t cm33_mpu_rbar3_ns; /*! Non-secure MPU Region 3 Base Address Register */
  uint32_t cm33_mpu_rlar3_ns; /*! Non-secure MPU Region 3 Limit Address Register */
  uint32_t cm33_mpu_rbar4_ns; /*! Non-secure MPU Region 4 Base Address Register */
  uint32_t cm33_mpu_rlar4_ns; /*! Non-secure MPU Region 4 Limit Address Register */
  uint32_t cm33_mpu_rbar5_ns; /*! Non-secure MPU Region 5 Base Address Register */
  uint32_t cm33_mpu_rlar5_ns; /*! Non-secure MPU Region 5 Limit Address Register */
  uint32_t cm33_mpu_rbar6_ns; /*! Non-secure MPU Region 6 Base Address Register */
  uint32_t cm33_mpu_rlar6_ns; /*! Non-secure MPU Region 6 Limit Address Register */
  uint32_t cm33_mpu_rbar7_ns; /*! Non-secure MPU Region 7 Base Address Register */
  uint32_t cm33_mpu_rlar7_ns; /*! Non-secure MPU Region 7 Limit Address Register */
  uint32_t cm33_sau_ctrl;
  uint32_t cm33_sau_rbar0;/*! SAU Region 0 Base Address Register */
  uint32_t cm33_sau_rlar0;/*! SAU Region 0 Limit Address Register */
  uint32_t cm33_sau_rbar1;/*! SAU Region 1 Base Address Register */
  uint32_t cm33_sau_rlar1;/*! SAU Region 1 Limit Address Register */
  uint32_t cm33_sau_rbar2;/*! SAU Region 2 Base Address Register */
  uint32_t cm33_sau_rlar2;/*! SAU Region 2 Limit Address Register */
  uint32_t cm33_sau_rbar3;/*! SAU Region 3 Base Address Register */
  uint32_t cm33_sau_rlar3;/*! SAU Region 3 Limit Address Register */
  uint32_t cm33_sau_rbar4;/*! SAU Region 4 Base Address Register */
  uint32_t cm33_sau_rlar4;/*! SAU Region 4 Limit Address Register */
  uint32_t cm33_sau_rbar5;/*! SAU Region 5 Base Address Register */
  uint32_t cm33_sau_rlar5;/*! SAU Region 5 Limit Address Register */
  uint32_t cm33_sau_rbar6;/*! SAU Region 6 Base Address Register */
  uint32_t cm33_sau_rlar6;/*! SAU Region 6 Limit Address Register */
  uint32_t cm33_sau_rbar7;/*! SAU Region 7 Base Address Register */
  uint32_t cm33_sau_rlar7;/*! SAU Region 7 Limit Address Register */
  uint32_t flash_rom_slave_rule;/*! FLASH/ROM Slave Rule Register 0 */
  uint32_t flash_mem_rule0;/*! FLASH Memory Rule Register 0 */
  uint32_t flash_mem_rule1;/*! FLASH Memory Rule Register 1 */
  uint32_t flash_mem_rule2;/*! FLASH Memory Rule Register 2 */
  uint32_t rom_mem_rule0;/*! ROM Memory Rule Register 0 */
  uint32_t rom_mem_rule1;/*! ROM Memory Rule Register 1 */
  uint32_t rom_mem_rule2;/*! ROM Memory Rule Register 2 */
  uint32_t rom_mem_rule3;/*! ROM Memory Rule Register 3 */
  uint32_t ramx_slave_rule;
  uint32_t ramx_mem_rule0;
  uint32_t ram0_slave_rule;
  uint32_t ram0_mem_rule0;/*! RAM0 Memory Rule Register 0 */
  uint32_t ram0_mem_rule1;/*! RAM0 Memory Rule Register 1 */
  uint32_t ram1_slave_rule; /*! RAM1 Memory Rule Register 0 */
  uint32_t ram1_mem_rule1;/*! RAM1 Memory Rule Register 1 */
  uint32_t ram2_mem_rule1;/*! RAM2 Memory Rule Register 1 */
  uint32_t ram3_mem_rule0;/*! RAM3 Memory Rule Register 0 */
  uint32_t ram3_mem_rule1;/*! RAM3 Memory Rule Register 1 */
  uint32_t ram4_slave_rule;
  uint32_t ram2_mem_rule0;
  uint32_t ram3_slave_rule;
  uint32_t ram1_mem_rule0;
  uint32_t ram2_slave_rule;
  uint32_t ram4_mem_rule0;/*! RAM4 Memory Rule Register 0 */
  uint32_t apb_grp_slave_rule;/*! APB Bridge Group Slave Rule Register */
  uint32_t apb_grp0_mem_rule0;/*! APB Bridge Group 0 Memory Rule Register 0 */
  uint32_t apb_grp0_mem_rule1;/*! APB Bridge Group 0 Memory Rule Register 1 */
  uint32_t apb_grp0_mem_rule2;/*! APB Bridge Group 0 Memory Rule Register 2 */
  uint32_t apb_grp0_mem_rule3;/*! APB Bridge Group 0 Memory Rule Register 3 */
  uint32_t apb_grp1_mem_rule0;/*! APB Bridge Group 1 Memory Rule Register 0 */
  uint32_t apb_grp1_mem_rule1;/*! APB Bridge Group 1 Memory Rule Register 1 */
  uint32_t apb_grp1_mem_rule2;/*! APB Bridge Group 1 Memory Rule Register 2 */
  uint32_t apb_grp1_mem_rule3;/*! APB Bridge Group 1 Memory Rule Register 3 */
  uint32_t ahb_periph0_slave_rule0;/*! AHB Peripherals 0 Slave Rule Register 0 */
  uint32_t ahb_periph0_slave_rule1;/*! AHB Peripherals 0 Slave Rule Register 1 */
  uint32_t ahb_periph1_slave_rule0;/*! AHB Peripherals 1 Slave Rule Register 0 */
  uint32_t ahb_periph1_slave_rule1;/*! AHB Peripherals 1 Slave Rule Register 1 */
  uint32_t ahb_periph2_slave_rule0;/*! AHB Peripherals 2 Slave Rule Register 0 */
  uint32_t ahb_periph2_slave_rule1;/*! AHB Peripherals 2 Slave Rule Register 1 */
  uint32_t ahb_periph2_mem_rule0;/*! AHB Peripherals 2 Memory Rule Register 0*/
  uint32_t usb_hs_slave_rule0; /*! HS USB Slave Rule Register 0 */
  uint32_t usb_hs__mem_rule0; /*! HS USB Memory Rule Register 0 */
  uint32_t sec_gp_reg0;/*! Secure GPIO Register 0 */
  uint32_t sec_gp_reg1;/*! Secure GPIO Register 1 */
  uint32_t sec_gp_reg2;/*! Secure GPIO Register 2 */
  uint32_t sec_gp_reg3;/*! Secure GPIO Register 3 */
  uint32_t sec_int_reg0;/*! Secure Interrupt Mask for CPU1 Register 0 */
  uint32_t sec_int_reg1;/*! Secure Interrupt Mask for CPU1 Register 1 */
  uint32_t sec_gp_reg_lock;/*! Secure GPIO Lock Register */
  uint32_t master_sec_reg;/*! Master Secure Level Register */
  uint32_t master_sec_anti_pol_reg;
  uint32_t cm33_lock_reg; /*! CM33 Lock Control Register */
  uint32_t mcm33_lock_reg; /*! MCM33 Lock Control Register */
  uint32_t misc_ctrl_dp_reg;/*! Secure Control Duplicate Register */
  uint32_t misc_ctrl_reg;
  uint32_t misc_tzm_settings;
} tzm_secure_config_t;

An implementation detail of the ROM is that the settings for these registers are (mostly) applied in the order shown in the structure. This means that the very first register that gets changed is VTOR which switches the vector table from the one in the ROM to the user provided one. Any faults that occur after VTOR is changed will be handled by user code, not ROM code. This turns out to have some "interesting" side effects.

(Un)locking debug access

The LPC55S69 offers debug access via standard ARM interfaces (SWD). Debug access can be configured to be always available, always disabled, or only available to authenticated users. These settings are designed to be applied at manufacturing time via the CMPA region. Debugging is disabled by default while executing in the ROM and only enabled (if allowed) as the very last step before jumping to user code. The debug settings are also locked out, preventing further modification from user code except in specific authenticated circumstances. Because debug access is highly sensitive, it makes sense to minimize the amount of time the ROM spends with it enabled.

If the debug settings are applied last, this means that the TrustZone preset settings must be applied before them. Combine this information with the implementation detail of how the preset setting are applied, if the code faults after VTOR is changed but before we apply the debug settings, it will be possible to run in user controlled code with debug registers open for modification.

How easy is it to actually trigger this? Very easy. Other registers in the preset structure include settings for the MPU. Setting the enable bit in MPU_CTRL without any other regions set is enough to trigger the fault. NXP actually says in their manual that you need to make sure the entire ROM region is configured as secure privileged and executable otherwise "boot process will fail". "fail" in this case is vectoring off into the appropriate fault handler of the user code.

This makes the following sequence possible:

  • Have debug disabled in the CMPA

  • Sign an image with TrustZone preset settings with a valid VTOR and MPU settings that exclude the ROM region

  • Have the MemManage fault handler follow the standard sequence to enable debugging

  • The image will trigger the fault handler and have debugging enabled despite the settings in the CMPA

This does require access to the secure boot signing key, but it’s a departure from the presentation of the CMPA settings as being independent of any possible settings in an image.

Extracting the UDS

One additional step in the setting of the debug registers is a final lockout of some PUF registers. The PUF (Physically Unclonable Function) is designed to tie secrets to a specific chip. When a secret is PUF encoded, it can only be decoded by that specific chip. The LPC55S69 uses the PUF to encode the Unique Device Secret (UDS) for use as the basis of a DICE identity. To ensure the identity is tied to the specific chip and cannot be cloned, access to the PUF index for the UDS is locked out after it is used.

The UDS is always locked out for secure boot images, but the ROM relies on the code path for debug settings to lock out for non-secure images. TrustZone preset settings can be used with non-secure CRC images which means that the previously described issue can be used to extract the UDS since the final lockout will never occur.

Requiring an unsigned image significantly limits the impact to cases such as the following:

  • Attacker at manufacturing time runs ISP command to generate the UDS on an unprogrammed LPC55S69

  • Attacker runs an unsigned image with a buggy TrustZone preset to extract the UDS

  • Attacker continues on with the rest of the manufacturing sequence, making sure not to re-generate the extracted UDS

This may be mitigated with sufficient tooling at manufacturing time but the issue still remains.

Is this a security issue?

There was disagreement between Oxide and NXP about whether this qualified as a true security vulnerability (Oxide’s opinion) vs. a gap in design and documentation (NXP’s opinion). The areas of disagreement were related to what exactly it was possible to do with these issues and what was required to make them happen. Unlocking the debug ports requires access to the secure boot signing keys and arguably if you can sign something with a bad TrustZone preset you don’t need to bother with debug port access; once your secure boot integrity has been compromised all bets are off. Oxide believes this undersells the potential for mitigation and why this should be considered a security issue: there could be circumstances where having debug port access would make extracting assets significantly easier.

Transparency is an Oxide value and that is what we strive for in bug reporting. Our goal is to make sure that issues are acknowledged and information about the bug is made widely available. NXP agreed to acknowledge this issue as a non-security errata and there will not be a CVE filed at this time. Given the narrow scope and lack of agreement between Oxide and NXP, filing a CVE would provide little benefit. If new information were to come to light from Oxide, NXP, or other researchers who are interested in our findings, we would re-evaluate this decision.

We are pleased that NXP is choosing to protect its customers by informing them of this gap. A bigger takeaway from this issue is to understand the limitations of secure/verified boot. A proper secure boot implementation will ensure that the only code that runs on a device is code that has been signed with an appropriate private key. Secure boot provides no assertions about the implementation of that code. The strength of secure boot is bounded by the code you choose to sign. In the absence of a fix for this errata, we will not be using the TrustZone preset data. If other customers choose to continue using TrustZone preset data they will need to be diligent about validating their inputs to avoid introducing gaps in the security model. Oxide has a commitment to open firmware to ensure our customers can have confidence in what code we will be signing to run on their machines.

Timeline

2023-08-16

Oxide discovers issue while reviewing image settings

2023-08-21

Oxide discloses issue to NXP PSIRT with a disclosure deadline of 2023-11-20

2023-08-21

Oxide PSIRT acknowledges receipt

2023-10-11

NXP requests meeting to discuss the report with Oxide

2023-10-19

Oxide and NXP meet to discuss the reported issues

2023-10-23

Oxide suggests documentation clarifications

2023-10-27

NXP agress to issue an errata

2023-11-20

Oxide publishes this blog post as a disclosure