使用DCI单步调试Intel CPU,调试运行在其上的UEFI代码

在本文中,我们将为读者详细介绍下列内容:

· 配置ASRock H370M-ITX/ac,以允许进行DCI DbC调试

· 使用Intel System Studio和System Debugger单步执行Coffee Lake-S i7-8700 CPU

· 在硬件上调试含有安全漏洞的UEFI应用程序示例

USB DCI DbC调试技术(JTAG over USB3)

对于使用更新的CPU和芯片组的读者来说,可以购买价值15美元的现成电缆,并通过单步执行方式来调试硬件线程。准确来说,这里需要使用的电缆是USB 3.0调试电缆;同时,与太网线缆类似,该电缆的内部也需要进行相应的跳线设置。使用该线缆时一定要注意,它是专门用于支持USB的机器的;如果将其用于不支持USB的机器,可能出现无法预料的情况。

对于型号较新的Intel CPU来说,通常都支持通过USB3进行调试;在这种情况下,我们可以使用现成的硬件,通过专有的直接连接接口(Direct Connection Interface,DCI)进行调试。这些方式适用于某些第6代CPU和芯片组组合,以及大多数第7代和更新型号的处理器合芯片组组合。我还没有找到具体的CPU/芯片组组合数据,但我从酷睿系列处理器推测出来的组合如下所示:

· Kaby Lake处理器 / Intel 100或200系列SunrisePoint芯片组

· Coffee Lake-S处理器 / Intel Z370、H370、H310或B360芯片组

· Kaby Lake R处理器 /第六代Intel酷睿芯片组

· Whiskey Lake-U处理器(8565U、8265U、8145U)

· Coffee Lake-S处理器 / H370、H310、B360芯片组

这些组合应支持“DCI USB 3.x Debug Class”调试。这意味着您只需要使用上面链接中的廉价调试电缆,即可完成相应的调试工作。请注意,如果不支持调试电缆的话,则需要购买英特尔的专有的插入设备。

根据我阅读过的相关文档来看,对于支持"DCI USB 3.x Debug Class"功能的机器来说,其USB3硬件能够对DCI命令进行解码,并将其转发给目标CPU上的适当硬件模块,进而转换为JTAG序列。英特尔公司System Debugger软件以及名为OpenDCI的DCI实现。由于该调试环境是使用Eclipse构建而成的,因此,该环境能够支持macOS、Linux和Windows平台。为用户提供了免费使用的、具有可续订许可的Intel System Studio

在本文中,我们将以Windows 10和Intel System Studio为例进行演示。

在ASRock H370M-ITX/ac上启用DCI功能

实际上,我们可以通过翻转UEFI变量中的某些标志位来启用和禁用UEFI中某些没有公开的功能。

对于研究过DCI的读者来说,可能发现某些相关文献中使用的是启用了DCI功能的BIOS版本,或使用的是UEFI的调试版本。我相信这些做法肯定很有帮助,但从一般意义上来说,这些方法并不具有通用性。所以,本文中采取的指导方针是通过“修改”UEFI来启用DCI功能。我发现,eiselekd编写的一篇如何启用DCI功能的文章非常值得一读。

· 通过chipsec工具将SPI内容转储到磁盘:chipsec_util spi dump rom.bin。

· 通过UEFITool工具打开rom.bin,并提取GUID 899407 D7-99FE-43 D8-9A 21-79ec 328 CAC 21(UEFI变量Setup)。

· 使用IFRExtractor印变量选项的文本表示。

H370M-ITX/ac所需的变量设置如下,我们已经在3.10和4.00版本的UEFI上进行了测试:

· Enable/Disable IED (Intel Enhanced Debug):偏移量0x960,设置为启用0x1

· CPU Run Control:偏移量0x663,设置为启用0x1

· CPU Run Control Lock:偏移量0x664,设置为禁用0x0

· Platform Debug COnnect:偏移量0x114F,设置为0x03以启用DCI DbC

· xDCI Support:偏移量0xABD,设置为启用0x1

为了修改和保存这些偏移,请遵循上面的指导,并可以借助于相关的工具UEFI Shell和RU.efi

使用调试电缆连接主机和目标计算机后,可以通过读取USB3设备类别标签来确认已启用DCI。此外,主机上应安装有Intel System Studio,目标系统的硬件配置应该为H370M-ITC/ac。如果DCI功能已经启用,那么主机的USB驱动程序将会读取到“Intel USB Native Debug Class Devices”。如果出现错误,您将看到“Port Reset Failed”消息。除此之外,我们还可以使用USB Tree View来轻松查看USB设备的详细信息。同时,Chipsec也会报告DCI是否已启用,但我发现未报告DbC的可用性;因此,最好在Windows中选择相应的USB设备驱动程序,以此确认UEFI选项的设置是否正确。

单步调试i7-8700

首先,让我们来回顾一下对硬件的要求和相关设置:

· 需要一台运行Windows 10且安装了Intel System Studio的主机

· 主机和目标i7-8700/H370M-ITX/ac通过USB3 DbC电缆进行连接

· 主机显示已经连接“Intel USB Native Debug Class Device”USB设备

中断目标机器的启动过程,以便进入UEFI设置环境(按F2)。当然,这一步并不是必需的,但它将有助于了解地址空间和其他布局方面的详细信息。我还没搞清楚如何使用DCI和DbC暂停CPU复位。

在Intel System Studio中,打开System Debugger,然后将目标连接配置为使用“8th Gen Intel Core Processors (Coffee Lake-S) _ Intel H370 Chipset Intel H310 Chipset Intel B360 Chipset for Consumer (Cannon Lake PCH)”,将连接方法配置为“Intel(R) DCI USB 3.x Debug Class”。

 成功后,您将看到类似于下面的状态输出内容:

22:02:20 [INFO ] TCA - IPConnection: Open Connection, configuration: CFL_CNP_OpenDCI_DBC_Only_ReferenceSettings.
22:02:57 [INFO ] Starting DAL ...
22:02:57 [DAL  ] The system cannot find the batch label specified - SetScriptPath
22:02:58 [DAL  ] Registering MasterFrame...
22:03:00 [DAL  ] Using Intel DAL 1.1905.602.100
22:03:00 [DAL  ] Using python.exe 2.7.15 (64bit), .NET 2.0.50727.8940, Python.NET 2.0.19, pyreadline 2.1.1
22:03:02 [DAL  ]     Note:    The 'coregroupsactive' control variable has been set to 'GPC'
22:03:10 [DAL  ] Using CFL_CNP_OpenDCI_DBC_Only_ReferenceSettings
22:03:10 [DAL  ] >>? DAL startup completed
22:03:10 [INFO ] Connection Manager: Status change: CONNECTED
    Connection: 8th Gen Intel Core Processors (Coffee Lake-S) _ Intel H370 Chipset Intel H310 Chipset Intel B360 Chipset for Consumer (Cannon Lake PCH)
    Target: 8th Gen Intel Core Processors (Coffee Lake-S) / Intel H370 Chipset, Intel H310 Chipset, Intel B360 Chipset for Consumer (Cannon Lake PCH)
    Connection Method: Intel(R) DCI USB 3.x Debug Class

 以及如下所示的输出内容:

 1.png

1.png

连接时,将会暂停CPU线程,并显示反汇编代码。如果CPU没有暂停并且单击“pause”按钮失败,则表示尚未完全启用DCI功能。例如,如果您遇到ExecutionControlUnableToHaltAllException异常,或者处理器处于“running”状态时不允许进行相关的操作,那么,请仔细检查UEFI变量Setup的设置情况。

成功连接后,将会看到:

 1.png

这样的话,我们就可以考察内存的内容,并使用相关的JTAG调试功能了。

在硬件上调试含有漏洞的UEFI应用程序示例

这一步非常简单,因此,这是一个很好的玩具示例,归根结底,是因为UEFI缺乏平台运行时安全机制,同时,UEFI开发工具包(EDK / UDK)也缺乏构建和编译方面的安全措施。

我们的目标是构建一个“玩具性质的”、含有安全漏洞的UEFI应用程序,并触发该漏洞,然后,使用主机上的System Debugger来观察其行为。我们需要做的第一件事情就是配置edk2构建环境。

下面,我们将修改HelloWorld应用程序,并使用以下代码替换MdeModulePkg/Application/HelloWorld/HelloWorld.c文件的内容。

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Protocol/LoadedImage.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
 VOID RunAsm();
 CHAR16* GetArgv(IN EFI_HANDLE ImageHandle)
{
  EFI_LOADED_IMAGE* li;
  EFI_GUID loaded_image_protocol = LOADED_IMAGE_PROTOCOL;
  gBS->HandleProtocol(ImageHandle, &loaded_image_protocol, (void**) &li);
  CHAR16* wargv = (CHAR16 *)li->LoadOptions;
  return wargv;
}
 
VOID RunMe()
{
  Print(L"You win\n");
  RunAsm();
}
 
UINT32 StrLenChar(CHAR8* src) {
  UINT32 ret = 0;
  while (src[ret++] != 0) {}
  return ret - 1;
}
 
VOID StrCpy(CHAR8* dst, CHAR16* src, UINT32 length) {
  CHAR8 *src8 = (CHAR8*)src;
  for (UINT32 i = 0; i < length; i++) {
    dst[i] = src8[(i*2)];
  }
 
  UINT64 loc = (UINT64)&RunMe;
  dst[length - 1] = 0;
  dst[length - 2] = 0;
  dst[length - 3] = 0;
  dst[length - 4] = 0;
  dst[length - 5] = ((loc >> (8 * 3)) & 0xFF);
  dst[length - 6] = ((loc >> (8 * 2)) & 0xFF);
  dst[length - 7] = ((loc >> (8 * 1)) & 0xFF);
  dst[length - 8] = ((loc >> (8 * 0)) & 0xFF);
}
 
 __attribute__((noinline)) VOID
 TestBufferOverflow(CHAR16* input)
 {
  /* Test stack buffer overflow */
 
  // Compiled with EDKII that auto-adds (-fno-stack-protector)
  CHAR8 buffer[32];
  StrCpy((CHAR8*)buffer, input, StrLen(input));
  buffer[StrLen(input)] = 0;
}
 
EFI_STATUS EFIAPI UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
) {
  // Run with: fs0:X64\HelloWorld.efi A*222
 
  Print(L"UefiMain=0x%p\n", &UefiMain);
  CHAR16* wargv = GetArgv(ImageHandle);
  UINT32 wargv_len = StrLen(wargv);
  TestBufferOverflow(wargv);
  return EFI_SUCCESS;
}

具体的构建命令为:

$ . ./edksetup.sh BaseTools
$ build -m MdeModulePkg/Application/HelloWorld/HelloWorld.inf -p MdeModulePkg/MdeModulePkg.dsc

然后,使用下列命令进行调试,该命令的详细解释,请参阅这篇文章

 $ qemu-system-x86_64 -bios /usr/share/OVMF/OVMF_PURE_EFI.fd -display none -nodefaults -serial stdio -hda fat:Build/MdeModule/DEBUG_GCC5

上面的代码是一个堆栈缓冲区溢出的示例程序。它会自动为我们填充被覆盖的ret地址。如果您想了解这种类型的安全漏洞的详细原理,请阅读这篇文章。注意,我们可以通过将文件读入易受攻击的堆栈变量来使该漏洞更加贴近现实(例如,删除自动填充的ret)。

使用默认edk2构建配置编译出的程序的执行流程如下所示:

1.png

我们的目标是将0x30个字符复制到缓冲区中,预期溢出0x20个字符,其中,8个字符用于保存RBX,16个字符用于保存RSP和RIP,最后8个字符将被填入RunMe的地址。

 为了获得快速反馈,我们将结果输出到ConsoleOut,然后使用以下代码重置CPU:

ASM_GLOBAL ASM_PFX(RunAsm)
ASM_PFX(RunAsm):
    mov $254, %al
    out %al, $100
    ret

当控制台不可用时,这个函数对于rip的盲测试控制来说是非常有用的。 

因为我们将会输出UefiMain的位置,所以,我们都可以发现,该应用程序每次执行时地址是不变的,并且,也可以知道在System Debugger中设置的硬件断点的位置,这样,我们就可以通过单步调试来考察溢出情况了。 

对于这里的UEFI构建来说,其位置为0x600BC69C,这意味着.text被加载到偏移量0x600BB000处,因为该子例程为0x169C。在这里,我们可以通过System Debugger添加更多的断点。 

有关DCI的详细介绍,以及除非进行显式调试否则需要禁用它的原因,请参阅这篇文章