TL; 博士
是的,文件是加密的,它是一个简单的 XOR 密码,密钥长度为 0x800 字节。
用于获取解密固件的 Python 脚本:
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)
演练
查看固件的标头,我们可以做出一些猜测(所有值都存储在 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:也许是签名
- 0x00013B9C : 差不多是固件文件长度的长度(原为0x13C00)
如果我们进一步查看固件 hexdump 并假设我们正在处理 CORTEX 设备或类似的东西,在偏移量 0x20 处,我们可以认为有一个向量表:
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:初始SP值
- 0x08001B3E:重置向量
- 0x0800D5F2:NMI向量
- 0x0800D5A4:硬故障
- 0x0800D5A2:内存管理故障
- 0x0800D5A0:总线故障
- 0x0800D5BE:使用错误
- 0x000026EF:保留
- 0x0000565C:保留
- 0x0000565C:保留
- 0x0000565C:保留
在这里我们可以注意到几个问题:
- 为什么初始 SP 值不对齐?
- 为什么保留向量不为NULL?
在这一点上,grepp 字节序列EF 26 00 00
在一个有趣的地方结束:
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..
我们可以看到值 0x000026EF 重复了 8 次,我们有值 0x0000565C(另一个保留向量)重复了 8 次,其他一些值也重复了。
我们很有可能在一些零页面内,这是一个简单的 XOR 密码,应用于整个文件。
可能 XOR 密钥的形式如下:
- key[0] 重复 8 次
- key[1] 重复 8 次
- key[2] 重复 8 次
- ...
- key[63] 重复 8 次
通过查看存储值 0x000026EF 的所有偏移量,我们可以计算它们之间的增量,我们可以猜测密钥长度为 0x800 字节。
希望我们可以在互联网上找到 3 个版本的固件:
- f553e034ba70138c270dac03e7dcda6f:
HW718V38_20170216.firmware
- 7440def1011f50749ff7f1ca8f7bcced:
HW718V39_20170221.firmware
- 0850c80192dcad7a0ce9a5446fb97f16:
HW718V40_20171008.firmware
为了提取完整的密钥,我们将每个固件文件分成 0x800 字节的块,并计算每 0x20 字节重复的每 0x04 字节的出现次数。
有了这个,我们就可以提取完整的密钥,使用以前的方法需要进行一些调整:
- 键[34] = 0x2BB2
- 键[38] = 0x3175
- 键[54] = 0xD6ADBCEF # 4 字节密钥......哇!?
- 键[58] = 0xDEADBEEF # 4 字节键...哇!?
- 键[60] = 0x2A4F
- 键[62] = 0x1162
- 键[63] = 0xE3ADBEEF # 4 字节键...哇!?
最后一步是知道加载地址的确切值,通过在加载地址 0x08000000 和文件偏移量 0x20 处使用处理器 ARM little-endian 开始反汇编固件,我们可以找到一个有趣的反汇编:
ROM:0800B854 LDR R1, =0xE000ED08
ROM:0800B856 LDR R0, =0x8003C00
ROM:0800F458 STR R0, [R1]
0xE000ED08 是向量表偏移寄存器,其中写入值 0x8003C00。
现在,我们可以在加载地址 0x8003C00 和文件偏移量 0x20 处使用 little-endian 的 ARM 处理器重新加载固件,然后我们可以开始进行逆向工程。