开源项目-基于Intel VT技术的Linux内核调试器

本开源项目将硬件虚拟化技术应用在内核调试器上,使内核调试器成为VMM,将操作系统置于虚拟机中运行,即操作系统成为GuestOS,以这样的一种形式进行调试,最主要的好处就是调试器对操作系统完全透明。如下图:

       图1 虚拟化调试器框架

    传统的内核调试器的工作原理是接管中断向量表中的INT1和INT3处理程序。前者对应的是 #DB异常,通常是设置了单步标志后引发的单步异常和CPU中DR寄存器中定义的内存访问异常引发。后者由int3这条指令引发,而int3这条指令用于调试断点。

    而基于硬件虚拟化技术的调试器以VMM的身份运行,操作系统作为GuestOS,那么VMM将最先截取到CPU异常。因此基于虚拟化技术的调试器不需要替换CPU中断向量表项中的中断处理程序指针,如此一来,我们再稍做一些其他安排,调试器将对操作系统完全透明。

    检测当前是否有调试器调试自己是非常容易的。因此上述这种透明化在一些特殊场合尤为必要。例如一个反病毒工程师试图调试一个伪装严密的Rootkit(内核木马),而Rootkit检测到当前自己正在被调试器调试,就避免进行一些危险的动作,这样反病毒工程师就很难通过调试跟踪来得知这个Rootkit样本的内部结构与危害。而基于虚拟化技术的调试器可以很方便的隐蔽自己,防止被调试程序得知自己正在被调试。

 

   

 传统调试器在特殊领域的应用具有一定困难。如果程序拥有反调试机制,那么就首先就需要破解他的反调试机制。而且Linux平台内置的内核调试器KDB使用方法过于复杂,需要重新编译内核。其它一些第三方内核调试器同样不能很好地应用于特殊领域,有一些存在兼容性问题,有一些同样存在易用性的问题,有一些已经停止开发。

   国外hyperdbg项目是一款基于Intel 硬件虚拟化技术的内核调试器。该调试器使用硬件虚拟化技术达到隐藏自身的目的,支持Windows和Linux操作系统。但是一直以来项目开发进度慢,接口不友好,功能单一,只有简单的单步命令和断点命令,对调试符号支持不好,很难将其应用于实际工作中。

   基于以上几点,针对特殊领域的应用,有必要开发一款具有反侦测功能、简单易用的内核调试器。VMXICE调试器就是一款基于Linux的内核调试器,同样使用了硬件虚拟化技术,将当前运行的操作系统运行级别降低为GUEST,而调试器运行级别为HOST,这样一来,不修改中断向量表就可以实现监视和拦截调试中断。操作系统被放置于GUEST,并不能探测在位于HOST的调试器软件,这样的做法被证明可以很好地将调试器隐藏。

 

    说明:这个开源项目是孟学政同学做的毕业设计,因为他本人是window下软件开发的大牛(习惯于用32位汇编编写程序),曾开出了基于intel VT的window内核调试器。在我讲授Linux内核操作系统之后,希望他能在Linux下实现同样的功能,以此为毕业设计,他完成了Linux内核调试全部功能。在此,我们将不仅把此项目的源代码全部放出,而且文档也开放。阅读相关文档和代码,你对硬件虚拟化的了解不再停在原理层面,对Linux内核也不仅仅止于阅读其代码,而是真枪实刀的动手实战。

   随后将陆续发布文档和代码。

 

1. 基于IntelVt技术的Linux内核调试器- 引言与IntelVT-x 技术简介

 

1.1 引言

硬件辅助虚拟技术,顾名思义就是硬件支持虚拟化,一般特指CPU的虚拟化。物理CPU虚拟出一个虚拟CPU给客户机使用,并且提供特权级保护支持,虚拟机软件不必关心指令的解析以及特权指令造成的安全问题。这种技术方便了虚拟机的开发,同时也提升了虚拟机的性能。

硬件虚拟化技术除了可以提高虚拟机性能外还有很多其它的用途,例如软件加密与保护,还可以被恶意程序利用。在2006年8月3日的拉斯韦加斯的黑帽大会上,COSEINC的安全研究员JoannaRutkowska演示了一个叫做BluePill的恶意程序,该程序使用硬件虚拟化技术侵入Windows操作系统底层并完美地隐藏起来,所有的AntiRootkit工具均无法检测其存在。

硬件虚拟化技术还可以应用到调试与反调试领域。很多恶意程序都有反调试功能,而基于硬件虚拟化技术的调试器作为VMM,可以优于操作系统响应并拦截调试异常,完美地将自己隐藏起来,被调试的代码很难进行反调试。难以被检测是基于硬件虚拟化技术的调试器最主要的优点。

 

    

 

图1-1传统内核调试器、虚拟化调试器与操作系统层次对比

2010年AristideFattori等人建立hyperdbg开源项目,该项目基于NewBlue Pill部分代码,最初实现了一个基于IntelVT-x术的Windows内核调试器,后来又支持Linux操作系统,但一直以来该项目进度迟缓,调试器功能单一且Bug太多,无法进行实际应用。本文重新设计了一个基于IntelVT-x的内核调试器,名字叫VMXICE,实现了调试器的基本功能以及符号的解析,满足对Linux内核模块的调试需求。

1.2 Intel VT-x技术简介

支持VT-x技术的CPU有两种运行模式,VMX模式和非VMX模式。VMX模式就是支持VT技术的主要模式,而非VMX模式则与不支持VT-x技术的CPU运行模式完全相同。而VMX模式又分为两种操作模式,VMXroot模式和为VMXnon-root模式,VMXroot模式是提供给VMM使用的,在这种模式下可以调用VMX指令集,VMM用以创建和管理虚拟机。而VMXnon-root模式就是虚拟机运行的模式,这种模式不支持VMX指令集。

VT技术有两种转换,分别为VMEntry和VMExit。VMEntry就是从VMXroot模式切换到VMXnon-root模式,简单的说,就是VMM切换到虚拟机这个转换过程。VMExit就是从VMXnon-root模式切换到VMXroot模式,同理可理解为从虚拟机返回到VMM这个过程。


图1-2VMM与客户机的切换

另外每个硬件虚拟的CPU都有一个表示其状态以及控制其特性的结构,VT-x技术的叫做VMCS。该结构包含3个部分:版本标志、VMX退出原因、数据区。而数据区则包含大量与CPU状态和控制相关的信息,包括虚拟机状态保存区(各种寄存器)、宿主机状态保存区、虚拟机运行控制域、VMEntry行为控制域、VMExit行为控制域、VMExit信息域。

支持VT-x技术的指令集叫做VMX指令集,该指令集总共有10条指令,如表1.1所示。

表1-1VMX指令集

指令

作用

VMPTRLD

加载一个VMCS结构体指针作为当前操作对象

VMPTRST

保存当前VMCS结构体指针

VMCLEAR

清除当前VMCS结构体

VMREAD

读VMCS结构体指定域

VMWRITE

写VMCS结构体指定域

VMCALL

引发一个VMExit事件,返回到VMM

VMLAUNCH

启动一个虚拟机

VMRESUME

从VMM返回到虚拟机继续运行

VMXOFF

退出VMX操作模式

VMXON

进入VMX操作模式

 
2.基于IntelVt技术的Linux内核调试器- 调试器主要原理与环境搭建
2.1 传统调试器原理
传统的内核调试器在实现断点功能时,都是修改中断向量表,捕获CPU的单步异常和断点异常来实现的。被调试软件只要检测中断向量表是否被修改就可以判断当前是否运行了内核调试器。
图2-1-1原理图:操作系统正常的CPU异常处理
图2-1-2原理图:传统调试器

2.2 硬件虚拟化技术的调试器原理

基于VT-x技术的内核调试器主要原理是创建一个虚拟机,将当前操作系统转置于虚拟CPU中运行。这时调试器属于VMM,客户机引发的CPU异常是会被VMM最先捕获到的。捕获异常并处理完毕后才会下发回客户机。调试器作为VMM只需处理与调试相关的单步和断点异常,并不需要做真正虚拟机做的大量虚拟工作。
图2-2-1原理图:基于虚拟化技术的调试器
总的来说虚拟化调试器的最终实现是:不修改中断向量表,且优于操作系统捕获到异常,并且可以决定是否将异常下发到虚拟CPU(操作系统)。而实现的最重要思路就是利用硬件虚拟化技术创建虚拟CPU,并将当前物理CPU上运行的操作系统转到虚拟CPU中。实际上就是CPU控制权的转移,或者说是CPU上下文的转移。
2.3 开发环境搭建
 

硬件环境:Intel CPU 32-bit 支持虚拟化(VT-x) 具体可通过查看/proc/cpuinfo 是否有“vmx” 这个flag:$ grep vmx /proc/cpuinfo

软件环境:32位非PAE内核发行版(distro)没有特定的要求目前已测试支持Ubuntu, Fedora, RHEL6(RHEL5 不支持,是因为本项目中使用的一些宏在KVM的库文件中,而RHEL5的内核并没有合并KVM的代码,这样导致在RHEL5中有编译错误)本软件是在Ubuntu环境下开发的。
开发环境:需要gcc、yasm和code::block集成开发环境

code::block是一款支持C/C++等多种语言编辑的跨平台集成开发环境。他有良好的用户接口,使用apt-getinstall codeblock即可一步安装。

yasm是一款汇编编译器,完全支持nasm语法,与微软的masm语法基本相同。它支持x86和AMD64指令集,有windows和linux等多种操作系统下运行的版本。使用apt-get install yasm 也可一步安装,在fedora下不能直接安装,可下载源码自行安装,下载地址:http://linux.softpedia.com/get/Programming/Assembler-Tools/Yasm-621.shtml

特殊要求:调试器需要在单核模式下运行,因此需要改变grub配置文件,为kernel加入maxcpus=1这个参数。

3.基于IntelVt技术的Linux内核调试器- 调试器设计与实现(1):基本框架

 

本内核调试器拥有如下几个主要模块:虚拟化框架(调试框架)、接口模块、反汇编引擎、键盘驱动模块、符号表模块、调试控制台模块等。以下内容为各个模块的设计与实现。

3.1虚拟化框架

虚拟化框架实现的主要功能就是创建一个虚拟CPU,并将在当前物理CPU上运行的操作系统转移到虚拟CPU上,而调试器则运行在当前CPU上。实际上就是CPU控制权的转移,或者说是CPU上下文的转移。

这样在虚拟CPU上运行的一切代码产生的CPU异常都将最先传递到物理CPU上,也就是我们的内核调试器上,调试器获取到执行权限后,可以阅读代码、下断点等操作,然后将执行权限交回。

这样的实现只依赖上述的两种切换:VMEntry和VMExit,也就是主机和虚拟机的切换,并不依赖中断向量表,因此传统的反调试手段诸如检测中断向量表等都无法探测到我们的内核调试器。

3.1.1检测CPU是否支持

我们的调试器在运行前要检测当前的CPU是否支持Intel-VT虚拟化技术,同时还要检测BIOS是否启用了该特性,否则调试器是根本无法使用的。

Linux的内核调试器是以内核模块的形式加载运行,因此它有最高CPU特权级(Ring0),可以执行特权指令。

调用CPUID这条指令可以检测当前CPU是否支持VMX指令集。读取MSR寄存器MSR_IA32_FEATURE_CONTROL的第4位可以检测BIOS是否启用VT特性。

3.1.2构造虚拟化环境

检测当前CPU支持硬件虚拟化之后就可以构造虚拟化环境了。首先设置虚拟机环境(VMCS),将当前CPU上下文完整地保存到VMCS中。然后启动虚拟机,这时候操作系统就被转到虚拟机中运行了。

一旦操作系统置于我们的虚拟CPU中运行,那么这个虚拟CPU上发生的任何异常,都会被我们运行在物理CPU上的VMM处理函数优先捕获到。我们主要关心单步异常和断点异常。在此基础上加以处理,就完成了内核调试器的基本框架。

图3-1时序图:构造虚拟化环境

3.2 界面

一个调试器还必须有一个友好的接口,我参考了Windows平台的著名调试器SoftICE,这是我最喜欢的一款调试器,他的开发公司已经被收购,并且早已停止了开发。作为一款划时代的优秀调试器,不再继续开发是一件非常遗憾的事情。

SoftICE使用字符接口,操作接口布局合理,简洁。从上到下依次为寄存器窗口、代码窗口、控制台窗口、控制台输入、状态区。如图3-2所示。

图3-2 SoftICE界面截图

我同样使用了这种设计,以纪念这款曾经无比辉煌的Windows内核调试器。如图3-3所示。

图3-3 我的VMXICE界面截图

3.2.1获取显存地址

在这个调试器中,接口部分我们只能通过直接写显存来实现,因为我们的调试器运行在物理CPU上,无法得到操作系统图形API的支持。

在加载调试器时调用FrameBuffer驱动程序接口可以获取到显存的物理位址。有了显存地址我们就可以随心所欲的画接口了。在接口方面我没有设计的很复杂,主要参考了Windows平台著名的SoftICE调试器,这是一个字符接口的内核调试器。为了实现这个接口我只需内置一个英文字库到调试器,就可以画出想要的字符接口了。

下面的代码实现了获取显存基地址以及当前显示模式参数:

#include<linux/fb.h>

 

VOIDVideoInit(void)

{

structfb_info *fbi = registered_fb[0];

 

VideoBuffer_va= (char *)fbi->screen_base;

VideoWidth= fbi->var.xres;

VideoHeight= fbi->var.yres;

VideoBitPerPixel= fbi->var.bits_per_pixel;

VideoPitch= fbi->fix.line_length;

 

printf("FrameBufferVirt: 0x%p\r\n",(PVOID)VideoBuffer_va);

printf("FrameBufferPhys: 0x%p\r\n",(PVOID)fbi->fix.smem_start);

printf("Resolution:%d x%d x%d\r\n",VideoWidth,VideoHeight,VideoBitPerPixel);

printf("Pitch:%d\r\n\r\n",VideoPitch);

 

StartX= (VideoWidth- GUI_Width* CHAR_WIDTH)/ 2;

StartY= (VideoHeight - GUI_Height * CHAR_HEIGHT) / 2;

 

pVideoBufferBak= (PUCHAR)kmalloc(VideoHeight * VideoWidth * 4,GFP_ATOMIC);

pVideoBufferPrint= (PUCHAR)kmalloc(VideoHeight * VideoWidth * 4,GFP_ATOMIC);

}

3.2.2输出字符与文本

有了显存基地址之后还不能直接输出字符,还需要一套字库,由于我们是英文字符接口,所以只使用256个字符的字库即可。

这里我使用了8*14*256的字库,使用如下方式定义:

charcFontData[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x81, 0xA5, 0x81,0x81, 0xBD, 0x99, 0x81, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E,0xFF, 0xDB, 0xFF, 0xFF, 0xC3, 0xE7, 0xFF, 0x7E, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x6C, 0xFE, 0xFE, 0xFE, 0xFE, 0x7C, 0x38, 0x10,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x7C, 0xFE, 0x7C,0x38, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x3C, 0x3C,0xE7, 0xE7, 0xE7, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00,0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x7E, 0x18, 0x18, 0x3C, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xC3,0xC3, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF……}

另外定义了一些函数用于支持字符输出,主要有:

ULONGPrintChar(ULONG x,ULONG y,ULONG ForeColor,ULONG BackColor,ULONGbTransparent,UCHAR CharAscii);

VOIDDrawCursor(ULONG x,ULONG y,BOOLEAN isInsertState,BOOLEAN ShowState);

VOIDPrintStr(ULONG x,ULONG y,ULONG ForeColor,ULONG BackColor,ULONGbTransparent,PUCHAR String,ULONG FillLine);

VOIDBackupScreen(void);

VOID RestoreScreen(void);

PrintChar可在屏幕坐标(x,y),以指定背景色和前景色画出字符。

DrawCursor可以在屏幕坐标(x,y)画出游标。

PrintStr是对PrintChar的进一步封装,用于输出一段字符串。

BackupScreenRestoreScreen用于呼出调试器前的屏幕备份和退出调试器恢复屏幕。



3.2.3控制台缓冲区

控制台相当于linuxshell。可以接收用户命令,显示命令结果等。控制台应具有历史记录和翻页功能,而一个屏幕显然不能容纳下所有的这些,必须使用控制台缓冲区记录这些。

我定义了一个高度为100行的控制台缓冲区。并且当超过100行内容时会自动将前面的内容删除,类似于数据结构中先进先出队列的概念,支持这种功能需要将缓冲区超长化,例如设计容纳100行数据,我们实际要定义超过100行数据的缓冲区,才能实现这个功能。也就是当有新内容进入时,先复制新内容,并检查当所有内容超过100行时,进行删减头部的操作。

与控制台有关的函数主要有如下定义:

VOIDConsolePrintChar(UCHAR c,ULONG ForeColor,ULONG BackColor);

VOIDConsolePrintCurrentPage(void);

VOIDConsolePrintPreviousPage(void);

VOIDConsolePrintNextPage(void);

VOIDConsolePrintStr(PUCHAR str,ULONG ForeColor,ULONG BackColor);

 

ConsolePrintChar输出一个字符到控制台缓冲区。

ConsolePrintStr是对上一个函数的封装,可以输出一个字符串。

ConsolePrintXXPage函数用于将控制台缓冲区内容输出到屏幕。

3.3键盘驱动

在我们的内核调试器中并不能收到操作系统处理好的键盘信号,因此我们必须自己完成这些操作。调试器在画出接口后需要不断的轮询键盘IO端口,以获取击键事件。

3.3.1键盘扫描码

获取到的数据是键盘扫描码,还需要一张键盘扫描表,用查表法确定按键。

扫描码定义在scancode.h头文件中,扫描码到键码的转换程序在scancode.c中定义,负责转换函数就是简单的switch语句。

 

3.3.2键盘模块接口

这个模块支持PS2接口的键盘,这种键盘只需要简单的IN/OUT指令即可响应键盘的输入。特别需要注意的是,当我们呼出调试器后,需要通过IOAPIC关闭键盘中断,否则我们对键盘的操作将影响到操作系统对键盘中断的处理。

最终导出了如下一个函数:

ULONGKeyboardReadKeystroke(PUCHAR pc, PBOOLEAN pisMouse);

该函数从PS2接口获取一个动作,这个接口可能被PS2鼠标影响,因此我们要过滤掉所有鼠标的响应,只需要读键盘的按键,按键码保存在pc指向的UCHAR变量中。

 

3.4 符号表

3.4.1符号表的作用

调试器反汇编出来的代码应该具有良好的可读性,在这方面我们需要符号表的帮助。

如果没有符号表,反汇编某段程序代码可能是这个样子

push 402010h

call [401067h]

假如有符号表的帮助,例如符号表中有以下记录:

401067 printf

那么上面的那段程序代码就可以反汇编为

push 402010h

call printf

这样我们一眼就可以看出这是调用了printf这个系统函数,大大提高了反汇编后的代码的可读性。

3.4.2符号表的实现

Linux的符号表位于/proc/kallsyms这个檔,这是一个虚拟的内存映像文件,里面包含了所有的内核、内核模块导出的、未导出函数的地址。

每次检索这个檔会有很大开销,最好的办法是将这张表读到内存中。我们知道既然这个文件是虚拟的内存映像文件,那么在Linux内核中肯定就存在已经组织好的符号表。通过研究kallsyms.h头文件发现了两个个有趣的函数:kallsyms_lookup_name和kallsyms_lookup。他们都是未导出的内核函数。通过调用kallsyms_lookup_name可以获取到内核中任意函数的地址。调用另外的kallsyms_lookup内核函数可以对一个地址解析,返回这个地址所在的函数名以及偏移量。使用这个函数可以将反汇编后的“call地址”转换成“call函数名+偏移量”这种形式,我们上面所说的可读性问题,完全可以通过这2个系统函数解决。但是这2个函数是未导出函数,定位他们的位址仍然需要读取/proc/kallsyms这个虚拟映像档。

3.4.3符号表模块接口

符号表的接口就是用过查询kallsyms檔得到的kallsyms_lookup_name和kallsyms_lookup函数,分别对应查函数地址,查函数名和偏移量。

posted on 2018-08-13 14:28  lucelu  阅读(1062)  评论(0编辑  收藏  举报

导航