V850

优质
小牛编辑
120浏览
2023-12-01

V850

我们先前讨论过,Uconnect系统能够与两个不同的CAN总线通讯。CAN通讯是由Renesas V850ES/FJ3芯片通讯,请参考CAN连接性章节。但是,OMAP芯片,我们在漏洞利用了D-Bus后在这上面执行了代码,这块芯片并不能发送CAN信息。不过,OMAP芯片可以与v850芯片通讯,而v850芯片可以发送CAN信息。

在调查头单元时,我们把V850和CAN通讯称作了 “IOS”。有意思的是,头单元(OMAP芯片)可以更新IOC(V850芯片),通常是通过U盘。接下来,我们会讨论IOC是如何更新的,并看看我们能不能利用这种机制在IOC上刷入篡改过的固件,从而允许我们在入侵了OMAP芯片后发送CAN信息。

模式

在任何时候,都可以在下面的三种模式下更新IOC。第一种是应用模式,也就是用户认为的 “常规”模式,因为这种模式具有引导程序、完整的固件和运行的应用代码。第二种模式是引导程序模式,设计用于更新IOC上的应用固件。最后,是引导程序更新模式,在这个模式中,IOC会进入一种可以更新引导程序的状态,而引导程序是负责把固件加载到内存并把IOC放入到应用中。

更新V850

再回过头去观察更新ISO中的’manifest.lua’,我们看到这里有一个文件就是用于更新IOC的应用固件-‘cmcioc.bin’。在后面你会看到,这个二进制文件的确是一个完整的V850固件,通过逆向工程,我们发现了更多有趣的点。

 43    ioc =
 44    {
 45       name        = "ioc installer.",
 46       installer   = "ioc",
 47       data        = "cmcioc.bin",
 48    }

通过更深入的调查’manifest.lua’,你会发现还有几个文件与IOC更新或相应的启动程序更新有关系。

  6 local units =
  7{
...
 19     ioc_bootloader =
 20     {
 21         name                = "IOC-BOOTLOADER",
 22         iocmode             = "no_check",
 23         installer           = "ioc_bootloader",
 24         dev_ipc_script      = "usr/share/scripts/dev-ipc.sh",
 25         bootloaderUpdater   = "usr/share/V850/cmciocblu.bin",
 26         bootloader          = "usr/share/V850/cmciocbl.bin",
 27         manifest_file       = "usr/share/V850/manifest.xml"
 28     },
 29     ioc =
 30     {
 31         name                = "IOC",
 32         installer           = "ioc",
 33         dev_ipc_script      = "usr/share/scripts/dev-ipc.sh",
 34         data                = "usr/share/V850/cmcioc.bin"
 35      },

实际用于IOC更新或启动程序更新的文件数量并不多。我们最感兴趣的还是应用代码,因为我们能从中找到最佳的机会来找到用于发送和接收CAN信息的代码,在下面用加粗的就是。

$ ls -l usr/share/V850/
total 1924
-r-xr-xr-x  1 charlesm  staff  458752 Jan 30  2014 cmcioc.bin
-r-xr-xr-x  1 charlesm  staff   65536 Jan 30  2014 cmciocbl.bin
-r-xr-xr-x  1 charlesm  staff  458752 Jan 30  2014 cmciocblu.bin
-r-xr-xr-x  1 charlesm  staff     604 Jan 30  2014 manifest.xml

注意:我们知道要逆向哪个文件,需要找到一种方法把修改过的固件刷到V850芯片上,所以我们可以平行移动代码执行,通过CAN总线来物理控制头单元。我们很幸运,系统上有一个二进制就是设计用于执行我们想要的操作。

通过‘iocupdate’可执行程序,IOC应用代码可以推入到Uconnect系统上的V850固件,这个可执行程序就是’ioc.lua’。

iocupdate -c 4 -p usr/share/V850/cmcioc.bin

‘iocupdate’的帮助文档证实了我们的初步分析,根据其说明,这个文件确实是用于从头单元向IOC发送二进制文件

%C: a utility to send a binary file from the host processor to the IOC
[options] <binary file name>
Options:
-c <n>   Channel number of IPC to send file over (default is /dev/ipc/ch4)
-p       Show progress
-r       Reset when done
-s       Simulate update
Examples:
/bin/someFile.bin         (will default to using /dev/ipc/ch4) 
-c7 -r /bin/someFile.bin  (will reset when done)
-sp                       (simulate update with progress notification)

在我们想出如何编程V850数据包之后,我们需要逆向并修改IOC应用固件来添加代码,从而接收并转发命令到CAN总线。最重要的部分是逆向IOC应用固件,因为我们这样会暴露必要的代码来发送和接收来自总线的CAN信息。幸运的是,我们发现IOC可以重新刷入固件,并且没有使用加密签名来验证固件是不是合法的。

逆向IOC

这次研究的主要目的不仅仅是为了证明汽车的通讯系统是可以入侵的(因为我们早就知道这一点了),而是为了证明在成功远程入侵后,我们先前研究证明过的攻击方法可以用同样的方式执行。

正如我们多次提到的,Uconnect系统使用了芯片组Renesas V850/Fx3与车内网络通讯。我们意识到,如果我们想要从车辆上发送并接收CAN信息,我们很可能需要逆向这个固件,来弄清楚究竟要如何调用与CAN相关的函数。

不出意外,我们会使用IDA Pro作为我们的逆向工程平台。我们很幸运,已经有一个写好的处理器模块符合我们的架构要求,NEC V850E1/ES [V850E1]

{%}

图-V850处理器类型

一旦固件加载到了IDA Pro,你就能看到固件中的第一条指令,这条指令会跳转到设置代码,初始化需要的值来实现功能。值得注意的是,简单地把跳转到初始化代码作为第一条指令在固件镜像中并不常见,Uconnect镜像只是凑巧对我们很友好:

{%}

图-跳转代码

在下面你会看到,一些寄存器都设置到了特定的值,其中最有趣的是“mov 0x3FFF10C, gp”,这样我们就知道了GP寄存器的值。GP寄存器是用于设置相对地址(随后讨论)。另外,我们根据0x77966上R5位置的值判断,镜像的起始地址是0x10000。

{%}

图-V850初始化代码

然后,我们可以回来并重新加载镜像ROM的起始位置,加载位置是0x10000。设置这些地址值能保证我们可以逆向所有必要的代码,并确保交叉引用会正确地暴露。

{%}

图-镜像地址

仅仅是因为我们获取到了可读的V850汇编代码,还不能说项目的逆向部分就完成了。相反,我们用了几个周的时间才通过逆向V850固件,获取到了所有必要的功能,从而修改固件镜像来通过无线接口来接收任意的CAN信息。

第一步是通过查找所有的代码来正常化IDB,修复IDA Pro无法弄明白的部分,创建函数并保证所有的函数调用和交叉引用是正确的。这一过程大都是自动进行的,通过在这些位置上查找特定的操作码并创建代码。IDA Python让这个任务变得很简单:

{%}

图-Python找到的代码函数

如果你的工作没有出错,你会在IDB的ROM片段中看到一篇蓝色的海洋,显示了所有确定了位置的代码和函数。

{%}

图-IDA Pro的ROM部分

现在IDB已经是标准的了,这样我们就可以读取数据表,让V850/Fx3处理器来弄明白这些片段、地址、寄存器和其他的重要信息,用于逆向出我们需要的特定信息。

搞清楚了V850的地址空间和相关的固件是我们的首要任务。只要阅读了V850的介绍文档并弄明白了不同区段上的代码、外围设备和RAM后,这个任务就会变得很简单。

{%}

图-V850的介绍文档

然后,我们可以在我们的IDB上创建合适的区段,来反映V850处理器的地址空间布局,用于运行我们的固件。我们知道ROM区段是从0x10000 开始的,直到0x70000,包含了我们的可执行代码。我们的处理器有32KB的RAM,映射的是0x3FF7000-3FFEFFF 。不出意外,变量就会储存在这个RAM区域中,并且在我们的IDB中显示,这个RAM区域中有很多交叉引用。另外还有一个特殊函数寄存器(SFR)区段。SFR是内存映射寄存器,其目的有很多。

最后,也是最有趣的,这有一个12KB的可编程外围设备I/O区域(PPA),这里面包括有CAN模块,与CAN模块相关的寄存器和相应的信息缓冲区。这个区域的基址是外围区域选择控制寄存器(BPC)指定的。一般对于微控制器来说,PPA的基址会固定到0x3FEC000。下面的图像中列出了我们在IDB中发现的所有区段。

{%}

图-Uconnect的固件区段

之前我们讨论过V850是如何利用GP的相对地址来获取RAM中的变量。你会发现,使用了GP中负偏移的代码反过来会变成一个虚拟地址。比如(如下),把值-0x2DAC移动到GP,有效地从0x3FFF10C中减去0x2DAC,这样我们就得到了一个地址:0x3FFC360。

{%}

图-基于GP的地址示例

我们写了一个脚本来遍历IDB中所有的函数,并为使用了GP相对地址的一些函数创建了一个交叉引用。

def do_one_function(fun):
       for ea in FuncItems(fun):
             mnu = idc.GetMnem(ea)

             # handle mova, -XXX, gp, REG
             if idc.GetOpnd(ea,1) == 'gp' and idc.GetOpType(ea,0) == 5:
                        opnd0 = idc.GetOpnd(ea,0)
                        if "unk" in opnd0:
                                continue
                        if("(" not in opnd0):
                                data_ref = gp + int(idc.GetOpnd(ea,0), 0)
                                print "MOV: Add xref from %x -> %x" % (ea, data_ref)
                                idc.add_dref(ea, data_ref, 3)

             # handle st.h REG, -XXX[gp]
             op2 = idc.GetOpnd(ea,1)
             if 'st' in mnu and idc.GetOpType(ea,0) == 1 and 'gp' in op2 and "(" not
in idc.GetOpnd(ea,1):
                        if "CB2CTL" in op2:
                                continue

                    end = op2.find('[')
                    if end > 0:
                           offset = int(op2[:end], 0)
                           print "ST: Add xref from %x -> %x" % (ea, gp + offset)
                           idc.add_dref(ea, gp + offset, 2)

              # handle ld.b -XXX[gp], REG
              op1 = idc.GetOpnd(ea,0)
              if 'ld' in mnu and 'gp' in op1 and idc.GetOpType(ea,1) == 1 and "(" not
in          idc.GetOpnd(ea,0):
                         if "unk" in op1:
                                 continue

                     end = op1.find('[')
                     if end > 0:
                            offset = int(op1[:end], 0)
                            print "LD: Add xref from %x -> %x" % (ea, gp + offset)
                            idc.add_dref(ea, gp + offset, 3)

这些代码和交叉引用能让你查看变量的引用位置,并跟踪它们来查找特定的功能。

{%}

图-RAM的外部引用

现在,我们已经将代码正常化了,并且RAM中的变量也有了交叉引用,接下来我们要填充PPA区段,因为CAN交互大多都是在这里进行。我们假设所有负责处理CAN的函数,比如读取总线中的信息,向队列写入信息,都会引用这个内存地址区域。在第20章,我们介绍了每个CAN模块的功能和寄存器。V850最多可以有4个CAN模块来处理每个数据包,但是,在我们的固件中,我们只发现了两个。

第20.5章中列出了CAN模块使用的所有寄存器和信息缓冲区。这些寄存器和信息缓冲区来自PBA的一个偏移。如果你还记得我们上面说过的,我们的微控制器使用的PBA是0x3FEC000。然后,我们可以遍历每个模块的所有寄存器和CAN缓冲区,并在IDB中为其创建名称,这样我们就可以查找交叉引用了,反过来,我们可以借此找到与CAN总线交互的代码。下面使我们写的一个Python脚本部分,能够将合适的名称填充到PPA。完整的脚本叫做’create_segs_and_regs.py’,观察这个脚本你就能知道所有的这些区段是如何创建的,填充是如何处理的。

{%}

图-在PPA中创建CAN值

接着,你可以前往IDB中的几个位置,检查这些位置上的布局和交叉引用。例如,下图中显示的就是CAN模块0中,CAN信息缓冲区的第二个和第三个位置(分别是01和02)。

{%}

图-CAN模块0的信息缓冲区 2 & 3

现在,IDB已经具有了RAM中变量的交叉引用,一个填充了CAN控制寄存器和信息缓冲区的PPA部分,还有一个完全正常化的ROM代码部分。目前,我们估计已经能看见PPA部分上,CAN信息缓冲区的外部引用,但是,我们很困惑,在代码节上,我们为什么观察不到任何到PPA的引用呢?

注意:要想发现错误,我们需要做很多,需要我们在ROM区段中以代码的形式列出了一些数据,但是无论如何,我们还是会继续。

既然我们无法找到任何可行的外部引用会引用相关的CAN代码,我们决定下载IAR工作台,有很多自动化领域的工程师都会使用这个平台来给V850处理器编译代码。IAR工作台中恰好提供了我们的处理器所使用的代码示例,并且还包含了用于发送和接收CAN信息的代码样本。

{%}

图-IAR中的V850 CAN 代码示例

We saw that the CTL register was being set to 0x200 to indicate that a transmission was about to occur and after scouring the Uconnect’s firmware, found a location that looked to be doing the exact same thing.

{%}

图-CAN message transmission code disassembly

然后,我们就可以完整的逆向‘can_transmit_msg’这个函数。我们本应该知道,代码并不会直接访问PPA,而是访问ROM中的变量,这个ROM指向的就是相关的CAN部分。只要你获取到了CAN模块阵列,并根据索引来访问这些模块,一切就都说得通了,请参考上面的IAR示例。我们现在已经有了函数的引用点,这些函数能够与CAN总线交互。

{%}

图-PPA CAN变量

除了ROM中与CAN通讯相关的变量,在RAM中还引用了CAN使用的信息缓冲区和控制寄存器。基本上,PPA中的数据都会复制到RAM,反之亦然,因为这些值可以在短时间内被覆盖。例如,我们逆向了函数‘can_read_from_ram’ 和d ‘can_write_to_ram’,这两个函数的作用分别就是把PPA中的数据放到RAM,读取RAM中的数据放到PPA。

{%}

图-can_read_from_ram

{%}

图-can_write_to_ram

在RAM中还有另外几个非常重要的区域储存着CAN ID,CAN数据长度和CAN信息数据。在RAM中储存着一些变量指针,这是发送CAN信息所不可缺少的。

{%}

图-RAM指针

通过跟踪CAN寄存器,信息缓冲区和RAM值,我们完整的逆向了好几个用于发送和接收CAN信息的函数。对我们来说,最有用的一个函数是’can_transmit_msg_1_or_3’,这个函数会从固定CAN ID阵列中提取一个索引,在我们的例子中,获取的是一个特殊的索引,指示我们正在提供一个用户提供的CAN ID;还有一个指向了数据长度和CAN信息数据的指针。通过向RAM中的几个位置填充值,我们可以让固件发送任意的CAN信息、控制ID、长度和数据。

{%}

图-can_transmit_msg_1_or_3

目前来说,我们的最大问题是,虽然我们有能力制作任何CAN信息,但是我们实际上没有调用函数的方法。我们可以通过修改固件来调用函数,但是我们想要找到一种方法来从OMAP芯片上发送CAN信息,使用V850作为一个代理。似乎我们有些本末倒置了,因为对于传输函数的直接调用是有限制的,没有任何函数能调用到OMAP芯片上。本质上说,Uconnect系统确实执行了一些CAN功能,但是我们无法通过入侵头单元来直接调用任何的函数,所以,我们需要另一种方式让我们的信息传递到总线上。

我们知道V850/Fx3也支持通过SPI和I2C的串行通讯,但是我们只看到过头单元与V850芯片之间使用过SPI通讯。所以,我们决定在固件中查找可以执行SPI数据解析的代码。SPI是一个非常简单的串行通讯协议,所以我们决定查找在线路上观察到的特定值,以及类似于逐字节数据解析的代码。

{%}

图-SPI通道7

在上面的例子中,你可以看到,0x22的值被用于比较0x4A1E6上的值,这与我们在SPI 通道7上面观察到的数据吻合。接下来,在下一章,我们会使用SPI协议和修改过的IOC固件,向V850芯片发送任意数据,填充变量并发送任意的CAN信息。

注意:为了保持简洁,在这一章中我们省略了大量的细节。和往常一样,有具体问题请联系我们的邮箱。我们用了几周的时间才完成了V850固件和SPI通讯的逆向,结果表明这一部分是整个项目中最耗时耗力的。

不用USB刷V850

IOC运行在V850芯片上,能够直接访问(读/写)CAN总线,所以,我们的目标就是修改IOC并想办法从Uconnect系统上与IOC通讯。如前文所述,这个固件没有签名,并且可以从头单元上更新。对于攻击者来说,最复杂的部分就是系统只能使用U盘来更新,作为一名远程攻击者,我们无法做到这一点。我们希望能在不使用USB设备的情况下,从OMAP芯片上刷V850。

前面的章节中,我们详细的介绍过,IOC的更新是‘iocupdate’二进制通过与SPI通道4通讯,使用类似ISO-14230的命令执行的。当在应用模式下时,也就是头单元“启动”状态下,‘iocupdate’二进制不会处理V850。在常规模式下,所有发送到V850的SPI信息都会被忽略。需要让IOC进入 “bootrom”模式,才能更新固件。

但是,让V850进入 “bootrom”的唯一办法是重置V850,然后重置OMAP芯片(这样攻击者就会丢失控制)。当OMAP处理器启动进入 “更新模式”时(要想让IOC进入 “bootrom”模式,OMAP需要进入 “更新模式”),OMAP处理器会尝试从USB设备更新。这种更新方式一般是硬编码的,无法更改。

我们的主要目的是让V850进入 “更新模式”,当然,是在没有USB设备参与的情况下。在这里,我们可以远程在文件系统中放入一个镜像,通过镜像来更新V850。很显然,我们无法依靠USB设备来发动远程攻击。

第一步是运行代码来重启v850进入引导程序模式,让OMAP进入更新模式。下面是使用的LUA代码:

onoff = require "onoff" 
onoff.setUpdateMode(true) 
onoff.setExpectedIOCBootMode("bolo") 
onoff.reset( "bolo")

下面的代码会让V850恢复成应用模式,让OMAP恢复到常规模式:

onoff = require "onoff" 
onoff.setExpectedIOCBootMode( "app") 
onoff.setUpdateMode(false) 
onoff.reset( "app")

下一步是尝试控制V850在bootrom模式中运行的代码。以及OMAP处理器在更新模式中运行的代码,从而让我们绕过USB设备检查。还记得,当OMAP处理启动备份时,我们无法与其通讯(远程接口无法启用)。我们可以通过检查更新模式中的机器是如何启动的,从而在更新模式中运行代码。文件’bootmode.sh’是首先执行的一个文件。

不幸运的是,我们无法修改’bootmode.sh’,因为这个文件位于一个不可写目录,下面是文件的一部分。

 #!/bin/sh

 #
 # Determine the boot mode from the third byte
 # of the "swdl" section of the FRAM.  A "U"
 # indicates that we are in Update mode.  Anything
 # else indicates otherwise.
 #
 inject -e -i /dev/mmap/swdl -f /tmp/bootmode -o 2 -s 1
BOOTMODE=`cat /tmp/bootmode`
echo "Bootmode flag is $BOOTMODE"
rm -f /tmp/bootmode

if [ "$BOOTMODE" != "U" ]; then
  exit 0
fi

echo "Software Update Mode Detected"
waitfor /fs/mmc0/app/bin/hd 2
if [ -x /fs/mmc0/app/bin/hd ]; then
   echo "swdl contents"
   hd -v -n8 /fs/fram/swdl
   echo "system contents"
   hd -v -n16 /fs/fram/system
else
   echo "hd util not detected on MMC0"
fi

你可以看到,如果OMAP芯片没有在更新模式中,剩下的所有文件都不会执行。如果OMAP芯片在更新模式中,OMAP就会继续进行并执行 ‘hd’程序。这个应用位于/fs/mmc0分区,可以修改成可写的,所以我们就修改了这个分区。因此,为了能在OAMP芯片进入更新模式,让v850进入引导程序模式执行代码,我们只需要用我们选择的代码来替换‘/fs/mmc0/app/bin/hd’。因为两个处理器都在适合的模式下,无论我们在’hd’中放什么,都能够更新V850的固件。

下面是我们修改后的’hd’:

#!/bin/sh

# update ioc
/fs/mmc0/charlie/iocupdate -c 4 -p /fs/mmc0/charlie/cmcioc.bin

# restart in app mode
lua /fs/mmc0/charlie/reset_appmode.lua

# sleep while we wait for the reset to happen
/bin/sleep 60

剩下要做的就是让‘/fs/mmc0’ 分区可写,在合适的位置放入合适的文件,然后重启进入引导程序模式。这些cao'zu都是在文件‘omap.sh’ 中完成的。

此次更新总共需要大约25秒,包括了在应用模式中启动备份所需要的时间。在应用模式中启动备份后,新的V850固件就会运行。