redballoonsecurity / ofrak

OFRAK: unpack, modify, and repack binaries.
https://ofrak.com
Other
1.86k stars 127 forks source link

Updating ResourceView attributes is unintuitive. #411

Open whyitfor opened 8 months ago

whyitfor commented 8 months ago

What is the use case for the feature? It would be nice to be able to updated a ResourceView field in OFRAK with code similar to the following:

>>> main_cb
ComplexBlock(virtual_address=4195898, size=38, name='main')
>>> main_cb.name = "wow"
    await main_cb.resource.save()
    wow = await binary_resource.get_only_descendant_as_view(
        v_type=ComplexBlock,
        r_filter=ResourceFilter(
            attribute_filters=(ResourceAttributeValueFilter(ComplexBlock.Symbol, "wow"),)
        ),
    )
>>> wow
ComplexBlock(virtual_address=4195898, size=38, name='wow')
>>> main_cb
ComplexBlock(virtual_address=4195898, size=38, name='wow')

Unfortunately, this fails, since the filter for a ComplexBlock with Symbol returns no results. Interestingly enough, filtering for the Symbol "main" works, but this actually returns a ComplexBlock where ComplexBlock.name == "wow"!

OFRAK currently works around this issue with the following unidiomatic, unintuitive code: https://github.com/redballoonsecurity/ofrak/blob/master/ofrak_core/ofrak/core/patch_maker/linkable_binary.py#L304.

Does the feature contain any proprietary information about another company's intellectual property? No.

How would you implement this feature? I would like to see the following code update indexable attributes:

main_cb.name = "wow"
await main_cb.resource.save()
wow = await binary_resource.get_only_descendant_as_view(
    v_type=ComplexBlock,
    r_filter=ResourceFilter(
        attribute_filters=(ResourceAttributeValueFilter(ComplexBlock.Symbol, "wow"),)
    ),
)

This probably involves updating indexable attributes whenever the underlying attribute is updated. We should never have inconsistency between the indexable attributes and the actual attributes, and ideally users do not need to know much about this distinction.

Are there any (reasonable) alternative approaches? Perhaps -- open to them!

Are you interested in implementing it yourself? Sure!

whyitfor commented 8 months ago

The following code snippet adapting example_1 can be used to recreate this issue:

"""
This example showcases the simplicity of performing binary code modifications with OFRAK.

The input program is a compiled binary ELF file which prints "Hello, World!" to the console.

The example performs code modification in the input binary (without extension). It leverages
Ghidra to analyze the executable binary, and Keystone to rewrite an instruction so that the
binary loops back to its beginning instead of returning and exiting at the end of the main function.

Someone is chasing its tail and never catching it 😹
"""
import argparse
import dataclasses
import os

import ofrak_angr
from ofrak import OFRAK, OFRAKContext, ResourceFilter, ResourceAttributeValueFilter
from ofrak.core import (
    ProgramAttributes,
    BinaryPatchConfig,
    BinaryPatchModifier,
    ComplexBlock,
    Instruction,
)
from ofrak.service.assembler.assembler_service_keystone import KeystoneAssemblerService

ASSETS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "assets"))
BINARY_FILE = os.path.join(ASSETS_DIR, "example_program")

async def main(ofrak_context: OFRAKContext, file_path: str, output_file_name: str):
    # Create resource
    binary_resource = await ofrak_context.create_root_resource_from_file(file_path)

    # Unpack resource
    await binary_resource.unpack_recursively()
    # Get the "main" function complex block
    main_cb = await binary_resource.get_only_descendant_as_view(
        v_type=ComplexBlock,
        r_filter=ResourceFilter(
            attribute_filters=(ResourceAttributeValueFilter(ComplexBlock.Symbol, "main"),)
        ),
    )
    print(f"main: {main_cb}")

    # What we want
    main_cb.name = "wow"
    await main_cb.resource.save()

    # What actually works
    # main_cb.resource.add_view(dataclasses.replace(main_cb, name="wow"))
    # await main_cb.resource.save()

    wow = await binary_resource.get_only_descendant_as_view(
        v_type=ComplexBlock,
        r_filter=ResourceFilter(
            attribute_filters=(ResourceAttributeValueFilter(ComplexBlock.Symbol, "wow"),)
        ),
    )
    print(f"wow: {wow}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--hello-world-file", default=BINARY_FILE)
    parser.add_argument("--output-file-name", default="./example_1_meow")
    args = parser.parse_args()

    ofrak = OFRAK()
    ofrak.injector.discover(ofrak_angr)
    ofrak.run(main, args.hello_world_file, args.output_file_name)