Introduction

Around a year ago a few of my colleagues and I were sitting in Benoit Forgette and Damien Cauquil’s “Whatever Pwn2Own” talk at the Insomni’Hack conference.

At this point, we already knew that we wanted to participate in one of the next Pwn2Own contests.

The Pwn2Own contests began in 2007 at the CanSecWest conference in Vancouver where the contestants were challenged to demonstrate the existence of vulnerabilities in the Mac OS X operating system. Since then, the Pwn2Own contests have continuously grown, now including various targets ranging from mobile phones to electric vehicle chargers or automotive operating systems at the most recent event, Pwn2Own Tokyo.

For multiple years the contests have been organized by Trend Micro’s Zero Day Initiative (ZDI). The ZDI also handles the responsible disclosure of the identified and reported vulnerabilities.

So, when the ZDI announced in July 2023 that the consumer-focus Pwn2Own event would return to Toronto in October we knew that it was time to choose our targets.

Target Selection

The announcement listed seven categories with each categories having 2-5 targets and each target having a cash price (20’000$ – 250’000$) and Master of Pwn points (2-25) associated to them. Both the cash price and the Master of Pwn points, which are tallied up at the end of the event to determine the overall winner of the contest, are a good indicator of the assumed difficulty of finding and exploiting one or more vulnerabilities to obtain unauthenticated remote code execution on the target.

For the Pwn2Own contest in Toronto the categories were:

  • Mobile Phones
  • The SOHO Smashup (as in Small Office/Home Office)
  • Surveillance Systems
  • Home Automation Hubs
  • Printers
  • Smart Speakers
  • NAS Devices
  • Google Devices

As it was our first participation, we decided to go for targets with a value of 2 to 3 Master of Pwn points which meant that we would be looking at devices in the printers and surveillance systems categories. We finally decided to target the Lexmark CX331adwe printer as there already was quite a bit of public tooling and writeup available on a similar model that was part of the previous year’s contest and the Synology BC500 camera since it was only just released in March 2023 and was the first surveillance camera sold by Synology.

The Targets Arrive

Both devices arrived a few days later in our office and while we initially had plans to start our journey with the Lexmark printer it quickly became clear that we would have to adapt.

After setting up the camera and having a quick glance at its functionality we decided to initially keep the pre-installed firmware and focus on the hardware and potential debug interfaces. We therefore opened the enclosure of the camera and started searching for debug ports. One unpopulated header that caught our attention was a 4 pins header on the camera board.

If you are familiar with embedded devices, you might know that a popular debug port is a simple UART interface that typically uses 4 pins consisting of ground (GND), power (VCC), receive data (RX) and transmit data (TX).

Since there were no labels for the pinout, we had to verify our assumption by identifying the individual pins. Luckily this can be done in a few easy steps using a simple multimeter.

First you want to identify the GND pin by using your multimeter’s continuity feature and connecting one probe to a pin / part that you know is connected to ground. After finding the GND pin you want to look for a pin with a steady voltage applied as this will be VCC. Depending on the device you might find different voltage levels but commonly it will be either 5V or 3.3V. While measuring the voltage on the pins you should also come across a pin with a fluctuating voltage. This will be your TX pin where the device is sporadically sending data, thus the voltage of the pin can be observed to fluctuate.

After mapping the pinout and soldering a header, that allowed us to connect a 3.3V USB to serial adapter, we connected our receiver and configured the necessary baud rate, data and stop bit size as well as parity option. These values can be determined using a logic analyzer or by using trial and error with commonly used values. For the BC500 camera the required configuration was a baud rate of 115200 bps, 8 data bits, 1 stop bit and no parity.

Once configured we got the output of the U-Boot bootloader and even got an interactive shell with a username and password prompt.

Loader Start ...
LD_VER 03.00.03

560_DRAM1_933_4096Mb 09/10/2021 09:54:28

No card inserted
Pad driving increased
SPI NAND MID=000000C2 DEV=00000012
tmp_addr 0x02000000
LdCtrl2 0x00000000
uboot_addr 0x0E000000
uboot_size 0x01FC0000
03B58
NVT_LINUX_SMP_OFFfdt 0x00100000
shm 0x00200000
jump 0x0E000000r
B?????? 2022 - 06:02:47 +0000)

CPU:   Novatek NT @ 960 MHz
DRAM:  256 MiB
???????? to 0x0ff0d000, Offset is 0x01f0d000 sp at 0fbe7b00
nvt_shminfo_init:  The fdt buffer addr: 0x0fbed648
ARM CA9 global timer had already been initiated
otp_init!
120MHz
otp_timing_reg= 0xff6050
 CONFIG_MEM_SIZE                =      0x10000000 
 CONFIG_NVT_UIMAGE_SIZE         =      0x01900000 
 CONFIG_NVT_ALL_IN_ONE_IMG_SIZE =      0x07800000 
 CONFIG_UBOOT_SDRAM_BASE        =      0x0e000000 
 CONFIG_UBOOT_SDRAM_SIZE        =      0x01fc0000 
 CONFIG_LINUX_SDRAM_BASE        =      0x01100000 
 CONFIG_LINUX_SDRAM_SIZE        =      0x0cf00000 
 CONFIG_LINUX_SDRAM_START       =      0x0c700000  
NAND:  drv_nand_reset: spi flash pinmux 0x0
id =  0xc2 0x12 0xc2 0x12
use flash on-die ecc
nvt spinand 4-bit mode @ 40000000 Hz
128 MiB
MMC:   NVT_MMC0: 0, NVT_MMC1: 1
Loading Environment from NAND... 

[CUT]
Please press Enter to activate this console. 
[CUT]

BC500_AD login:

Dumping the Firmware

With the shell over UART requiring a username and password our next goal was to obtain valid credentials.

By interrupting the U-Boot bootloader during the startup of the camera we entered the U-Boot shell, which allows us to directly interact with hardware components such as the NAND storage, the memory as well as the SD card which would be used by the camera to locally persist the recordings.

Knowing the partition layout and addresses from the normal output received when booting the device, we could directly access the individual partitions in U-Boot.

nand: device found, Manufacturer ID: 0xc2, Chip ID: 0x12
nand: Macronix MX35LF1GE4AB 1GiB 3.3V
nand: 128 MiB, SLC, erase size: 128 KiB, page size: 2048, OOB size: 64
Scanning device for bad blocks
random: fast init done
Bad eraseblock 171 at 0x000001560000
            12 fixed-partitions partitions found on MTD device spi_nand.0
Creating 12 MTD partitions on "spi_nand.0":
0x000000000000-0x000000040000 : "loader"
0x000000040000-0x000000080000 : "fdt"
0x000000080000-0x0000000c0000 : "fdt.restore"
0x0000000c0000-0x0000002c0000 : "uboot"
0x0000002c0000-0x000000300000 : "uenv"
0x000000300000-0x000000340000 : "product"
0x000000340000-0x000000740000 : "kernel0"
0x000000740000-0x0000036c0000 : "rootfs0"
0x0000036c0000-0x000003ac0000 : "kernel1"
0x000003ac0000-0x000006a40000 : "rootfs1"
0x000006a40000-0x000008000000 : "userdata"
0x000000000000-0x000008000000 : "all"

To dump the firmware, we therefore used the “nand read <to memory> <from nand> <length>” functionality of U-Boot to load a region of the NAND storage into memory and then dump it from there again into a file on the SD card using “fatwrite mmc <device> <from memory> <file> <length>”. The same process was then repeated for all partitions.

$ nand read 1800000 0x000006a40000 015c0000
$ fatwrite mmc 0:1 1800000 userdata.dump 015c0000

The following list shows the most important partitions and their type using the file command.

$ file *.dump
fdt.dump:      Device Tree Blob version 17, size=17314, boot CPU=0, string block size=1570, DT structure block size=15688
kernel0.dump:  u-boot legacy uImage, Linux-4.19.91, Linux/ARM, OS Kernel Image (Not compressed), 2316192 bytes, Mon Dec 12 05:59:29 2022, Load Address: 0X008000, Entry Point: 0X008000, Header CRC: 0XEE119A1, Data CRC: 0X917A3590
kernel1.dump:  ISO-8859 text, with very long lines (65536), with no line terminators
rootfs0.dump:  UBI image, version 1
rootfs1.dump:  ISO-8859 text, with very long lines (65536), with no line terminators
uboot.dump:    data
uenv.dump:     data
userdata.dump: UBI image, version 1

It can also be noted that the dumps of kernel1 and rootfs1 are different to their kernel0 and rootfs0 counter-parts. The reason is that the BC500 camera has two kernel and rootfs partitions where the secondary partition is written during an update. At the point of initially dumping, these partitions were still empty as we hadn’t yet updated the firmware on the device.

Firmware Update

Since we are already talking about the firmware of the Synology BC500 at this point we quickly want to discuss the firmware update package that can be downloaded from Synology’s website.

The analysis of the firmware update package revealed that its structure consists of a header with version information as well as scripts to be executed at the beginning and end of the firmware update process and an array of partition entries where each partition entry has a name, update script and image of the partition.

Using Python Construct we created a simple script that allowed us to easily extract the firmware package.

#!/bin/env python

import construct.lib
from construct import *

fileName = "Synology_BC500_1.0.6_0294.sa.bin"

firmwareFormat = Struct(
    "header" / Struct(
        "version" / PaddedString(8, 'ascii'),
        "firmware_version" / PaddedString(16, 'ascii'),
        "model" / PaddedString(8, 'ascii'),
        Padding(56), # padded with 0
        Bytes(16),
        Padding(16), # padded with 0
        Bytes(4),
        "partition_count" / BytesInteger(2, swapped=True),
        "pre_script" / Prefixed(BytesInteger(4, swapped=True), Compressed(GreedyBytes, "zlib")),
        "post_script" / Prefixed(BytesInteger(4, swapped=True), Compressed(GreedyBytes, "zlib")),
    ),
    "partitions" / Array(this.header.partition_count,
        Struct(
            "name" / PaddedString(64, 'ascii'),
            "script_length" / BytesInteger(4, swapped=True),
            "image_length" / BytesInteger(4, swapped=True),
            "script" / FixedSized(this.script_length, Compressed(GreedyBytes, "zlib")),
            "image" / FixedSized(this.image_length, Compressed(GreedyBytes, "zlib")),
        )
    ),
    "signature" / Bytes(512),
)

with open(fileName, mode='rb') as fileObject:
    fileContent = fileObject.read()
    firmwareObject = firmwareFormat.parse(fileContent)

    for partition in firmwareObject['partitions']:
        with open(fileName + '_' + partition['name'].replace('\x00','') + '.sh', mode='wb') as writeObject:
            writeObject.write(partition['script'])
        with open(fileName + '_' + partition['name'].replace('\x00','') + '.bin', mode='wb') as writeObject:
            writeObject.write(partition['image'])

    with open(fileName + '_pre.sh', mode='wb') as writeObject:
        writeObject.write(firmwareObject['header']['pre_script'])

    with open(fileName + '_post.sh', mode='wb') as writeObject:
        writeObject.write(firmwareObject['header']['post_script'])

Filesystem Analysis

The partitions that caught our interest were the rootfs and the userdata partition as based on the name these were likely to contain the root file system and configuration. Both of these partitions are using the UBI (Unsorted Block Images) format.

Since there are not many user space tools to interact with files containing UBI volumes a simple alternative is to use the NAND simulator (nandsim) under Linux to simulate a NAND flash in RAM.

To create a new NAND flash we need to specify the ID bytes of the flash we would like to simulate. In our case this is a Macronix MX35LF1GE4AB which we can also see during the boot process in the console.

nand: device found, Manufacturer ID: 0xc2, Chip ID: 0x12
nand: Macronix MX35LF1GE4AB 1GiB 3.3V
nand: 128 MiB, SLC, erase size: 128 KiB, page size: 2048, OOB size: 64

This immediately shows us the manufacturer and chip ID as well. Otherwise, we could also look at the datasheet of the corresponding flash storage.

We therefore run the following command to create our simulate NAND flash:

$ sudo modprobe nandsim first_id_byte=0xc2 second_id_byte=0x12

But unfortunately, the IDs are unknown and the command will fail.

modprobe: ERROR: could not insert 'nandsim': No such device

So instead, we create a NAND flash with a similar specification (1GiB, 128 KiB erase size and 2048 page size) using load_nandsim.sh.

$ sudo bash load_nandsim.sh 1024 128 2048
Loaded NAND simulator (1024MiB, 128KiB eraseblock, 2048 bytes NAND page)

Looking at the output of dmesg we notice that a simulated Samsung NAND 1GiB 3,3V 8-bit device was created.

[14965870.991475] nand: device found, Manufacturer ID: 0xec, Chip ID: 0xd3
[14965870.991477] nand: Samsung NAND 1GiB 3,3V 8-bit
[14965870.991479] nand: 1024 MiB, SLC, erase size: 128 KiB, page size: 2048, OOB size: 64
[14965870.991489] flash size: 1024 MiB
[14965870.991491] page size: 2048 bytes
[14965870.991492] OOB area size: 64 bytes
[14965870.991493] sector size: 128 KiB
[14965870.991494] pages number: 524288
[14965870.991495] pages per sector: 64
[14965870.991496] bus width: 8
[14965870.991497] bits in sector size: 17
[14965870.991498] bits in page size: 11
[14965870.991499] bits in OOB size: 6
[14965870.991499] flash size with OOB: 1081344 KiB
[14965870.991501] page address bytes: 5
[14965870.991501] sector address bytes: 3
[14965870.991502] options: 0x8
[14965870.992212] Scanning device for bad blocks
[14965871.005615] Creating 1 MTD partitions on "NAND 1GiB 3,3V 8-bit":

As we were only interested in the userdata and rootfs dump at this point we did not partition the NAND device and just repeated the following steps for both dumps.

To write the previously obtained dump to the NAND device we use nandwrite.

$ sudo nandwrite -p -q /dev/mtd0 userdata.dump

To interact with the UBI volume(s) we need to connect UBI to the NAND device.

$ sudo modprobe ubi
$ sudo ubiattach -p /dev/mtd0 -O 2048
UBI device number 0, total 8192 LEBs (1040187392 bytes, 992.0 MiB), available 7888 LEBs (1001586688 bytes, 955.1 MiB), LEB size 126976 bytes (124.0 KiB)
$ ls /dev/ubi*
ubi0      ubi0_0    ubi0_1    ubi_ctrl  

For the userdata.dump we obtain two volumes. Additional information such as their names can be obtained using ubinfo.

$ sudo ubinfo /dev/ubi0
ubi0
Volumes count:                           2
Logical eraseblock size:                 126976 bytes, 124.0 KiB
Total amount of logical eraseblocks:     8192 (1040187392 bytes, 992.0 MiB)
Amount of available logical eraseblocks: 7888 (1001586688 bytes, 955.1 MiB)
Maximum count of volumes                 128
Count of bad physical eraseblocks:       0
Count of reserved physical eraseblocks:  160
Current maximum erase counter value:     2
Minimum input/output unit size:          2048 bytes
Character device major/minor:            509:0
Present volumes:                         0, 1

$ sudo ubinfo /dev/ubi0_0
Volume ID:   0 (on ubi0)
Type:        dynamic
Alignment:   1
Size:        100 LEBs (12697600 bytes, 12.1 MiB)
State:       OK
Name:        data_app
Character device major/minor: 509:1

$ sudo ubinfo /dev/ubi0_1
Volume ID:   1 (on ubi0)
Type:        dynamic
Alignment:   1
Size:        38 LEBs (4825088 bytes, 4.6 MiB)
State:       OK
Name:        data_log
Character device major/minor: 509:1

We mount the volume ubi0_0 (data_app) as a UBIFS file system and get access to the configuration of the camera and related files.

$ sudo mount -t ubifs /dev/ubi0_0 /mnt/ubifs/
$ tree /mnt/ubifs/
/mnt/ubifs/
├── active.json
├── bootstatus
├── dhcpcd
├── duid
├── factory
├── group
├── group-
├── localtime -> /usr/share/zoneinfo/EET
├── passwd
├── passwd-
├── synoapi_rsa_key
├── synoapi_rsa_key.pub
├── var
│   └── db
└── webd
    ├── https.crt
    ├── https.csr
    ├── https.key
    ├── https.pem
    ├── rootCA.crt
    ├── rootCA.key
    └── rootCA.srl

5 directories, 17 files

To analyze the rootfs dump we repeat the same process and write the dump to the NAND device using nandwrite.

$ sudo nandwrite -p -q /dev/mtd0 rootfs0.dump

Note: When dealing with proper UBI images instead of a MTD dump and especially when using real devices you should prefer ubiformat instead of nandwrite as it is able to persist erase counters and deal with error correction e.g.

$ sudo ubiformat -f Synology_BC500_1.0.6_0294.sa.bin_rootfs.bin -O 2048 /dev/mtd0

Once the flash is written we proceed again using ubiattach.

$ sudo modprobe ubi
$ sudo ubiattach -p /dev/mtd0 -O 2048
UBI device number 0, total 8192 LEBs (1040187392 bytes, 992.0 MiB), available 7662 LEBs (972890112 bytes, 927.8 MiB), LEB size 126976 bytes (124.0 KiB)

Using ubinfo we obtain the meta information including the name of the volume.

$ ubinfo /dev/ubi0_0 
Volume ID:   0 (on ubi0)
Type:        dynamic
Alignment:   1
Size:        364 LEBs (46219264 bytes, 44.0 MiB)
State:       OK
Name:        rootfs
Character device major/minor: 509:1

Directly trying to mount the volume will fail. Looking at the first few bytes of the UBI volume we can see that it is a squashFS file system instead of using the UBIFS file system.

$ sudo head /dev/ubi0_0
hsqs[CUT]

Before mounting we therefore need to obtain a block device since /dev/ubi0_0 is a character device. This can be done using the ubiblock utility.

$ sudo ubiblock -c /dev/ubi0_0

Note: Instead of using the ubiattach and ubiblock user space tools it is also possible to provide the MTD device and block as options directly when loading the UBI module.

$ sudo modprobe ubi mtd=0,2048 block=0,0

The UBI block device is now available.

$ ls /dev/ubi
ubi0         ubi0_0       ubiblock0_0  ubi_ctr

This can be mounted giving access to the root file system of the camera.

$ sudo mount -t squashfs -o loop /dev/ubiblock0_0 /mnt/rootfs/
$ tree /mnt/rootfs/
/mnt/rootfs/
├── bin
│   ├── apr-1-config
│   ├── arch -> busybox
│   ├── ash -> busybox
│   ├── base32 -> busybox
│   ├── busybox
[CUT]

Getting Access

After having obtained a copy of the file system it was time to get access to the shell exposed over UART.

In addition to the configuration (active.json) the userdata partition also contains a passwd file. The contents of the file reveal two users on the system: root and synodebug

root:![CUT]:0:0::/root:/bin/sh
synodebug:$6$[CUT]:0:1101::/root:/bin/sh

While the root user is locked, the synodebug user could be interesting.

Since we have the root file system extracted, we search for synodebug and find the binary /bin/central_server where the password of the synodebug user is being set.

$ grep -iR synodebug . 2>/dev/null
Binary file ./bin/central_server matches

From the function above, we see that the password of the synodebug user is updated whenever we change the password for the user of the camera web interface, using the same password for the synodebug user.

Finally we can login to the UART shell.

BC500_AD login: synodebug
Password:
BC500_AD Linux shell...
root@BC500_AD:~$

Now follow us as we reveal how the story continues over the next few days.