我想从逆向的角度做了深入了解JTAG,JTAG是许多嵌入式CPU使用的硬件级别调试机制,我希望通过这篇文章从逆向工程师的角度解释如何使用JTAG,并在此过程中提供一些实际示例。
通过这篇文章,我希望做到以下几点:
1. 解释JTAG的工作原理;
2. 演示如何发现和利用未知目标上的JTAG端口/接口;
3. 提供一些当前可用于与JTAG接口交互的OSS工具的概述;
4. 利用JTAG提取固件并调试目标。
另外,在概述之前,我列出一些学习JTAG的资源
· Cyphunk’s Embedded Analysis Page
· FPGA4Fun JTAG Overview
· Blackbox JTAG Reverse Engineering
JTAG是一种硬件接口,旨在帮助开发人员和测试人员进行调试。JTAG最初是为了测试集成电路而开发的,更具体地说,是对被测目标上的IO引脚进行采样。这种类型的调试接口使工程师可以在不需要物理引脚本身的情况下测试PCB上的连接。JTAG接口通过以下概述的状态机进行控制:
关于此级别的JTAG,要记住的重要事情之一是它涉及两个寄存器,即指令寄存器和数据寄存器。要使用这些寄存器,必须使用以下接口信号输入上述状态机中的正确状态:
使用TMS和TCK线浏览状态机,同时分别通过TDI和TDO写入或读取数据。在TCK的上升沿对TMS进行采样,这意味着必须先声明TMS线,然后才能将TCK切换为在状态机中导航。然后根据JTAG状态机的状态将数据移入指令寄存器(IR)或数据寄存器(DR)。当完成一个操作(或更新DR / IR相后)所得到的数据可以被移位通过输入DR的Shift-DR状态。有了这些原语,制造商可以实现他们希望通过JTAG实现的任何功能。
JTAG标准将IR和DR视为移位寄存器,因此,可以将多个目标链在一起。
简而言之,JTAG定义了一个状态机,该状态机至少使用4个信号进行导航。有了此状态机,最终用户可以从两个移位寄存器IR和DR进行写入和读取。
JTAG寄存器
JTAG利用两个主要寄存器,指令寄存器和数据寄存器。指令寄存器用于确定JTAG控制器将要执行的功能,例如存储器读取或存储器写入。数据寄存器然后用作指令寄存器的附加输入,对于前面的示例,它们可以用于提供要读取或写入的地址。这些寄存器的大小可以根据其功能而有所不同。
要写入寄存器,将执行以下步骤,我们以IR为例:
1. 输入Test Logic Reset状态(TLR)(可以通过断言TMS线路并循环CLK5次来完成此操作);
2. 进入Select IR Scan状态;
3. 进入Capture IR状态;
4. Enter Shift IR–这是我们将数据从TDI加载到IR的地方;
5. 进入Exit IR状态;
6. 进入Update IR状态–此阶段将值“锁存”到IR中。
此后,如果不需要数据寄存器,则将执行该操作,并将结果(如果有)加载到数据寄存器中以移出。但是,许多指令也需要在操作之前填写数据寄存器。在这种情况下,一旦数据寄存器被写入并更新,就将执行该操作,并且结果可以移出数据寄存器。
某些指令不需要加载DR,例如,如果我们已将IDCODE指令加载到IR(1110b)中,则会将处理器的IDCODE值加载到数据寄存器中,以便我们计时并继续读取TDO。要从中读取结果TDO,将导航到该Shift-DR状态,并在上输入32位TDI,这将导致数据寄存器中的数据TDO在线上移出。请参见下图,直观地了解如果向IR加载IDCODE指令后会发生什么情况。
重要的是要记住,IR并且DR可以将其视为移位寄存器,这意味着当我们使用新值更新它们时,旧值随后会移出TDO。
JTAG标准定义了以下指令寄存器:
· BYPASS
· 该指令连接TDI并TDO
· 在此Shift DR状态下,数据从传输TDI到TDO的延迟为一个TCK周期
· Capture DR状态期间将0装入数据寄存器
· 这可用于确定扫描链中有多少个设备
· IDCODE
· 加载时,将设备代码ID寄存器选择为TDI和TDO之间的串行路径
· 在Capture-DR状态下,将32位设备ID代码加载到此移位部分中
· 在Shift-DR状态下,此数据被移出,最低有效位在前
· 核心JTAG概念:
· 该状态机被导航用4个信号:TCK,TMS,TDO和TDI
· TDI用于提供输入,TDO用于输出
· 使用此状态机,可以将数据移到IR(Shift IR)和DR(Shift DR)中
· 可以将指令寄存器(IR)视为函数,而将数据寄存器(DR)视为该函数的参数
· 随着数据移入DR和IR,先前的内容移出TDO
· 一旦将数据移入这些寄存器,就可以执行操作(除少数保留指令外,完全取决于主机实现)
· 数据被读出通过将其移动到目标的出TDO从数据在寄存器Shift DR状态。
因此,既然我们已经研究了JTAG的底层工作原理,那么我们应该讨论为什么我们会关心它,以及该接口如何为逆向工程师授予对有用功能的访问权限。JTAG接口最常用的应用程序之一是硬件级调试(因此,本文的标题)。这是由芯片制造商实现的,并且可能因芯片而异,但是,针对ARM目标的硬件级调试的最常见实现之一是ARM的CoreSight调试接口。这与我在上一篇文章中通过SWD进行通信的实现相同,只是在这种情况下,调试访问端口是通过JTAG进行通信的。JTAG实现的细节可以在这里找到。对我们来说幸运的是,可以使用一些出色的OSS工具与这些端口进行通信-这篇文章将重点介绍使用OpenOCD。
http://infocenter.arm.com/help/topic/com.arm.doc.ddi0314h/DDI0314H_coresight_components_trm.pdf
https://wrongbaud.github.io/stm-xbox-jtag/
https://static.docs.arm.com/ihi0031/c/IHI0031C_debug_interface_as.pdf
OpenOCD负责利用JTAG或SWD接口向最终用户授予通过CoreSight DAP公开的调试接口提供的各种原语。Coresight / DAP体系结构相当复杂,在本篇文章(已经很长时间)中无法涵盖,因此我有可能将其保存在另一篇文章中。
从逆向工程师的角度进行此类操作时,对协议基础知识有扎实的了解非常重要。当进行逆向硬件(或软件)时,由于总是存在无限的未知数,因此你希望掩盖自己的基本事实。接下来的几节将讨论如何利用我们对这些协议的低级知识来帮助我们走上通过JTAG进行硬件级调试的途径。我们需要做的第一件事是确定引脚排列,以及是否暴露的引脚允许访问JTAG接口。
确定引脚排列
JTAG信号线通常组合在一起,有时(如果非常幸运的话),你将看到以下标头之一:
但是,如果你发现类似的内容,则可能没有确切的信号分组,因此,我们将讨论如果假设某个引脚用于JTAG,则如何确定引脚。在进行类似这样的逆向工程时,你想从已知的知识开始。因为我们知道大多数制造商至少会进行IDCODE,所以BYPASS我们看看如何利用这两个说明。
如果你确定自己认为是潜在的JTAG标头或引脚,但不知道引脚,则可以使用这两个寄存器的行为来确定引脚。
由于该IDCODE寄存器通常作为默认IR加载,因此可以通过执行以下操作来测试假定的引脚排列:
1. 将角色分配给潜在的输出引脚(TMS,TCK等);
2. 输入Test Logic Reset状态;
3. 输入Select DR Scan,Capture DR,Shift DR;
4. 时钟32点的值TDI,并监测TDO一个有效的IDCODE值;
5. IDCODE如果看起来有效,请检查移出的值!否则,请重新分配引脚并重复!
除了利用IDCODE默认情况下经常将寄存器加载到IR中这一事实外,我们还可以利用IR和DR都充当移位寄存器这一事实,因此,如果我们假设一个通用的寄存器长度(32位通常有效),我们可以尝试执行以下操作以强行设置插脚:
1. 将角色分配给潜在的输出引脚(TMS,TCK等);
2. 使用这些假定值输入Test Logic Reset状态;
3. 输入Shift IR状态;
4. 移入一个唯一的32位值 TDI;
5. TDI在监视你在TDO上的唯一模式时,继续将1切换为开(请确保至少进行32次操作!)。
6. 如果发现了模式,那就恭喜!否则,为引脚选择新的分配并重复!
这两种方法是通过前面提到的使用JTAGEnum脚本,还有JTAGULATOR。
确定指令长度
一旦确定了目标的引脚排列,即可开始真正的分析。下一步是确定IR / DR的长度。为此,从IR开始,进入Shift IR状态,并TDI使用1024或4096之类的大数字以1的on填充链,然后以0 TDI计数。一旦完成,只需继续以1的on 计数,计数即可在出现0之前花费的时钟周期数TDO。这将告诉你IR的长度。有了该名称后,就可以输入Shift DR状态并重复此过程以确定DR的状态。
http://urjtag.org/
本文的目标将是我最近从一台较旧的笔记本电脑中恢复的三星M.2 SSD,在查看了PCB并找出可能是JTAG接头的地方之后,我想从头到尾概述一下该过程。
实际示例:查找JTAG标头/确定引脚排列
如前所述,JTAG线路经常被分组-因此,从硬件角度看一个新平台时,寻找大于5的引脚分组总是一个好的开始。幸运的是,在这个目标上,PCB外侧有9个过孔。让我们开始检查驱动器在正常操作状态下这些引脚的电压电平。
从最初开始-这些电压值不会告诉我们任何信息,那么我们可以根据已有的信息确定什么?首先,我们有一个GND,通过在万用表上使用连续性模式并针对USB连接器的屏蔽层(当然要拔掉目标!)进行测试,很容易确定GND。接下来,我们有一条1.8V的线,通常会期望它是TMS,因为在大多数文档中建议将其保持在高电平。
为了确定引脚排列,我们将使用Raspberry Pi和JTAGEnum项目。该脚本使用上述方法来尝试识别JTAG引脚。同样重要的是,这里的逻辑电平为1.8V,因此,如果要与该目标接口,我们将需要使用逻辑电平转换器。JTAGEnum.sh使用Raspberry Pi的GPIO线来激活目标接口,在shell脚本中,它们包含GPIO值的映射,如下所示:
# define BCM pins (mapped directly to /sys/class/gpio/gpio${pin[N]})
# 5v 5v g 14 15 18 g 23 24 g 25 8 7 1 g 12 g 16 20 21
# 3v 2 3 4 g 17 27 22 3v 10 9 11 g 0 5 6 13 19 26 g
使用上表,我们将以下GPIO连接到未知头:
JTAGenum.sh我们将pins变量修改如下:
pins=(9 11 25 2 3 10)
pinnames=(pin1 pin2 pin3 pin4 pin5 pin6)
现在将引脚连接好,并且逻辑电平转换器就位,我们可以运行了JTAGenum.sh。
运行如下所示连接的脚本会产生大量结果,可以在此处看到输出。对我们来说幸运的是,它正确地标识了两种可能的配置,如下所示:
FOUND! ntrst:pin4 (RPi GPIO 2) tck:pin6 (RPi GPIO 10) tms:pin1 (RPi GPIO 9) tdo:pin3 (RPi GPIO 25) tdi:pin2 (RPi GPIO 11) IR length: 4
FOUND! ntrst:pin5 (RPi GPIO 3) tck:pin6 (RPi GPIO 10) tms:pin1 (RPi GPIO 9) tdo:pin3 (RPi GPIO 25) tdi:pin2 (RPi GPIO 11) IR length: 4
接下来,脚本运行ID扫描。你可能会注意到为此生成了很多结果,我们如何过滤这些结果?你可以执行一些操作来筛选结果,例如,扫描链上可能只有1-2个设备(CPU和闪存),因此我们可以立即忽略那些具有2-3个以上条目的设备。接下来,你可以排除序列长(大于4-5)为1或0的序列。幸运的是,在此列表中,有一个我之前见过的ID:0x4ba00477 -该ID用于ARM Cortex内核,在尝试访问Beaglebone Black时,我已经见过它。
ntrst:pin4 tck:pin6 tms:pin1 tdo:pin3 tdi:pin2 devices: 1
0x4ba00477
ntrst:pin4 tck:pin6 tms:pin1 tdo:pin3 tdi:pin5 devices: 1
0x4ba00477
ntrst:pin5 tck:pin6 tms:pin1 tdo:pin3 tdi:pin2 devices: 1
0x4ba00477
ntrst:pin5 tck:pin6 tms:pin1 tdo:pin3 tdi:pin4 devices: 1
0x4ba00477
你会注意到,在进行IDCODE扫描时,for的值会TDI有所不同,这是因为此方法完全不依赖,TDI因此只是一种猜测。
幸运的是,其中一些结果与模式扫描非常吻合,因此我们现在可以假设我们知道JTAG接口的引脚排列!
实际示例:使用UrJtag确定指令长度
尽管OpenOCD非常适合与DAP控制器接口和连接到调试内核,但是UrJTAG项目非常适合与底层JTAG接口。我们可以使用它们通过有用的discover命令来检测各种DR长度。此方法使用前面提到的相同原理来选择,IR然后将大量的1移入DR,然后移入0,然后对多个1进行计时,直到在TDO上读取0!
UrJTAG可以使用位于~/.jtag/rc我的rc文件如下
pi@raspberrypi:~ $ cat .jtag/rc
cable gpio tck=10 tms=9 tdi=11 tdo=25
detect
discover
下面我们可以看到使用这些命令运行UrJTAG的结果:
pi@raspberrypi:~ $ sudo -E jtag
UrJTAG 2019.12 #
Copyright (C) 2002, 2003 ETC s.r.o.
Copyright (C) 2007, 2008, 2009 Kolja Waschk and the respective authors
UrJTAG is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
There is no warranty for UrJTAG.
warning: UrJTAG may damage your hardware!
Type "quit" to exit, "help" for help.
Initializing GPIO JTAG Chain
IR length: 4
Chain length: 1
Device Id: 01001011101000000000010001110111 (0x4BA00477)
Unknown manufacturer! (01000111011) (/usr/local/share/urjtag/MANUFACTURERS)
Detecting IR length ... 4
Detecting DR length for IR 1111 ... 1
Detecting DR length for IR 0000 ... 1
Detecting DR length for IR 0001 ... 1
Detecting DR length for IR 0010 ... 1
Detecting DR length for IR 0011 ... 1
Detecting DR length for IR 0100 ... 1
Detecting DR length for IR 0101 ... 1
Detecting DR length for IR 0110 ... 1
Detecting DR length for IR 0111 ... 1
Detecting DR length for IR 1000 ... 35
Detecting DR length for IR 1001 ... 1
Detecting DR length for IR 1010 ... 35
Detecting DR length for IR 1011 ... 35
Detecting DR length for IR 1100 ... 1
Detecting DR length for IR 1101 ... 1
Detecting DR length for IR 1110 ... 32
我想在这篇文章中重点介绍UrJTAG,因为在查看具有完全未知的扫描链或DAP架构的目标时,它非常有用。对我们来说幸运的是IDCODE,此目标的告诉我们它是ARM架构,我们很可能将能够使用CoreSight DAP,为此,我们将使用OpenOCD。如果你正在寻找一无所知的扫描链,那么我通常从UrJtag开始,只是要获取所有寄存器的映射。UrJTAG的python绑定也可以很好地工作,并且可以用于与JTAG进行低级接口。
由于我们知道目标上JTAG接口的引脚排列,因此我们现在可以继续使用OpenOCD与之通信。我之所以选择OpenOCD是因为它具有对ARM MCU的出色调试支持,尤其是使用CoreSight的Cortex系列。我们需要做的第一件事是选择一个硬件适配器,我们将使用FT2232H中断模块。
通过FT2232H进行JTAG调试
了解了引脚排列后,我们现在可以尝试使用OpenOCD与DAP进行通信。为此,我们将使用FT2232H适配器,在本文中,我将使用标准的FT2232H分支板。这些板可用于与多个硬件级别的接口交互,并具有出色的软件支持。你可能还记得我曾经将它们用于SWD以及转储SPI闪存。使用该板,以及3.3V至1.8V逻辑电平转换器,我们可以将其连接到目标,如下所示:
接下来,我们将从关于目标的已知变量开始编写配置文件。
source [find target/swj-dp.tcl]
# This is using the name on the SoC
if { [info exists CHIPNAME] } {
set _CHIPNAME $CHIPNAME
} else {
set _CHIPNAME s4ln045x01
}
# This is the TAP ID that we discovered in the previous step
if { [info exists CPUTAPID] } {
set _CPUTAPID $CPUTAPID
} else {
set _CPUTAPID 0x4ba00477
}
# Set the speed of our adapter
adapter_khz 200
# We are indeed using JTAG
transport select jtag
# We don't have a SRST pin, only TRST it would seem
reset_config trst_only
# Here we create the JTAG TAP/DAP, defining the location and characteristics of our DAP
swj_newdap $_CHIPNAME cpu -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id $_CPUTAPID
dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu
set _TARGETNAME $_CHIPNAME.cpu
当我们使用此配置文件运行openocd时,结果如下:
wrongbaud@wubuntu:~/blog/samsung-jtag$ sudo openocd -f minimodule.cfg -f config.cfg
Open On-Chip Debugger 0.10.0+dev-01040-ge7e681ac (2020-01-27-18:55)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "jtag". To override use 'transport select '.
Warn : Transport "jtag" was already selected
Info : clock speed 200 kHz
Info : JTAG tap: s4ln045x01.cpu tap/device found: 0x4ba00477 (mfg: 0x23b (ARM Ltd.), part: 0xba00, ver: 0x4)
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : JTAG tap: s4ln045x01.cpu tap/device found: 0x4ba00477 (mfg: 0x23b (ARM Ltd.), part: 0xba00, ver: 0x4)
Warn : gdb services need one or more targets defined
现在让我们看一下DAP,看看那里是否还有其他相关信息:
> dap info 0
DAP transaction stalled (WAIT) - slowing down
DAP transaction stalled (WAIT) - slowing down
AP ID register 0x24770002
Type is MEM-AP APB
MEM-AP BASE 0x80000000
ROM table in legacy format
Component base address 0x80000000
Peripheral ID 0x0000080000
Designer is 0x080, Part is 0x0, Unrecognized
Component class is 0x1, ROM table
MEMTYPE system memory not present: dedicated debug bus
ROMTABLE[0x0] = 0x1003
Component base address 0x80001000
Peripheral ID 0x04008bbc14
Designer is 0x4bb, ARM Ltd.
Part is 0xc14, Cortex-R4 Debug (Debug Unit)
Component class is 0x9, CoreSight component
Type is 0x15, Debug Logic, Processor
ROMTABLE[0x4] = 0x2003
Component base address 0x80002000
Peripheral ID 0x04008bbc14
Designer is 0x4bb, ARM Ltd.
Part is 0xc14, Cortex-R4 Debug (Debug Unit)
Component class is 0x9, CoreSight component
Type is 0x15, Debug Logic, Processor
ROMTABLE[0x8] = 0x3003
Component base address 0x80003000
Peripheral ID 0x04008bbc14
Designer is 0x4bb, ARM Ltd.
Part is 0xc14, Cortex-R4 Debug (Debug Unit)
Component class is 0x9, CoreSight component
Type is 0x15, Debug Logic, Processor
ROMTABLE[0xc] = 0x4003
Component base address 0x80004000
Invalid CID 0x00000000
ROMTABLE[0x10] = 0x5003
Component base address 0x80005000
Invalid CID 0x00000000
ROMTABLE[0x14] = 0x6003
Component base address 0x80006000
Invalid CID 0x00000000
ROMTABLE[0x18] = 0x7003
Component base address 0x80007000
Invalid CID 0x00000000
ROMTABLE[0x1c] = 0x8003
Component base address 0x80008000
Invalid CID 0x00000000
ROMTABLE[0x20] = 0x9003
Component base address 0x80009000
Invalid CID 0x00000000
ROMTABLE[0x24] = 0xa003
Component base address 0x8000a000
Invalid CID 0x00000000
ROMTABLE[0x28] = 0xb003
Component base address 0x8000b000
Invalid CID 0x00000000
ROMTABLE[0x2c] = 0xc003
Component base address 0x8000c000
Invalid CID 0x00000000
ROMTABLE[0x30] = 0xd003
Component base address 0x8000d000
Invalid CID 0x00000000
ROMTABLE[0x34] = 0xe003
Component base address 0x8000e000
Invalid CID 0x00000000
ROMTABLE[0x38] = 0xf003
Component base address 0x8000f000
Invalid CID 0x00000000
ROMTABLE[0x3c] = 0x0
End of ROM table
首先要指出的是这是Cortex R4,有了这些附加信息,我们可以在配置文件中创建目标,该目标应授予对MEM-AP的访问权限,以便进行调试。这可以通过添加以下行来完成:
target create $_TARGETNAME.1 cortex_r4 -endian $_ENDIAN -dap $_CHIPNAME.dap
在这一行中,我们可以尝试通过halt命令暂停目标并通过mdwOpenOCD提示符读取内存:
> halt
MPIDR not in multiprocessor format
target halted in Thumb state due to debug-request, current mode: Supervisor
cpsr: 0x80000133 pc: 0x0001abfc
D-Cache: disabled, I-Cache: disabled
> mdw 0x800000000 10
DAP transaction stalled (WAIT) - slowing down
0x800000000: eafffffe ea000005 ea000006 ea000006 ea00000b e320f000 ea00000e eafffffe
0x800000020: ea0000e3 eafffffe
在这里,我们测试逐步运行的固件:
> halt
MPIDR not in multiprocessor format
target halted in ARM state due to debug-request, current mode: Supervisor
cpsr: 0x80000113 pc: 0x0000e10c
D-Cache: disabled, I-Cache: disabled
> step
target halted in ARM state due to breakpoint, current mode: Supervisor
cpsr: 0x80000113 pc: 0x0000e110
D-Cache: disabled, I-Cache: disabled
成功运行,我们可以单步调试固件。接下来,让我们使用此功能获取一些RAM转储,此页面概述了内存模型,因此我们可以将其用作参考。可以使用OpenOCD通过dump_image命令将内存转储到文件中。
> halt
MPIDR not in multiprocessor format
target halted in ARM state due to debug-request, current mode: Abort
cpsr: 0x200001d7 pc: 0x00000048
D-Cache: disabled, I-Cache: disabled
Data fault registers DFSR: 00000008, DFAR: 9f7e3000
Instruction fault registers IFSR: 00000000, IFAR: 00000000
> dump_image SDRAM.bin 0x20000000 0xA0000000
> dump_image RAM.bin 0 0xFFFFFFF
最后,让我们看一下这些RAM转储并将它们加载到GHIDRA中,看它们是否有意义:
太好了,我们有一些外部参照,并且init代码看起来还不错。看起来还好像在UART上提供了某种调试菜单,这很可能是我们引脚上的8/9引脚!可以肯定地说这是一个有效的RAM转储,并以此结束本文。
这是一篇很长的文章,实际上,它可能应该分成2-3个部分。通过这篇文章,我们学习了JTAG的底层功能,以及如何使JTAG成为逆向工程师的利器。我们还能够通过JTAG访问未记录的目标,提取内存并单步运行固件。这里还有很多事情要做,例如确定闪存芯片本身是否可以通过JTAG转储,RE固件以寻找有趣的方式来从驱动器中恢复数据。
http://www2.futureware.at/~philipp/ssd/TheMissingManual.pdf
· https://github.com/thesourcerer8/SSDdiag
· http://www2.futureware.at/~philipp/ssd/TheMissingManual.pdf
参考及来源:https://wrongbaud.github.io/jtag-hdd/