一种通过篡改特定代码数据修复嵌入式产品BUG的方法
一、前言
在嵌入式产品开发中,难以避免地会因为各种原因导致最后出货的产品存在各种各样的BUG,通常会给产品进行固件升级来解决问题。记得之前在公司维护一款BLE产品的时候,由于前期平台预研不足,OTA参数设置不当,导致少数产品出现不能OTA的情况,经过分析只需改变代码中的某个参数数值即可,但是产品在用户手里,OTA是唯一能更新代码的方式,只能给用户重发产品。后来在想,是否可以提前做好一个接口,支持动态地传输少量代码到产品中临时运行,通过修改特定位置的Flash代码数据来修复产品的棘手BUG?多留一个后门,有时候令产品出棘手问题的往往是那么一两行代码或者几个初始化的参数不对,那么这种方法也可以应应急,虽然操作比较骚,
本文记录了自己的探究过程,需要对汇编语言的编码和实现机制有基本的掌握。
二、创建演示工程
本文以STM32F103C8T6单片机为例创建演示工程,分为app和bootloader两个工程。即将mcu的Flash分为“app”和“bootloader”两个区域, bootloader放在0x8000000为起始的24KB区域内,app放在0x8006000为起始的后续区域。bootloader完成对app的Flash数据修改。
1、app工程
注意app的工程需要在keil上修改ROM起始地址。
还要在app代码的开头设置向量偏移(调用一行代码):
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x6000);
app工程的逻辑为:先顺序执行3个不同速度的LED闪灯过程(20ms、200ms、500ms、切换亮灭),最后进入到一个循环状态每秒切换一次LED的状态闪烁。代码如下:
void init_led(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_ResetBits(GPIOB, GPIO_Pin_10);GPIO_SetBits(GPIOB, GPIO_Pin_10);
}void led_blings_1(void)
{uint32_t i;for (i = 0; i < 10; i++){GPIO_SetBits(GPIOB, GPIO_Pin_10); delay_ms(20); GPIO_ResetBits(GPIOB, GPIO_Pin_10); delay_ms(20);}
}void led_blings_2(void)
{uint32_t i;for (i = 0; i < 10; i++){GPIO_SetBits(GPIOB, GPIO_Pin_10); delay_ms(200); GPIO_ResetBits(GPIOB, GPIO_Pin_10); delay_ms(200);}
}void led_blings_3(void)
{uint32_t i;for (i = 0; i < 10; i++){GPIO_SetBits(GPIOB, GPIO_Pin_10); delay_ms(500); GPIO_ResetBits(GPIOB, GPIO_Pin_10); delay_ms(500);}
}int main()
{NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x6000);SysTick_Init(72);init_led();led_blings_1();led_blings_2();led_blings_3();while (1){GPIO_SetBits(GPIOB, GPIO_Pin_10); delay_ms(1000); GPIO_ResetBits(GPIOB, GPIO_Pin_10); delay_ms(1000);}
}
为了分析汇编和查看bin文件数据,我们需要在keil中添加两条命令,分别生成.dis反汇编和.bin的代码文件。(具体的目录情况依葫芦画瓢)
fromelf --text -a -c --output=all.dis Obj\Template.axf
fromelf --bin --output=test.bin Obj\Template.axf
先将app的代码烧写进单片机,注意烧写设置里面选择“Erase Sectors”只擦除需要烧写的地方。
2、bootloader工程
在bootloader中分为两部分,不变的代码部分和变动的代码部分(error_process函数)。初次编译的时候error_process写为空函数,当我们有需求对App进行修改的时候,我们重新编译工程对error_process函数进行填充。为了重新编译工程的时候不影响之前函数的链接地址,特意将error_process函数放到代码区的最后0x8000800地址处,理由是原来工程大小是1.51KB,擦除页大小是2KB,所以需要2KB对齐,对齐处的地址就选择0x8000800为起始。代码如下:
#define FLASH_PAGE_SIZE 2048
#define ERROR_PROCESS_CODE_ADDR 0x8000800void error_process(void) __attribute__((section(".ARM.__at_0x8000800")));void init_led(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_ResetBits(GPIOB, GPIO_Pin_10);GPIO_SetBits(GPIOB, GPIO_Pin_10);
}uint32_t pageBuf[FLASH_PAGE_SIZE / 4];void error_process(void)
{}void eraseErrorProcessCode(void)
{FLASH_Unlock();FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);FLASH_ErasePage(ERROR_PROCESS_CODE_ADDR);FLASH_Lock();
}void(*boot_jump2App)();void boot_loadApp(uint32_t addr)
{uint8_t i;if (((*(vu32*)addr) & 0x2FFE0000) == 0x20000000) {boot_jump2App = (void(*)())*(vu32*)(addr + 4); __set_MSP(*(vu32*)addr);for (i = 0; i < 8; i++){NVIC->ICER[i] = 0xFFFFFFFF; NVIC->ICPR[i] = 0xFFFFFFFF; }boot_jump2App(); while (1);}
}int main()
{uint32_t flag;SysTick_Init(72);flag = *((uint32_t *)ERROR_PROCESS_CODE_ADDR);if ((flag != 0xFFFFFFFF) && (flag != 0)){init_led();GPIO_ResetBits(GPIOB, GPIO_Pin_10); delay_ms(1000);delay_ms(1000);error_process();eraseErrorProcessCode();}boot_loadApp(0x8006000);while (1);
}
一进main函数就读取0x8000800地址处的32位数据,如果不是全F或者全0那么这个地方是有函数体存在需要执行的,那么将LED亮起2秒钟代表bootloader识别到有处理程序需要执行(当然这里还需要加一些error_process代码数据是否完整之类的判断机制,这里演示先略去)。执行完处理程序后将处理程序擦除(数据变为全F),避免以后每次上电都重复擦写Flash。
error_process函数代码的数据由产品正常使用期间通过数据接口传入直接写入到0x8000800处(这部分的demo略去),编译后查看生成的bin文件将error_process部分的代码截取出来传输到Flash地址0x8000800处。
bootloader的代码烧写进单片机时注意烧写设置里面选择“Erase Sectors”只擦除需要烧写的地方。keil设置里ROM地址改回0x08000000。
三、修改app的特定参数
在app的工程中以“led_blings_1”函数为例,反汇编如下:
$ti.led_blings_1led_blings_10x08006558: b510 .. PUSH {r4,lr}0x0800655a: 2400 .$ MOVS r4,#00x0800655c: e010 .. B 0x8006580 ; led_blings_1 + 400x0800655e: f44f6180 O..a MOV r1,#0x4000x08006562: 4809 .H LDR r0,[pc,#36] ; [0x8006588] = 0x40010c000x08006564: f7fffea2 .... BL GPIO_SetBits ; 0x80062ac0x08006568: 2014 . MOVS r0,#0x140x0800656a: f7ffffaf .... BL delay_ms ; 0x80064cc0x0800656e: f44f6180 O..a MOV r1,#0x4000x08006572: 4805 .H LDR r0,[pc,#20] ; [0x8006588] = 0x40010c000x08006574: f7fffe98 .... BL GPIO_ResetBits ; 0x80062a80x08006578: 2014 . MOVS r0,#0x140x0800657a: f7ffffa7 .... BL delay_ms ; 0x80064cc0x0800657e: 1c64 d. ADDS r4,r4,#10x08006580: 2c0a ., CMP r4,#0xa0x08006582: d3ec .. BCC 0x800655e ; led_blings_1 + 60x08006584: bd10 .. POP {r4,pc}$d0x08006586: 0000 .. DCW 00x08006588: 40010c00 ...@ DCD 1073810432
由于led是20ms交替亮灭一次,如果我们觉得这个参数有问题想改成100ms,从汇编上来说就是要改变两行代码:
0x08006568: 2014 . MOVS r0,#0x14
0x08006578: 2014 . MOVS r0,#0x14
改为
0x08006568: 2064 2 MOVS r0,#0x64
0x08006578: 2064 2 MOVS r0,#0x64
bootloader工程中error_process的函数实现如下:
void error_process(void)
{#define MODIFY_FUNC_ADDR_START 0x08006558uint32_t alignPageAddr = MODIFY_FUNC_ADDR_START / FLASH_PAGE_SIZE * FLASH_PAGE_SIZE;uint32_t cnt, i;// 1. copy old codememcpy(pageBuf, (void *)alignPageAddr, FLASH_PAGE_SIZE);// 2. change code.pageBuf[90 + 256] = (pageBuf[90 + 256] & 0xFFFF0000) | 0x2064;pageBuf[94 + 256] = (pageBuf[94 + 256] & 0xFFFF0000) | 0x2064;// 3. erase old code, copy new code.FLASH_Unlock();FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);FLASH_ErasePage(alignPageAddr);cnt = FLASH_PAGE_SIZE / 4;for (i = 0; i < cnt; i++){FLASH_ProgramWord(alignPageAddr + i * 4, pageBuf[i]);}FLASH_Lock();
}
由于Flash的2KB页擦除特性,这里先将待修改代码区的Flash页数据拷贝到缓冲buffer里,然后修改buffer里的数据,之后擦除Flash相关页,最后将buffer里修改后的数据重新写回到Flash里去。error_process函数的反汇编如下:
$t.ARM.__at_0x8000800error_process0x08000800: b570 p. PUSH {r4-r6,lr}0x08000802: 4d1a .M LDR r5,[pc,#104] ; [0x800086c] = 0x80060000x08000804: 142a *. ASRS r2,r5,#160x08000806: 4629 )F MOV r1,r50x08000808: 4819 .H LDR r0,[pc,#100] ; [0x8000870] = 0x200000080x0800080a: f7fffcbd .... BL __aeabi_memcpy ; 0x80001880x0800080e: 4818 .H LDR r0,[pc,#96] ; [0x8000870] = 0x200000080x08000810: f8d00568 ..h. LDR r0,[r0,#0x568]0x08000814: f36f000f o... BFC r0,#0,#160x08000818: f2420164 B.d. MOV r1,#0x20640x0800081c: 4408 .D ADD r0,r0,r10x0800081e: 4914 .I LDR r1,[pc,#80] ; [0x8000870] = 0x200000080x08000820: f8c10568 ..h. STR r0,[r1,#0x568]0x08000824: 4608 .F MOV r0,r10x08000826: f8d00578 ..x. LDR r0,[r0,#0x578]0x0800082a: f36f000f o... BFC r0,#0,#160x0800082e: f2420164 B.d. MOV r1,#0x20640x08000832: 4408 .D ADD r0,r0,r10x08000834: 490e .I LDR r1,[pc,#56] ; [0x8000870] = 0x200000080x08000836: f8c10578 ..x. STR r0,[r1,#0x578]0x0800083a: f7fffd53 ..S. BL FLASH_Unlock ; 0x80002e40x0800083e: 2035 5 MOVS r0,#0x350x08000840: f7fffcca .... BL FLASH_ClearFlag ; 0x80001d80x08000844: 4628 (F MOV r0,r50x08000846: f7fffccd .... BL FLASH_ErasePage ; 0x80001e40x0800084a: 14ae .. ASRS r6,r5,#180x0800084c: 2400 .$ MOVS r4,#00x0800084e: e007 .. B 0x8000860 ; error_process + 960x08000850: 4a07 .J LDR r2,[pc,#28] ; [0x8000870] = 0x200000080x08000852: f8521024 R.$. LDR r1,[r2,r4,LSL #2]0x08000856: eb050084 .... ADD r0,r5,r4,LSL #20x0800085a: f7fffd0d .... BL FLASH_ProgramWord ; 0x80002780x0800085e: 1c64 d. ADDS r4,r4,#10x08000860: 42b4 .B CMP r4,r60x08000862: d3f5 .. BCC 0x8000850 ; error_process + 800x08000864: f7fffcfe .... BL FLASH_Lock ; 0x80002640x08000868: bd70 p. POP {r4-r6,pc}$d0x0800086a: 0000 .. DCW 00x0800086c: 08006000 .`.. DCD 1342423040x08000870: 20000008 ... DCD 536870920
那么这124个字节就是最终要传输到0x8000800处的函数数据。传输完毕后软复位mcu,bootloader将app的Flash数据进行篡改,达到改变程序功能的目的。
为什么要在bootloader运行时篡改app的数据?按理说在app运行时接收到error_process函数的更新数据后可以立刻运行,但是由于涉及到对app自身代码的修改,涉及Flash修改的一些相关函数有可能会被暂时破坏而导致代码运行崩溃。
四、跳过app的某些函数
如果想跳过“led_blings_1”函数,有2种方法:
1、函数内部跳过
即将以下汇编语句
0x0800655a: 2400 .$ MOVS r4,#0
修改为
0x0800655a: e013 .$ B 0x08006584
在“led_blings_1”函数入口处指令修改直接跳转到函数出口处。至于汇编的机器码和用法文末有相关资料可以查阅。
因为修改处的字节偏移为0x55a,是pageBuf下标为342元素的高2Byte,需要在error_process函数中做如下修改:
pageBuf[342] = (pageBuf[342] & 0x0000FFFF) | 0xe0130000;
2、函数调用处跳过
main函数汇编如下:
$ti.mainmain0x080065f8: f44f41c0 O..A MOV r1,#0x60000x080065fc: f04f6000 O..` MOV r0,#0x80000000x08006600: f7fffe5c ..\. BL NVIC_SetVectorTable ; 0x80062bc0x08006604: 2048 H MOVS r0,#0x480x08006606: f7ffff01 .... BL SysTick_Init ; 0x800640c0x0800660a: f7ffff85 .... BL init_led ; 0x80065180x0800660e: f7ffffa3 .... BL led_blings_1 ; 0x80065580x08006612: f7ffffbb .... BL led_blings_2 ; 0x800658c0x08006616: f7ffffd3 .... BL led_blings_3 ; 0x80065c00x0800661a: e011 .. B 0x8006640 ; main + 720x0800661c: f44f6180 O..a MOV r1,#0x4000x08006620: 4808 .H LDR r0,[pc,#32] ; [0x8006644] = 0x40010c000x08006622: f7fffe43 ..C. BL GPIO_SetBits ; 0x80062ac0x08006626: f44f707a O.zp MOV r0,#0x3e80x0800662a: f7ffff4f ..O. BL delay_ms ; 0x80064cc0x0800662e: f44f6180 O..a MOV r1,#0x4000x08006632: 4804 .H LDR r0,[pc,#16] ; [0x8006644] = 0x40010c000x08006634: f7fffe38 ..8. BL GPIO_ResetBits ; 0x80062a80x08006638: f44f707a O.zp MOV r0,#0x3e80x0800663c: f7ffff46 ..F. BL delay_ms ; 0x80064cc0x08006640: e7ec .. B 0x800661c ; main + 36$d0x08006642: 0000 .. DCW 00x08006644: 40010c00 ...@ DCD 1073810432
下面是调用语句
0x0800660e: f7ffffa3 .... BL led_blings_1 ; 0x8006558
直接将此语句改为空语句nop(0xbf00)即可跳过调用,由于该命令占用4个字节,nop是两个字节的命令,所以替换为两个nop命令。
0x0800660e: bf00bf00 .... NOP
因为修改处的字节偏移为0x60e,是pageBuf下标为387元素的高2Byte和下标为388元素的低2Byte,需要在error_process函数中做如下修改:
pageBuf[387] = (pageBuf[387] & 0x0000FFFF) | 0xbf000000;
pageBuf[388] = (pageBuf[388] & 0xFFFF0000) | 0x0000bf00;
记得之前看过软件破解的一些教程,教怎么去绕过一些验证的,比较简单的方法也是这种类似的操作,通过修改验证入口来跳过验证。
五、总结
mcu跑程序无非就是按照固有的机制去取指令然后运行,我们升级代码往往是整套代码完全替换,本文提出一种通过对原代码少量Flash数据进行直接篡改的方式来修复固件bug的方法,这也许可以作为一个产品的后门使用。本文为了简单只演示核心想法,具体流程的完备性未做考虑。
六、参考资料
《ARM指令计算机器码,自己归纳整理的ARM THUMB指令机器码表》
《thumb长跳转指令(BL)机器码详解》
《armv7-A系列5- arm 指令集以及编码》
《armlink使用方法详解》
bootloader.rar
app.rar
链接:https://pan.baidu.com/s/10puoQW5YI7TKDH8jlr-Gmg 提取码:q6er
一种通过篡改特定代码数据修复嵌入式产品BUG的方法相关推荐
- 基于python的分布式扫描器_一种基于python的大数据分布式任务处理装置的制作方法...
本发明涉及数据处理技术,具体是一种基于python的大数据分布式任务处理装置. 背景技术: 本发明提供一种分布式队列任务处理方案和装置,该方法可以提供分布式处理python任务,任务类型包括爬虫及其他 ...
- AI人工智能标记数据的技术:类型、方法、质量控制、应用
AI人工智能 标记数据 在人工智能(Artificial Intelligence,简称AI)领域中,标记数据是非常重要的一环.它是指对原始数据进行标记和注释,以便机器学习算法可以理解和利用这些数据. ...
- 数据库分库分表(sharding)系列(五) 一种支持自由规划无须数据迁移和修改路由代码的Sharding扩容方案...
为什么80%的码农都做不了架构师?>>> 版权声明:本文由本人撰写并发表于2012年9月份的<程序员>杂志,原文题目<一种支持自由规划的Sharding扩容方 ...
- 黑马头条推荐系统完整版(包括虚拟机和数据,代码已修复过可完美跑起来)
链接:(72条消息) 黑马头条推荐系统完整版(包括虚拟机和数据,代码已修复过可完美跑起来)-Python文档类资源-CSDN文库 如果网盘失效请加v获取
- 使用Arthas进行生产代码热修复
Arthas主页 https://alibaba.github.io/arthas/index.html 什么是Arthas(阿尔萨斯) Arthas 是Alibaba开源的Java诊断工具,深受开发 ...
- SwiftUI之深入解析如何处理特定的数据和如何在视图中适配数据模型对象
一.前言 阅读了我的前两篇博客的朋友,应该都熟练掌握了 SwiftUI 如何创建一个任何相关信息的展示视图和各个视图之间的相互组合,以及动态生成一个展示相关信息的可滚动列表,用户可以点击列表项去查看其 ...
- java定时增量同步,一种可配置的定时数据同步方法与流程
本发明涉及数据交换技术,尤其涉及一种可配置的定时数据同步方法.解决企业内部异构系统之间的数据同步问题.主要利用计算机多线程技术.XML技术.数据库技术实现.具有简单配置.快速部署.灵活扩展的特点,并且 ...
- vsc 搜索特定代码_特定问题的通用解决方案:何时编写代码以及何时编写代码...
vsc 搜索特定代码 by Rina Artstain 通过丽娜·阿斯特斯坦 特定问题的通用解决方案:何时编写代码以及何时编写代码 (Generic solutions to specific pro ...
- python 删除特定行数据_怎么用 Python 做数据分析实例
01 生成数据表 第一部分是生成数据表,常见的生成方法有两种,第一种是导入外部数据,第二种是直接写入数据. Excel 中的文件菜单中提供了获取外部数据的功能,支持数据库和文本文件和页面的多种数据源导 ...
最新文章
- 嵌入式学习笔记-记录系统启动次数
- AtCoder Beginner Contest 072
- python输入一组数据、进行简单的统计_《利用Python进行数据分析》学习笔记——第二章(3)...
- 对于mysql加索引,删除索引,添加列,删除列,修改列顺序的最佳办法测试
- Apollo进阶课程㊷丨Apollo实战——车辆与循迹驾驶能力实战
- 使用JetBrains dotMemory 4.0分析内存
- python pep8_Python 代码风格 和 PEP8
- 双活架构保服务24小时在线
- CodeForces - 986E Prince's Problem
- JS === 实现多个光标跟随事件
- 分数加减法混合计算机,1649.新人教版五年级数学下册第三课 分数加减混合运算(教案)(教学设计)(国家级一等奖、适合公开课).doc...
- 怎么把QQ音乐里wav格式转换成MP3
- Word中删除全部页眉页脚的方法
- Ubuntu10.10 CAJView安装 读取nh\kdh\caj文件 成功
- 网恋背后的骗局:那些被宰杀掉的猪!必看!
- Windows XP SP2下载[转自Mydrivers.com]
- c语言小游戏----改版斗兽棋
- 苹果连不上电脑服务器未响应,苹果电脑服务器未响应怎么办
- 【Python爬虫学习】一、Request
- 【NI Multisim 14.0原理图设计基础——元器件分类】