JlinkV10的固件验证缺陷我年前已发布刷机工具, 但缺陷是利用就得刷机一次再刷回.
发布前在某移动设备开发群谈论时候, 群友说v10会检查固件签名啊, 你怎么搞, 我就说签名区外面的空间我可以放代码, 能放五百字节呢, 完全塞得下.
他表示以前的很老版本固件倒是有过任意写bug, 可惜修复了, 虽然没有透露细节, 但我想群友能找到, 我也来找个.
然后我挨个看命令处理, 还真看到个栈覆盖bug. 这个bug并不是他说的那个, 而且栈太小了, 不大好利用.
这我可就不同意了, 数着字节写代码是我们中年程序员的基础技能啊!
先看看出问题的函数fine_write_read反编译代码:
此代码有用户控制缓冲区长度问题. 首先会接收三个长度变量, 我把它们取名为writelen, readlen, somelen. 第一个writelen控制接下来的缓冲区接收尺寸, 然后usbrxbuf函数接收对应长度的字节放入writebuf, 此数组分配在栈上. 这里可以超写, 覆盖LR.
然后再看memcpy的目标, 以writebuf地址+第二个长度偏移=最终地址, 复制4字节readed值到此地址. 构成了一个任意写, 正常情况下readed值等于readlen, 但可以不正常.
这个writebuf是栈上的数组, 打开栈结构窗口看看
只要从writebuf地方写0x2C个字节, 就覆盖到了保存的LR了. 因为程序序言是一个单纯的PUSH {LR}, 所以也没有其他寄存器的值需要恢复.
最简单的利用方法就是让usbrxbuf函数对writebuf写入0x2C字节, 覆盖保存的LR到我们写入的缓冲区地址. 首先我们需要知道执行到此处时, sp具体的值, 好跳转到我们写入的代码中.
我写了一个小工具, 通过usb命令触发fine_write_read这个处理函数, 接收长度writelen给了2C, 这2C的内容都是AA, 然后使用SWD连接jlink的芯片, 在BL usbrxbuf处下断, 然后执行命令断下, 查看此时的sp值, 100840A0. 执行usbrxbuf后writebuf, replybuf, 返回地址(LR)都被填充成了AA.
因为从BL usbrxbuf直到函数返回还有三个BL, 要让栈里LR生效, 必须执行到POP PC. 我们依次执行这三个BL, 在执行第一个BL syncM0FINEGPIO后就出了问题, 我们送进来的AA被这个函数给清零了! 从replybuf开始的0x2C字节都被清了, 都超过了栈上的LR清到了父函数的栈里面.
为了直观一些, 我画了fine_write_read函数的栈. 这个函数顾名思义, 是上位机和使用FINE接口(瑞萨的协议)的开发板通讯用的, 上位机送出writelen长度的数据, 调试探头通过操作GPIO引脚送给开发板, 然后由引脚读回readlen长度的数据放到replybuf, 并连同读取长度一起发给上位机.
我们当然不想让replybuf被覆盖, 所以readlen写的0, 按理说syncM0FINEGPIO也会返回0, 也不会往replybuf写值, 只有memcpy会将replybuf开始4字节覆盖为0(readed)啊. 跟进去syncM0FINEGPIO调了一下, 发现执行了某条写入内存指令后, 栈里的replybuf连同后面的LR等瞬间清零了. 这指令操作就是[20000008]=1, 这个地址在数据手册里描述为M4/M0共享内存, 经过分析这个syncM0FINEGPIO函数是给M0配参数和等待它完成的协作函数.
此时就要讲一下这个J-Link V10的硬件特征了, 他的cpu有M4/M0两个核心, 看来为了让GPIO模拟协议保持时序稳定不受RTOS影响, J-Link程序员单独用M0来完成协议模拟. 20000008是M4/M0之间的状态量, M4切换状态到1等待0, M0等待1完成后切换为0并继续等待1.
那为什么本应该按照readlen来跳过读取的M0会按照writelen来覆盖我们的replybuf呢? 为了覆盖到位, writelen是不可以设置成0的.
我估计应该是因为我们没有经过前置的操作, 直接发送FINE请求, M0里运行的程序并不是为了FINE协议准备的. 我没有瑞萨的开发板, 没法实际接上FINE接口看看固件命令执行流程.
不过连翻带猜发现了一个select_if命令, 当选择3号interface时候, M0的app(Reset Vector地址的函数)就是匹配FINE读写的.
在我们工具加入select_if命令后, 经过调试, 成功走到了POP {PC}.
接下来我们就要考虑下塞下更多代码的办法了. 目前我们能利用的空间是分开的两段, somelen应该是fine_write_read命令中一个保留的字段, 因此函数中没有用到他, 内容会保留. 从他开始0x14字节的绿色区域可以放代码, 然后replybuf的内容前面4字节会被memcpy覆盖为m0程序返回的readed, 所以需要避让开4字节. 后面到LR也有0x14字节的蓝色区域, 也是可以放代码的. 那么最后的父函数栈是不是可以继续覆盖作为代码呢? 实际是不行的. 走完uxbrxbuf函数, 可以观察到父函数栈确实可以被覆盖, 但是走usbtxbuf函数发送回应时候, 设备就会崩溃重启, 回不到POP {PC}了. usbtxbuf函数会发送replybuf, 长度为readlen+4, 查看usbtxbuf函数, 我们会看到如果buf或者len任一项为0, 该函数就会直接返回, 不会调用其他子函数. 那么如果我们把readlen设置为-4, 这个发送函数不就直接返回了吗? 话虽如此, 但readlen同时还传给M0作为从GPIO引脚读取到replybuf的长度, 如果传个负值会不会M0又开始狂写replybuf呢?
查看M0的app后发现, 读取部分是判断readlen非0后至少读一个字节再判断已读字节数是否小于writelen的. 这里的判断是BLT, 有符号. 因此读了1字节后循环条件1小于-4不成立不再循环.
但…节外生枝的是M4往M0传readlen参数时候会左移3位将readlen字节数转换为bit数, 然后在M0中右移3位转换回byte, 因此readlen传FFFFFFFC(-4)会丢掉高3位变1FFFFFFC(536870908). 程序员看似为了设计做的无意义转换恰好封堵了这种绕过发送函数的办法.
不过还不能绝望, 因为J-Link没有让M0从flashA执行(估计是因为放在sram中是零等待周期, 模拟出来的时序更准), 而是放在sram0中. 所以我们可以实时补掉M0的app. 我们可以分两步, 第一步不超收, 补掉M0的app后就返回, 因为我们payload执行时候, M0是位于循环等待20000008的状态, 而M0也没有icache, 所以补了别的地方下一轮gpio操作执行就是补丁后的代码. 我们补的就是这个右移3位的代码LSRS R5, R5, #3, 直接补为EORS R5, R5, 这样下一次协作函数发什么M0都以为readlen是0, 从而不会动replybuf. 外面的M4看来readlen还是-4, 还能用来绕过uxbtxbuf发送函数. 测试果然可行, 收个12c大小, 已经破坏了父函数栈, 甚至穿了task的栈空间, 但usbtxbuf没有用到栈直接返回了, 最后POP {PC}流程到我们代码. 测试中12C实际写穿到IP栈的栈了, 但只要我payload里禁用中断, RTOS也不会切换到IP栈线程.
Name
Priority
Stack
StackEnd
StackSize
J-Link
5
100832F8
100840F8
E00
Telnet
11
100846A0
10084aa0
400
CPULoadPerTask
25
10085E44
10085ec4
80
BGTask
3
10082334
10082734
400
USB IP Task
200
20003A78
20003E60
3E8(1000.)
J-Link Server
9
100829F8
10082DF8
400
USB MSDTask
6
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-4-6 14:47
被曾半仙编辑
,原因: 增加XP兼容版本