Example 2: Simple Code Modification
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.
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
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 😹
Example OFRAK script:
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91 | import argparse
import os
import ofrak_ghidra
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"),)
),
)
# Get the ret instruction within the main function
ret_instruction = await main_cb.resource.get_only_descendant_as_view(
v_type=Instruction,
r_filter=ResourceFilter(
attribute_filters=(ResourceAttributeValueFilter(Instruction.Mnemonic, "ret"),)
),
)
# Assemble the code modification using Keystone assembler
# Modification: jump to the main function's entry point, creating an infinite loop
assembler_service = KeystoneAssemblerService()
program_attributes = await binary_resource.analyze(ProgramAttributes)
new_instruction_bytes = await assembler_service.assemble(
assembly=f"jmp {main_cb.virtual_address}",
vm_addr=ret_instruction.virtual_address,
program_attributes=program_attributes,
)
# Patch in the modified bytes
range_in_root = await ret_instruction.resource.get_data_range_within_root()
binary_injector_config = BinaryPatchConfig(
range_in_root.start,
new_instruction_bytes,
)
await binary_resource.run(BinaryPatchModifier, binary_injector_config)
# Dump the modified program to disk
await binary_resource.pack()
await binary_resource.flush_data_to_disk(output_file_name)
print(f"Done! Output file written to {output_file_name}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--hello-world-file", default=BINARY_FILE)
parser.add_argument("--output-file-name", default="./example_2_infinite_hello")
args = parser.parse_args()
ofrak = OFRAK()
ofrak.injector.discover(ofrak_ghidra)
ofrak.run(main, args.hello_world_file, args.output_file_name)
|