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.
!chmod +x /examples/assets/example_program
!./examples/assets/example_program
Hello, World!
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")
!chmod +x hello_summercon
! ./hello_summercon
Summercon!
OFRAK (Open Firmware Reverse Analysis Konsole) is a software tool that combines the ability to unpack, analyze, modify, and repack binaries.
OFRAK is designed to be extended with additional components and capabilities, in addition to its core framework.
When it comes to an embedded device's firmware...
You don't know what's there
You don't know what isn't there
You don't know how to change it
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.
We need something that:
Resource
¶ResourceView
¶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)
Identifier
components perform logic to determine what tags to add to a resource.await resource.identify()
list(resource.get_tags())
[FilesystemEntry, GenericBinary, LinkableBinary, File, Program, Elf]
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.
await resource.unpack()
children = list(await resource.get_children())
print(len(children))
print(children[1])
69 Resource(resource_id=48c0eb6470494a12982290d57c6733d2, tag=[ElfHeader], data=ddb67e9dfbca4572ba2b007a1ef29534)
Analyzer
components exract more detailed information about a resource.ResourceAttributes
. from ofrak.core.architecture.attributes import ProgramAttributes
await resource.analyze(ProgramAttributes)
ProgramAttributes(isa=<InstructionSet.X86: 'x86'>, sub_isa=None, bit_width=<BitWidth.BIT_64: 64>, endianness=<Endianness.LITTLE_ENDIAN: 'little'>, processor=None)
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.
import ofrak_components_binary_ninja
import ofrak_components_ghidra
# ofrak.discover(ofrak_components_binary_ninja)
ofrak.discover(ofrak_components_ghidra)
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.
Modifier
s 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.
from ofrak_components.string import (StringFindReplaceConfig,
StringFindReplaceModifier)
await resource.run(StringFindReplaceModifier,
StringFindReplaceConfig(
"Hello, World!",
"Summercon!",
null_terminate=True
)
)
ComponentRunResult(components_run={b'StringFindReplaceModifier'}, resources_modified=set(), resources_deleted=set(), resources_created=set())
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".
await resource.pack()
await resource.flush_to_disk("hello_summercon")
# 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
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)
# Make LOAD program header non-executable
await load_program_header.resource.run(
ElfProgramHeaderModifier,
ElfProgramHeaderModifierConfig(
p_flags=load_program_header.p_flags &
~ElfProgramHeaderPermission.EXECUTE.value
),
)
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())
# Dump the modified program to disk
await resource.pack()
await resource.flush_to_disk("hello_no_run")
!chmod +x hello_no_run
!./hello_no_run
Segmentation fault (core dumped)
wat do u mean program no run?? 😿
First, lets use dynamips (Cisco router emulator) to see what booting looks like.
resource = await context.create_root_resource_from_file(
"ssb288xx.BE-01-007.sbn"
)
await resource.unpack()
resource
Resource(resource_id=c455e62245244186b30442c9b88c36ae, tag=[FilesystemEntry,GenericBinary,UImage,File], data=e580cb8ca4bc409289a6a4147b26fc90)
# 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()
# 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()
# 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')
# 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,
)
# 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
),
}
)
!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; }
!cat trust_vuln_patches_src/handle_crypto_smc_patch.S
LDR R0, [R5, #0x18] STR R3, [R5, #0xc]
FunctionReplacementModifier
¶# 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
ComponentRunResult(components_run={b'FunctionReplacementModifier'}, resources_modified=set(), resources_deleted=set(), resources_created=set())
Using PatchMaker directly!
# 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,
)
# 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
fem
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))
# Inject Patch
await patch_maker.inject_patch(fem, uimage_body.resource)
Resource(resource_id=aae1868d479e44f58effc8fca6d403dd, tag=[GenericBinary,BinaryNinjaAnalysisResource,LinkableBinary,UImageBody,Program], data=033d2028fc714e088b5571b4d7ca6090)
_ = await uimage.resource.pack()
await uimage.resource.flush_to_disk("ssb288xx.BE-01-007.sbn.patched")
We're interested in seeing what you do with OFRAK :)
resource = await context.create_root_resource_from_file(
"/examples/assets/example_program"
)
await resource.unpack()
# OFRAK You!
await resource.pack()
ComponentRunResult(components_run=set(), resources_modified=set(), resources_deleted=set(), resources_created=set())