基于IntelVt技术的Linux内核调试器 - 2

时间:2021-09-30 03:29:24
 

4 基于IntelVt技术的Linux内核调试器- 调试器设计与实现(2):调试核心

4.1反汇编引擎

如果说调试框架是一个调试器的灵魂,那么接口与反汇编引擎就是一个调试器的身体。我们在调试过程中是要阅读指令代码的,而反汇编引擎则提供将二进制元指令翻译成可阅读的汇编代码这个功能。

设计并实现一个初级的反汇编引擎很简单,但是计算机指令系统并不简单,将这个反汇编引擎实现到可以实际应用的级别需要不断地调试与修复Bugs,这个过程需要耗费大量精力。所以我选择了开源反汇编引擎。虽然网上有很多开源反汇编引擎,但是大部分都依赖于用户态的C库,导致不能很好地移植到内核模块上使用,而且具有优秀效率的反汇编引擎很少。这里我选择了libudis86,它是一个开源反汇编引擎,我只对其语法转换部分进行了一些修改,使其反汇编出来的指令格式符合比较好的阅读习惯。

4.1.1libudis86的基本使用方法

Libudis86库的使用很简单,只要一个提供各种信息的结构体,调用ud_disasmembe函数即可。例如下面定义了一个反汇编某个指令的函数:

ULONGOriDisasm(PUCHAR str,ULONG Eip)

{

UDISud_obj;

ULONGlen;

 

ud_init(&ud_obj); //初始化结构体

ud_set_mode(&ud_obj,32); //设置为32位元CPU模式

ud_set_syntax(&ud_obj,UD_SYN_INTEL); //结果使用Intel语法

ud_set_pc(&ud_obj,(int)Eip); //设置反汇编起点

ud_set_input_buffer(&ud_obj,(uint8_t*)Eip,32); //设置输入缓冲区

len=ud_disassemble(&ud_obj); //开始反汇编

strcpy(str,ud_insn_asm(&ud_obj)); //复制结果

returnlen;

}

该函数反汇编Eip指向的二进制元指令码,将结果复制到str指向的缓冲区中。

4.1.2使用libudis86反汇编某段程序

上面的函数OriDisasm只能反汇编一条指令,通常我们的调试器需要反汇编一段程序。OriDisasm在反汇编一条指令结束后会返回该指令长度(位元组),递增指令指针循环继续这项操作即可反汇编一段代码。

4.1.3向上反汇编:递减命中率算法

调试器的反汇编窗口应该像控制台窗口一样具有翻页功能,这样便于我们查阅反汇编代码,但是单纯地使用libudis86只能从上往下进行反汇编,如果我们需要向上翻页,则需要从下往上翻页。看似简单,实际上有一个很重要的难题。

我们知道x86汇编指令是不定长的,一条指令可能最小只占用1个字节,最长可能占用15个字节,而我们并不知道某一条指令它的上一条指令占用多少个位元组,因此不能直接获取某一条指令的上一条指令是什么。例如下面的指令:

004017F0 75 64 jnz short 00401856

004017F2 8B45 10 mov eax, dword ptr [ebp+10]

004017F5 C1E8 10 shr eax, 10

004017F8 83F8 07 cmp eax, 7

第一列是指令位址,第二列是指令二进制元编码,第三列是对应的汇编代码。假设当前我们反汇编的位置是4017F8那么我们对这个内存位置的83F8 07进行反汇编可以得到cmpeax,7这条指令,但是我们并不知道这条指令的上一条指令从什么位址开始,如果我们贸然猜测上一条指令的位址显然我们会得到一个错误的反汇编结果。例如对4017F6反汇编得到:

004017F6 E8 1083F807 call 08389B0B

结果是一个占用5个位元组的call指令,而且这个指令覆盖了位于4017F8本来正确的结果。

我设计了一个算法用以解决这个问题,算法的思想是递减地址指针,从这个指标开始向下循环反汇编,当长度刚刚好到达我们预期的位置时,记录上一条指令的位置。然后将这些结果进行统计。

例如当前指令是

004017F8 83F8 07 cmp eax, 7

我们想要知道他的上一条指令是什么,就递减地址,得到4017F7,反汇编得到:

004017F7 1083 F807754B adc byte ptr [ebx+4B7507F8], al

结果覆盖了4017F8,此结果作废。继续向上递减。

004017F6 E8 1083F807 call 08389B0B

结果覆盖了4017F8,此结果作废。继续向上递减。

004017F5 C1E8 10 shr eax, 10

004017F8 83F8 07 cmp eax, 7

结果可能正确,记录下上一条指令位址4017F5,命中率为1次。

继续向上递减。

004017F4 10C1 adc cl, al

004017F6 E8 1083F807 call 08389B0B

覆盖了4017F8,此结果作废。继续向上递减。

004017F3 45 inc ebp

004017F4 10C1 adc cl, al

004017F6 E8 1083F807 call 08389B0B

覆盖了4017F8,此结果作废。继续向上递减。

004017F2 8B45 10 mov eax, dword ptr [ebp+10]

004017F5 C1E8 10 shr eax, 10

004017F8 83F8 07 cmp eax, 7

刚好得到4017F8,记录下4017F8的上一条指令是4017F5,因为刚刚记录命中了一次,因此当前的统计结果是:4017F8上一条指令4017F5,命中率2次。

就这样继续不断递减地址,从递减后的地址向下反汇编,当结果刚好可以到达4017F8时记录下上一条指令位址。假设我们向上递减200字节,记录结果可能如下。

4017F5 40次

XXXXXX 1次

那么根据统计结果上看,上一条指令有很大可能是从4017F5开始,那么我就可以认为上一条指令是4017F5。当然这个结论可能不正确,因为这毕竟是统计学结果。如果想要得到更准确的结果,我们可以采样更多的数据,例如向上递减1000字节。以保证结果的可靠性。

实现好的代码片段如下:

typedefstruct {

ULONGpInstrAddr; //上一条指令的位址

ULONGHitCount; //命中次数

}PREV_INSTR_HITTEST,*PPREV_INSTR_HITTEST;

 

ULONGGetPrevIp(ULONG Eip)

{

PREV_INSTR_HITTESTHitTest[16];

ULONGCurrentAddr = Eip - 1;

ULONGPrevAddr;

ULONGDisasmLimit = 0x100;

ULONGlen;

ULONGi;

ULONGPrevAddr_MaxHit = 0;

ULONGMaxHit = 0;

 

if(!Eip)

returnFALSE;

 

memset(&HitTest,0,sizeof(HitTest));

while(DisasmLimit)

{

PrevAddr= CurrentAddr;

if(!IsAddressExist(PrevAddr)) //保证地址空间可读

break;

 

while(1)

{

len= FastDisasm(PrevAddr);

if(len!= -1 && len) 

{

if(len+ PrevAddr >= Eip)

{

AddHit(&HitTest[0],16,PrevAddr);

break;

}

elseif(len + PrevAddr > Eip)

{

break;

}

}

else

{

break;

}

PrevAddr+= len;

}

 

DisasmLimit--;

CurrentAddr--;

}

 

for(i= 0; i < 16; i++)

{

if(HitTest[i].HitCount> MaxHit)

{

MaxHit= HitTest[i].HitCount;

PrevAddr_MaxHit= HitTest[i].pInstrAddr;

}

}

returnPrevAddr_MaxHit;

}