现在的位置: 首页 > 综合 > 正文

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

2013年11月02日 ⁄ 综合 ⁄ 共 5526字 ⁄ 字号 评论关闭

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;

}

4.2调试控制台

调试控制台是整个调试器的工作中心,这个控制台将响应用户的一切输入,完成用户所需的调试功能。

4.2.1命令解析

4.2.1.1数据结构的设计

我将所有命令统一定义在一张表里,这个表是如下的数据结构:

typedefstruct{

CHAR*Cmd; //命令前缀

CHAR*Desc; //命令描述

CHAR*Usage; //命令用法描述

CHAR*Example; //命令用例

PCMD_HANDLERpHandler; //命令分发处理函数

}CMD_HELP,*PCMD_HELP;

这样我就可以把所有命令统一定义,如下所示:

CMD_HELPCmdHelp[] = {

{"BC","Clearbreakpoint","BC [*|id]",NULL,CmdClearBreakpoint},

{"BL","Listcurrent breakpoints","No param forBL",NULL,CmdListBreakpoint},

{"BPX","Breakpointon execute","BPX [addr] if [condition] do [cmd]","bpxntsetvaluekey if \"[[esp+8]+4]==\"imagepath\"\"do \"? byte [esp+4]\"\n",CmdSetSwBreakpoint},

{"CPU","Displaycpu registers information","No param forCPU",NULL,CmdDisplayCpuReg},

{"!DB","Displayphysical memory(byte)","!DB [address]","!db39000\n",CmdDisplayPhysicalMemoryByte},

{"!DW","Displayphysical memory(word)","!DW [address]","!dw39000\n",CmdDisplayPhysicalMemoryWord},

{"!DD","Displayphysical memory(dword)","!DD [address]","!dd39000\n",CmdDisplayPhysicalMemoryDword},

{"DB","Displaymemory(byte)","DB [address|symbolname]","db[esp+4]\n",CmdDisplayMemoryByte},

{"DW","Displaymemory(word)","DW [address|symbolname]","dw[esp+4]\n",CmdDisplayMemoryWord},

{"DD","Displaymemory(dword)","DD [address|symbolname]","dd[esp+4]\n",CmdDisplayMemoryDword},

};

这样定义有以下几个好处:

1、实现命令提示功能,例如输入一个B的时候,通过查这张表可以将所有B打头的指令列出来,让用户一目了然B打头的指令有哪些。

2、当用户输入了一个确定的指令例如BPX时,可以将BPX的语法自动提示给用户。

3、当用户输入HBPX请求BPX指令的用法时。可以将BPX指令的语法和用例打印出来。

4、结构体定义了命令分发处理函数,例如输入BPX就可以跳转到BPX处理函数CmdSetSwBreakpoint,由BPX函数负责解析这个函数。输入BC则跳转到BC处理函数CmdClearBreakpoint中。

4.2.1.2帮助信息与命令提示

有了上述定义的表,就可以很方便地获取指令帮助信息和命令提示功能。只需枚举表项即可。

4.2.1.3一般命令的解析

首先对输入语句的开头进行判断,进入相应的命令处理函数中,继续分割出命令的所有参数,然后对参数进行数值转换处理,例如可能需要将字符串转换成10进制或16进制数值类型。最后实现该命令。

4.2.1.4使用LL-1分析法计算表达式

有些时候我们需要输入表达式,例如查看esi+14h处指针指向的内存数据,使用命令:

dd[esi+eax*4+14]

[esi+eax*4+14]就是一个表达式,首先计算esi+eax*4+14,方括号表示对指标取值,得到该指针处的地址。

对表达式的分析我使用了LL-1分析法:算法从左到右分析表达式,设置符号栈和值栈,例如对表达式[1+4]>=45&&(([1+8]<9)||([1+c]&10))分析,步骤如下:

4-1LL-1分析法计算步骤

符号栈

值栈

[

1

[

1

+4]>=45&&(([1+8]<9)||([1+c]&10))

[ +

1

4]>=45&&(([1+8]<9)||([1+c]&10))

[ +

1 4

]>=45&&(([1+8]<9)||([1+c]&10))

[5]

>=45&&(([1+8]<9)||([1+c]&10))

>=

[5]

45&&(([1+8]<9)||([1+c]&10))

>=

[5] 45

&&(([1+8]<9)||([1+c]&10))

&&

1

(([1+8]<9)||([1+c]&10))

&& (

1

([1+8]<9)||([1+c]&10))

&& ( (

1

[1+8]<9)||([1+c]&10))

&& ( ( [

1

1+8]<9)||([1+c]&10))

&& ( ( [

1 1

+8]<9)||([1+c]&10))

&& ( ( [ +

1 1

8]<9)||([1+c]&10))

&& ( ( [ +

1 1 8

]<9)||([1+c]&10))

&& ( (

1 [9]

<9)||([1+c]&10))

&& ( ( <

1 [9]

9)||([1+c]&10))

&& ( ( <

1 [9] 9

)||([1+c]&10))

&& (

1 0

||([1+c]&10))

&& ( ||

1 0

([1+c]&10))

抱歉!评论已关闭.