DEF CON 30 Badge Fun with OFRAK

The TL;DR? We used OFRAK to rewrite the badge firmware so that it auto-plays the solution for Challenge 1.

Est. read time: 20 min read

The code referenced in this writeup can be foundย here.

ย 

ย 

DEF CON 30 just ended, and the badge this year was awesome. It included a playable synthesizer with a few instrument presets, as well as buttons, a screen, and a small speaker. Everything on the badge was driven by a Raspberry Pi Pico. As usual, the badge also had an associated reverse engineering challenge.

ย 

ย 

Several of us fromย Red Balloon Security attended and mannedย  booths in the Aerospace Village and Car Hacking Village. Many of our demos were based on OFRAK, which we released publicly at DEF CON 30. Since OFRAK is a binary reverse engineering and modification platform, it naturally became our tool of choice for badge firmware modification.

ย 

ย 

This post walks through using OFRAK to modify the DEF CON 30 Badge firmware in fun and exciting ways. We are unabashedly building off ofย this great write-up.ย @reteps, we owe you a beer! (Or a ginger ale, since it seems like you may not be old enough to drink just yet.)

ย 

This write-up is long, so feel free to skip ahead to the parts that interest you:

Table of Contents

Set up OFRAK

To walk through this writeup with us, you will need to installย picotoolย andย ofrak. Run these steps in the background while you read the rest of this document.

For this writeup, we used theย redballoonsecurity/ofrak/ghidraย Docker image.

  1. Make sure you have Git LFS set up.

    which git-lfs || sudo apt install git-lfs || brew install git-lfs
    git lfs install
  2. Cloneย OFRAK.

    git clone https://github.com/redballoonsecurity/ofrak.git
    cd ofrak
  3. Install Docker.

  4. Build an OFRAK Docker image with Ghidra. This will take several minutes the first time, but should be quick to rebuild later on. Continue reading and come back when it is finished!

    # Requires pip
    python3 -m pip install --upgrade PyYAML
    
    DOCKER_BUILDKIT=1 \
    python3 build_image.py --config ./ofrak-ghidra.yml --base --finish

    Check it is installed by looking forย redballoonsecurity/ofrak/ghidraย near the top of the output of the following command.

    docker images
  5. Run an OFRAK Docker container.ย These instructionsย have more information about running OFRAK interactively.

    mkdir --parents ~/dc30_badge
    
    docker run \
      --rm \
      --detach \
      --hostname ofrak \
      --name ofrak \
      --interactive \
      --tty \
      --publish 80:80 \
      --volume ~/dc30_badge:/badge \
      redballoonsecurity/ofrak/ghidra:latest
  6. Check that it works by going toย http://localhost. You should see the OFRAK GUI there.

We use picotoolto export the firmware image.

  1. Install the dependencies. For example, on Ubuntu:

    sudo apt install build-essential pkg-config libusb-1.0-0-dev cmake make
    
    git clone https://github.com/raspberrypi/pico-sdk.git
    git clone https://github.com/raspberrypi/picotool.git
  2. Build picotool.

    pushd picotool
    mkdir --parents build
    cd build
    PICO_SDK_PATH=../../pico-sdk cmake ..
    make -j
    sudo cp picotool /usr/local/bin/
    popd

You can now useย picotoolย to export the firmware image from the device. To do this, the badge must be inย BOOTSEL. To put the badge inย BOOTSEL, hold down the badgeโ€™s down button while powering the device, or short theย J1ย pins on the back with a jumper wire. You can now connect the device to your computer over micro USB.

If you have done this correctly, runningย picotoolย should give the following output:

				
					$ sudo picotool info -a
Program Information
 name:          blink
 description:   DEF CON 30 Badge
 binary start:  0x10000000
 binary end:    0x100177cc

Fixed Pin Information
 0:   UART0 TX
 1:   UART0 RX
 25:  LED

Build Information
 sdk version:       1.3.0
 pico_board:        pico
 boot2_name:        boot2_w25q080
 build date:        Jul 17 2022
 build attributes:  Debug

Device Information
 flash size:   2048K
 ROM version:  3
				
			

You can now dump the badge firmware as a raw binary file, badge_fw.bin, using the following command:

				
					mkdir --parents ~/dc30_badge
sudo picotool save -a -t bin ~/dc30_badge/badge_fw.bin
				
			

Insert logo with OFRAK GUI

First things first โ€“ let’s replace the DEF CON logo that appears when the badge is powered on with an OFRAK logo!

1. Load the image into the OFRAK GUI.

2. We know fromย the reteps writeupย that the DEF CON logo is at offsetย 0x13d24, so we can use the โ€œCarve Childโ€ feature in the OFRAK GUI to unpack it as a separate resource.

ย 

Carve from offsetย 0x13d24ย with a size of 80 by 64 pixels, each of which is stored in a single bit (so divide by 8 to get the number of bytes).

3. Download the child and verify that it’s the correct range by loading it in GNU Image Manipulation Program (GIMP).

Looks good!

ย 

4. Download this pre-built OFRAK Logo from here, or expand more information about building a custom image below.

  1. For making a custom image, first, create a new canvas and load your image as a layer resized for the canvas.


  2. Load your image, and resize it and invert the colors if necessary. The OFRAK Logo is a great candidate image.


  3. Convert the image to 1-bit color depth with dithering. (For more about dithering, check out this article.)




  4. Merge all the layers into one by right-clicking in the layers pane on the left.




  5. Export the image with Ctrl+Shift+E (Cmd on Mac), or use File > Export As.... Pick PNG.



  6. Convert the PNG to raw 1-bit data with ImageMagick, based on the instructions here.

    # Install ImageMagick if you don't have it
    which convert || sudo apt install imagemagick || brew install imagemagick
    
    # Convert the image
    convert myimage.png -depth 1 GRAY:shroomscreen.bin
    
    # Verify that it is 640 bytes
    wc -c shroomscreen.bin

5. Use the OFRAK GUI “Replace” feature to replace the data.

6. Pack the whole thing back up.

7. Download the resulting firmware image and flash it onto the device.

				
					cp "$(ls -rt ~/Downloads | tail -n 1)" ~/dc30_badge/ofrakked.bin
sudo picotool load ~/dc30_badge/ofrakked.bin

				
			

8. Verify that it works by booting up the badge.

Looks good!

We can now automate this step in future firmware mods by using the following Python function:

				
					async def ofrak_the_logo(resource: Resource):
      """
      Replace the DEF CON logo with OFRAK!
      """
      logo_offset = 0x13d24
      ofrak_logo_path = "./shroomscreen.data"
      with open (ofrak_logo_path, "rb") as f:
          ofrak_logo_bytes = f.read()
      resource.queue_patch(Range.from_size(logo_offset, len(ofrak_logo_bytes)), ofrak_logo_bytes)
      await resource.save()
				
			

Change some strings

It is easy to use OFRAK to change strings within the badge firmware. The function ofrak_the_strings (listed below) changes the “Play” button on the badge’s menu to display “OFRAK!” and hijacks the credits, giving credit to OFRAK mascots (“mushroom”, “caterpillar”) and “rbs.”

				
					async def ofrak_the_strings(resource: Resource):
        """
        Change Play menu to OFRAK!

        Update credits to give credit where due
        """
        # First, let's overwrite Play with "OFRAK!"
        await resource.run(
            StringFindReplaceModifier,
            StringFindReplaceConfig(
                "Play",
                "OFRAK!",
                True,
                True
            )
        )
        # Let's overwrite credits with OFRAK animal names
        await resource.run(
            StringFindReplaceModifier,
            StringFindReplaceConfig(
                "ktjgeekmom",
                "mushroom",
                True,
                False
            )
        )
        await resource.run(
            StringFindReplaceModifier,
            StringFindReplaceConfig(
                "compukidmike",
                "caterpillar",
                True,
                False
            )
        )
        await resource.run(
            StringFindReplaceModifier,
            StringFindReplaceConfig(
                "redactd",
                "rbs",
                True,
                False
            )
        )
				
			

Press any key to win Challenge 1

OK, now on to Challenge 1! For those of you who didn’t participate in BadgeCon: You win Challenge 1 on the DEF CON Badge if you play the melody to Edward Grieg’s Peer Gynt.

ย 

Peer Gynt is nice, but some of us can’t play the piano (or are too lazy). We want to win Challenge 1 without any musical skills/effort.

ย 

The reteps writeup points us to a two-byte binary patch that does just that. The ofrak_challenge_one function below patches the badge firmware such that pressing any key wins Challenge 1!

				
					async def ofrak_challenge_one(resource: Resource):
      """
      Win challenge 1 by pressing any key!
      """
      check_challenge_address = 0x10002DF0
      win_address = 0x10002E20
      jump_asm = f"b {hex(win_address)}"
      jump_bytes = await assembler_service.assemble(
          jump_asm, check_challenge_address + 4, ARCH_INFO, InstructionSetMode.THUMB
      )

      await resource.run(
          BinaryInjectorModifier,
          BinaryInjectorModifierConfig([(0x10002DF0 + 4, jump_bytes)]),
      )
				
			

You’re welcome.

Autoplay Notes (Piano Player) to win Challenge 1

Jumping right to the win condition is fun and all, but isn’t half the fun of the badge that it makes sounds? What if we could just have it… make sounds? Sounds that happen to make us win?

ย 

The goal of this section is to use OFRAK to patch the badge firmware into “Player Piano” mode: When you start Challenge 1, the badge autoplays Peer Gynt for you and you win. This is not too complicated, but it requires us to put on our Reverse Engineer hats and dig deeper into the firmware.

ย 

Step 1: Reverse Engineering

The first step was to pull the firmware and throw it into Ghidra. Luckily, we didn’t have to start from scratch.

ย 

Step 0: Plagiarize Survey the Literature

Shoutout (again) to the reteps writeup, which was a great starting point. If he shared his Ghidra project, we didn’t see it, but in his writeup we could see one important function labeled and with a full address! What he called z_add_new_note_and_check at 0x10002df0, we called check_challenge, but it does the same thing either way. That was essentially our starting point, from which all other analysis stemmed.

Step 1v2: Reverse Engineering

Our first approach was looking at code xrefs to check_challenge since A) that was our foothold and we did not have any other good starting points, and B) the latest note played was passed to this function, so it seemed to make sense to trace that data flow and find out how the latest note played is read. Then, in theory, we could write a new note there programmatically. The immediate problem was that most usages of check_challenge were in a function we affectionately called big_chungus because it was large and hard to understand. The decompilation looked like this:

Which was essentially unusable except in very local instances.

ย 

The next approach we took was looking at strings. We quickly found some interesting strings we had seen on the screen, so we followed those references and found a number of functions related to drawing pixels (below screenshot shows them after they were labeled):

This led to the functions that drew each of the menus, which gave us a good idea of the state machine that the firmware uses. Throughout the process, we used OFRAK to experiment with different hypotheses by injecting bits of assembly to poke at addresses. For example:

				
					async def overwrite_state_pointers(resource):
    # Effect: main menu does not change image when i move to different options
    # (they are still selected, as we can click through them)
    new_state_pointer_bytes = struct.pack("<i resource.run binaryinjectormodifier binaryinjectormodifierconfig new_state_pointer_bytes async def main root_resource="await" ofrak_context.create_root_resource_from_file root_resource.add_tag root_resource.add_attributes root_resource.add_view firmware_size await root_resource.save overwrite_state_pointers and other experiments... root_resource.flush_to_disk>
				</i>
			

This helped us to confirm or reject these hypotheses. It was also just fun to change the behavior. We used this function to change all of the keys’ associated light colors to green, since the code for that is all in a big regularly-patterned block and we could iterate over it at constant offsets:

				
					async def set_all_key_lights(resource, rgb):
      first_color_load_vaddr = 0x10004cf0
      color_loads_offset = 0xe

      set_red_instr = f"movs r0, #0x{rgb[0]:x}"
      set_green_instr = f"movs r1, #0x{rgb[1]:x}"
      set_blue_instr = f"movs r2, #0x{rgb[2]:x}"

      mc = await assembler_service.assemble(
          "\n".join([set_blue_instr, set_green_instr, set_red_instr]),
          first_color_load_vaddr,
          arch_info,
          InstructionSetMode.THUMB,
      )
      
      resource.run(
          BinaryInjectorModifier,
          BinaryInjectorModifierConfig(
              [
                  (color_load_vaddr, mc)
                  for color_load_vaddr in range(first_color_load_vaddr, 0x10004dc2, color_loads_offset)
              ]
          ),
      )
				
			

After mucking around for a while, we were not completely sure we had found the “source” of the notes. We had some ideas, though they would require more complex experiments, which would be cumbersome to write in assembly. At this point, we decided to set up the OFRAK PatchMaker for the badge firmware.

Step 2: PatchMaker

The PatchMaker is a Python package for building code patch blobs from source and injecting them into an executable OFRAK resource. In this case, we wanted to be able to “mod” the badge firmware by just writing out some C code with full access to the existing functions and data already in the device.

ย 

The first step is to set up the toolchain configuration:

				
					
TOOLCHAIN_CONFIG = ToolchainConfig(
    file_format=BinFileType.ELF,
    force_inlines=False,
    relocatable=False,
    no_std_lib=True,
    no_jump_tables=True,
    no_bss_section=True,
    compiler_optimization_level=CompilerOptimizationLevel.SPACE,
    check_overlap=True,
)
TOOLCHAIN_VERSION = ToolchainVersion.GNU_ARM_NONE_EABI_10_2_1
				
			

This is pretty standard stuff for C-patching an existing firmware. We decided to use the PatchFromSourceModifier to do that actual patching, as it hides some of the nitty-gritty of building a patch (though it consequently has fewer options than going through the core PatchMaker API).

The next step is to define the symbols that can be used from the patch source code. These need to be exposed to PatchMaker by adding some LinkableSymbol data structure to the existing Program:

				
					LINKABLE_SYMBOLS = [
    # Existing variables in binary
    LinkableSymbol(0x20026eea, "notes_held_bitmap", LinkableSymbolType.RW_DATA, InstructionSetMode.NONE),
    LinkableSymbol(0x200019d8, "octave", LinkableSymbolType.RW_DATA, InstructionSetMode.NONE),
    LinkableSymbol(0x20001991, "most_recent_note_played", LinkableSymbolType.RW_DATA, InstructionSetMode.NONE),
    LinkableSymbol(0x200063d8, "notes_played", LinkableSymbolType.RW_DATA, InstructionSetMode.NONE),
    LinkableSymbol(0x20026f01, "instrument", LinkableSymbolType.RW_DATA, InstructionSetMode.NONE),

    # Existing functions in binary
    LinkableSymbol(0x10005074, "draw_rect_white", LinkableSymbolType.FUNC, InstructionSetMode.THUMB),
    LinkableSymbol(0x10004fc4, "write_character", LinkableSymbolType.FUNC, InstructionSetMode.THUMB),
    LinkableSymbol(0x1000503c, "write_text", LinkableSymbolType.FUNC, InstructionSetMode.THUMB),

]

# ... Then later add to resource with:

await resource.run(
        UpdateLinkableSymbolsModifier,
        UpdateLinkableSymbolsModifierConfig(tuple(LINKABLE_SYMBOLS)),
    )
    

				
			

And they need to be exposed to the C code by declarations, as one might normally see in a header:

				
					#include <stdint.h>

extern uint16_t notes_held_bitmap;
extern uint8_t octave;
extern uint8_t most_recent_note_played;
extern uint8_t notes_played[];
extern uint8_t instrument;

extern void draw_rect_white(unsigned int x, unsigned int y, unsigned int x_end, unsigned int y_end);
extern void write_character(char c, int x, int y, int color); // 0=white, 1=black
extern void write_text(const char* str, int x, int y, int color); // 0=white, 1=black</stdint.h>
				
			

Then we could write some C code referencing those; no spoilers though, we’ll show that code later! To actually build it, we create an empty root resource to hold the source code and run PatchFromSourceModifier:

				
					async def patch_in_function(ofrak_context, root_resource: Resource):
      """
      Patch in the auto-player that plays the sequence to solve challenge 1.
      """
      # Not strictly necessary, but nice to really clear all "free space"
      await overwrite_draw_volume_info(resource)

      source_bundle_r = await ofrak_context.create_root_resource(
          "", b"", tags=(SourceBundle,)
      )
      source_bundle: SourceBundle = await source_bundle_r.view_as(SourceBundle)
      with open(PATCH_SOURCE, "r") as f:
          await source_bundle.add_source_file(f.read(), PATCH_SOURCE)

      await resource.run(
          UpdateLinkableSymbolsModifier,
          UpdateLinkableSymbolsModifierConfig(tuple(LINKABLE_SYMBOLS)),
      )

      await resource.run(
          PatchFromSourceModifier,
          PatchFromSourceModifierConfig(
              source_bundle_r.get_id(),
              {
                  PATCH_SOURCE: (
                      Segment(
                          ".text",
                          DRAW_VOLUME_RANGE.start,
                          0,
                          False,
                          DRAW_VOLUME_RANGE.length() - 0x50,
                          MemoryPermissions.RX,
                      ),
                      Segment(
                          ".rodata",
                          DRAW_VOLUME_RANGE.end - 0x50,
                          0,
                          False,
                          0x50,
                          MemoryPermissions.R,
                      ),
                  ),
              },
              TOOLCHAIN_CONFIG,
              TOOLCHAIN_VERSION,
          ),
      )
				
			

The source bundle resource ID, the TOOLCHAIN_CONFIG, and TOOLCHAIN_VERSION were already explained but what about the Segments?

ย 

Step 3: Free Space & Segments

ย 

In order to inject code, we obviously need a location to inject it into. There are three options for how to obtain this:

ย 

  1. Find some unused space in the binary.
  2. Enlarge/extend the firmware binary so more bytes are loaded into memory.
  3. Replace something that already exists in the binary.

ย 

These are roughly ordered from “best” to “worst.” Ideally we want to change as little possible in the binary. In this situation though, we were limited to the third option:

ย 

  1. We did not have complete knowledge of the binary and could not say with 100% confidence that some part was unused (this is usually the case).
  2. We did not yet have an OFRAK packer/unpacker for uf2, the file format the binary was in.

ย 

So the next task was to choose something to overwrite. We found the function that drew the little volume slider on the side, and this seemed a good choice because:

ย 

  • It would free up a decent amount of space (over 256 bytes to drop THUMB code in).
  • It was called often and consistently (alongside other screen-updating code).
  • Removing it would give us some real estate on the right edge of the screen to write/draw new stuff to!

ย 

We verified that this would have no ill effects by gutting the contents of the function with nop instructions:

				
					async def overwrite_draw_volume_info(resource):
      """
      Creates free space! But you no longer get to see the current volume and the nice arrows
      telling you which way to adjust it.
      """
      # Creates free space! But you no longer get to see the current volume
      # and the nice arrows telling you you can adjust it

      return_instruction = await assembler_service.assemble(
          "mov pc, lr",
          DRAW_VOLUME_RANGE.end - 2,
          ARCH_INFO,
          InstructionSetMode.THUMB,
      )

      nop_sled = await assembler_service.assemble(
          "\n".join(
              ["nop"] * int((DRAW_VOLUME_RANGE.length() - len(return_instruction)) / 2)
          ),
          DRAW_VOLUME_RANGE.start,
          ARCH_INFO,
          InstructionSetMode.THUMB,
      )

      final_mc = nop_sled + return_instruction
      assert len(final_mc) == DRAW_VOLUME_RANGE.length()

      await resource.run(
          BinaryInjectorModifier,
          BinaryInjectorModifierConfig([(DRAW_VOLUME_RANGE.start, final_mc)]),
      )
				
			

If we are just patching in some compiled C patch over the existing code, NOPing it out first isn’t strictly necessary, but it is a good sanity check that removing the function is probably fine. It also verifies the function does what we think it does: The volume slider is gone!

With our target address picked out, we defined the PatchMaker Segments where our compiled code and data would be inserted:

				
					Segment(
    ".text",
    DRAW_VOLUME_RANGE.start,
    0,
    False,
    DRAW_VOLUME_RANGE.length() - 0x50,
    MemoryPermissions.RX,
),
Segment(
    ".rodata",
    DRAW_VOLUME_RANGE.end - 0x50,
    0,
    False,
    0x50,
    MemoryPermissions.R,
),

				
			

The first is for the code, and the second is a healthy allocation for read-only data, like constants and strings.

At this point we were ready to start writing some C.

Step 4: The Payload

We wrote a number of experiments in C code, experimenting with various memory addresses and functions we were investigating. C is brilliant because it is so much nicer to work in than assembly, but just as unsafe. One trick we used liberally was the ability to cast memory locations to whatever pointer type we wanted: this allowed us to quickly iterate and peek/poke addresses that we thought contained interesting data Here are some snippets from our experiments:

				
					char instrument = *((char*) 0x20026f01);
write_character(instrument + 0x30 , 0x70, 12, 0);

char most_recent_c = most_recent_note_played;  // is an index form, not the actual note string
write_character(most_recent_c, 0x70, 22, 0);

write_character(notes_played[0x2d - 1], 0x7a, 22, 0);


int button_held = *((int*) 0xd0000004);
// Just copying the Ghidra decomp for these comparisons
// It's easier than thinking about which bit is being checked
if (-1 
				
			

This writes out the index of the currently selected instrument, and below that draws the two most recently played notes.

The characters drawn (“@”, “<“) representing the notes just happen to be ASCII; they are uint8_t indexes in essentially a long array of all possible notes in all octaves, so 84 values. G# in the lowest octave is the first visible “character”, at 0x20 meaning ” ” (space), below this the draw_character function just draws a white rectangle. Then B in the highest octave is the highest byte, 0x6B (“k”). Here “@” and “<” mean the most recent notes played are E and C in the 4th octave.

ย 

Recall that write_character is a function analyzed from the existing binary, and we can call it and link against it like writing normal C code! This is the power of PatchMaker.

ย 

At this point we had a good loop: Follow some code and/or data in Ghidra for a while until we think we understand it, then write a C patch to use that knowledge to test our theory. After a little bit, we had found a bitmap at 0x20026eea that seemed to store the info about which keys were currently held; some experiments confirmed this. At this point, we had all the information we needed to write a “Player Piano” for the badge!

ย 

Step 5: Forward Engineering

ย 

After all the reverse engineering, there were a few “forward” engineering challenges to consider, so we’ll just rapid fire through them:

ย 

Timing

We wanted the notes to be audible one after the other, so that meant we had to time them. We didn’t find any timing functions, and probably would not “trust” them even if we did. We decided to just use a counter we would increment each time our function was called (like a C static local variable) and play/increment notes according to that. This meant we needed some R/W space, which we implemented quick & dirty by finding some free scratch space and defining pointers to those as LinkableSymbols.

ย 

We got the addresses by going to the memory segment we had defined in Ghidra for in-memory RW data, and finding the address at which we stopped seeing references. Luckily this was 0x20026f04, not near an obvious page-end boundary, so we felt reasonably confident we could read/write to it as much as we wanted. Then we defined the LinkableSymbols for it:

				
					FREE_SCRATCH_SPACE = 0x20026f04
...

# Added these to the UpdateLinkableSymbolsModifierConfig shown earlier:

LinkableSymbol(FREE_SCRATCH_SPACE, "counter", LinkableSymbolType.RW_DATA,InstructionSetMode.NONE),
LinkableSymbol(FREE_SCRATCH_SPACE + 0x8, "seq_i", LinkableSymbolType.RW_DATA,InstructionSetMode.NONE),
LinkableSymbol(FREE_SCRATCH_SPACE + 0x10, "state", LinkableSymbolType.RW_DATA,InstructionSetMode.NONE),

				
			

In C we could use those as extern r/w variables:

				
					extern int counter;
extern int seq_i;
extern int state;

...

counter += 1;
    
    
if (counter >= NOTE_PERIOD) {
    seq_i += 1;
    if (seq_i >= (SEQUENCE_LENGTH + REST_COUNT)){
        seq_i = 0;
    }

    counter = 0;
}
else if (counter >= (NOTE_PERIOD - NOTE_HELD_T) && seq_i 
				
			

Storing and writing the sequence

Since the target we needed to write notes to was a bitmap, where each bit is a single note, it made sense to define each note as the bit in the bitmap it was mapped to. This could either be represented as bit index (i.e. 0x3 means “third bit”) or a bit mask (i.e. 0x8 means “third bit” because the third bit is set). In the end we chose bit index because it was more compact, requiring only one byte per note in the 12 notes (plus 3 samples).

				
					typedef enum {
    C = 0,
    C_SHARP = 1,
    D = 2,
    D_SHARP = 3,
    E = 4,
    F = 5,
    F_SHARP = 6,
    G = 7,
    G_SHARP = 8,
    A = 9,
    A_SHARP = 11,
    B = 13,
    SAMPLE_1 = 10,
    SAMPLE_2 = 12,
    SAMPLE_3 = 14,
} note_bit_type;

#define NOTE(bit_idx) (0x1 
				
			

Then, we could store the correct sequence as a constant and iterate over that. The correct sequence could be found in memory (in the octave-offset representation we explained in the earlier Payload section) at address 0x1000dac8 (thanks again to reteps for finding this.) Converted to our C enums:

				
					const note_bit_type note_sequence[] = {
    G, E, D, C, // C@>@C@
    D, C, D, E, // >@
    G, E, G, A, // C@CE
    E, A, G, E, // @EC@
    D, C, G, E, // ><c@ d c e>@
    G, E, D, C, // C@>@>@
    G, E, G, A, // C@CE
    E, A, B, G_SHARP, // @EGD
    F_SHARP, E, // B@
};</c@>
				
			

Then to write the note:

				
					note_bit_type next_note_bit = note_sequence[seq_i];
notes_held_bitmap |= NOTE(next_note_bit);

				
			

Starting playing the sequence

Initially, we had the sequence play in a loop forever, as soon as the “Play” menu came up.

ย 

This got a bit annoying. We had already figured out a few of the other inputs we could use to trigger the sequence, and settled on all three of the samples being played at once when in a specific instrument. Then switching out of that instrument would stop the sequence. This was much better for our sanity. We also added some initialization code for the counters, just to be sure they would start at 0. We wrote some specific magic value to one of our scratch variables to keep track of whether the state was initialized or not. A saner alternative would have been to find the initialization/startup code and hook into that, but this was a bit easier.

				
					if (instrument != AUTOPLAY_INSTRUMENT){
    state = 0x0;
    return;
}

int all_3_samples_held = CHORD(SAMPLE_1, SAMPLE_2, SAMPLE_3);

if (state != 0xed){
    if (!((notes_held_bitmap & all_3_samples_held) ^ all_3_samples_held)){
        counter = 0;
        seq_i = 0;
        state = 0xed;
    }
    else{
        return;
    }
}
   

				
			

We arbitrarily chose the violin as the autoplay instrument.

Closing Thoughts

This was good, fun and an exercise in using OFRAK “recreationally.” We, of course, are partial to OFRAK, but it was great scripting everything in Python and having access to a library of very helpful binary analysis and patching functionality.

ย 

Some future additions that could be done on this badge FW modification:

ย 

  • Making the autoplayer a separate “instrument” so it shows up on the instrument select screen. It would be a neat trick, but you’d have to stop the badge from thinking it’s an actual instrument and trying to play sounds that don’t exist (there appear to be jump tables for each instrument)
  • Making multiple new instruments for different pre-set tracks
  • Recording sequences of notes as new pre-set tracks at runtime
  • Using the various drawing functions to draw pictures according to the notes played, like a music visualizer

ย 

All of these would require rather significant additional space, so we would need a way to extend the firmware for sure. Sit tight for an OFRAK Modifier for that!

ย 

Some sticking points with OFRAK we noticed that got us thinking:

ย 

  • It bothered us (aesthetically and practically) that we were defining functions and data in two places: The “extern” declarations in source/header, and the LinkableSymbol that actually defined the value. It seems practical and more convenient to define functions along with their type in one place, perhaps just pulling these straight from Ghidra, and have OFRAK creating the declaration and definition without any more user input needed.
  • Managing data sections (both R and RW) through the PatchFromSourceModifier API is a bit impractical. This can always be tricky with PatchMaker, but the Modifier’s API abstracts away the guts that it is unfortunately necessary to bury your hands in to get things working smoothly. For example, we originally tried to used LLVM instead of GNU, but LLVM stubbornly insisted that extern pointers to data had to first be loaded as an indirect pointer from the .rodata section, which pointed to an address in the .bss section, where the address of the variable would hopefully be contained. GNU was happy to just load the variable address directly from the .rodata section. Managing an additional section was more effort than switching toolchains, which is a testament to interoperability and modularity in PatchMaker but a flaw in PatchFromSourceModifier.

ย 

Perhaps these will become pull requests you’ll see landing in core OFRAK shortly ๐Ÿ™‚

ย 

Hope you enjoyed our work!ย  Maybe next you can build something else cool on top of the badge!

ย 

โ€” Edward Larson & Jacob Strieb

ย 

LEVERAGE OUR EXPERTISE FOR YOUR SECURITY NEEDS

Reach out to learn more about our embedded security offering and to schedule a demo.

LEVERAGE OUR EXPERTISE FOR YOUR SECURITY NEEDS

Reach out to learn more about our embedded security offering and to schedule a demo.

LEVERAGE OUR EXPERTISE FOR YOUR SECURITY NEEDS

Reach out to learn more about our embedded security offering and to schedule a demo.

LEVERAGE OUR EXPERTISE FOR YOUR SECURITY NEEDS

Reach out to learn more about our embedded security offering and to schedule a demo.