一次对STM32f103某固件的破解
m24h2022/08/25原创 软件综合 IP:上海
关键词
STM32F103Hack

昨晚夜深人静正好做点Hack的事 于是我破解了我自己某个装置的固件 用于研究 学习

显然 该固件是加密的 一眼兜过去 依稀能在0x200处看到有STM32的向量表形式 既大量重复的数据 而且从重复周期看 显然是一种128位块的ECB加密方式

vector-encrypted.png

第一个猜测当然是当前最受欢迎的AES-128加密了 但是其实还有点不对 (后面发现是AES-256) 我还是想通过SWD接口弄下固件再说 ..... 失败了 ICode被保护 禁止获取flash内容

唉 能弄到多少是多少吧 STM32有个漏洞 就是当发生错误或者中断时 SWD接口会暴露出中断向量地址 比如通过openocd给个"reset halt" 那么PC值会被告知 这个值装自于中断向量表 这意味着 中断向量表所在处有一个4byte内容被暴露出来...通过移动中断向量表 产生各种中断 就能4byte 4byte地得到被保护的Flash内容....但是有部分保留的中断号没有办法引发中断 所以 这个方法只能获得大概90%的Flash内容

关于这个漏洞的具体可用的脚本请移步XXXXXXXXXXXXXXXXXX/romanoLT/stm32f1-firmware-extractor

上面的脚本 建议不能破解的地方 选择填入"00BF00BF" (--value 选项) 就是两个NOP 而产生的结果 可以用这个脚本转成bin文件 然后尝试用IDA解 (也可以用--binary选项 应该不用下面的脚本转换 直接输出二进制文件 但我当时没有选这个)

open F,">extr.bin";
binmode F;
while(<>) {
	chomp;
	if (m/^\s*[0-9A-F]{8}\s*:\s*([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})\s*$/i) {
		print F pack('CCCC',hex($4),hex($3),hex($2),hex($1));
	}
}
close F;

虽然不是完全破解 但是还是能够发现 原设备有个bootloader 对送过来的固件进行解密再进行IAP 同时甚至还检测MPU ID .... 他的解密有明显的行位移 GF域乘2 异或RoundKey等AES解密特征 而且每块解密13轮半 (无论AES多少 块都是128位 AES-128的话 是9轮半) .... 确定是AES-256-ECB 就差密钥了

不管怎么样 密钥一定存放在某处 尤其在内部RAM中 不管ROM里面怎么操作 大多数情况下 总会在某些时刻在RAM里面形成连续存放的密钥 ... 幸好这个设备还有一个漏洞 DCode没有被保护 于是我在openOcd使用"mdb 0x20000000 20480"命令获取了整个RAM的映像的log 

> mdb 0x20000000 20480
0x20000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 00 08 00 00 00 00 00 00 00 00 
0x20000020: ..... 此处密钥为厂商隐去 ..... 
0x20000040: 00 00 00 00 00 00 00 00 00 63 00 08 00 00 00 00 02 00 00 00 64 10 00 20 ac 01 00 20 00 00 00 bf 
0x20000060: dc 01 00 20 00 10 10 00 00 00 00 00 01 02 03 04 01 02 03 04 06 07 08 09 02 04 06 08 00 00 00 00 
0x20000080: 00 f0 03 00 00 02 00 00 f8 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
0x200000a0: 00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 00 
0x200000c0: 00 00 00 00 00 00 80 02 02 20 00 00 00 45 46 54 4f 4e 45 58 20 49 41 50 20 46 6c 61 73 68 44 69 
0x200000e0: 73 6b 20 20 20 31 2e 30 20 00 80 02 02 21 00 00 00 56 69 6c 74 72 6f 78 2d 49 41 50 20 20 20 46 
0x20000100: 6c 61 73 68 20 44 69 73 6b 20 31 2e 30 20 03 00 00 00 00 06 00 00 00 00 00 00 70 00 00 00 00 00 
0x20000120: 00 0a 00 00 00 00 00 00 00 00 00 00 00 00 01 f7 00 00 02 00 00 00 00 08 00 00 01 f8 02 00 02 00 
0x20000140: 04 00 1f 00 06 00 00 00 01 00 00 00 1a 03 36 00 44 00 38 00 41 00 34 00 35 00 39 00 39 00 35 00 
0x20000160: 30 00 35 00 35 00 00 00 00 01 55 00 11 54 00 08 25 1b 00 08 25 1b 00 08 25 1b 00 08 25 1b 00 08 
0x20000180: 25 1b 00 08 25 1b 00 08 25 1b 00 08 19 54 00 08 25 1b 00 08 25 1b 00 08 25 1b 00 08 25 1b 00 08

然后再用下面这个perl脚本把上面的文本log 转成了二进制文件

die "Usage : $0 <log file> <out file>\n" unless $#ARGV>=1;
open Fin, "<$ARGV[0]" or die "cannot open '$ARGV[0]' for reading\n";
open Fout, ">$ARGV[1]" or die "cannot open '$ARGV[1]' for writing\n";
binmode Fout;
while(<Fin>) {
	chomp;
	if (m/^\s*0x[0-9A-F]+\s*:([\s 0-9A-F]*)$/i) {
		$t=$1;
		while ($t=~m/([0-9A-F][0-9A-F])/ig) {
			print Fout pack('C',hex($1));	
		}
	}
}
close Fin;
close Fout;

又再写了一个Python脚本对RAM二进制文件里面的所有32字节 尝试作为密钥碰撞解密 (就是解一小段疑是中断向量的密文 并且如果密钥正确结果应该是0x0800xxxx)

from Crypto.Cipher import AES
import binascii
import sys

def decrypt(key, ctext):
	cipher = AES.new(key, AES.MODE_ECB)
	return cipher.decrypt(ctext)

if len(sys.argv)<2 :
	print('Try to decrypt using dictionary data file')
	print('Usage: python', sys.argv[0], '<dictionary file>')
	exit(1)

#some cipher text with known decrypted result
data=binascii.unhexlify('E16DC345FAC37AAB741BB44C3EC3D537E16DC345FAC37AAB741BB44C3EC3D537')

#get dictionary data
f=open(sys.argv[1], "rb")
keys=f.read()
f.close()
#some suggest
keys+= b'\x00'*32 + b'\x55'*32 + b'\xAA'*32+ b'\xFF'*32

#for AES-256
nkey=32

n=len(keys)-nkey
i=0
while i<n:
	key=keys[i:i+nkey]  
	dec=decrypt(key, data)
	if dec[3]==0x08 and dec[7]==0x08: # vectors of stm32
		print ("i:","%04X" % i,'  key:',binascii.hexlify(key),'   dec:',binascii.hexlify(dec))
	if  dec[0]>13 and dec[0]<128 and dec[1]>13 and dec[1]<128 and dec[2]>13 and dec[2]<128 and dec[3]>13 and dec[3]<128 and \
	    dec[4]>13 and dec[4]<128 and dec[5]>13 and dec[5]<128 and dec[6]>13 and dec[6]<128 and dec[7]>13 and dec[7]<128 and \
	    dec[8]>13 and dec[8]<128 and dec[9]>13 and dec[9]<128 and dec[10]>13 and dec[10]<128 and dec[11]>13 and dec[11]<128 and \
	    dec[12]>13 and dec[12]<128 and dec[13]>13 and dec[13]<128 and dec[14]>13 and dec[14]<128 and dec[15]>13 and dec[15]<128 :
	    #maybe printable hex file?
		print ("i:","%04X" % i,'  key:',binascii.hexlify(key),'   dec:',dec)
	i+=1

 还真碰到死耗子了

key-found.png

好吧 密钥就是厂商名加上大多数中国人最喜欢的数字

然后用openssl进行整个固件文件的解密

openssl enc -d -aes-256-ecb -in rom.14 -out rom.bin -K <密钥> -iv 00000000000000000000000000000000 -nopad -nosalt

然后在IDA里面加载 注意因为有bootloader 加载的地址不是0x8000000 从向量表漏洞获取的不完全flash内容可以看出 明显第二段App从0x8006400开始的 .... 加载后 觉得固件应该是正常解密了 反汇编看起来没有问题

吐槽一下 实际固件只有37k那么大 固件文件却有73k那么长 还超出了它用的STM32f103C8T6能容纳的长度 我开头还以为有啥特别之处 ..... 后来发现 固件文件头里有个参数 取反后是固件实际有用长度 指导解密和IAP 到那么长就停了 ....也就是说 固件文件中 后面一半的内容 都是没用的 纯粹只是为了假装功能很复杂程序很长 sticker  

就这样了 破解过程应该具有一定的普遍性 只是大家要注意 用于研究学习 不要搞坏事哦

-------------------------------------------

另: 上面的perl代码第一行 <log file>和<out file> 被自动加上了HTML反标签</log file>和</out file> 而且我已经选了"代码"是"perl"而不是其他 这个应该是这个网站的bug 我没法修改 修改了又会自动加上 ... 要用的话 请Copy后自己手动去除

又另 上报之后 这个问题已经被网站的科创程序员修复 已经没这个问题了 速度真快


[修改于 2年3个月前 - 2022/08/25 19:42:32]

来自:计算机科学 / 软件综合
8
12
已屏蔽 原因:{{ notice.reason }}已屏蔽
{{notice.noticeContent}}
~~空空如也
smith
2年3个月前 IP:广东
907666

说来IDA pro反编译的STM32代码, 和X86以及Java反编译还是不能比, 我之前反编译了几个别人的hex, 感觉都很难看出原来的代码逻辑。。。


有什么好分析思路吗


引用
评论(2)
2
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
m24h作者
2年3个月前 IP:上海
907667
引用smith发表于1楼的内容
说来IDA pro反编译的STM32代码, 和X86以及Java反编译还是不能比, 我之前反编译了几...

比起没有伪C代码 而且可能出现各种硬核技巧的51反编译来说 已经好太多了

STM32的程序的基本功能 都有大致框架(或者说相似的库实现) 从初始化 主循环 中断处理这几点入手也容易找到应用的逻辑

如果能推测作者用的库 并签名了相应的库 可以剔除许多来自库函数的干扰

另一个可能的利器是lumina服务器 也许可以分享和利用其他人反编译的成果 节省在库和常用函数上的开销 但是要花钱(所以我没用 但是感觉是不错的功能 虽然有个免费的服务器 效果几乎没有 毕竟搞反编译的人少)

最难的是 许多逻辑其实是在程序之外 然后特化在程序里面 就只能知其然不知其所以然了

反编译没有什么定势的路线 每个人目的也不同 有的想改关键参数 有的想得到未披露的逻辑 有的为了仿制迁移 尽量忽略不关心的就是了 比如我如果想得到的是其中某个算法 那么看到是围绕端口和外设之类的代码 都直接略过


引用
评论
5
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
amo
2年3个月前 IP:广东
907744
引用smith发表于1楼的内容
说来IDA pro反编译的STM32代码, 和X86以及Java反编译还是不能比, 我之前反编译了几...

我接触过不少历史遗留旧产品的源码,用IDE打开直接看C都很费劲很费劲……假如你反编译的固件就是用这种级别源码编译出来的,能看出逻辑还真是一个奇迹


引用
评论
1
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
m24h作者
2年3个月前 修改于 2年3个月前 IP:上海
907784
引用amo发表于3楼的内容
我接触过不少历史遗留旧产品的源码,用IDE打开直接看C都很费劲很费劲……假如你反编译的固件就是用这种...

是啊 我为了研究一个未公开的协议 最开始研究的固件 没有加密 基于51 。。。纯汇编 处处是内存换页代码 各种硬核技巧在螺蛳场里做道场

我看绝望了 宁可破解一个基于STM32的固件 哪怕加上破解工作量也值了

至于C代码或者其他高级语言 我从未怕过 早就业精于此了

引用
评论
4
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
zjsx8192
2年1个月前 IP:广东
910033

这个读取固件的脚本为什么我运行时候有错误,是不是需要修改?我的是python3.8的

引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
m24h作者
2年1个月前 IP:上海
910045
引用zjsx8192发表于5楼的内容
这个读取固件的脚本为什么我运行时候有错误,是不是需要修改?我的是python3.8的

我不知道你说的是哪个脚本 我似乎没有提供读取固件的脚本 如果是GitHub上的那个 需要自己分析一下 都是源代码公开的东西

而我提供的脚本有些是perl的

如果有其他问题 比如兼容性 甚至发布时候网页转码带来的问题 也需要自己研究一下



引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
zjsx8192
2年0个月前 IP:广东
910055
引用m24h发表于6楼的内容
我不知道你说的是哪个脚本 我似乎没有提供读取固件的脚本 如果是GitHub上的那个 需要自己分析一下...

好的,谢谢,我说的较本是那个github的。

引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论

想参与大家的讨论?现在就 登录 或者 注册

所属专业
上级专业
同级专业
m24h
进士 学者 机友
文章
52
回复
896
学术分
1
2020/01/22注册,2时58分前活动

个人开源项目: XXXXXXXXXXXXXX

主体类型:个人
所属领域:无
认证方式:手机号
IP归属地:上海
文件下载
加载中...
{{errorInfo}}
{{downloadWarning}}
你在 {{downloadTime}} 下载过当前文件。
文件名称:{{resource.defaultFile.name}}
下载次数:{{resource.hits}}
上传用户:{{uploader.username}}
所需积分:{{costScores}},{{holdScores}}下载当前附件免费{{description}}
积分不足,去充值
文件已丢失

当前账号的附件下载数量限制如下:
时段 个数
{{f.startingTime}}点 - {{f.endTime}}点 {{f.fileCount}}
视频暂不能访问,请登录试试
仅供内部学术交流或培训使用,请先保存到本地。本内容不代表科创观点,未经原作者同意,请勿转载。
音频暂不能访问,请登录试试
支持的图片格式:jpg, jpeg, png
插入公式
评论控制
加载中...
文号:{{pid}}
投诉或举报
加载中...
{{tip}}
请选择违规类型:
{{reason.type}}

空空如也

加载中...
详情
详情
推送到专栏从专栏移除
设为匿名取消匿名
查看作者
回复
只看作者
加入收藏取消收藏
收藏
取消收藏
折叠回复
置顶取消置顶
评学术分
鼓励
设为精选取消精选
管理提醒
编辑
通过审核
评论控制
退修或删除
历史版本
违规记录
投诉或举报
加入黑名单移除黑名单
查看IP
{{format('YYYY/MM/DD HH:mm:ss', toc)}}