Running with a peripheral device

Ryan Torvik - May 24, 2023 - 5 minute read

Tulip Tree Technology

Last time, we created our tenkey calculator device in python. Now let’s hook that device into our Emerson project and run our firmware again.

Add our device to the project YAML

In order to let the processor communicate with our device, we have to add it to the memory map. Last time, we saw that the firmware is trying to communicate to a device that’s mapped in at address 0x2000_0000. Now that we’ve created that device, let’s drop it in our system configuration YAML.

name: tenkey
processor:
  architecture: generic.mips32
  initial_registers:
    pc: 0x1fc00000
    sp: 0x1000
  details:
    keystone: mips32be
  devices:
    - name: ram
      start_address: 0x00000000
      size:          0x02000000 # 32MB of ram
      kind: generic.sparse_ram
      endian: big
    - name: flash
      start_address: 0x1fc00000
      size:          0x00400000
      kind: generic.rom
      endian: big
      details:
        backing_file: flash.img
    - name: tenkey_device
      start_address: 0x20000000
      size: 0x8
      kind: plugin.python
      endian: little
      details:
        path: tenkey.py

You’ll see that we added our tenkey_device at address 0x2000_0000 and made it only size 8. The size just tells us the range of addresses that will be sent to this device. Since we are only need 4 byte accesses at 0x2000_0000 and 0x2000_0004 and this is a toy example, we’ll just put the size at 8. Real systems have lots more that go into determining the size of the memory windows for a device. Finally, we set the path to our python script relative to the project directory.

Run Emerson

Just like before, we’ll run our Emerson docker image, forward the port Emerson is listening on, and map in our tenkey project directory.

You’ll notice that there is a .snap file in the tenkey directory. When we previously HaltedOnError, Emerson automatically took a snapshot so we can review what happened later. If we had enabled save points, we would be able to rewind more thoroughly review the error.

Just like before, we load our project up.

Now that it’s running, let’s figure out if our firmware does the calculation we expect it to.

Here’s the original source of the main function.

int main(void) {
    int nums[10] = {1};
    int last = 0, current = 1;
    const int LEN = sizeof(nums)/sizeof(int);
    for (int i = 0; i < LEN - 1; i++) {
        load_register(nums[i]);
        select_add();
        nums[i+1] = read_result();
    }
    return nums[LEN - 1];
}

Our Emerson project is using the actual compiled instructions, so we need a way to affiliate our source code with the instructions that Emerson is going to run. We are returning from main the result of our fibonacci calculation. We need to figure out where the return value of main is stored. Then we can breakpoint it and see what the result is.

For a refresher on how we compiled the code for different scenarios, see our post on Looking at Firmware. We are going to open the PE file version of our executable code in BinaryNinja.

View the code in Binary Ninja

Binja has several different intermediate representations to give you different ways of looking at a target binary. It’s High-IL takes the assembly bytes, does some thorough analysis of it, and generates C code that very closely resembles our source code.

You can see that the instructions that do the return start at offset 0x6c0. The Medium Level IL gives a view that maps register names to the variables it has figured out. It also flattens out the loops into if-then-branch constructs which more accurately represent how the instructions actually work.

You can see in the Medium Level IL that the value in $v0_2 is being returned from this function. This probably refers to the register v0. Even if we don’t know semantics of a MIPS call return, we can figure out what’s happening with BinaryNinja.

The Low Level IL gives even more architecture specific information; inferring where variables are stored on the stack, etc.

And finally, the disassembly view gives us the actual instructions that are in this section of the compiled code.

You can see here that we return the value in the register v0. Looking at our original source code, we can see that after we are done computing the fibonacci number, we return it from main. So, if we put a breakpoint on the instruction that sets the value in v0, step forward one, and v0 should have our computed fibonacci number.

Set a breakpoint and see what happens

You’ll recall, we compiled our firmware in 2 ways. We compiled it as a standard PE file (above) that can be loaded into Binja Cloud. We also stripped out our code and made a Position Independent Executable (PIE). In our system configuration, we loaded the PIE at 0x1fc0_0000. Doing some quick arithmetic, 0x6b0 - 0x620 = 0x90. So, we will set our breakpoint at 0x1fc0_0090 .

To do that we type bp 0x1fc00094 into the Emerson console. Then click on the Go button.

See where we’ve stopped. Notice the value in v0. The next instruction is going to overwrite it with the value in 0x3c(sp) which is sp + 0x3c. Since sp is 0xFB0 we’ll look at the memory starting at 0xFEC. You’ll notice that this is the same value we are currently holding in v0. We’ll step through this just to make sure.

If we step forward one, v0 is still 0x100, that’s 256 in decimal. That’s the result of our fibonacci calculation!

But, is that right value? We are supposed to be calculating the 10th fibonacci number. Did we do that?

The result

55 ≠ 256

Nope.

Well, that’s unfortunate. Something is amiss. Either we’re looking in the wrong place, our device is doing the wrong thing, or our firmware is incorrect (or a combination of the three). Next time, we’ll spend some time debugging and setting up tests for our firmware.

Previous Post Next Post

Tulip Tree Technology
Learn Deep | Dream Big

© 2024 Tulip Tree Technology