vesperW · 1 天前

如何篡改单片机特定代码数据?

你们开发的产品如果有 bug 了,你们会通过什么什么方式修复 bug?

今天就来给大家分享一种方式实现篡改单片机特定代码数据,并修复单片机 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 起始地址。

image.png

还要在 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

Image

先将 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 0x8000800

void 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”函数为例,反汇编如下:

    $t
    i.led_blings_1
    led_blings_1
        0x08006558:    b510        ..      PUSH     {r4,lr}
        0x0800655a:    2400        .$      MOVS     r4,#0
        0x0800655c:    e010        ..      B        0x8006580 ; led_blings_1 + 40
        0x0800655e:    f44f6180    O..a    MOV      r1,#0x400
        0x08006562:    4809        .H      LDR      r0,[pc,#36] ; [0x8006588] = 0x40010c00
        0x08006564:    f7fffea2    ....    BL       GPIO_SetBits ; 0x80062ac
        0x08006568:    2014        .       MOVS     r0,#0x14
        0x0800656a:    f7ffffaf    ....    BL       delay_ms ; 0x80064cc
        0x0800656e:    f44f6180    O..a    MOV      r1,#0x400
        0x08006572:    4805        .H      LDR      r0,[pc,#20] ; [0x8006588] = 0x40010c00
        0x08006574:    f7fffe98    ....    BL       GPIO_ResetBits ; 0x80062a8
        0x08006578:    2014        .       MOVS     r0,#0x14
        0x0800657a:    f7ffffa7    ....    BL       delay_ms ; 0x80064cc
        0x0800657e:    1c64        d.      ADDS     r4,r4,#1
        0x08006580:    2c0a        .,      CMP      r4,#0xa
        0x08006582:    d3ec        ..      BCC      0x800655e ; led_blings_1 + 6
        0x08006584:    bd10        ..      POP      {r4,pc}
    $d
        0x08006586:    0000        ..      DCW    0
        0x08006588:    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 0x08006558

    uint32_t alignPageAddr = MODIFY_FUNC_ADDR_START / FLASH_PAGE_SIZE * FLASH_PAGE_SIZE;
    uint32_t cnt, i;

    // 1. copy old code
    memcpy(pageBuf, (void *)alignPageAddr, FLASH_PAGE_SIZE);

    // 2. change code.
        //由于Flash操作2KB页的特性,0x08006558不满2kb,因此偏移为0x558,0x558/4=342
    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_0x8000800
    error_process
        0x08000800:    b570        p.      PUSH     {r4-r6,lr}
        0x08000802:    4d1a        .M      LDR      r5,[pc,#104] ; [0x800086c] = 0x8006000
        0x08000804:    142a        *.      ASRS     r2,r5,#16
        0x08000806:    4629        )F      MOV      r1,r5
        0x08000808:    4819        .H      LDR      r0,[pc,#100] ; [0x8000870] = 0x20000008
        0x0800080a:    f7fffcbd    ....    BL       __aeabi_memcpy ; 0x8000188
        0x0800080e:    4818        .H      LDR      r0,[pc,#96] ; [0x8000870] = 0x20000008
        0x08000810:    f8d00568    ..h.    LDR      r0,[r0,#0x568]
        0x08000814:    f36f000f    o...    BFC      r0,#0,#16
        0x08000818:    f2420164    B.d.    MOV      r1,#0x2064
        0x0800081c:    4408        .D      ADD      r0,r0,r1
        0x0800081e:    4914        .I      LDR      r1,[pc,#80] ; [0x8000870] = 0x20000008
        0x08000820:    f8c10568    ..h.    STR      r0,[r1,#0x568]
        0x08000824:    4608        .F      MOV      r0,r1
        0x08000826:    f8d00578    ..x.    LDR      r0,[r0,#0x578]
        0x0800082a:    f36f000f    o...    BFC      r0,#0,#16
        0x0800082e:    f2420164    B.d.    MOV      r1,#0x2064
        0x08000832:    4408        .D      ADD      r0,r0,r1
        0x08000834:    490e        .I      LDR      r1,[pc,#56] ; [0x8000870] = 0x20000008
        0x08000836:    f8c10578    ..x.    STR      r0,[r1,#0x578]
        0x0800083a:    f7fffd53    ..S.    BL       FLASH_Unlock ; 0x80002e4
        0x0800083e:    2035        5       MOVS     r0,#0x35
        0x08000840:    f7fffcca    ....    BL       FLASH_ClearFlag ; 0x80001d8
        0x08000844:    4628        (F      MOV      r0,r5
        0x08000846:    f7fffccd    ....    BL       FLASH_ErasePage ; 0x80001e4
        0x0800084a:    14ae        ..      ASRS     r6,r5,#18
        0x0800084c:    2400        .$      MOVS     r4,#0
        0x0800084e:    e007        ..      B        0x8000860 ; error_process + 96
        0x08000850:    4a07        .J      LDR      r2,[pc,#28] ; [0x8000870] = 0x20000008
        0x08000852:    f8521024    R.$.    LDR      r1,[r2,r4,LSL #2]
        0x08000856:    eb050084    ....    ADD      r0,r5,r4,LSL #2
        0x0800085a:    f7fffd0d    ....    BL       FLASH_ProgramWord ; 0x8000278
        0x0800085e:    1c64        d.      ADDS     r4,r4,#1
        0x08000860:    42b4        .B      CMP      r4,r6
        0x08000862:    d3f5        ..      BCC      0x8000850 ; error_process + 80
        0x08000864:    f7fffcfe    ....    BL       FLASH_Lock ; 0x8000264
        0x08000868:    bd70        p.      POP      {r4-r6,pc}
    $d
        0x0800086a:    0000        ..      DCW    0
        0x0800086c:    08006000    .`..    DCD    134242304
        0x08000870:    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 函数汇编如下:

    $t
    i.main
    main
        0x080065f8:    f44f41c0    O..A    MOV      r1,#0x6000
        0x080065fc:    f04f6000    O..`    MOV      r0,#0x8000000
        0x08006600:    f7fffe5c    ..\.    BL       NVIC_SetVectorTable ; 0x80062bc
        0x08006604:    2048        H       MOVS     r0,#0x48
        0x08006606:    f7ffff01    ....    BL       SysTick_Init ; 0x800640c
        0x0800660a:    f7ffff85    ....    BL       init_led ; 0x8006518
        0x0800660e:    f7ffffa3    ....    BL       led_blings_1 ; 0x8006558
        0x08006612:    f7ffffbb    ....    BL       led_blings_2 ; 0x800658c
        0x08006616:    f7ffffd3    ....    BL       led_blings_3 ; 0x80065c0
        0x0800661a:    e011        ..      B        0x8006640 ; main + 72
        0x0800661c:    f44f6180    O..a    MOV      r1,#0x400
        0x08006620:    4808        .H      LDR      r0,[pc,#32] ; [0x8006644] = 0x40010c00
        0x08006622:    f7fffe43    ..C.    BL       GPIO_SetBits ; 0x80062ac
        0x08006626:    f44f707a    O.zp    MOV      r0,#0x3e8
        0x0800662a:    f7ffff4f    ..O.    BL       delay_ms ; 0x80064cc
        0x0800662e:    f44f6180    O..a    MOV      r1,#0x400
        0x08006632:    4804        .H      LDR      r0,[pc,#16] ; [0x8006644] = 0x40010c00
        0x08006634:    f7fffe38    ..8.    BL       GPIO_ResetBits ; 0x80062a8
        0x08006638:    f44f707a    O.zp    MOV      r0,#0x3e8
        0x0800663c:    f7ffff46    ..F.    BL       delay_ms ; 0x80064cc
        0x08006640:    e7ec        ..      B        0x800661c ; main + 36
    $d
        0x08006642:    0000        ..      DCW    0
        0x08006644:    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; 

END

来源:嵌入式专栏

推荐阅读

欢迎大家点赞留言,更多 Arm 技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。

推荐阅读
关注数
2896
内容数
303
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息