Blink Mini RE, Part 4 -- Staring into the heart of the binary

With pretty graphs in coincidentally non-binary colors!

project:blink-re reverse-engineering python Jupyter Numpy osint

Because I had the privilege of obtaining factory-fresh ROMs from the cameras (as covered in the last episode), I realized that I could use those to perform some deeper analysis of the ROM’s structure. So, that’s what this post will set out to do.

This post was made in a Jupyter Notebook!

Loading and visualizing the dumps

First, we will start out by loading the dumps into our environment.

import numpy as np
from pathlib import Path
from os import PathLike


def read_flash_file(path: PathLike) -> np.array:
    """Loads a file as a numpy array of uint8s"""
    with Path(path).open('rb') as f:
        return np.frombuffer(f.read(), dtype=np.uint8)

flashes = Path('./flash-dumps')

# We will trim to 2 MiB because everything past that is 0xFF.
MIB = 1 << 20
old = read_flash_file(flashes / 'raw-flash.bin')[:2 * MIB]  # The original image that I got
new1 = read_flash_file(flashes / 'new1.bin')[:2 * MIB]
new2 = read_flash_file(flashes / 'new2.bin')[:2 * MIB]

Now, it’s one thing to read bytes, but I personally find visualization a lot more useful. I’ll use a technique somewhat similar to http://binvis.io/ to do this, but I’ll emulate it in Matplotlib.

import math

import matplotlib.ticker as ticker
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.colors import ListedColormap


def mk_byteclass_cmap():
    """
    Inspired by the https://binvis.io/ byteclass colormap.

    - ZERO: 0
    - LOW: [1, 31]
    - ASCII: [32, 126]
    - HIGH: [127, 254]
    - FF: 255
    """
    base = cm.get_cmap('viridis', 256)

    newcolors = base(np.linspace(0, 1, 256))
    newcolors[0] = [0, 0, 0, 1]
    newcolors[32:127] = [0, .2, 1, 1]
    newcolors[255] = [1, 1, 1, 1]
    return ListedColormap(newcolors)


byteclass_cmap = mk_byteclass_cmap()


def pad_to(b: np.array, length: int, pad_val = 0) -> np.array:
    """
    Pads an array to some length.
    """
    padding_length = length - len(b)  # how much to pad by
    return np.pad(b, (0, padding_length), constant_values=pad_val)  # perform the padding



def plot_bin(b: np.array, *, ax=None, offset_tick_interval=128, line_tick_interval=128, binline_length=1024, **imshow_kwargs):
    # Pad the binary to a multiple of the binline_length
    padded_length = math.ceil(len(b) / binline_length) * binline_length  # how long we want it to be
    padded = pad_to(b, padded_length, 0)

    # Turn the binary into a 2D image by reshaping it.
    image = np.reshape(padded, (-1, binline_length)).T

    # Assemble the kwargs, and plot the binary.
    imshow_kwargs = {'cmap': byteclass_cmap, **imshow_kwargs}
    plt.imshow(image, **imshow_kwargs)
    plt.ylabel('Line offset')
    plt.xlabel('Address')

    # Formatting for the tickmarks.
    def fmt_slice_index(x, p):
        x = int(x) * binline_length
        return f"{x:6x}"

    def fmt_slice_offset(x, p):
        x = int(x)
        return f"{x:3x}"

    axes = plt.gca()
    axes.get_xaxis().set_major_locator(ticker.MultipleLocator(line_tick_interval))
    axes.get_xaxis().set_major_formatter(ticker.FuncFormatter(fmt_slice_index))
    axes.get_yaxis().set_major_locator(ticker.MultipleLocator(offset_tick_interval))
    axes.get_yaxis().set_major_formatter(ticker.FuncFormatter(fmt_slice_offset))


_, axs = plt.subplots(3, 1, figsize=(20, 15))

for title, b, ax in zip(['Old', 'New1', 'New2'], [old, new1, new2], axs):
    plt.sca(ax)
    plot_bin(b)
    plt.title(title)

plt.tight_layout()
plt.colorbar(ax=axs)
plt.show()

png

A couple of big things stand out right here:

  • New1 and New2 look identical.
  • Old’s first half is extremely similar to New1 and New2.
  • There is a lot of complex data up until a certain point.
  • Past 0x1c0000, it is entirely 0xFF except for a few small lines.

If you remember the A/B firmware update scheme from last post, it seems as though new firmwares only get one app image in the first half of the ROM. The old firmware was updated, so the second half is filled in with a newer firmware version.

Now, let’s perform some diffs to confirm our hypotheses.

Diffing New1 with New2

We’ll first look at the differences between the two factory-fresh images to see what kind of variation they have, if at all.

new1_d_new2 = (new1 != new2)

plt.figure(figsize=(20, 10))
plot_bin(new1_d_new2, vmin=0, vmax=1)
plt.title(r'New1 $\neq$ New2')

plt.colorbar()
plt.show()

png

Between New1 and New2, all of the data looks identical except for a tiny sliver at the end. Let’s confirm this by listing all the differing bytes.

print(' # OFFSET  N1 N2')
print('-- ------  -- --')
n = 0
for i, (a, b) in enumerate(zip(new1, new2)):
    if a == b:
        continue
    print(f'{n:02} {i:06x}  {a:02x} {b:02x}')
    n += 1
 # OFFSET  N1 N2
-- ------  -- --
00 1f0013  46 42
01 1f0015  7c e8
02 1f0016  63 d8
03 1f0017  05 7e
04 1f0018  4a 62
05 1f0019  ca ef
06 1f001a  54 41
07 1f001b  a7 02
08 1f001c  e1 ff
09 1f001d  48 85
10 1f001e  67 63
11 1f001f  93 84
12 1f0020  46 8a
13 1f0021  a9 ce
14 1f0022  19 b6
15 1f0023  1c 92
16 1f0024  75 e6
17 1f0025  df dd
18 1f0026  8f 2a
19 1f0027  05 b9
20 1f0028  a3 25
21 1f0029  34 60
22 1f002a  4b 18
23 1f0034  38 39
24 1f0035  33 43
25 1f0036  46 37
26 1f0037  45 34
27 1f0054  38 42
28 1f0055  41 38
29 1f0056  46 42
30 1f0057  44 42
31 1f006c  c1 a3
32 1f006d  54 75
33 1f006e  17 c2
34 1f006f  e6 7e

The two binaries are almost entirely identical to each other, except for only a few bytes after 0x1f0000. That must be where the the device-specific information region is stored, like the serial number or MAC.

That’s not the only device-specific information, because it seems that the old value had a few more tiny strips of data, perhaps around 0x1c0000 or 0x1d0000.

Diffing Old with New

Since both of the new images are practically identical, I’ll just diff old with new1.

old_minus_new1 = old.copy()
old_minus_new1[new1 == old] = 0xff  # clear similar bytes

_, axs = plt.subplots(3, 1, figsize=(20, 15))

for title, b, ax in zip(
    ['Old', r'Old - New1', 'New1'],
    [old, old_minus_new1, new1],
    axs
):
    plt.sca(ax)
    plot_bin(b)
    plt.title(title)

plt.tight_layout()
plt.colorbar(ax=axs)
plt.show()

png

Interestingly, the entire first half of the ROM looks the same! That would confirm the A/B software update hypothesis. Admittedly, I didn’t update the camera more than once, but it seems to be the case that it keeps the factory ROM.

However, this is just a visualization. Are they really completely identical up until that midpoint?

for ab_border, (n, o) in enumerate(zip(new1, old)):
    if n != o:
        print(f'{ab_border} = 0x{ab_border:x}')
        break
898636 = 0xdb64c

Yes they are. So perhaps there might be a header before the first firmware, and that would mean the two headers are byte-identical too.

Isolating the A/B apps

The other really cool thing is that this diff reveals a way for us to possibly isolate the firmware images. Let’s zoom in on that border region, and look at the extent 0xdb000 to 0xdc000 to see what’s there.

_, axs = plt.subplots(3, 1, figsize=(20, 10))

for title, b, ax in zip(
    ['New1', r'Old', 'Old - New1'],
    [new1[0xdb000:0xdc000], old[0xdb000: 0xdc000], old_minus_new1[0xdb000:0xdc000]],
    axs
):
    plt.sca(ax)
    plot_bin(b, offset_tick_interval=8, line_tick_interval=8, binline_length=32)
    plt.title(title)

plt.tight_layout()
plt.colorbar(ax=axs)
plt.show()

png

There appears to be some fascinating structure to the images, but it might not be captured by a visualization; let’s actually look at the bytes themselves.

from pprint import pprint

from hexdump import hexdump

prefix_of_b = old[ab_border:ab_border + 128]

pprint(bytes(prefix_of_b))
print()
hexdump(prefix_of_b)
(b'BUND\x00\x00\x00\x00\x00\rU\xe0owl \x00\x00\x00\x11\x00\x00\x00\x00'
 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\t\x00J\x00\x00\x010'
 b'\x00\x00\x00\x11\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01D'
 b'\x00\x05\x1b\xa8\x00\x00\x00\x03\x00\x00\x00\x00\x00\x05\x1c\xec'
 b'\x00\x00K\xd0\x00\x00\x00\x04\x00\x00\x00\x00\x00\x05h\xbc\x00\x01h$'
 b'\x00\x00\x00\x06 \x19\x03(\x00\x06\xd0\xe0\x00\x00\x04\xa4\x00\x00\x00\x08'
 b'\x07-bv\x00\x06\xd5\x84\x00\x06U$')

00000000: 42 55 4E 44 00 00 00 00  00 0D 55 E0 6F 77 6C 20  BUND......U.owl
00000010: 00 00 00 11 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 01 00 09 00 4A  00 00 01 30 00 00 00 11  .......J...0....
00000030: 00 00 00 02 00 00 00 00  00 00 01 44 00 05 1B A8  ...........D....
00000040: 00 00 00 03 00 00 00 00  00 05 1C EC 00 00 4B D0  ..............K.
00000050: 00 00 00 04 00 00 00 00  00 05 68 BC 00 01 68 24  ..........h...h$
00000060: 00 00 00 06 20 19 03 28  00 06 D0 E0 00 00 04 A4  .... ..(........
00000070: 00 00 00 08 07 2D 62 76  00 06 D5 84 00 06 55 24  .....-bv......U$

What is BUND?

When I was poking around in Ghidra earlier, I believe I encountered that BUND thing, but way earlier in the image. It looks like a magic number. I also looked it up online and there were no relevant results for it.

Is there another occurrence of it in the image?

bund_magic = b'BUND\0\0\0\0\0\r'

# Is there an appearance of BUND between 0 and ab_border?
bund1_offset = bytes(new1).find(bund_magic)
bund1_offset, hex(bund1_offset)
(7168, '0x1c00')
# Is there an appearance of BUND *after* the one at ab_border?
bund_magic in bytes(new1)[bund1_offset + 1:]
False

So there is only one other BUND, it is close to the beginning of the image, and it’s at a suspiciously round offset 0x1c00. Let’s see what comes after it.

prefix_of_a = bytes(new1)[bund1_offset:bund1_offset + 128]

pprint(bytes(prefix_of_a))
print()
hexdump(prefix_of_a)
(b'BUND\x00\x00\x00\x00\x00\r\x9aLowl \x00\x00\x00\x11\x00\x00\x00\x00'
 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\t\x00$\x00\x00\x010'
 b'\x00\x00\x00\x18\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01H'
 b'\x00\x05\xd7\xb0\x00\x00\x00\x03\x00\x00\x00\x00\x00\x05\xd8\xf8'
 b'\x00\x00L\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x06$\xf8\x00\x01l\xf0'
 b'\x00\x00\x00\x06 \x19\x03(\x00\x07\x91\xe8\x00\x00\x04\xa4\x00\x00\x00\x08'
 b'\x07E\x98P\x00\x07\x96\x8c\x00\x05\xd8\x86')

00000000: 42 55 4E 44 00 00 00 00  00 0D 9A 4C 6F 77 6C 20  BUND.......Lowl
00000010: 00 00 00 11 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 01 00 09 00 24  00 00 01 30 00 00 00 18  .......$...0....
00000030: 00 00 00 02 00 00 00 00  00 00 01 48 00 05 D7 B0  ...........H....
00000040: 00 00 00 03 00 00 00 00  00 05 D8 F8 00 00 4C 00  ..............L.
00000050: 00 00 00 04 00 00 00 00  00 06 24 F8 00 01 6C F0  ..........$...l.
00000060: 00 00 00 06 20 19 03 28  00 07 91 E8 00 00 04 A4  .... ..(........
00000070: 00 00 00 08 07 45 98 50  00 07 96 8C 00 05 D8 86  .....E.P........

It looks like prefix_of_a is almost identical to prefix_of_b, which strongly suggests that our BUND string is a magic number followed by a standard header format. 'BUND\0\0\0\0\0\r' must be the magic number for the firmware!

Carving out BUND A and BUND B

I’ll call our A/B apps BUNDs from now on because that’s what they start with and it’s easier to refer to them that way.

We’ll find the end of BUND B by searching backwards in the image for non-0xFF values the way we did before, except we’ll start our search at 0x1c0000 so that we don’t get the user data segments.

for i in range(0x1c0000, 0, -1):
    if old[i] != 255:
        end_of_b = i + 1
        break

end_of_b, hex(end_of_b)
(1772588, '0x1b0c2c')

And now, we can easily carve out both BUNDs because we know the following things:

  • BUND A starts at bund1_offset
  • BUND B ends at end_of_b
  • ab_border is the dividing line between BUND A and BUND B
bund_a = old[bund1_offset:ab_border]
bund_b = old[ab_border:end_of_b]

print(f'BUND A length = {len(bund_a)} = 0x{len(bund_a):x}')
print(f'BUND B length = {len(bund_b)} = 0x{len(bund_b):x}')

_, axs = plt.subplots(2, 1, figsize=(20, 10))

pad_len = 0xe0000

for title, b, ax in zip(
    ['BUND A', 'BUND B'],
    [pad_to(bund_a, pad_len, 0xff), pad_to(bund_b, pad_len, 0xff)],
    axs
):
    plt.sca(ax)
    plot_bin(b, binline_length=512)
    plt.title(title)

plt.tight_layout()
plt.colorbar(ax=axs)
plt.show()
BUND A length = 891468 = 0xd9a4c
BUND B length = 873952 = 0xd55e0

png

We now have our two carved BUNDs! If we wanted to, we could dump them to a file like so.

with Path('flash-dumps/bund_a.bin').open('wb') as f:
    f.write(bytes(bund1))

with Path('flash-dumps/bund_b.bin').open('wb') as f:
    f.write(bytes(bund2))

Some observations:

  • The two BUNDs are remarkably similar in terms of structure, which makes sense. You can see a lot of striping patterns in one that line up with the other’s.
  • BUND A overall seems slightly longer than BUND B, even though BUND B is ostensibly newer than BUND A. They may have optimized their code. Who knows.
  • The individual regions of A are not all longer than those in B.

It would be quite interesting to overlay this with Ghidra-decompiled data, but that might be something I do another time.

Simple diff of the BUNDs

This wasn’t very useful, but I’ll just leave this here for reference. A smarter diff, like the Unix one, would be much more useful because of the uneven regions of the firmware.

bund_a_pad, bund_b_pad = pad_to(bund_a, pad_len, 0xff), pad_to(bund_b, pad_len, 0xff)

bund_same = bund_a_pad.copy()
bund_same[bund_a_pad != bund_b_pad] = 0xff

bund_diff = bund_a_pad.copy()
bund_diff[bund_a_pad == bund_b_pad] = 0xff

ndiff = len(bund_a_pad[bund_a_pad != bund_b_pad])
print('Total bytes in BUND A:', len(bund_a))
print('Total bytes in BUND B:', len(bund_b))
print('Different bytes:', ndiff)
print('Same bytes:', len(bund_a) - ndiff)
print(f'{ndiff * 100 / len(bund_a):.04f}% difference')

_, axs = plt.subplots(2, 1, figsize=(15, 10))

for title, b, ax in zip(
    ['Same bytes', 'Different bytes'],
    [bund_same, bund_diff],
    axs
):
    plt.sca(ax)
    plot_bin(b, binline_length=512)
    plt.title(title)

plt.tight_layout()
plt.colorbar(ax=axs)
plt.show()
Total bytes in BUND A: 891468
Total bytes in BUND B: 873952
Different bytes: 868827
Same bytes: 22641
97.4603% difference

png

OSINT of what the hell BUND means

I don’t know anything about German, but Google seems to, and they say that “bund” has the meaning of federation, union, alliance, with the implication that it is of a political nature. Examples might include Bundesrepublik Deutschland, Bundeswehr, or the German-American Bund that was for Nazi supporters.

I googled for stuff like “BUND file format” or “BUND magic number” but nothing good came out of that, which implies that it’s probably a custom or proprietary file format. Alternatively, it could be something stupid like an ELF but with BUND\0\0\0\0\0\r rather than \x7FELF.

I suspected that it could be someone’s name, or multiple people’s names as an acronym, but I stalked the Linkedins of Immedia Semiconductor and Blink and didn’t find anything.

Conclusion and next steps

Also, I carved out two BUNDs from the firmware. That’s pretty good, and I can probably feed them into rbasefind for more accurate results.

A really exciting thing, though, is OFRAK, which will be released around DEFCON, and that will let me analyze this flash a lot deeper.

TL;DR: I made pretty graphs :3 and did some binary carving >:)