While looking for new devices to perform reverse engineering on, I became interested in Bosch’s FlexiDome line of cameras, specifically the FlexiDome 7000, a day/night surveillance camera. This blog post demonstrates how I reverse engineered the firmware file format for the FlexiDome 7000, used that information to unpack earlier firmware versions, discovered how firmware encryption was implemented, reverse engineered the firmware encryption, and wrote an unpacker that supports all tested firmware versions.
This research demonstrates that although manufacturers offer firmware updates to enhance security for legacy products (in this case, through encryption), the limitations of legacy products may prevent them from achieving the level of security of current models that are designed to support the latest security functionality.
- Hex editor: A computer program that allows for manipulation of the fundamental binary data that constitutes a computer file.
- binwalk, a tool for identifying file contents, entropy measurement, and more.
- An open-source tool we developed at Anvil Ventures which was capable of unpacking all tested versions of Bosch camera firmware 1Anvil-Developed Open-Source Tool (https://github.com/anvilsecure/BoschFirmwareTool)
A few quick Google searches didn’t turn up much public security research on the FlexiDome 7000, so I downloaded the current firmware image file. I then unpacked it and attempted to do some reverse engineering and subsequently some bug hunting. However, I soon discovered that the firmware file I had downloaded contained encrypted data.
After searching available release notes for various versions of Bosch camera firmware, I found the following statement Article: 2Bosch IP Video increased security with firmware (
In order to upload version 6.51 to a device running a firmware version below 6.50, you need to upgrade first to version 6.50, since older firmware versions do not support firmware file decryption.
This statement led me to believe that I might be able to unpack a version prior to 6.50. To test this assumption, I downloaded several prior versions and began trying to reverse engineer the file format and obtain its contents.
Firmware Reverse Engineering
I started with a sample firmware file prior to version 6.50, which I downloaded from the Bosch website 3EXTEGRA IP 9000 Firmware Maintenance Release Download (https://downloadstore.boschsecurity.com/FILES/CPP4_H.264_6.32.0124_EXTEGRA_only.fw) This is a maintenance release of firmware for the EXTEGRA IP 9000 camera. I loaded the file into a hex editor to begin working on it. This file was an unencrypted example.
My initial process was to try to figure out the basic structure of the file. Essentially, I wanted to learn:
- How is the file structured? (e.g. headers and then data, multiple sections)
- Where is some of the basic metadata stored? (Length, checksums, magic numbers, etc.)
To explore these questions, I wanted to break the file down into metadata and actual firmware data. Before we discuss that, it helps to understand that many file formats such as ZIP, Windows PE, and others have structural features in common.
File Format Primer
Many file formats use one or more “magic numbers,” a constant numerical or text value used to identify the file as belonging to a certain format. For example, ZIP files generally start with PK\x03\x04 as an identifier. Magic numbers give a file parser a quick way to test the file before performing more intensive parsing.
Other common fields can help a parser identify the contents and where in the file they are located. In a binary file or archive, these include length, one or more checksums, and other metadata.
When reverse engineering an unknown file format, it helps to consider the type of data a file is expected to contain. Many firmware file formats contain either separate files (i.e. an archive) or data segments that are directly written into the nonvolatile storage on a device.
Bosch Firmware Format
At first glance, I found that this firmware file was broken into what appeared to be sections. The first 1-2KB of data in a hex editor revealed a sparse set of values, followed by a large, seemingly random segment. The very first value (10 12 20 03) in the firmware was found in a handful of locations in the file, always at the start and the end of a 1024-byte long section. Each of these “sections” was followed by what appeared to be binary data. Shown below is the first 64 bytes of the 1024-byte header.
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00000000 10 12 20 03 00 00 00 10 00 00 00 00 00 00 06 32 .. ............2 00000010 03 BC B4 90 00 00 00 00 CF 77 AE FA 00 00 00 00 .¼´.....Ïw®ú.... 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
First came the magic number for what I assumed were header sections. A handful of other values stood out, all 32-bit values:
- 00 00 06 32 matched the version in the firmware file itself (6.32)
- The 32-bit value following that (03 BC B4 90) matched the length of the entire file, minus the length of the header.
- CF 77 AE FA appeared to be random, but such values are frequently checksums or cyclic redundancy checks in formats like this. A quick check in the hex editor showed that the Checksum-32 of the file following this header matched this value.
These are all big endian values, which means the most significant byte (or “big end”) is placed at the byte with the lowest address. In my experience, most binary file formats are designed with a single endianness in mind, and figuring out which is in use becomes clear as different fields (e.g. length) are found in the file format.
This was already enough information to write a few lines of Python code and break up the file into sections, each with its own headers and data.
Data segments in this file appeared to be random / encrypted, but were they? A good first check is to use a tool that can perform entropy measurement, such as binwalk. The output of binwalk on the data following a file header in this file looks like:
DECIMAL HEXADECIMAL ENTROPY -------------------------------------------------------------------------------- 0 0x0 Rising entropy edge (0.985878) 12675072 0xC16800 Falling entropy edge (0.827170) 12828672 0xC3C000 Rising entropy edge (0.997896) 19488768 0x1296000 Rising entropy edge (0.977441) 19611648 0x12B4000 Rising entropy edge (0.983196) 19673088 0x12C3000 Falling entropy edge (0.791758) 19765248 0x12D9800 Falling entropy edge (0.671344) ...snip...
Another several dozen lines follow. The data clearly isn’t random, so some form of obfuscation is likely. Encrypted data would have an entropy of >0.99 when seen by Binwalk's entropy analysis. Since the documentation I found (and listed at the beginning of this post) pointed to encryption only being used on later versions, logic dictated that some other form of obfuscation was in play.
My assumption here was that the contents of this section were one or several files, presumably containing binary data and executable code. Therefore, when looking at this form of data in a hex editor, I expected to see segments of data separated with padding. Usually, an executable image will space out sections with padding made of zero (\x00) bytes. Looking at the header again, there are long runs of a single character throughout the data, such as:
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 03BCB740 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB 03BCB750 9C EF ED BC 42 42 42 42 42 42 42 42 42 42 42 42 œïí¼BBBBBBBBBBBB 03BCB760 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB 03BCB770 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB 03BCB780 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB 03BCB790 9C EF ED BC 42 42 42 82 23 32 32 73 1D 34 27 30 œïí¼BBB‚#22s.4'0 03BCB7A0 31 2B 2D 2C 6C 36 3A 36 6C 16 1D 24 24 24 24 42 1+-,l6:6l..$$$$B
After a bit of thinking, my first guess was that those should be padding or zero bytes, and that the image was XOR-encoded. A few lines of quick Python scripting to examine the XOR obfuscation resulted in data looking like:
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00000000 DE AD AF FE 00 2B 3E C0 61 72 6D 2E 61 70 70 31 Þ.¯þ.+>Àarm.app1 00000010 2E 67 7A 2E 54 5F 66 66 66 66 00 00 00 00 00 00 .gz.T_ffff...... 00000020 00 00 00 00 00 00 00 00 00 2B 3E 77 02 30 00 40 .........+>w.0.@ 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Clearly this was some form of file header, followed by a file name and additional metadata. I confirmed that I had usable file contents by running
binwalk on the resulting data section.
DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 64 0x40 gzip compressed data, has original file name: "app1_rel", from NTFS filesystem (NT), last modified: 2018-12-13 14:31:51 2834176 0x2B3F00 gzip compressed data, maximum compression, has original file name: "aacenc.dll", from FAT filesystem (MS-DOS, OS/2, NT), last modified: 2018-12-13 14:25:05 3007904 0x2DE5A0 gzip compressed data, maximum compression, has original file name: "ambarella.dll", from F AT filesystem (MS-DOS, OS/2, NT), last modified: 2018-12-13 14:24:50 4416928 0x4365A0 gzip compressed data, maximum compression, has original file name: "autofocus.dll", from F AT filesystem (MS-DOS, OS/2, NT), last modified: 2015-11-18 04:36:35 ...snip...
Reverse Engineering Firmware Encryption
The above work resulted in a set of firmware images and various static files served by the device’s web server. From reviewing the Bosch documentation, I knew that there should be a firmware version that is both obfuscated and supports encryption. However, Bosch has been rather diligent when it comes to culling old firmware download links from their website. Nearly every 6.50 link I could find was either slightly too new (and therefore encrypted) or slightly too old (offering no encryption support).
I did, however, find the file I needed through a single working link on an Italian blog post 4Italian blog post with link to legacy Bosch firmware: Testing a Bosch IP Camera (DINION IP 5000i IR) (https://enzocontini.blog/2018/06/02/testing-a-bosch-ipcamera-dinion-ip-5000i-ir/).
The link took me to a download for a maintenance release for CPP7.3-based cameras 5CPP7.3-Based Camera Firmware Download (https://downloadstore.boschsecurity.com/FILES/CPP7.3_FW_6.50.0133.fw). This file is not listed on the Cameras and Encoders page, Firmware tab 6Bosch Download Area: Library of Current Firmware (https://downloadstore.boschsecurity.com/index.php).
The main runtime for these devices is contained within a file named app1_rel in the archive, and the code is very well annotated with logging statements and other data which helped me find a place to start reversing.
Found in the main firmware upload handling function, the above function:
- Allocates a C_Key structure, which is a base class for all key types in the firmware.
- Then sets the type to RSA public key, and sets the key modulus in
A second call to
SetComponent is used to set the public exponent to the commonly-used value 0x10001. In this code, the
g_rsa_key buffer contains the
n component of the RSA public key, and the e component is set in a stack allocated buffer. Both buffers are loaded into a form of arbitrary precision integer structure inside of
At this point, I encountered a minor complication: The algorithm used RSA public key decryption to decrypt an AES key from the firmware file, which I discovered while reverse engineering the application firmware. Generally, cryptography libraries do not expose an API for performing public key decryption, but I knew from prior experience that OpenSSL does. Therefore, I wrote a short C program using OpenSSL to expose the API and confirm that I could decrypt the AES key (and then the binary). This decryption is not necessarily a security flaw in that the assumption is that the value of a public key is public or known. However, being able to alter the public key would be a flaw.
AES Key Location
One open question remained: Where is the AES key located in the firmware? Bosch used formerly empty space in the headers to store 256 bytes of additional data. This is followed by encrypted data. Using the aforementioned C program, I attempted to decrypt a section and got:
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00000000 00 00 00 20 1F 6C 87 6F F4 62 19 16 33 85 17 81 ... .l‡oôb..3….. 00000010 1D A7 67 E3 D1 1E DD 90 59 61 18 4E 75 7F EA 80 .§gãÑ.Ý.Ya.Nu.ê€ 00000020 53 7C 2E 4A 1B 66 31 63 CB 4F 67 2E D8 80 E1 EC S|.J.f1cËOg.Ø€áì 00000030 54 35 F8 B7 T5ø·
The layout is relatively simple here:
- 4 bytes key length. All keys I’ve seen were 32 bytes for AES-256
- AES key of length bytes
- AES initialization vector (IV), a random number used in combination with a secret key to encrypt data, always 16 bytes
Using the above key and AES IV, I was able to successfully decrypt the data in that section using AES-256-CBC decryption. That said, simply having access to the AES key is not enough to modify or create new valid firmware files unless we also have the certificate used to validate the signature. The CPP7.3-based firmware appears to also have a signature in the header, and initial reversing of the firmware confirmed code related to those checks. I have not found the exact certificate being used to validate the signatures.
Bosch Firmware Unpacking Tool
As part of this blog post, we at Anvil Ventures have also open sourced a tool capable of unpacking both pre- and post-6.50 firmware versions. It also automatically unpacks “RomFS” contents, which are essentially archives of the files that the device serves from its web server. We hope that anyone interested in doing additional research on Bosch’s camera line will find the tool useful.
Source and precompiled binaries are available at: https://github.com/anvilsecure/BoschFirmwareTool
Several lessons can be taken from attempting to add security features to existing products in this manner:
- Securely adding security features (especially encryption) for a legacy product is very difficult. At times, there is no way to make older product versions as secure as new versions. In this example, Bosch updated the firmware for older products to add a firmware encryption feature, to supplement the legacy obfuscation mechanism and presumably make reverse engineering these devices more difficult. However, to make this new firmware encryption feature available for older products, Bosch had to make a security compromise: Bosch included the firmware encryption key in a transitional version of the firmware that was not, by itself, encrypted. Anyone who manages to obtain older firmware versions can bootstrap their way into using the encrypted newer firmware and have the obvious ability to decrypt the newer firmware files.
- Security cameras are long-lived, and difficult to update. Camera deployments are likely to be isolated from the internet, so the only way to update encryption keys is to include them in the firmware binary.
- Secrets (in this case, encryption keys and prior versions of firmware) cannot be removed or rescinded once they have been released publicly.
About the Author
James Thomas is a Principal Security Engineer at Anvil who is focused on embedded systems and application security. A security consultant in recent years, James was previously a security researcher at the Idaho National Laboratory where he performed penetration testing on a variety of Industrial Control Systems and Critical Infrastructure-related systems and also designed tools to protect against his successful attacks. This is the first of several posts on James’ research and side projects.