Reverse Engineering Hanwha Security Camera Firmware File Decryption with Ida Pro
4 months ago
4
I recently took a look at a commercial-grade IoT security camera made by Hanwha: the WiseNet XNF-8010RW. I wanted to perform a security audit of this device just like I would do during an IoT pentest.
WiseNet XNF-8010R by Hanwha
The first thing I did was to disassemble the device and trace the UART console on an unpopulated ribbon cable connector.
Next, I dumped the device firmware by desoldering the BGA nand flash chip.
Even though I had the firmware of my device, I thought it would be interesting to see if firmware was available on the company’s website. I was in luck!
WiseNet XNF-8010R Firmware Download Page
I downloaded the XNF-8010R_2.10.04_20230328_R640.zip file and unzipped it. This resulted in the file XNF-8010R_2.10.04_20230328_R640.img.
By running the file command on this firmware file, I could determine right away that it is encrypted via openssl.
$ file XNF-8010R_2.10.04_20230328_R640.img
XNF-8010R_2.10.04_20230328_R640.img: openssl enc'd data with salted password
How does the file command know this is encrypted with openssl? It’s all in the file header:
We are presented with a dilemma. The password to decrypt the firmware file is contained somewhere inside the firmware itself.
But let’s think about that… The decryption password needs to be somewhere on the running device in order to decrypt the firmware file and apply the upgrade.
Luckily we already performed the beginning steps to break out of our chicken and egg-style dilemma. We’ve already dumped the firmware from the physical device! Let’s dig into the firmware dump.
We performed a chip-off firmware extraction of a BGA Nand flash chip on the camera. If you watched the video embedded above, you will remember that our Nand flash chip contained spare area. We dumped the firmware twice: once including and once excluding that spare area from the firmware dump file. For our analysis, we are going to use the version without the spare area include. We will name this file wisenet-nospare.bin.
The first thing we want to do with this firmware file is to split it up into the various partitions that exist. You may recall that we saw the partition table addresses nicely printed out in the UART logs:
Partition Table Obtained via UART
We can then use dd to split the single files into all of the partitions with the following bash script:
Next, we want to find where in the firmware the decryption is likely occurring. The only lead we currently have is the openssl is being used, but this actually tells us a lot. Our current hypothesis is that the openssl enc command is being used. Let’s start with strings!
Both of those strings are performing decryption of a file with openssl. The first one is using a keyfile and the second is using a password. We expect a password so let’s look at what strings are before and after that string.
First we ID the partition that has the openssl enc -aes-256-cbc -d -k string:
When we use strings wisenet-nospare.bin | less and search around the area, we see “MODELINFO_MODEL_DECRYPTIONKEY” referenced. We find more interesting strings in a similar structure:
Now we can load part8.bin_21510144_elf.raw into Ida Pro for analysis. It recognizes it as a ARM Little-endian binary. We keep all the default analysis options enabled and select “OK” to start.
Loading Binary into IDA Pro
The analysis takes a few minutes to complete. Then the first thing we want to do is look for our string that led us to this binary initially.
Navigation: View -> Open subviews -> Strings
We found our string and navigated to its address.
IDA Strings Subview
String Address
Navigation: Right Click on “aOpensslEncAes2_2” -> List cross references to…
Here we can see that the reference to this string is in a function called decrypt_imageFile! We are on the right track.
String Cross-References
After navigating to the decrypt_imageFile and running the decompiler we can see that a function get_modelStr is passed two arguments. The first argument is 122 and the second is a buffer.
It clearly seems like it’s referencing that MODELINFO_MODEL_DECRYPTIONKEY! Let’s try to decrypt! We will try all of the MODELINFO_MODEL_DECRYPTIONKEY and MODELINFO_CONFIG_BACKUP_KEY values we found to be sure.
#!/bin/bash
pass_list=("ZK5EA1tGT4AHOUCUU5tUMuQCfA2fbjnSEgOiLR104eI=""ZK5ECtTH+jkYs87uEEUrwyZ9+wsFkWpk3AbtnTEY3gs=""dCTjgwaXvsPzQ0QRlweM7IP7T0ytT7G25rEM4g+17cQ=""dCTjgwaXvsPzFQeOg5gg6+uMOKSuoreXJ/o3UsgJY+o=""dCTjgwaXvsPzFUpdPq8wCPu5bGNwygQ7fFBQp+bM+As=""dCTjgwaXvsPzQ0QRlweM7IP7T0ytT7G25rEM4g+17cQ=""fc7pA9zD9Qa3+CUjr4w5AglNX9LT3SihkH2mENLyYNc=")# we need to try all of these since the camera might have a different default digest than our machinedigest_list=("md5""sha1""sha256""sha384""sha512")fwfile="XNF-8010R_2.10.04_20230328_R640.img"outdir="out"mkdir -p $outdirrm -rf ${outdir}/*
count=0for pass in "${pass_list[@]}";dofor digest in "${digest_list[@]}";do openssl enc -aes-256-cbc -d -md ${digest} -k "${pass}" -in ${fwfile} -out ${outdir}/try${count}_${digest}.bin
done((count++))done
That didn’t work. All of those passwords and digest method combinations give us the same error:
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
bad decrypt
80CB45AA4A7F0000:error:1C800064:Provider routines:ossl_cipher_unpadblock:bad decrypt:providers/implementations/ciphers/ciphercommon_block.c:107:
Every “decryption key” value is 32 bytes when base64 decoded and seemingly random (high entropy). It’s possible the decryption keys are themselves encrypted.
Let’s follow get_modelStr() down a chain of wrapper functions.
This function is clearly the place that those encrypted + base64-encoded strings will be decrypted. Let’s walk through the major parts of this function.
First, we see the string “zeppelin” be constructed into a string. Let’s call this buffer zeppelin_buff for now. This seems like the making of a hardcoded password…
Hardcoded "zeppelin" String
Then we see the HashedKeyCipher(out_buffer,1,6,zeppelin_buff) constructor called. So we are hashing the “zeppelin” string but with what function? We’ll dig into that later.
Next we call Utility::Security::HashedKeyCipher::get_decryptor on that object that was returned from the last call. So those arguments have a lot of weight in determining how things get decrypted.
Now we have a few possible passphrases to decrypted the firmware file with. Let’s update our script from before:
#!/bin/bash
pass_list=("XNF-8010R""HTWQNF-8010""HTWXNF-8010R""XNF-8010RVM""XNF-8010RV""QNF-8010")# we need to try all of these since the camera might have a different default digest than our machinedigest_list=("md5""sha1""sha256""sha384""sha512")fwfile="XNF-8010R_2.10.04_20230328_R640.img"outdir="out"mkdir -p $outdirrm -rf ${outdir}/*
count=0for pass in "${pass_list[@]}";dofor digest in "${digest_list[@]}";do openssl enc -aes-256-cbc -d -md ${digest} -k "${pass}" -in ${fwfile} -out ${outdir}/try${count}_${digest}.bin
done((count++))done
Note that we are also looping over a bunch of different digest functions because we don’t know what the default digest function the openssl version on the camera is.
Here is the result of running file on all of our attempts:
$ file out/*
out/try0_md5.bin: data
out/try0_sha1.bin: data
out/try0_sha256.bin: data
out/try0_sha384.bin: data
out/try0_sha512.bin: data
out/try1_md5.bin: data
out/try1_sha1.bin: data
out/try1_sha256.bin: data
out/try1_sha384.bin: data
out/try1_sha512.bin: data
out/try2_md5.bin: gzip compressed data, last modified: Tue Mar 28 10:42:57 2023, from Unix, original size modulo 2^32 112394240out/try2_sha1.bin: data
out/try2_sha256.bin: data
out/try2_sha384.bin: data
out/try2_sha512.bin: data
out/try3_md5.bin: data
out/try3_sha1.bin: data
out/try3_sha256.bin: data
out/try3_sha384.bin: data
out/try3_sha512.bin: data
out/try4_md5.bin: data
out/try4_sha1.bin: data
out/try4_sha256.bin: data
out/try4_sha384.bin: data
out/try4_sha512.bin: data
out/try5_md5.bin: data
out/try5_sha1.bin: data
out/try5_sha256.bin: data
out/try5_sha384.bin: data
out/try5_sha512.bin: data
We did it! Note that try2 corresponds to the passphrase “HTWXNF-8010R”.
Let’s unpack our freshly decrypted firmware:
$ cd out
$ mkdir fw
$ mv try2_md5.bin fw/fw.tar.gz
$ cd fw
$ tar -zxf fw.tar.gz
$ ls -l
total 155620drwxr-xr-x 2 nmatt nmatt 4096 Mar 282023 BFRS
drwxr-xr-x 2 nmatt nmatt 4096 Mar 282023 boot_fw
-rw-r--r-- 1 nmatt nmatt 52144865 Jun 5 00:18 fw.tar.gz
drwxr-xr-x 2 nmatt nmatt 4096 Mar 282023 fwupgrade_data
-rwxr-xr-x 1 nmatt nmatt 559312 Mar 282023 fwupgrader
-rwxr-xr-x 1 nmatt nmatt 13604110 Mar 282023 ramdisk_xnf8010r.wn5.gz
-rwxr-xr-x 1 nmatt nmatt 4898638 Mar 282023 uImage
-rwxr-xr-x 1 nmatt nmatt 88121472 Mar 282023 work_xnf8010r.wn5
So I mentioned at the beginning that the camera model I performed the firmware extraction on is XNF-8010RW and we observe that the firmware decryption password was HTWXNF-8010R. We can see that the firmware encryption passphrase is basically just the model number of the device!
Let’s test this hypothesis on another firmware file that we don’t have a physical device for.
For this test I selected the WiseNet TNO-L4040TR device which has a firmware image file called TNO-L4040TR_2.21.11_20240923_R81.img.
Using the same patterns as we saw in the other device, we guessed that the passphrase is HTWTNO-L4040TR.
This was a fun mental exercise and hopefully a good demonstration of how a persistent attacker is going to reverse-engineer your hardcoded password/encryption scheme. It is not a matter of “IF”; it’s a matter of “WHEN”.