Skip to content

ResourceView

Introduction

A ResourceView is a Python class which represents some firmware abstraction. It is another way of "looking at" an OFRAK resource, with an interface specific to that firmware abstraction. For example:

elf = await resource.view_as(Elf)
for section in elf.get_sections():
    print(section.name)
elf is an instance of the Elf ResourceView. It has a method get_sections which is specific to ELF files. This method gets the resources of each section in the ELF, and then gets a view of them as an ElfSection. The ElfSection view has a field name which corresponds to an attribute of the underlying resource.

A class inheriting from ResourceView is also a valid tag to use on resources - It is a ResourceTag. Really it is a special case, a ViewableResourceTag, which means that a resource with this tag can supply a ResourceView of that type. In the previous example, resource must have been tagged as an Elf.

A ResourceView is a dataclass, so it has a number of fields and an auto-generated constructor to populate them. It may also have some methods. A ResourceView can be used on its own, and like a normal Python class. For example you can instantiate views, and set/get their fields without interacting with an OFRAK resource at all.

We can look at another example:

@dataclass
class Symbol(ResourceView):
  name: str
  vaddr: int

# Simple to instantiate
new_sym = Symbol("main", 0x1000100)
print(new_sym.name)  # >> "main"
print(hex(new_sym.vaddr))  # >> "0x100000"
The example shows how a viewable tag is declared (Symbol) and how it can be used independently of any resources. We can also get a view from a resource, once the resource has that viewable tag:

my_resource.add_tag(Symbol)
my_sym = await my_resource.view_as(Symbol)

print(my_sym.name)
print(hex(my_sym.vaddr))

The output of the last 2 print statements is not shown. What actual values does that view have? Where do they come from? This is explained in the next section.

ResourceView and ResourceAttributes

In short, when the view is created in the view_as call, OFRAK attempts to analyze the resource to find those values. Like a normal analyze call, it will first check if the resource already has up-to-date attributes and use those; otherwise it will look for an appropriate analyzer. In this case the attributes do not exist, and OFRAK will look for an analyzer which outputs AttributesType[Symbol]. AttributesType[Symbol] is simply a way to access the class of ResourceAttributes associated with Symbol. This ResourceAttributes class is automatically generated.

We don't need to rely on an analyzer - we can also add the attributes manually:

my_resource.add_tag(Symbol)
my_resource.add_attributes(AttributesType[Symbol]("foo", 0x1000200))
my_sym = await my_resource.view_as(Symbol)

print(my_sym.name)  # >> "foo"
print(hex(my_sym.vaddr))  # >> "0x1000200"

ResourceViews provide a way to access the underlying resource (if it exists). .resource returns a Resource and is how you should access the resource when you need it. If the view does not have an underlying resource, a ValueError is raised:

new_sym = Symbol("main", 0x1000100)
new_sym.resource  # ValueError("Cannot access ResourceView's resource because it has not been set!")

Relationship Between View and Resource

Adding methods to get other resources/views related to the current resource is a common pattern. Because this depends on the view having a resource, it will fail for views which are not created from a resource. One rule of thumb to avoid accidentally calling a method which requires an underlying resource is that methods which interact with the resource tree must be async; therefore synchronous methods are almost always safe to call, while asynchronous methods probably require an underlying resource.

Not only can you create a view from a resource, but you can go the other way around:

new_sym = Symbol("main", 0x1000100)
new_sym_r = await parent_resource.create_child_from_view(new_sym, data_range=Range(0x120, 0x140))
new_sym2 = await new_sym_r.view_as(Symbol)

Notice that when we create the child, we need to pass in a data range. The view does not hold any information about the data or data mapping, so that must be supplied when the resource is created. Once new_sym_r is created, we can again request a view from it. We'll find that it has all the same attributes as the original new_sym.

Views are read-only: Modifying the fields of a view will not modify the attributes of the underlying resource, nor will modifying the attributes of the underlying resources automatically update the fields of an existing view. Instead, the underlying resource should be explicitly modified (ideally by a Modifier component) and then a new view should be created by calling resource.view_as(...) again.

my_sym1 = await my_resource.view_as(Symbol)
my_sym1.name = "get_pwned"

my_sym2 = await my_resource.view_as(Symbol)
assert my_sym1.name == my_sym2.name  # Fails because only the field of my_sym1 is changed!
await my_sym1.resource.run(ExampleSymbolModifier, ExampleSymbolModifierConfig("modified name"))
assert my_sym1.name == "modified name"  # Fails because the view is not modified, only the resource
assert my_sym2.name == "modified name"  # Fails because the view is not modified, only the resource

my_sym3 = await my_resource.view_as(Symbol)
assert my_sym3.name == "modified name"  # Passes because fresh view includes resource modification

When To Use

Now that you know how to use views, when should you use them? Get a view of a resource when you either need to access functionality that a view provides through one of its methods, or when you are going to be reading several attributes from a resource.