TL;DR
Yes the file is encrypted, it is a simple XOR cipher with a key length of 0x800 bytes.
Python script for getting decrypted firmware:
import struct
FILENAME = "HW718V40_20171008.firmware"
def xor_cipher(data, key):
import sys
from itertools import cycle
if (sys.version_info > (3, 0)):
return bytes(a ^ b for a, b in zip(data, cycle(key)))
else:
return bytes(bytearray(a ^ b for a, b in zip(bytearray(data), cycle(bytearray(key)))))
key = struct.pack('<I', 0x00001AE7) * 8 + struct.pack('<I', 0x000026EF) \
* 8 + struct.pack('<I', 0x0000565C) * 8 + struct.pack('<I',
0x00001109) * 8 + struct.pack('<I', 0x00005618) * 8 \
+ struct.pack('<I', 0x000077D6) * 8 + struct.pack('<I', 0x00000F45) \
* 8 + struct.pack('<I', 0x000024AE) * 8 + struct.pack('<I',
0x00001C42) * 8 + struct.pack('<I', 0x00006D4C) * 8 \
+ struct.pack('<I', 0x0000754E) * 8 + struct.pack('<I', 0x0000449C) \
* 8 + struct.pack('<I', 0x000056A2) * 8 + struct.pack('<I',
0x00003EED) * 8 + struct.pack('<I', 0x0000106C) * 8 \
+ struct.pack('<I', 0x00000C7D) * 8 + struct.pack('<I', 0x0000578C) \
* 8 + struct.pack('<I', 0x000045CB) * 8 + struct.pack('<I',
0x0000253C) * 8 + struct.pack('<I', 0x00007F0F) * 8 \
+ struct.pack('<I', 0x00006513) * 8 + struct.pack('<I', 0x000004C0) \
* 8 + struct.pack('<I', 0x00006BFF) * 8 + struct.pack('<I',
0x0000711C) * 8 + struct.pack('<I', 0x00004829) * 8 \
+ struct.pack('<I', 0x00000B03) * 8 + struct.pack('<I', 0x000038E2) \
* 8 + struct.pack('<I', 0x000020AC) * 8 + struct.pack('<I',
0x00005708) * 8 + struct.pack('<I', 0x000064FA) * 8 \
+ struct.pack('<I', 0x00007984) * 8 + struct.pack('<I', 0x00004C79) \
* 8 + struct.pack('<I', 0x00007ED1) * 8 + struct.pack('<I',
0x00000F4F) * 8 + struct.pack('<I', 0x00002BB2) * 8 \
+ struct.pack('<I', 0x00005FA3) * 8 + struct.pack('<I', 0x00001073) \
* 8 + struct.pack('<I', 0x00002046) * 8 + struct.pack('<I',
0x00003175) * 8 + struct.pack('<I', 0x000005A5) * 8 \
+ struct.pack('<I', 0x000054CF) * 8 + struct.pack('<I', 0x00001B4E) \
* 8 + struct.pack('<I', 0x0000264E) * 8 + struct.pack('<I',
0x00003A88) * 8 + struct.pack('<I', 0x00002C1A) * 8 \
+ struct.pack('<I', 0x000022D6) * 8 + struct.pack('<I', 0x000019C0) \
* 8 + struct.pack('<I', 0x00002B57) * 8 + struct.pack('<I',
0x00003C82) * 8 + struct.pack('<I', 0x00007C61) * 8 \
+ struct.pack('<I', 0x0000530F) * 8 + struct.pack('<I', 0x00007AD2) \
* 8 + struct.pack('<I', 0x00007414) * 8 + struct.pack('<I',
0x00001ADD) * 8 + struct.pack('<I', 0xD6ADBCEF) * 8 \
+ struct.pack('<I', 0x00003E66) * 8 + struct.pack('<I', 0x000061DD) \
* 8 + struct.pack('<I', 0x00002330) * 8 + struct.pack('<I',
0xDEADBEEF) * 8 + struct.pack('<I', 0x0000475D) * 8 \
+ struct.pack('<I', 0x00002A4F) * 8 + struct.pack('<I', 0x00001F15) \
* 8 + struct.pack('<I', 0x00001162) * 8 + struct.pack('<I',
0xE3ADBEEF) * 8
buf = open(FILENAME, "rb").read()
xbuf = xor_cipher(buf, key)
open(FILENAME + ".out", "wb").write(xbuf)
Walk-through
Looking at the header of the firmware, we can make some guess (all values stored in Little-endian):
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 04 83 57 48 9C 3B 01 00 C8 CF 28 00 00 1E 2B 6A .ƒWHœ;..ÈÏ(...+j
00000010 47 00 0B 00 80 01 00 00 E4 CF 28 00 38 FE 28 00 G...€...äÏ(.8þ(.
- 0x48578304 : Maybe a signature
- 0x00013B9C : Almost the length of the firmware file length (originally 0x13C00)
If we look further in the firmware hexdump and assume we are dealing with a CORTEX device or something similar, at offset 0x20 we could think that there is a vector table:
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000020 CF 36 00 20 3E 1B 00 08 F2 D5 00 08 A4 D5 00 08 Ï6. >...òÕ..¤Õ..
00000030 A2 D5 00 08 A0 D5 00 08 BE D5 00 08 EF 26 00 00 ¢Õ.. Õ..¾Õ..ï&..
- 0x200036CF : Initial SP Value
- 0x08001B3E : Reset Vector
- 0x0800D5F2 : NMI Vector
- 0x0800D5A4 : Hard Fault
- 0x0800D5A2 : Memory Management Fault
- 0x0800D5A0 : Bus Fault
- 0x0800D5BE : Usage Fault
- 0x000026EF : RESERVED
- 0x0000565C : RESERVED
- 0x0000565C : RESERVED
- 0x0000565C : RESERVED
Here we can notice few problems:
- Why the Initial SP Value is not aligned?
- Why the reserved vector are not NULL?
At this point, grepping the sequence of bytes EF 26 00 00
end up in an interesting place:
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00012000 E7 1A 00 00 E7 1A 00 00 E7 1A 00 00 E7 1A 00 00 ç...ç...ç...ç...
00012010 E7 1A 00 00 E7 1A 00 00 E7 1A 00 00 E7 1A 00 00 ç...ç...ç...ç...
00012020 EF 26 00 00 EF 26 00 00 EF 26 00 00 EF 26 00 00 ï&..ï&..ï&..ï&.. << 0x000026EF REPEATED 4 TIMES
00012030 EF 26 00 00 EF 26 00 00 EF 26 00 00 EF 26 00 00 ï&..ï&..ï&..ï&.. << 0x000026EF REPEATED 4 TIMES
00012040 5C 56 00 00 5C 56 00 00 5C 56 00 00 5C 56 00 00 \V..\V..\V..\V..
00012050 5C 56 00 00 5C 56 00 00 5C 56 00 00 5C 56 00 00 \V..\V..\V..\V..
00012060 09 11 00 00 09 11 00 00 09 11 00 00 09 11 00 00 ................
00012070 09 11 00 00 09 11 00 00 09 11 00 00 09 11 00 00 ................
00012080 18 56 00 00 18 56 00 00 18 56 00 00 18 56 00 00 .V...V...V...V..
00012090 18 56 00 00 18 56 00 00 18 56 00 00 18 56 00 00 .V...V...V...V..
We can see that the value 0x000026EF is repeated 8 times, and we have the value 0x0000565C (the other reserved vector) repeated 8 times, and some others values are repeated too.
There is a big chance that we are inside some ZERO pages and it is a simple XOR cipher which is applied on the whole file.
Maybe the XOR cipher key is in the following form:
- key[0] repeated 8 times
- key[1] repeated 8 times
- key[2] repeated 8 times
- ...
- key[63] repeated 8 times
By looking at all the offset where are stored the value the 0x000026EF, we can compute the delta between them, we can make a guess that the key length is 0x800 bytes.
Hopefully we can find 3 version of the firmware on the internet:
- f553e034ba70138c270dac03e7dcda6f :
HW718V38_20170216.firmware
- 7440def1011f50749ff7f1ca8f7bcced :
HW718V39_20170221.firmware
- 0850c80192dcad7a0ce9a5446fb97f16 :
HW718V40_20171008.firmware
In order to extract the full key, we split each firmware file in block of 0x800 bytes chuncks, and compute the number of occurence of every 0x04 bytes repeated every 0x20 bytes.
With this we are able to extract the full key, some adjustments were required using the previous method:
- keys[34] = 0x2BB2
- keys[38] = 0x3175
- keys[54] = 0xD6ADBCEF # 4 bytes key ... wat !?
- keys[58] = 0xDEADBEEF # 4 bytes key ... wat !?
- keys[60] = 0x2A4F
- keys[62] = 0x1162
- keys[63] = 0xE3ADBEEF # 4 bytes key ... wat !?
The last step is now to know the exact value for the loading address, by starting to disassemble the firmware with a processor ARM little-endian at a loading address 0x08000000 and file offset 0x20, we can find an interesting disassembly:
ROM:0800B854 LDR R1, =0xE000ED08
ROM:0800B856 LDR R0, =0x8003C00
ROM:0800F458 STR R0, [R1]
0xE000ED08 is the Vector Table Offset Register and the value 0x8003C00 is written inside it.
Now we can reaload our firmware with an ARM processor in little-endian at the loading address 0x8003C00 and file offset 0x20 and we can start working on the reverse engineering.
binwalk -E <your file>
? The distribution of higher-entropy data is not as uniform as what I would expect from compressed or encrypted data. Furthermore, there are sizable low entropy regions. What are the results of runningstrings -n 9 <your file>
? Have you looked at a hex dump of the first few hundred bytes?