近来准备搞搞usb,翻出我的正版开发板,启动正版visualgdb,插上盗版v9, 工程向导识别不到开发板无法下一步,于是我换了一下最新版本的7.58c驱动,打开准备升级一下固件来着,好家伙,直接弹出一个好家伙,大意是:

"现在连接的探头是个克隆版jlink,在克隆硬件上用我们的软件既不合理又不合法,请联系我们并附上截图。"

虽然后面我发现工程向导从openocd里面间接驱动jlink才能下一步并且成功调试,可这个segger的提示深深的打动了我。

众所周知,假货宝上的v9已经不知道出过多少版本了,大的小的,带壳的裸板的,都是包最新驱动,还真没遇到segger检测到的情况,去问了卖家和程序员一样的回答”我这里好好的”。

得,网上搜也搜不到信息,估计是因为clone提示文字也是全新的,以前好像出错提示是defective,新的提示是clone。

卖家不管,我自己折腾,首先我怀疑是不是那个签名问题,于是找了下jlink_x64.dll弹框的地方的函数,发现没有call它的调用,只有一个传参引用。不死心,开x64dbg跟了一下,是从线程调用来的。

那么再看这个传参引用的位置,x64dbg用animate trace记录了一下会发现他在检测和比较逗号分隔的特性字符串,比较到特定的字符串”RDI”就跳转到将这个弹窗函数入参的分支了。如果将字符串比较的jz都给跳过,则不会弹窗。

回到IDA调试和整理下这个函数,首先我们要摸清这个逗号分隔的字符串哪来的,是从一个0x80字节的缓冲区统计来的,每0x10开头是ascii的部分加到字符串里。

如果是做过山寨版的朋友可能就知道了,这信息是在0800BF20开始的地方,而0800BF00处是序列号。

通过整理和调试, 得出的这个新版驱动的弹窗分支条件依次如下:

0 序列号不能为黑名单里面的那几个。此条不重要,因为没人去用那些特殊序列号。

1 新型号不能内置GDBFull,有了直接报错。所有型号不能内置RDDI,有了直接报错。

2 硬件版本v9~v11并且序列号开头为26,5,82的,和版本号v1并且序列号开头为80的,不许内置JFlash或RDI特性。

看到这里聪明的小伙伴可能要问了,这些不都是专门针对盗版的吗?但随着我在网上和闲鱼搜集正版的序列号,我发现26开头是edu的特征,而5开头是base版的特征,80开头是edu mini的特征。这里说的开头就是第8、9位的数字。

山寨版的序号那可就五花八门了,有很多直接-1(4294967295)。在搜索中我还看到小窍门原版edu并 通过addfeature指令增加jflash和rdi特性的,还在论坛看到了Segger去年成立中国部门,坛友表示担忧,还有最近原版自己加feature的坛友被报告clone的帖子。

略作思考我觉得我破案了,这个崭新的中国部门怕是读了论坛的帖子后,把这个当作成果汇报上去了,然后segger程序员一琢磨,来个根据型号限制功能, 可齐活了,这一砖头主要砸到了买正版edu并且加了内置特性的,盗版序列号-1或者瞎写的都没被误伤。

我们可以选择补dll,拆机重刷,但我选择了最程序正义的一种:让jlink自己去掉feature。

此篇文章也就是从一个和嵌入式开发关系不大的视角上展示如何利用基础推理能力来拨开云雾得报大仇,阅读只需要有一定的调试经验,不需要做漏洞分析。好了,闲话不多说,我们正式就顺着这个Feature字符串来摸。因为新版驱动的commander不支持AddFeature指令了,我在老版JlinkARM.dll搜索发现AddFeature命令附近有一个ClearFeatures。

这个命令也是非公开的,和AddFeature、ChangeSN一样的流程,执行后会把现有ota的Features区域全部修改为0,发送更新ots信息请求让设备去更新。但我测试了一下设备上的固件却无法成功的把GDBFull或者JFlash字样给修改为00。

通过查阅STM32的flash编程手册PM0059,明确说可以将非0的bit改为0,不需要擦除再改写。

再看看固件更新ots有啥限制。固件怎么来呢,可以从JLinkARM.dll解。老版本的方法大家都知道了吧,新版本7.2后厂家给一部分固件加了压缩,我就用另外思路写了个工具解压它。

可以参考附件:

            pos = 0;            otsptr = ots_sig;            do            {                func_readbf00_ots_b700_sig(flashb, pos, 1u);                // 如果features都是0, 那么此处可以通过                newb = *otsptr++;                if ( (uint8_t)(flashb[0] & newb) != newb )                    goto retneg1;                ++pos;            }            while ( pos < 0x900 );

固件中对客户端发过去的新内容检查也是检查没有出现0变1的bit,然后就送入内存中的函数来修改flash了。

看了下内存中的这个函数,写的歪七扭八的,除了加了个跳过写入FF功能,没有会导致非FF不擦写的bug。编程手册上每次写入都要拉高一次PG,它给简化为设置一次,循环写入了。估计其实不需要。

话说其实修改flash的代码完全没必要放在内存,因为要修改的目标地址是sector2,和执行的都不在一个sector,而且它末尾还调用了flash里面的memcmp函数判断写入是否成功,白隔离了。

    flash->CR = 0x200;                                          // PSize=Word(32bit)    flash->CR |= 1u;                                            // PG    // 跳过新写入FF请求    while ( *src == -1 )    {        ++src;        ++dest;prognextword:        if ( !--bufword )            goto done;    }    w32 = *src++;    *dest++ = w32;    do    {        wwdg->CR = 0x7F;        iwdg->KR = 0xAAAA;        SR = flash->SR;    }    while ( (SR & 0x10000) != 0 );                              // FLASH_SR_BSY    if ( (SR & 0xFE) == 0 )        goto prognextword;                                      // 没有5bit错误位done:    flash->CR = 0x80000000;                                     // lock

修改里面没有额外的判断了,没有什么猫腻,感觉是STM32本身不允许非全1的被改写?也就是说ST资料写错了?

抛开这个不谈,我们要还原已经包含ascii的features区域,势必要重刷该区域所在的sector并且写入擦除前的内容,这也是flash修改的常规操作了, Features位于0800BF20,所以要擦掉08008000~0800BFFF对应的sector2。

那么原本更新bf00的代码为啥不去做擦除重写呢?实际这个函数根据最后一个参数是有擦除sector功能的。估计是厂家设计的时候模拟的单次OTP,不擦硬改,所以一旦不是FF了就不能再次写入。

如果不大改函数读出全部再回写,08008000~0800B6FF这13.75k的内容就会变FF。(后记:实际变了也无所谓,应该这段是空的),所以我选择写一段代码来完成读出/擦除/写回操作。

要测试我们写的代码是否工作正常,一定得在真实环境下调试,强迫症半仙表示,还得是由原版的bootloader引导的。接下来我们分析Bootloader。

Bootloader怎么来呢,可以从正版里面提取。可以搜索thxlp的”人人都可以提取V9的Bootloader一文”。

IDE当然选用了IAR,因为经过分析,JLinkV9的固件和Bootloader都是IAR编译的,最新的758c固件是IAR 6.40.5编译的,bootloader也应该是6.40.x编译。

当然了我们用IAR 7.x 8.x都是可以的,segger用的还是stdperiph库,我们也可以用LL/HAL开发。只要搞清bootloader怎么才肯加载我们刷入的app”分区”就可以了。

bootloader自身和信息存储被官方安排在08010000之前,app分区从08010000开始到0803FFFF, Vector为08010000,可用内存是20000008开始到20020000,这些在工程选项里面可以设置。

经过分析bootloader引导时候检查内存20000000处不是0x12344321和app分区的08010210处是不是” J-Link V9”,还有08010000起的2FFFE字节的crc是不是和固件末尾的2字节crc相等。这个crc的算法是CRC-16/KERMIT。

正好IAR的链接器有嵌入checksum功能,经过苦苦搜索,我并没有找到怎么用他的自定义CRC生成KERMIT校验的选项来。或许我应该写个小工具自己来postbuild里面修改out文件?

就在我放弃并且补丁了bootloader来调试后,经过一顿尝试,我找到了这个组合:

按照这个选项设置,链接时候生成的crc便可通过bootloader的校验。

当然不想填充的话,那补bootloader也是可以的,位置如下

ROM:08000E7C 03 D1                      BNE    locret_8000E86 ; patch to nop!

注意看我源码里的main.c,

#pragma location=0x08010210

__root const char fwversion[] = "J-Link V9 "

这段可以让指定地址出现这个字串,满足bootloader对此处检测。

我们把bootloader加入到raw binary image里面,设置好符号,段名,然后在icf加入定位指令指示它放置到08000000位置,这样我们调试app时候, 就会将app和bootloader一起烧录并调试了。

然后我们还要知道的是因为bootloader本身已经完成了主频初始化和对应的flash延迟配置,我们这里不用重复初始化。如果是STM32CubeMX建立的模板,不要调用SystemClock_Config。可以参考我提供的工程,下载调试可以看到bootloader放行了我们程序,并且成功的断在了main函数处。

接下来我们还要完成一个额外的功能才可以安心的往正版里面刷,我们的代码执行完自身代码后,需要返回刷机状态,这样才可以继续刷回原版固件。不然每次都进我们固件,回不去了。

通过分析固件和bootloader,只需要我们将20000000处置为12344321这个魔法数值,然后芯片复位就可以。bootloader引导过程检测到这个标志就会进入恢复状态,恢复状态官方工具连上去就会刷固件进去。

经过少量的调试,按照编程手册做好了擦除功能,重刷后dump可以验证我们每次烧进去时候序列号和feature在重启前就被清掉了,同时我们还保留了B700开始的0x800大小的签名。

万里长城剩下最后一步,怎么把固件给刷进去,这也难不倒我们,首先我们看原版固件里面进入刷机状态代码,也就是设置vectorbase和重启代码,发现是在06号指令里面调用的,也就是cmd_06_update_firmware,然后会给主机回应1并且复位让bootloader进入恢复状态。

嵌入式开发的内容不是我们的重点,看代码说明就可以。

bootloader的06号指令的处理程序里,先回应0,然后收一个变长长度,然后收取0x500字节的固件头部,根据头部和本身已有固件的特征,判断是刷机,还是假装刷机。这个假装的意思是它照常收你的固件但不刷到flash,还会在收完你查询固件版本时候,返回给你缓冲的假版本号,增加破解成本,不过毫无用处。

真刷机检测的内容如下,这四个条件至少需要满足一个:

1、现有固件Banner不是JLink V9

2、现有固件crc出错

3、现有固件末尾08003FF00没有www.segger.com

4、现有固件日期比传进来的新固件老(或新/老日期任一方为0)

在我们不拆机烧写的前提下,前三项都无法控制,但第四项可以,我们传进来的新日期可以比现有版本新或者为0。日期要新好说,把版本号里面的年份往大里改就可以,但日期为0是怎么回事呢,原来segger在这里留了一个后门, 当处理月份缩写发现月份不在他内置的12种缩写内,则对应的日期直接默认为0。

这个日期算法就这么任性,精确到分钟但是它每年是按照12*31天算的。所以我们随便写个错误月份或者不写都可以无视版本号,这也是invalidatefw官方降级固件背后的小窍门吧。

刷机工具写的很快啊,主要操作就是让jlink进入刷机模式,然后对读取的固件扩大并填充FF和www.segger.com并计算校验,最后发给刷机状态的jlink。如果想要自动化我还可以刷之前存一下现有固件,最后一步再恢复,但懒得写了。

刷完经过验证确实达到自动抹除序列号和feature的效果了。通过调整代码还可以只处理feature到默认列表,不动序号。

搞完这一切要不要挖新坑呢!我去买了正版的v10 edu,下篇折腾折腾v10的吧,不过nxp的文档和bug我算是领教过了,感觉比stm棘手。

还有一个方向就是我们可以用固件来刷bootloader?搞个自制bootloader能完成引导和刷机就能替代官方的吧,我看了下就实现了10个命令,比完全自制固件要可行?

以下是V10挖坑的更新

我已提取v10的bootloader,分析了下,它启动固件前多了一些检测但跟固件相关的还是jlink v10字样和末尾的crc,其他的判断正版本来就能通过。V10因为内置bootloader设置了CRP标志,封锁了串口刷bootloader,但我们用固件来刷bootloader还是可以的。谁让他把”OTS”和bootloader放在一起呢,架构设计就这样了,能刷OTS就得让我刷bootloader。

特别要提醒各位,要从正版里面提取bootloader,因为买了个山寨版拆开发现它用的是LPC4325,它flashA只有0x60000字节,只能放下0x58000大小的固件,而官方固件是0x78000大小。所以它直接没bootloader直接复位函数跳转到app分区。

而且就算把官方的bootloader弄个去也要去掉签名校验, 还要改刷机部分写入范围。可能因为4327/4337/4357等能代用的芯片都被炒高了?我问了下4337剪板都要一百块。同时我还发现有点山寨版卖家自制了bootloader,这就不一般了,我刚才还说想挖坑做OpenV9自制bootloader呢,别人都做了V10了,佩服。

v10 bootloader和v9的不同主要是厂家不一样,逻辑上程序员没动,返回bootloader用同样的代码。甚至地址也保留用了20000000。这在lpc43xx上可不是主sram,不晓得他们咋想的。

v10的sn位于1a005e00,feature开始于1a005e20,刚好也是sector2(0x1A004000-0x1A005FFF)。

然后刷正版过程中,又发现刷进去的自制固件不执行,通过修改原版固件刷机测试,发现v10的固件1A0开始有签名,bootloader会检查固件开始2A0到固件末尾-0x100字节的内容是否符合该签名,大胆推测多半是公钥解密的防止刷入未授权固件。但没关系啊,我们把代码塞进这个范围之外的地方,不就可以免签名执行了吗?都不用找漏洞。

为了多压榨点空间,我禁用了中断,并且去掉了除了0x20后的所有中断服务指针,这样可以把0x20开始指针区域到1A0之间三百多和末尾二百多字节利用起来,感觉写iap擦除够用了。懂点单片机开发的小伙伴可能要惊奇了,这vector指针区域也能放代码执行的吗,确实可以我都试过了。

最新更新

因为之前说的山寨版无法测试bootloader刷机,咸鱼拍正版又没等到合适的,因此冒险用真机刷机,刷进去才发现不知道是vector指针精简过头还是什么原因,执行完demo没能重启到bootloader状态。

因为正版的电路板飞线抹除费事,我把它拆到山寨版的板子上调的时候发现,不是有个给全局变量赋魔法数值的语句吗,在动了优化选项后被优化没了。改正后我再刷就不砖了!之前在山寨版上测试擦除demo是可以的。想办法塞下去就行。有朋友或许要问了,你这才五百多字节,想塞入一些复杂点的算法都不行啊?其实有两种办法:

1、这五百多字节塞入一个串口接收循环,把代码收到sram执行,这芯片有136k sram,完全够了(比尔盖茨微笑)。

2、这五百多字节就做一件事,补丁掉bootloader,然后下次刷自制就不校验签名了,啥时候不想支持自制了再补回去。

测试成功,整体不到0x180字节塞到头部刷机就会清掉烦恼的根源了,不过因为要借用官方固件,我得想个办法合法的操作,或许备份当前固件合成进去, 刷完再恢复原版固件?

顺便贴下正版edu调试接口定义:

注意:想调试需要修改CRP标志,也就是flash 1A0002FC位置的四字节值, 官方是0x12345678,修改为FFFFFFFF或者其他非特征值,重启后才可以调试。

附件:iar自制工程,jlink bootloader和固件的代码片段,jlinkupdater应用,自制迷你固件。

附录:我如何判断固件用的IAR版本呢,主要是看不同iar的库lz77_init.o里面的iar_lz77_init3函数大小,哪些版本和固件里该函数大小13E吻合。我如何判断是stdperiph库是因为在我自动命名vector的函数指针时候,发现有多余的在startup_stm32f205xx.s中不存在的中断服务。而标准库没有细分到子型号,里面有这些中断服务入口。我如何发现固件用的是IAR编译的呢,看Reset入口处的特征和main之前要执行的初始化数据函数(__iar_data_init3)。