Dr. Ang Cui
Wyatt Ford
Edward Larson

July 9, 2022 Summercon

This material is based in part upon work supported by the DARPA under Contract No. N66001-20-C-4032. Any opinions, findings and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the DARPA.

OFRAK Summercon!¶

One more time... in Python!¶

In [2]:
!chmod +x /examples/assets/example_program
!./examples/assets/example_program
Hello, World!
In [3]:
from ofrak import OFRAK
from ofrak_components.string import (
    StringPatchingModifier,
    StringPatchingConfig
)

ofrak = OFRAK()
context = await ofrak.create_ofrak_context()
resource = await context.create_root_resource_from_file(
    "/examples/assets/example_program"
)
# Identify
await resource.identify()
# Analyze
data = await resource.get_data()
hello_world_offset = data.find(b"Hello, World!")
assert hello_world_offset != -1
# Modify
await resource.run(StringPatchingModifier,
    StringPatchingConfig(
        hello_world_offset, "Summercon!", null_terminate=True
    )
)
# Pack
await resource.pack()
await resource.flush_to_disk("hello_summercon")
In [4]:
!chmod +x hello_summercon
! ./hello_summercon
Summercon!

Overview¶

OFRAK Past¶

Cast of Characters¶

Ang Floating

Better Living

Stare Blob

Stare Blob

Stare Blob

Stare Blob

FRAK Where? FRAK Why? 😿¶

OFRAK Present¶

What... Why.... How....¶

Cast of Characters¶

OFRAK What¶

OFRAK (Open Firmware Reverse Analysis Konsole) is a software tool that combines the ability to unpack, analyze, modify, and repack binaries.

Open¶

OFRAK is designed to be extended with additional components and capabilities, in addition to its core framework.

Firmware¶

  • First class support for firmware, packed filesystem, monolithic firmware

Reverse¶

  • Reverse-engineer and reverse binary changes (unpack, modify, repack)!

Analysis¶

  • OFRAK brings together analysis from a range of tools:
    • libmagic, LLVM, Ghidra, BinaryNinja, Angr, etc. ...

Konsole¶

  • Python API for literate, readable and reproducible scripts
  • Graphical User Interface (GUI) for interactive exploration and visualization

OFRAK Why?¶

When it comes to an embedded device's firmware...

You don't know what's there

  • How much do you know about the code that, say, your car is running?

You don't know what isn't there

  • Standard security practices we take for granted in desktop/mobile are often not present.

You don't know how to change it

  • Vendor often has complete control and complete responsibility for what runs on the device.

Obviously, we're not the only ones who have noticed this¶

Part of OFRAK's development has been funded as part of the DARPA Assured Micropatching (AMP) program.

From DARPA Assured MicroPatching website:

Our society’s infrastructure is increasingly dependent on software deployed on a wide variety of computing devices other than commodity personal computers, such as industrial equipment, automobiles, and airplanes. Unlike commodity computers that have short upgrade cycles and are easily replaceable in case of failure, these computing devices are intended for longer service, and are hard to replace…

The goal of the Assured Micropatching (AMP) program is to create the capability for rapid patching of legacy binaries in mission critical systems, including the cases where the original source code version and/or build process is not available.

A typical Red Balloon workflow...¶

We need something that:

  • Is capable of all of the above
  • Allows us to transfer and build on knowledge
  • Can test, reproduce, and scale builds

OFRAK How?¶

OFRAK models firmware in a way that reflects how we reverse-engineer and modify it¶

Everything is a Resource¶

ResourceView¶

In [6]:
elf = await root_resource.view_as(Elf)
for section in await elf.get_sections():
    print(section)
ElfSection(virtual_address=0, size=0, name='<no-strings>', section_index=0)
ElfSection(virtual_address=40960, size=16384, name='<no-strings>', section_index=1)
ElfSection(virtual_address=57344, size=1632, name='<no-strings>', section_index=2)
ElfSection(virtual_address=58976, size=1440, name='<no-strings>', section_index=3)
ElfSection(virtual_address=60416, size=32, name='<no-strings>', section_index=4)
ElfSection(virtual_address=60448, size=32470752, name='<no-strings>', section_index=5)
ElfSection(virtual_address=32531200, size=28, name='<no-strings>', section_index=6)

Components¶

  • Encapsulate procedures that can be targeted on resources
  • Can use external tools (binwalk, Ghidra) or custom implementations
  • 5 main components

Identify¶

  • Identifier components perform logic to determine what tags to add to a resource.
  • Answers the question "What is this resource?"
In [8]:
await resource.identify()
list(resource.get_tags())
Out[8]:
[FilesystemEntry, GenericBinary, LinkableBinary, File, Program, Elf]

Unpack¶

  • Unpacking is like "zooming in" on a resource to break it up into finer details.

  • Unpacker components determine the pieces that make up a resource and create child resources for each of those.

  • Sometimes the children that get unpacked from a resource are literally pieces of the resource, such as the ELF headers and sections in an ELF file.

  • Sometimes the children are more abstract. For example, unpacking a ZIP file will create the decomprossed contents as children.

In [9]:
await resource.unpack()
children = list(await resource.get_children())
print(len(children))
print(children[1])
69
Resource(resource_id=48c0eb6470494a12982290d57c6733d2, tag=[ElfHeader], data=ddb67e9dfbca4572ba2b007a1ef29534)

Analyze¶

  • Analyzer components exract more detailed information about a resource.
  • This information is structured in the form of ResourceAttributes.
In [10]:
from ofrak.core.architecture.attributes import ProgramAttributes

await resource.analyze(ProgramAttributes)
Out[10]:
ProgramAttributes(isa=<InstructionSet.X86: 'x86'>, sub_isa=None, bit_width=<BitWidth.BIT_64: 64>, endianness=<Endianness.LITTLE_ENDIAN: 'little'>, processor=None)

Q: What about non-trivial analysis?¶

A: Binary analysis "engine" integrations¶

  • Common OFRAK representation for things like BasicBlock, Instruction, etc.

  • Then ask the analysis backend of choice to get information that OFRAK represents as resources, attributes, tags, etc.

Write a whole OFRAK script before realizing that BinaryNinja will not analyze ShitCore 420420 instruction set? Change one line to use Ghidra.

In [ ]:
import ofrak_components_binary_ninja
import ofrak_components_ghidra

# ofrak.discover(ofrak_components_binary_ninja)
ofrak.discover(ofrak_components_ghidra)

Modify¶

  • Where OFRAK leaps beyond what other RE tools offer is in the ability to modify firmware in a straightforward way.

  • Modifier components perform some modification on a resource. If this sounds vague... yes.

  • Modifiers can be as simple as overwriting a few bytes of data...

  • ...Or as complicated as replacing a function in a compiled binary by compiling a new function to overwrite the original, complete with checking the size to avoid any unintended modifications and linking against preexisting functions in the binary.

In [12]:
from ofrak_components.string import (StringFindReplaceConfig, 
    StringFindReplaceModifier)

await resource.run(StringFindReplaceModifier,
    StringFindReplaceConfig(
        "Hello, World!",
        "Summercon!",
        null_terminate=True
    )
)
Out[12]:
ComponentRunResult(components_run={b'StringFindReplaceModifier'}, resources_modified=set(), resources_deleted=set(), resources_created=set())

Q: What about building new code directly into an existing binary?¶

A: PatchMaker¶

  • Inputs: Code with similiar amount of information as normal development process
    • C source
    • Assembly source
    • Existing object files
    • Symbol information
      • e.g. { “printf”: 0xdeadbeef} is enough to use printf in source.
  • Outputs: Correctly Situated Binary Data
    • File with Executable format (e.g. ELF)

Shoutout to Dr. Volker Barthelmann!¶

Pack¶

  • Re-packing firmware allow OFRAK to transparently make modifications, regardless of the larger format of the firmware.

  • Packer components repack the children of a resource back into the original format of the parent. Often this means modifying the parent's data.

  • Sometimes these changes are significant – consider how modifying even one byte of uncompressed data could change the entirety of the packed data. What matters is that any expectations of the original parent format are maintained, for example, checksums would be recalculated by a packer.

  • If Unpackers are like "zooming in", packers are like "zooming out".

In [13]:
await resource.pack()
await resource.flush_to_disk("hello_summercon")

OFRAK Use Cases¶

Use Case 1: Explore 🐈¶

Mysterious Binary #1¶

Mysterious Binary #2¶

Mysterious Binary #3¶

Use Case 2: Break Things 😹¶

Change permissions for the LOAD program header to make section non-executable¶

In [15]:
# Unpack and get LOAD program header
resource = await context.create_root_resource_from_file(
    "/examples/assets/example_program"
)
await resource.unpack()
elf = await resource.view_as(Elf)
load_program_header = None
for program_header in await elf.get_program_headers():
    if (
        program_header.p_type == ElfProgramHeaderType.LOAD.value
        and program_header.p_flags & 
        ElfProgramHeaderPermission.EXECUTE.value
    ):
        load_program_header = program_header
        break
assert load_program_header
load_program_header
Out[15]:
ElfProgramHeader(segment_index=2, p_type=1, p_offset=0, p_vaddr=0, p_paddr=0, p_filesz=2104, p_memsz=2104, p_flags=5, p_align=2097152)
In [16]:
# Make LOAD program header non-executable
await load_program_header.resource.run(
    ElfProgramHeaderModifier,
    ElfProgramHeaderModifierConfig(
        p_flags=load_program_header.p_flags & 
        ~ElfProgramHeaderPermission.EXECUTE.value
    ),
)
Out[16]:
ComponentRunResult(components_run={b'ElfProgramHeaderModifier'}, resources_modified={b'\xad\x9e\xabFW;D\xa9\xbb\xf6\x03~q_\xd7\x1f', b'\xb1\xc8j\xff\xdd\xfcA\xc0\xa4\xb22\x80X\xb8u]'}, resources_deleted=set(), resources_created=set())
In [17]:
# Dump the modified program to disk
await resource.pack()
await resource.flush_to_disk("hello_no_run")
In [18]:
!chmod +x hello_no_run
!./hello_no_run
Segmentation fault (core dumped)

wat do u mean program no run?? 😿

Use Case 3: Modify Firmware¶

An oldie but a goodie: Modify Cisco Router Firmware¶

First, lets use dynamips (Cisco router emulator) to see what booting looks like.

Mkay. Let's go find that and modify in OFRAK!¶

Boot it up!¶

Use Case 4: Patch "Unpatchable" Vulnerabilities¶

Why Cisco No Patch? 🤔¶

  • They don't know how to
  • They don't want to
  • It's not their code...

Can WE patch? OFRAK! WE can!¶

In [31]:
resource = await context.create_root_resource_from_file(
    "ssb288xx.BE-01-007.sbn"
)
await resource.unpack()
resource
Out[31]:
Resource(resource_id=c455e62245244186b30442c9b88c36ae, tag=[FilesystemEntry,GenericBinary,UImage,File], data=e580cb8ca4bc409289a6a4147b26fc90)
In [32]:
# Oh hi UImage! Unpack and set memory mapping
uimage = await resource.view_as(UImage)
uimage_header = await uimage.get_header()
uimage_body = list(await uimage.get_bodies())[0]
await uimage_body.resource.create_child_from_view(
    CodeRegion(
        virtual_address=uimage_header.get_load_vaddr(),
        size=uimage_header.get_data_size(),
    ),
    data_range=Range(0, await uimage_body.resource.get_data_length()),
    additional_attributes=(ProgramAttributes,)
)
uimage_body.resource.add_tag(Program)
await uimage_body.resource.save()
In [34]:
# Unpack all the things...
program = await uimage_body.resource.view_as(Program)
code_region = await uimage_body.resource.get_only_child_as_view(
    CodeRegion
)
_ = await code_region.resource.unpack_recursively()
In [35]:
# Make sure our functions are there...
printf = await program.get_function_complex_block("printf")
handle_uboot_crypto_smc = await program.get_function_complex_block(
    "handle_uboot_crypto_smc"
)
bounds_check = await program.get_function_complex_block("bounds_check")

print(printf); print(handle_uboot_crypto_smc); print(bounds_check)
ComplexBlock(virtual_address=1876957000, size=52, name='printf')
ComplexBlock(virtual_address=1876956380, size=268, name='handle_uboot_crypto_smc')
ComplexBlock(virtual_address=1877046224, size=64, name='bounds_check')
In [36]:
# Initialize Toolchain use for patching
toolchain = ToolchainVersion.LLVM_12_0_1
toolchain_config = ToolchainConfig(
    file_format=BinFileType.ELF,
    force_inlines=True,
    relocatable=False,
    no_std_lib=True,
    no_jump_tables=True,
    no_bss_section=True,
    create_map_files=True,
    compiler_optimization_level=CompilerOptimizationLevel.SPACE,
    debug_info=True,
)
In [37]:
# Define symbols that we will link against!
await program.define_linkable_symbols(
    {
        printf.name: (
            printf.virtual_address, LinkableSymbolType.FUNC
        ),
        handle_uboot_crypto_smc.name: (
            handle_uboot_crypto_smc.virtual_address, LinkableSymbolType.FUNC
        ),
        bounds_check.name: (
            bounds_check.virtual_address, LinkableSymbolType.FUNC
        ),        
    }
)

What are our patches?¶

In [39]:
!cat trust_vuln_patches_src/check_bounds.c
unsigned int check_bounds(unsigned int address, unsigned int length)
{
    unsigned int end_address = address + length;
    if ((address > 0x5FFFFFFF) && (end_address < 0x6FBFFFFF) && (length <= 0xFC00000)) {
        return 0;
    }
//    printf("Error::Buffer address range (%x-%x) invalid\n", address, end_address);
    return -1;
}
In [40]:
!cat trust_vuln_patches_src/handle_crypto_smc_patch.S
LDR R0, [R5, #0x18]
STR R3, [R5, #0xc]

Let's patch bounds check with the FunctionReplacementModifier¶

In [41]:
# Patch bounds_check with `FunctionReplacementModifier`
trust_zone_patches_src = await create_source_resource(
    "trust_vuln_patches_src"
)
function_replace_config = FunctionReplacementModifierConfig(
    trust_zone_patches_src,
    {"bounds_check": "check_bounds.c"},
    toolchain_config,
    toolchain,
)
await program.resource.run(
    FunctionReplacementModifier, function_replace_config
)
ld.lld: warning: cannot find entry symbol _start; not setting start address
Out[41]:
ComponentRunResult(components_run={b'FunctionReplacementModifier'}, resources_modified=set(), resources_deleted=set(), resources_created=set())

Now let's patch handle_crypto_smc so it works :)¶

Using PatchMaker directly!

In [42]:
# Specify patch segments
handle_crypto_asm_source = "trust_vuln_patches_src/handle_crypto_smc_patch.S"
patch_address = handle_uboot_crypto_smc.virtual_address + 0x24
patch_size = 8
handle_crypto_asm_segment = Segment(
    segment_name=".text",
    vm_address=patch_address,
    offset=0,
    is_entry=False,
    length=patch_size,
    access_perms=MemoryPermissions.RX,
)
null_data = Segment(
    segment_name=".data",
    vm_address=0,
    offset=0,
    is_entry=False,
    length=0,
    access_perms=MemoryPermissions.RW,
)
In [43]:
# Initialize Patchmaker
build_dir = tempfile.mkdtemp()
exec_path = os.path.join(build_dir, "fem")
patch_maker = PatchMaker(
    program_attributes=program_attributes,
    toolchain_config=toolchain_config,
    toolchain_version=toolchain,
    build_dir=build_dir,
)

#Make BOMs
handle_crypto_asm_bom = patch_maker.make_bom(
    name="handle_crytpo",
    source_list=[handle_crypto_asm_source],
    object_list=[],
    header_dirs=[]
)
handle_crypto_config = PatchRegionConfig(
    handle_crypto_asm_bom.name + "_patch",
    build_segment_dict(
        handle_crypto_asm_bom,
        {handle_crypto_asm_source: 
            (handle_crypto_asm_segment, null_data)
        },
    ),
)
fem = patch_maker.make_fem(
    [(handle_crypto_asm_bom, handle_crypto_config)],
    exec_path,
)
ld.lld: warning: cannot find entry symbol _start; not setting start address
In [44]:
fem
Out[44]:
FEM(name='handle_crytpo', executable=LinkedExecutable(path='/tmp/tmp6ruo0slo/fem', file_format=<BinFileType.ELF: 'elf'>, segments=(Segment(segment_name='', vm_address=0, offset=0, is_entry=False, length=0, access_perms=<MemoryPermissions.R: 1>), Segment(segment_name='.rbs_handle_crypto_smc_patch_text', vm_address=1876956416, offset=5376, is_entry=False, length=8, access_perms=<MemoryPermissions.RX: 5>), Segment(segment_name='.rbs_handle_crypto_smc_patch_data', vm_address=0, offset=5384, is_entry=False, length=0, access_perms=<MemoryPermissions.RW: 3>), Segment(segment_name='.bss', vm_address=0, offset=5384, is_entry=False, length=0, access_perms=<MemoryPermissions.RW: 3>), Segment(segment_name='.symtab', vm_address=0, offset=5384, is_entry=False, length=32, access_perms=<MemoryPermissions.R: 1>), Segment(segment_name='.shstrtab', vm_address=0, offset=5416, is_entry=False, length=100, access_perms=<MemoryPermissions.R: 1>), Segment(segment_name='.strtab', vm_address=0, offset=5516, is_entry=False, length=4, access_perms=<MemoryPermissions.R: 1>)), symbols={'$a': 1876956416}, relocatable=False))
In [45]:
# Inject Patch
await patch_maker.inject_patch(fem, uimage_body.resource)
Out[45]:
Resource(resource_id=aae1868d479e44f58effc8fca6d403dd, tag=[GenericBinary,BinaryNinjaAnalysisResource,LinkableBinary,UImageBody,Program], data=033d2028fc714e088b5571b4d7ca6090)
In [46]:
_ = await uimage.resource.pack()
await uimage.resource.flush_to_disk("ssb288xx.BE-01-007.sbn.patched")

Use Case 5: OFRAK You!¶

We're interested in seeing what you do with OFRAK :)

In [48]:
resource = await context.create_root_resource_from_file(
    "/examples/assets/example_program"
)
await resource.unpack()

# OFRAK You!

await resource.pack()
Out[48]:
ComponentRunResult(components_run=set(), resources_modified=set(), resources_deleted=set(), resources_created=set())

OFRAK Future¶

Follow ofrak.com for updates & news :)

No, srsly...¶

  • OFRAK release in first half of August 2022
  • Still figuring out lawyer part, but likely:
    • Core Python APIs will be publicly available
    • GUI component available for download
  • Looking forward to community contributions

Sign up for notifications of release at ofrak.com¶

And if you want to get even more involved, we're hiring!¶