文章总结: 这篇文章详细介绍了D-Link路由器M30A1_FW110B02固件的解密过程,作者通过分析固件结构,提取了盐值、初始化向量(IV)和密钥,并使用AES-128-CBC算法成功解密。文章重点解释了OpenSSL的EVP_BytesToKey密钥派生函数的工作原理,展示了如何通过SHA-256哈希运算生成密钥和IV。作者提供了完整的解密流程和代码示例,为类似固件解密提供了实用参考。
综合评分: 94
文章分类: 二进制安全,逆向分析,漏洞分析,IoT安全,代码审计
M30A1_FW110B02固件解密,AES、EVP-ByTokey(SHA-256)
原创
朝阳
Sec朝阳
2025年12月4日 16:45
江苏
1、信息收集
我们拿到M30A1的110版本先binwalk看一下,里面只有一个0x41的OpenSSL 加密,salted,并且盐也给我们了。
编辑
能确定是加密态,熵值接近1。
编辑
这里发现了Salt
2、加盐算法
我们在了解是OpenSSL加密那就先去了解一下他的加密逻辑。
DECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------65 0x41 OpenSSL encryption, salted, salt: 0xE5C9BB84466AF3C5
这里有三个参数,65是一个10进制的数,标识加密部分在文件中的起始位置
0x41是16进制表示,与65相同
下面的是加密的描述
OpenSSL encryption, salted, salt:0xE5C9BB84466AF3C5OpenSSL encryption:表示文件这一部分用了OpenSSL工具加密salted:表示加密过程中使用的盐值。salt:提供了盐的具体值,即0xE5C9BB84466AF3C5这个盐是生成密钥的一部分。
3、核心引擎AES-128 Encryption
我们能找到1.02版本是AES-128加密,而且在更新的版本更新公告中也没发现其他的安全升级。
AES
AES-128是对称加密的,意味着加密解密都是同样的一把钥匙(Key),我们需要找出这个Key
这里的128含义是密钥(Key)长度为128位,也就是16字节(128 ÷ 8),换算成Hex字符串是32个字符。
CBC
AES不会把整个文件一口气加密,把文件切成一块一块的,每块16字节处理,但是问题在于如果只是每块独立加密,ECB模式,那么文件里相同的 的内容,加密后还是相同的密文,能看得出来特征。
如果解决就引用了CBC模式,他是连锁反应,第二块加密依赖于第一块结果,第三块,依赖第二块加密结果,以此类推,但是第一块加密需要依赖的就是IV,也就是初始化向量这样一个概念来作为加密引擎。
填充
AES的块大小固定是16字节,如果文件大小不是16的倍数会用到OpenSSL的默认填充方式,他会在文件末尾补上几个字节,凑齐16的倍数,解密把他们丢掉。
加密逻辑
OpenSSL是一个工具库,它在加密有两种方案(1)、基于密码加密输入端:我们只需要一个密码(Password)和一个Salt(盐)过程:1、OpenSSL使用KDF(密钥派生函数,如MD5或PBKDF2)混合密码和Salt2、算出一个Key/IV3、用算出来的Key/IV去跑AES文件特征:文件头通常有Salted__字样,后面跟着8字节的盐(2)、基于原始密钥(Raw Key/iv)输入端:直接给AES引擎塞入Key(16字节)和IV(16字节)过程:跳过派生,直接加密文件特征:Key:通常硬编码在解密程序(如u-boot或解密脚本),文件找不到IV:通常放在文件头部的某个固定位置,比如前16字节,或偏移0x20处,因此解密必须用到。
总结
我们回归D-Link固件想要破解,就必须填上三个空
1、Key
一个16字节的Hex串,一般藏在路由器上个版本系统文件里,或者写死在解密程序的二进制代码里,但对于D-Link这个是通用。
2、IV(初始化向量)
16字节,他一定在固件文件里,因为解密程序运行时第一件事就是读取文件头的IV
3、算法
加密方式,我们能确定是AES-128-CBC
4、追踪IV、Key、确定密文长度、文件头分析。
那我们这一套非常之明确了,这就是第一种玩法嘛,非常明显的Salt__后的8字节,我们提取出来,然后还有一个点就是确定密文长度。我们一步一步来,先来最简单的确定密文长度。
一、头部元数据校验(确定加密长度)
由于在1.20版本中的一些发现,这个未加密的文件头中会有加密长度,我们寻找一下。
非常明显的一点就是0x50之前都是明文,以0x50作为一个分界线,我们可以在这个文件头中找到这个加密长度,或者从0x50开始往后截取成一个新的文件。我们先看一下整个文件大小。
我们先查询到整个文件的长度,这个作用是比对长度,以此为标准,因为明文占用的空间非常的小,大部分都是加密的,因此我们找那串16进制的符合。
我们写了一个脚本。我们先把猜测的加密部分提取出来,比对一下大小。
这里应该是截的有些偏差,不过大概能判断出这两个位置是加密长度,因为去除头部明文差不多是这个长度。
我们应该从0x51开始截取,我们再截一次。
dd if=M30A1_FW110B02.bin bs=1 skip=80 of=payload_80.bin
好像区别不大。
但我们在提起盐的部分发现从0x41(salt+加密长度)开始截取的长度刚好就是38871424,那么我们找的文件长度是没问题的,剔除salt就是整个加密长度。
二、提取Salt
我们一开始使用的binwalk很明显的发现了0X41开始就是Salt__,然后后面8字节是salt值。
我们先把这个salt提取出来。
ENCSIZE=$(hexdump -s 0x18 -n 4 -e '1/4 "%u\n"' M30A1_FW110B02.bin)echo "ENCSIZE=$ENCSIZE"ENCSIZE=38871424dd if=M30A1_FW110B02.bin of=raw_enc.bin bs=1 skip=$((0x41)) count=$ENCSIZE
我们先提取盐和加密数据这一段,从0x41开始。
这次就是对的,也符合我们一开始用binwalk找到的salt: 0xE5C9BB84466AF3C5。
//从第 73 字节 (0x49) 开始,切 8 个字节,存为 salt.bindd if=salt+ciphertext.bin bs=1 skip=8 count=8 of=salt.bin//查看提取出来的 Salt (HEX 格式)xxd -p salt.bin
确定这是我们的salt,接下来寻找IV。
dd if=salt+ciphertext.bin bs=1 skip=16 of=ciphertext.bin
三、寻找IV
首先我们理清一下逻辑,我们再看一遍这两种方法,一种是基于密码加密、一种是基于原始密钥,我们有Key,Salt那就可以尝试基于原始密钥的方式了,去寻找IV尝试解密,我们直接给AES塞入Key和IV直接加密,由此我们大概率能猜到IV。
为什么说IV我们可以找到,我们可以确认算法是AES-128,这是在升级公告中所提到的,IV长度需求是16字节,我们紧盯着0x41之前的数据。
Salt之前的数据,前32字节(0x00-0x1F)元数据区,这里存放着魔术头、固件大小、校验和版本号等,元数据结束后紧接着通常是加密参数,我们最重要的就是IV,然后0x41又是Salt,这是OpenSSL的标准头,其实我们要找的IV必须存在0x20-0x40这一段空窗期,不然我们没办法在解密前拿到他,是这样的。
接下来我们分析这一段吧,把他了解透彻。
MH01就是固件的签名,魔术头,01应该代表版本号v1,然后0x10这一行也有一个MH01再次确认一遍,0x18是我们大概是加密数据的长度。然后再往后走就发现了两行奇怪的数据,我们看一下。
这两行看到了奇怪的数据,那我们根据之前的想法也大概率能想到是IV,这32的ASCII字符表示16字节的HEX IV。
为什么这32字节是16位的IV呢,因为AES算法在底层运算时是需要16字节的二进制数据的,我们开始截取。
(1)、提取
我们提取就要从0x20开始,也就是32字节开始,切32字节,把IV保存成一个文件
(2)、确认IV
如果这段数据是IV的字符串表示,他应该只包含0-9和a-f(或A-F)字符,他不会出现乱码,我们可以试试cat和hendump验证验证。
我们大概率可以确认是IV
接下来验证一下字符个数,也就是长度。
5、尝试解密
既然我们知道他是用OpenSSL加密的,我们尝试用OpenSSL解密,我们调用OpenSSL尝试一下。
aes-128-cbc:指定算法,告诉这个OpenSSL是AES加密,128位的,CBC模式。
-D:Decrypt(解密)不加这个是默认加密
-K:Key(密钥)通用钥是
-iv(小写):切出来的IV
-in:输入文件(纯密文)
-out:输出文件(解开后的固件)
openssl aes-128-cbc -d -K xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -iv $(cat IV.txt) -in clean_ciphertext.bin -out decrypted_b451.bin
有一些突破,但是不知道是不是这样。
编辑
偶然回头以看发现我们已经成功破解了,我们回顾一下解密这一部分。但是写到这里我整个人还是有点疑惑,疑惑在于不论我是否解密成功他都会报错
6、解密回顾
一、提取加密字段、IV
echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > IV.txt写入IV
精确提取从Salted__头部开始的完整加密数据块
dd if=M30A1_FW110B02.bin of=raw_enc.bin bs=1 skip=65
但是有时候拆分数据时候会发生错误,建议用dd来截取。
我们可以把签名这一段忽略
这里面我们把0x41开始截取,这样我们能得到一个带盐+密文的字段,然后后续我们再提盐。
提取到这样我们整个文件就工整很多了,然后我们提取后8位的Salt,然后再把整个Salt部分去除掉就变成了完整的加密部分。
二、核心解密
下一步我们就用OpenSSL解密,我们一开始手上是掌握了Key的,然后使用命令。
openssl aes-128-cbc -d -pass pass:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -in raw_enc.bin -out try_pass_mode.bin
三、密码学模式推断与试错
首先确定了密文的起始字节为Salted__,证实用了OpenSSL的EVP框架,Key派生是必须的。
然后基于D-Link更新公告和IV长度,锁定算法为AES-128-CBC
四、KDF(密钥派生成功)
改变Key的角色,激活OpenSSL的自动派生机制
运行-pass模式,将Key是为Password,结果就是OpenSSL自动读取文件中的Salt,并使用Password+Salt派生出Key/IV,然后我们成功实现了,熵值降到0.69,证明Key是Password,而bad decrypt是虚假报错,数据主体已解密。
五、EVP_BytesTokey派生逻辑实现
#!/bin/bash# --- 1. 定义常量 ---PASSWORD="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"# 假设 salt 已经从 raw_enc.bin 中提取并保存在这个文件里SALT_FILE="extracted_salt.bin" # --- 2. KDF 第一轮:生成 D1 ---echo "--- KDF 1/2: 生成 D1 (SHA256(Password + Salt)) ---"# 拼接 Password 和 Salt,并计算 SHA256 摘要{ printf "%s" "$PASSWORD"; cat "$SALT_FILE"; } | openssl dgst -sha256 -binary > D1.binecho "D1.bin 创建完成 (32 bytes)"# --- 3. KDF 第二轮:生成 D2 ---echo -e "\n--- KDF 2/2: 生成 D2 (SHA256(D1 + Password + Salt)) ---"# 拼接 D1.bin、Password 和 Salt,并计算 SHA256 摘要{ cat D1.bin; printf "%s" "$PASSWORD"; cat "$SALT_FILE"; } | openssl dgst -sha256 -binary > D2.binecho "D2.bin 创建完成 (32 bytes)"# --- 4. 拼接并提取 Key/IV ---echo -e "\n--- 提取 Key 和 IV ---"# 将 D1 和 D2 拼接成完整的 64 字节密钥材料cat D1.bin D2.bin > KM.bin# 提取 Key (前 16 字节用于 AES-128)dd if=KM.bin of=derived_key.bin bs=1 count=16 status=none# 提取 IV (从 16 字节开始,提取 16 字节)dd if=KM.bin of=derived_iv.bin bs=1 skip=16 count=16 status=none# --- 5. 打印结果 (十六进制) ---echo -e "\n派生出的 Key 和 IV (十六进制):"KEY_HEX=$(xxd -p derived_key.bin | tr -d '\n')IV_HEX=$(xxd -p derived_iv.bin | tr -d '\n')echo "KEY (-K): $KEY_HEX"echo "IV (-iv): $IV_HEX"# --- 6. 最终解密命令示例 ---# 使用派生出的 Key 和 IV,对纯密文文件 (cipher.bin) 进行解密# openssl aes-128-cbc -d -K $KEY_HEX -iv $IV_HEX -in cipher.bin -out manual_code_decrypt.bin
六、解密实质
一开始的Key我们原以为是用于解密也就是-K模式,然后被KDF流程处理了,但是这个Key是作为KDF的输入,也就是-Pass模式,他被OpenSSL读取,然后与Salt混合,派生出了真的AES Key和IV。
也就是说我们提供的Hex串作为Pasword,必须进入KDF与Salt混合,才能产生出真的Key和IV
七、手动解密流程
如果我们想脱离OpenSSL的-pass模式,我们需要以下三个步骤。
(1)、定义常量
PASSWORD="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" SALT_FILE="extracted_salt.bin"
我们需要把Key当作Password,然后把Salt单独放一个文件中。
(2)、运行KDF逻辑,生成Key
这里使用SHA-256两轮KDF逻辑,只提取key
D1 = SHA256({Password}+{Salt})//生成D1(SHA256(Password + Salt)){ printf "%s" "$PASSWORD"; cat "$SALT_FILE"; } | openssl dgst -sha256 -binary > D1.binD2 = (SHA256(D1 + Password + Salt))//生成 D2 (SHA256(D1 + Password + Salt)) { cat D1.bin; printf"%s""$PASSWORD"; cat "$SALT_FILE"; } | openssl dgst -sha256 -binary > D2.bin
拼接并提取Key(前16字节)
cat D1.bin D2.bin > KM.bin dd if=KM.bin of=derived_key.bin bs=1 count=16 status=none
# 将 Key 转换为十六进制字符串
KEY_HEX=$(xxd -p derived_key.bin | tr -d '\n') echo"Derived Key: $KEY_HEX"
(3)、混合解密(Code Mode)
现在用我们派生出的Key和Header中的IV对纯密文解密。
openssl aes-128-cbc -d \ -K "$KEY_HEX" \ -iv "$IV_HEX" \ -in cipher.bin \ -out hybrid_decrypt.bin
我们可以得到派生的Key和IV。
openssl aes-128-cbc -d -K xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -iv xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -in cipher.bin -out manual_decrypt_success.bin
然后再执行即可。
编辑
这里我们想不依赖-pass模式,手动解密,把Key当作Password然后追踪一下全流程,这样来说说可以更清晰的其加密过程。
echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | xxd -r -p > password_raw.bin//准备好Key也就是Passwordecho "E5C9BB84466AF3C5" | xxd -r -p > salt_raw.bin//盐{ cat password_raw.bin; cat salt_raw.bin; } | openssl dgst -sha256 -binary > D1.bin//生成D1echo -n "D1 (HEX): " && xxd -p D1.bin | tr -d '\n' && echo ""//打印D1D1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx{ cat D1.bin; cat password_raw.bin; cat salt_raw.bin; } | openssl dgst -sha256 -binary > D2.bin//生成D2echo -n "D2 (HEX): " && xxd -p D2.bin | tr -d '\n' && echo ""//打印D2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcat D1.bin D2.bin > KM.bin//拼接完整密钥dd if=KM.bin of=derived_key.bin bs=1 count=16 status=none//提取Key前16字节dd if=KM.bin of=derived_iv.bin bs=1 skip=16 count=16 status=none//提取Key后16字节echo -n "派生 Key: " && xxd -p derived_key.bin | tr -d '\n' && echo ""# 10. 打印派生的 Key (D1)# 11. 打印派生的 IV (D1)echo -n "派生 IV: " && xxd -p derived_iv.bin | tr -d '\n' && echo ""
成功
八、算法原理(IV和Key来源)
为什么是SHA-256哈希运算,因为每次运行SHA-256都能产出32字节的数据。
但是问题在于我们在文件头看到了数据只有IV没见到Key的身影,原因就在于如果只进行一轮的话确实刚好足够32字节,但这样Key和IV必须从这同一个32字节中分出来。
但是如果Key和IV都是同一轮生产出的话会很不安全,基本是打名牌,最安全的方式就是第一次产出32字节提供给Key,第二次产出提供给IV,这样总计64字节,也会很安全。因此厂商必须进行两次SHA-256运算,才能保证Key和IV都有足够的密钥数据,这是更安全的。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Sec朝阳 朝阳《M30A1_FW110B02固件解密,AES、EVP-ByTokey(SHA-256)》