IDA实例教程

来源:互联网 发布:化工工艺流程设计软件 编辑:程序博客网 时间:2024/04/29 18:36

IDA实例教程

 

1 软件环境

 

       静态分析有很多好处,例如加壳的程序(尽管对于高手来说这并不会耗费太多时间),我们不需要寻找OEP,也不需要解除自校验,只要修复IAT,DUMP下来就可以动手分析了。假如你需要修改程序,可以使用内存补丁技术。动态与静态,调试器与反汇编器结合可以简化分析任务,帮助我们理解代码。因此掌握一种反汇编器是非常必要的。IDA可以说是这方面的首选工具,它为我们提供了丰富的功能,以帮助我们进行逆向分析。这从IDA复杂的工作界面便可以知道。

 

种类繁多的工具栏

 

       在分辨率不高的情况,这些工具栏与反汇编窗口挤在小屏幕里,看起来不爽。我一般把它关闭(查看=>工具栏=>主工具栏)以获得更好的视觉效果。当我们需要这些功能的时候,直接使用快捷键就可以了。下面是常用快捷键的清单:

 

快捷键

功能

注释

C

转换为代码

一般在IDA无法识别代码时使用这两个功能整理代码

D

转换为数据

A

转换为字符

N

为标签重命名

方便记忆,避免重复分析。

添加注释

R

把立即值转换为字符

便于分析立即值

H

把立即值转换为10进制

Q

把立即值转换为16进制

B

把立即值转换为2进制

G

跳转到指定地址

 

X

交叉参考

便于查找API或变量的引用

SHIFT+/

计算器

 

ALT+ENTER

新建窗口并跳转到选中地址

这四个功能都是方便在不同函数之间分析(尤其是多层次的调用)。具体使用看个人喜好

ALT+F3

关闭当前分析窗口

ESC

返回前一个保存位置

CTRL+ENTER

返回后一个保存位置

 

       在工具栏下面的便是工作窗口。主要的窗口分页有“IDA View-A”、“Name”、“Strings”、“Exports”和“Imports”。对于后面3项相信大家都不会陌生了,它们分别是字符参考,输出函数参考和输入函数参考。Name是命名窗口,在那里可以看到我们命名的函数或者变量。这四个窗口都支持索引功能,可以通过双击来快速切换到分析窗口中的相关内容,使用起来十分方便。

 

简单输入几个字符即可定位目标

 

         IDA View-A是分析窗口,支持两种显示模式,除了常见的反汇编模式之后,还提供图形视图以及其他有趣的功能。

 

IDA的反汇编窗口

 

一般我们在分析的时候,并不关心程序的机械码,所以IDA为我们自动隐藏了这些信息。如果你有需要,可以通过以下步骤来设置:

 

选项=>常规=>反汇编=>显示反汇编行部分=>机械码字节数=>修改为你允许显示的大小

 

       现在让我们以论坛脱壳版块置顶帖的那个经典为例,看看图形视图的表现。首先我们到以下连接下载:http://bbs.pediy.com/upload/bbs/unpackfaq/notepad.upx.rar

 

你能通过图形视图及其缩略图快速找到壳的出口吗?

 

       如图所示,标签40EA0E便是壳的出口代码的地址。在OD中直接跳到该地址,下断点,然后运行到该处,再单步便能看到OEP了。假如希望通过跳转法找OEP,相信图形视图比你在OD一个一个跳转跟随,要快得多。

       再来看看这个壳的另类脱法。直接运行该程序, DUMP下来,再使用IMPORTREC的IAT AutoSearch功能修复输入表。用IDA打开修复了输入表的DUMP文件。在IMPORT窗口随便选一个API,随便通过交叉参考跳转到一个函数的代码。

 

此处为文件输入表的位置

 

       我选了RegQueryValueExA,通过交叉参考,来到Sub_402488处的函数代码。

 

       用鼠标拖动缩略图中的虚线框到上方,便能看到该CALL的头部了。然后按下图指示操作:

 

在函数标记上点击鼠标右键

 

       处于最上层的函数,便是OEP了,使用PE工具修改文件入口为10CC。现在函数可以正常工作了。这个方法的原理是通常我们写程序都有如下流程:

 

Main proc

                     //代码

                     CALL  FUN1

                     //代码

                     CALL  FUN2

                     //代码

END proc

 

       所以处于函数调用最上层的便是MAIN函数了。当然这个方法局限性很大,这里只是对该功能的一种介绍。我们留意到图表功能有两个选项,在上面的例子中,我们使用的是“交叉参考到”。我想细心的朋友大概能通过“交叉参考来自”左边的小图标猜出它的用途了。该功能可以显示目标函数调用了什么函数,当然也包括API。这样除了观察函数的输入参数来判断是否关键CALL之外,又多了一个参考途径。

 

 

2        强大的IDC

 

有时我们需要分析一些非文件格式的代码,例如ShellCode,远线程注入和病毒。这些代码的特点便是动态获取API,这给静态分析带来困难。尽管IDA支持分析2进制文件,但是缺少IAT的情况下,分析起来跟不方便。频繁的切换调试器查看并不是一个好方法。IDC是IDA的脚本语言,它功能强大,为我们提供了另一条与调试器交互的途径。

 

如何使调试器获得IDA分析得出的符号?

IDA提供多种文件格式输出,调试器可以通过解释这些文件获得一些符号。你可以通过文件菜单中的“创建文件”获得更多的信息。

以OD为例,它的GODUP插件支持解释MAP文件(还能加载IDA的SIG)。在IDA中使用如下步骤:

菜单:      文件=>创建文件=>创建MAP文件

即可创建MAP文件,然后切换到OD,使用如下步骤便能获得符号了:

菜单:      插件=>GODUP Plugin=>Map Loader=>Load labels

 

仍然以那个经典的UPX加壳的NOTEPAD为例子,这次我们用OD打开,在到达OEP之后DUMP下来,不修复输入表,直接用IDA载入后看到下图:

 

丰富的文件载入选项

 

需要注意的是Make imports segment是PE文件特有的选项,该选项会隐藏输入表区域的所有数据,同时你获得的好处便是能在图表功能中看到API的调用。假如你希望查看在输入表的范围内的代码或者数据,你需要使用从菜单中选择“编辑”=>“区段”以删除遮挡数据的部分区段。

为了更真实的模拟从内存中截取代码的情况,在这里选择Binary file,载入偏移量选400000(根据实际代码在内存中的基址来选择),然后IDA就开始尝试分析可能存在于该文件中的代码了。对照OD中的OEP地址,在IDA中可以看到以下代码:

 

seg000:004010CC                 push    ebp

seg000:004010CD                 mov     ebp, esp

seg000:004010CF                 sub     esp, 44h

seg000:004010D2                 push    esi

seg000:004010D3                 call    ds:dword_4063E4

seg000:004010D9                 mov     esi, eax

seg000:004010DB                 mov     al, [eax]

seg000:004010DD                 cmp     al, 22h

seg000:004010DF                 jnz     short loc_4010FC

OEP处的部分代码

 

OD中对应的显示:

 

004010D3    FF15 E4634000   call    dword ptr [4063E4]   ; kernel32.GetCommandLineA

 

使用以下ollyscript(附件中的ollyGetSym.txt)提取IAT的符号:

 

var ea

var Ecount                                                //0分隔号的记数器

var oFile

 

ask "请输入IAT起始地址"

cmp $RESULT, 0

je ECancel

mov ea, $RESULT

ask "输出文件?"

cmp $RESULT, 0

je ECancel

mov oFile, $RESULT

 

TryGetSym:

GN [ea]                                                    //获取该地址的符号

cmp $RESULT,00000000             //OLLYSCRIPT是区分00000000和0的

je ETest

WRTA oFile,$RESULT_2

 

mov Ecount,0

add ea,4

jmp TryGetSym

 

ECancel:

msg "无效输入"

ret

 

ETest:

cmp Ecount,1                                    //不同模块的地址以0分隔

je Send                                              //若存在两个DWORD的0则认为是末尾

add Ecount,1

add ea,4

jmp TryGetSym

 

SEnd:

Ret

 

使用下面IDC脚本获取符号并对相应地址重命名:

 

#include <idc.idc>

 

static main() {

  auto Sbuffer,ea,zcount,filehandle,fileName,CustEa;

fileName = AskFile (0,"*.*","打开IAT符号文件");

CustEa = AskAddr(0,"目标IAT地址");

filehandle = fopen(fileName,"r");

for (ea = CustEa; zcount < 2; ea = ea + 4){

       if (Dword(ea) !=0){

       Sbuffer = readstr(filehandle);

              if(strlen(Sbuffer) < 2){         //ollyscript的输出文件存在无效字符                            Sbuffer = readstr(filehandle); //如果字符无效则再取一次字符

                                          }

       MakeNameEx (ea,Sbuffer,SN_AUTO );    //为对应DWORD改名

       zcount = 0;

                            }

       else{

       zcount = zcount + 1;

              }

                                          }

fclose(filehandle) ;

}

GetSym.idc

 

正如ollyscript接近于ASM,IDC的函数及其语法也近似于C语言(详见IDA的帮助),在编写几个脚本之后,便能轻松掌握它的用法。

 

seg000:004010CC                 push    ebp

seg000:004010CD                 mov     ebp, esp

seg000:004010CF                 sub     esp, 44h

seg000:004010D2                 push    esi

seg000:004010D3                 call    ds:GetCommandLineA_

seg000:004010D9                 mov     esi, eax

seg000:004010DB                 mov     al, [eax]

seg000:004010DD                 cmp     al, 22h

seg000:004010DF                 jnz     short loc_4010FC

现在可以正常显示函数调用的API了

 

下面来看看另外一个例子中IDC的表现。附件中的Exvirus.v是一个木马程序。当然这里并不是要分析这个木马,更不会运行它,在静态分析的环境下,很安全。

 

几乎都是乱码的窗口

 

加密了的字符,总要在使用之前解密。也就是说可以通过加密字符的交叉引用定位解密代码。

 

lea     edx, [ebp+var_4]

mov     eax, offset s_XsXQqSxUsSq ; "抿遽翦燥镬桢祓巢宇狃箬雉"

call    sub_404BEE

通过交叉引用定位的函数

 

由字符参考中的“SOFTWARE\Borland\Delphi\RTL”可以判断该木马是用Delphi编写的(也可从函数的参数传递约定判断)。在详细分析之前,先在菜单中进行如下步骤的操作:

 

文件=>加载文件=>加载FLIRT签名文件=>Delphi7 RTL/VCL/CLX

 

现在IDA将会根据Delphi的函数特征识别出一些库函数,这样可以减少很多工作量。


函数较长,这里只列出关键代码。判断这部分为关键代码主要是因为整个函数就只有该处是循环。解密是对一定长度的数据进行运算,因此会有一个循环对字符中的数据逐一解密。然后从输入参数与寄存器或者堆栈的关联便可以理解函数的关键部分是如何工作的。由于IDA已经为我们识别出Delphi的库函数,所以这里很容易便知道解密的方便是对目标字符的每个字节都加上80h。下面来看看我如何使用IDC来完成解密字符的工作。


CODE:00404C2C                 mov     [ebp+var_8],1  //已处理字符记数器

CODE:00404C2C

CODE:00404C33

CODE:00404C33 loc_404C33:             ; CODE XREF: sub_404BEE+6Aj

CODE:00404C33                 mov     eax, [ebp+var_4]

CODE:00404C36                 mov     edx, [ebp+var_8]

CODE:00404C39                 mov     bl, [eax+edx-1]      //单字节取字符解密

CODE:00404C3D                 add     bl, 80h

CODE:00404C40                 lea     eax, [ebp+var_C]

CODE:00404C43                 mov     edx, ebx

CODE:00404C45     call    @System@@LStrFromChar$qqrr17System@AnsiStringc

CODE:00404C45

CODE:00404C4A                 mov     edx, [ebp+var_C]

CODE:00404C4D                 mov     eax, edi

CODE:00404C4F                 call    @System@@LStrCat$qqrv

CODE:00404C4F

CODE:00404C54                 inc     [ebp+var_8]

CODE:00404C57                 dec     esi     //字符长度=0跳出循环,解密完毕

CODE:00404C58                 jnz     short loc_404C33

 


 

 

 

 















Decode.idc

 

       既然可以通过加密字符定位目标函数,那么也可以通过加密函数定位加密字符。通过使用解密函数的交叉引用,往上搜索,解密第一条mov eax,mem32中的字符。当然这里个脚本写得有点简陋,并不能完全解决程序中的加密字符。这个就任务就留给读者来挑战吧。这里要注意的是我在编写IDC的过程中遇到很多BUG,这是因为IDA区分大小写(调试了很久才知道)。此外要转换数据类型得先把原来的分析结果取消才可以。最后要看到下图的窗口,在运行脚本后,你需要重新打开字符参考窗口(不会自动刷新)。

 

解密后的字符参考窗口

 


#include "idc.idc"

 

static main() {

  auto ea,x,y,z,zbyte,SRange,TStrLen,DeCodeBuffer,DeCodeCounter,NotTarget;

 

x = 0x404bee;

 

  for ( y=RfirstB(x); y != BADADDR; y=RnextB(x,y) ){  //通过交叉参考取得函数调用地址

       for (SRange = 4; SRange < 0x50; SRange++){

              z = y - SRange;

              zbyte = Byte(z);

              if (zbyte == 0xb8){   //mov eax,mem32的机械码是b8

                     zbyte = Dword(z + 1);

                     ea = Dword(zbyte);

                     if (ea != 0xFFFFFFFF){   //判断mem32是否有效,防止识别错指令

                            if (Byte(zbyte - 1) == 1){  //在字符指针前一个字节写入处理标记

                                   break;             //避免重复处理

                                                  }

                            PatchByte (zbyte - 1,1);

                            TStrLen = 0;

                            while (TStrLen < 0x30){   //解密的循环

                            DeCodeCounter = zbyte + TStrLen;

                            DeCodeBuffer = Byte(DeCodeCounter) + 0x80;

                            if (DeCodeBuffer == 0x80) break;

                            PatchByte (DeCodeCounter,DeCodeBuffer);

                            TStrLen++;

                                                        }

                            MakeUnknown (zbyte,TStrLen,0);   //取消IDA原来的分析结果

                            MakeStr (zbyte, DeCodeCounter);   //把该位置标记为字符

                            break;

                                            }

                                  }

                                               }

                                                       }

              }


 

 

 

 

 

 

 

 

 

 


























3        静态脱壳

 

上一节我们用IDC完成了字符解密的工作,既然脱壳的过程实际就是对源程序的解密,现在让我们来尝试在不运行壳的情况下把壳解决掉。首先到下面连接下载一个壳:

 

http://www.pediy.com/tools/PACK/Protectors/MSLRH/MSLRHv0.31a.rar

 

主页对这个壳的介绍是可以作为Unpackme练练手,现在就以该壳的主程序作为例子讲解如何静态脱壳。首先用IDA加载该壳的主程序。

 

seg005:004560FA loc_4560FA:        ; CODE XREF: start:loc_4560F4j

seg005:004560FA                 call    sub_456109

seg005:004560FA

seg005:004560FA start           endp             //入口函数的结尾   

seg005:004560FA

seg005:004560FF

seg005:004560FF

seg005:004560FF

seg005:004560FF sub_4560FF  proc near   ; CODE XREF: seg005:00456104p

seg005:004560FF                             ; sub_456109p   //红色

seg005:004560FF                 call    sub_456DEF

seg005:004560FF

seg005:004560FF sub_4560FF      endp

seg005:004560FF

seg005:00456104                 call    sub_4560FF

seg005:00456104

seg005:00456109

seg005:00456109

seg005:00456109

seg005:00456109 sub_456109     proc near  ; CODE XREF: start:loc_4560FAp

seg005:00456109     call    near ptr sub_4560FF+1  //+1表示反汇编出现混乱

 

正常的交叉参考标记是绿色,当显示为红色时则证明与其他部分的反汇编代码产生冲突。另外在jcc,jmp和call后面出现“+X”的符号(X为任意数字),一般也为反汇编出现混乱。在正式分析之前,我们必须找到花指令的规律,编写脚本,除去它的影响。现在我们从最初产生影响的地方开始。点击地址4560FF,按D

 

seg005:004560FF byte_4560FF      db 0E8h;     CODE XREF: seg005:00456p

seg005:00456100 unk_456100      db 0EBh ; ?  ; CODE XREF: sub_456109p

seg005:00456101                 db  0Ch

seg005:00456102                 db    0

seg005:00456103                 db    0

seg005:00456104                 call    near ptr byte_4560FF

注意00456104处也是花指令之一,它的作用就是让IDA误以为004560FF处为有效指令。因此也在该位置上按D,将其转换为数据。而在00456100处按C转换为代码。

 

seg005:004560FA                 call    sub_456109

seg005:004560FA

seg005:004560FA start           endp

seg005:004560FA

seg005:004560FA ; ---------------------------------------------------------------------------

seg005:004560FF                 db 0E8h

seg005:00456100 ; ---------------------------------------------------------------------------

seg005:00456100

seg005:00456100 loc_456100:               ; CODE XREF: sub_456109p

seg005:00456100                 jmp     short loc_45610E

seg005:00456100

seg005:00456100 ; ---------------------------------------------------------------------------

seg005:00456102                 db    0

seg005:00456103                 db    0

seg005:00456104                 db 0E8h

seg005:00456105                 db 0F6h ; ?

seg005:00456106                 db 0FFh

seg005:00456107                 db 0FFh

seg005:00456108                 db 0FFh

seg005:00456109

seg005:00456109

seg005:00456109 sub_456109   proc near  ; CODE XREF: start:loc_4560FAp

seg005:00456109                 call    loc_456100

seg005:00456109

seg005:0045610E

seg005:0045610E loc_45610E:    ; CODE XREF: seg005:loc_456100j

seg005:0045610E                 add     esp, 8

 

现在我们手动修正了一处被花掉的代码。我们知道OPCODE的E8和EB后面的实际是一个相对地址偏移,而不是地址编码(反汇编翻译成地址是便于分析)。因此可能你已经想到通过搜索内存中的相应指令序列,然后告诉IDA什么是代码,什么则不是。读者可以先试试自己找出壳中花指令的规律,然后对比一下结果。


经过手动整理之后,发现壳使用了下面4种花指令代码:

 

call    label1

db 0E8h

label2:

jmp    label3

db 0

db 0

db 0E8h

db 0F6h ;

db 0FFh

db 0FFh

db 0FFh

label1:

call    label2

label3:

add     esp, 8

花指令1

 

     Jz     label1

     Jnz    label1

     db 0EBh

db  2

label1:

     jmp   label2

     db 81h

label2:

花指令2

 

push    eax

call    label1

db 29h

db 5Ah

label1:

pop     eax

imul    eax, 3

Call    label2

db 29h

db 5Ah

label2:

add     esp, 4

pop     eax

花指令3

 

Jmp label1

db 68h

Label1:

Jmp label2

db 0CDh, 20h

Label2:

Jmp label3

db 0E8h

Label3:

花指令4

 

在知道花指令结构之后,容易写出下面脚本用NOP(0x90h)来代替干扰的反汇编器的数据:

 

static PatchJunkCode() {

  auto x,FBin,ProcRange;

 

FBin = "E8 0A 00 00 00 E8 EB 0C 00 00 E8 F6 FF FF FF";

// 花指令1的特征码

for (x = FindBinary(MinEA(),0x03,FBin);x != BADADDR;x = FindBinary(x,0x03,FBin)){

 

x = x +5;

PatchByte (x,0x90);

x = x + 3 ;

PatchByte (x,0x90);

x++;

PatchWord (x,0x9090);

x =x +2 ;

PatchDword (x,0x90909090);

 

                                                                      }

 

FBin = "74 04 75 02 EB 02 EB 01 81";

// 花指令2的特征码

for (x = FindBinary(MinEA(),0x03,FBin);x != BADADDR;x = FindBinary(x,0x03,FBin)){

 

x = x + 4;

PatchWord (x,0x9090);

x = x + 4;

PatchByte (x,0x90);

 

                                                                      }

FBin = "50 E8 02 00 00 00 29 5A 58 6B C0 03 E8 02 00 00 00 29 5A 83 C4 04";

// 花指令3的特征码

for (x = FindBinary(MinEA(),0x03,FBin);x != BADADDR;x = FindBinary(x,0x03,FBin)){

 

x = x + 6;

PatchWord (x,0x9090);

x = x + 11;

PatchWord (x,0x9090);

                                                                      }

 

FBin = "EB 01 68 EB 02 CD 20 EB 01 E8";

//花指令4的特征码

for (x = FindBinary(MinEA(),0x03,FBin);x != BADADDR;x = FindBinary(x,0x03,FBin)){

 

x = x+2;

PatchByte (x,0x90);

x = x+3;

PatchWord (x,0x9090);

x = x+4;

PatchByte (x,0x90);

                                                                      }

              }

 

通过观察可知花指令中并不包含任何有意义的数据,在花指令的前后,堆栈是平衡的,各寄存器的数值也是不变的。IDC提供了隐藏区域的命令,现在来看看以下脚本:

 

static HideJunkCode() {

  auto x,y,FBin;

 

FBin = "E8 0A 00 00 00 E8 EB 0C 00 00 E8 F6 FF FF FF";

 

for (x = FindBinary(MinEA(),0x03,FBin);x != BADADDR;x = FindBinary(x,0x03,FBin)){

MakeUnknown (x,0x17,1);

y = x + 0x17;

HideArea (x,y,atoa(x),atoa(x),atoa(y),-1);

 

                                                                      }

 

FBin = "74 04 75 02 EB 02 EB 01 81";

 

for (x = FindBinary(MinEA(),0x03,FBin);x != BADADDR;x = FindBinary(x,0x03,FBin)){

MakeUnknown (x,0x09,1);

y = x + 0x09;

HideArea (x,y,atoa(x),atoa(x),atoa(y),-1);

 

                                                                      }

 

FBin = "50 E8 02 00 00 00 29 5A 58 6B C0 03 E8 02 00 00 00 29 5A 83 C4 04";

 

for (x = FindBinary(MinEA(),0x03,FBin);x != BADADDR;x = FindBinary(x,0x03,FBin)){

MakeUnknown (x,0x17,1);

y = x + 0x17;

HideArea (x,y,atoa(x),atoa(x),atoa(y),-1);

 

                                                                      }

 

FBin = "EB 01 68 EB 02 CD 20 EB 01 E8";

 

for (x = FindBinary(MinEA(),0x03,FBin);x != BADADDR;x = FindBinary(x,0x03,FBin)){

MakeUnknown (x,0x0a,1);

y = x + 0x0a;

HideArea (x,y,atoa(x),atoa(x),atoa(y),-1);

                                                                      }

}

 

由于花指令的关系,会使IDA错误识别指令,可能隐藏区域的边界刚好在一条指令的机械码中间,这样隐藏的操作便会失败。因此在隐藏指令执行之前,先使用MakeUnknown将目标代码设置为未识别的状态。在完成隐藏和替换之后,再使用分析引擎分析代码。

 

static main() {

  auto x,FBin,ProcRange;

 

HideJunkCode();

 

PatchJunkCode();

 

AnalyzeArea (MinEA(),MaxEA());

              }

CleanJunkCode.idc

 

在运行脚本之后,现在让我们看看修复的成果。

 

seg005:0045639F                 rdtsc

seg005:004563A1                 push    eax

seg005:004563A2                 rdtsc

seg005:004563A4 ; seg005:004563A4     //被隐藏的区域

seg005:004563BB                 sub     eax, [esp-8+arg_4]

seg005:004563BE ; seg005:004563BE

seg005:004563C7 ; ---------------------------------------------------------------------------

seg005:004563C7

seg005:004563C7 loc_4563C7:    ; CODE XREF: sub_4563B3:loc_4563C4j

seg005:004563C7                 add     esp, 4

seg005:004563CA ; seg005:004563CA

seg005:004563E1                 cmp     eax, 0FFFh

seg005:004563E6 ; seg005:004563E6

seg005:004563F0

seg005:004563F0 loc_4563F0:    ; CODE XREF: sub_4563D9:loc_4563EDj

seg005:004563F0                 jbe     short loc_45640D

seg005:004563F0

seg005:004563F2 ; seg005:004563F2

seg005:004563FC

seg005:004563FC loc_4563FC:   ; CODE XREF: sub_4563D9:loc_4563F9j

seg005:004563FC                 int     3   ; Trap to Debugger

seg005:004563FD                 mov     ax, 0FEh

seg005:00456401 ; seg005:00456401

seg005:0045640A

seg005:0045640A loc_45640A:  ; CODE XREF: sub_4563D9:loc_456407j

seg005:0045640A          out     64h, ax    ; AT Keyboard controller 8042.

seg005:0045640A                   ; Resend the last transmission

seg005:0045640A

seg005:0045640D ; seg005:0045640D

修复之后的代码

 

除了“sub eax, [esp-8+arg_4]”(实际上是sub eax,[esp])看起来有点怪之后,一切正常。作为一个壳,在解决了花指令之后,剩下的问题便只有反调试代码和解密(解压缩)代码了。例如上面列出的代码是通过时间校验检查调试器,一旦检查到,便使用特权级指令,让程序发生异常,无法继续运行下去。当然,我们在静态的环境下,反调试技巧对于我们来说,毫无意义。尽管如此,我们仍然需要知道程序会在什么时候运行到什么地方,最常见的利用系统的机制莫过于SEH了,现在来看看下面代码:

 

seg005:00456A9B                 call    $+5

seg005:00456AA0                 add     dword ptr [esp+0], 136Fh

seg005:00456AA7                 push    large dword ptr fs:0

seg005:00456AAE                 mov     large fs:0, esp

设置SEH的代码

 

“call $+5”指令后堆栈里的内容便是它的下一条指令在内存中的地址。这是病毒常用的重定位技巧。shift+/输入0x00456AA0+0x136F便能计算出异常处理函数的地址(457E0F)了。

 

seg005:0045745C                 xor     eax, eax

seg005:0045745E                 movzx   eax, byte ptr [eax]

产生异常的代码

 

现在我们应该跳到457E0F继续分析。我想你已经了解如何在静态环境下跟踪程序的流程,现在就让我们跟着程序的流程把解密相关的代码找出来。

 

seg005:00459191                 push    ecx

seg005:00459192                 xor     ecx, ecx

seg005:00459194                 call    $+5

seg005:00459199                 pop     edi

seg005:0045919A                 add     edi, 9C4h

seg005:004591A0                 pop     edx

seg005:004591A1                 add     edx, 15h

seg005:004591A4 loc_4591A4:           ; CODE XREF: sub_459149+6Bj

seg005:004591A4                 movzx   eax, byte ptr [ecx+edi]

seg005:004591A8                 xor     eax, edx

seg005:004591AA                 mov     [ecx+edi], al

seg005:004591AD                 inc     ecx

seg005:004591AE                 cmp     ecx, 93h

seg005:004591B4                 jb      short loc_4591A4

解密代码

 

       容易看出这就是解密代码,在循环之中,且有修改内存的指令。至于解密的KEY,其实就是00459191处ECX的值+15h。我希望你还记得到达这里之前曾经看过下面代码:

 

seg005:004587B6                 mov     eax, [esp+0Ch]

seg005:004587BA                 xor     ecx, ecx

seg005:004587BC                 xor     ecx, [eax+4]

seg005:004587BF                 xor     ecx, [eax+8]

seg005:004587C2                 xor     ecx, [eax+0Ch]

seg005:004587C5                 xor     ecx, [eax+10h]

 

       这一段是检查硬件断点的代码,假如没有设置硬件断点,那么ECX的结果应该是0。假如你不能理解为什么,我建议你看看SEH以及关于反硬件断点的一些文章。

       在知道解密代码的所有关键要素之后,就可以开始动手写脚本了。

 

 

 

 

#include "idc.idc"

 

static main() {

  auto StartAddr,cKey,Cbuffer,Counter;

 

StartAddr = 0x00459199 + 0x9c4;

cKey = 0x15;

 

for (Counter = 0 ; Counter < 0x93; Counter ++){

Cbuffer = Byte(StartAddr) ^cKey;         // movzx   eax, byte ptr [ecx+edi]

                                    // xor     eax, edx

PatchByte(StartAddr,Cbuffer);               // mov     [ecx+edi], al

StartAddr++;

                                          }

              }

Patch1.idc

 

       在00459BF7和0045B1FC处可以看到类似的加密代码,就不把脚本给出来了,我把它放在附件中,分别为PATCH2.idc和PATCH3.idc。在第三次解密之后,终于看到不同的解密代码了,代码比较多,我把隐藏区域的部分删掉:

 

seg005:00461F8D                 call    $+5

seg005:00461F92                 pop     ecx

seg005:00461F9D                 sub     ecx, 5

seg005:00461FAA                 xor     ebx, ebx

seg005:00461FB6                 mov     eax, 0BE9Ch

seg005:00461FC5                 mov     edi, ecx

seg005:00461FD1                 sub     edi, eax

seg005:00461FDD                movzx   eax, byte ptr [edi]

seg005:00461FEA                 add     ebx, eax

seg005:00461FF6                 inc     edij

seg005:00462001                 cmp     edi, ecx

seg005:0046200D                 jb      short loc_461FDD

自校验代码

 

自校验代码的两个特征,一是读取代码,二是循环,对于那种单纯与校验结果比较控制流程的程序,我们是不需要理会自校验的。但是在这个例子里,紧跟后面的代码便是解密代码,并且自校验值作为解密KEY,我们就得计算出它的校验值。

 

seg005:0046200F                 mov     edi, offset unk_447000

seg005:00462014                 mov     ecx, 0BC00h

seg005:00462019 ; seg005:00462019

seg005:00462023                 movzx   eax, byte ptr [edi]

seg005:00462030                 add     bl, bh

seg005:00462032                 xor     bl, bh

seg005:00462034                 xor     al, bl

seg005:00462040                 mov     [edi], al

seg005:0046204C                 inc     edi

seg005:00462057                 dec     ecx

seg005:00462062                 jnz     short loc_462019

自校验后的解密代码

 

相信有了前面的经验,要编写出以下脚本并不难。要注意的是由于之前修复花指令曾经修改过文件,因此在编写好脚本之后,必须重新加载程序,然后按顺序把解密脚本运行一次,确保解出正确的代码。此外还需注意下面代码:

 

seg005:00462064                 call    $+5

seg005:00462069                 pop     ecx

seg005:0046206A                 sub     [ecx+16h], ebx

seg005:0046206D                 popa

seg005:0046206E                 pusha

seg005:0046206F                 mov     esi, offset unk_447000

seg005:00462074                 lea     edi, [esi-46000h]

seg005:0046207A                 push    edi

seg005:0046207B                 or      ebp, 0FFFFFFFFh

seg005:0046207E                 push    offset sub_4528D0

seg005:00462083                 retn

自修改代码

 

这里0046206A的代码实际就是以前面的校验值对0046207E处的指令修改,校验不正确便无法得出正确的返回地址。在写脚本的时候遇到一个问题是,解密代码使用了BL和BH,即BX的低八位和高八位的寄存器。我们可以先将校验值写进一个DWORD,然后获取其中第一个BYTE和第二个BYTE,便可以得到它的值了。由此便可得出下面的脚本:

 

#include "idc.idc"

 

static main() {

  auto StartAddr,EndAddr,cKey,lKey,hKey,Cbuffer,Kbuffer,Counter;

 

EndAddr = 0x00461F92 - 0x5;

cKey = 0;

 

for (StartAddr = EndAddr - 0x0BE9C; StartAddr < EndAddr; StartAddr ++){

 

cKey = cKey + Byte(StartAddr);    // movzx   eax, byte ptr [edi]

                              // add   ebx, eax

                                    }

 

Kbuffer = Dword(MinEA());       //从镜象基址借用1个Dword

PatchDword(MinEA(),cKey);    

lKey=Byte(MinEA());           //转换成bl

hKey=Byte(MinEA()+1);         //转换成bh

StartAddr = 0x447000;

 

for (Counter = 0x0BC00 ; Counter !=0 ; Counter --){

 

lKey=lKey + hKey;              // add     bl, bh

lKey=lKey ^ hKey;              // xor     bl, bh

Cbuffer = Byte(StartAddr) ^lKey;  // movzx   eax, byte ptr [edi]

                              // xor     al, bl

PatchByte(StartAddr,Cbuffer);            // mov     [edi], al

StartAddr++;

                                    }

 

StartAddr = 0x462069+0x16;

PatchByte(MinEA(),lKey);

cKey = Dword (MinEA());

Cbuffer = Dword(StartAddr) - cKey;

PatchDword(StartAddr,Cbuffer);

PatchDword(MinEA(),Kbuffer);          //恢复原来的数据

        }

Patch4.idc

 

在还原代码之后,容易看出0046207E处,PUSH+ RET相当于一个绝对跳转,现在让我们看看4528D0处的代码。在4528D0处按P,IDA将认为该处为函数的起点,并为函数建立图形视图。

 

流程的缩略图

 

看起来很复杂。或者的确复杂,但是我们只需要将它还原成IDC代码就可以了,甚至不需要我们理解算法的思想。可能你觉得在去除花指令影响之后,用OD改EIP直接运行相关代码也可以,内联汇编,写插件也可以。实际工作的时候,当然效率优先,选择最高效率的方法,但是将低级语言代码还原成高级语言代码,还是有一定意义的,例如你觉得C代码更容易理解一点,那么你可以先把汇编转成C代码,再理解。现在让我们切换到反汇编窗口再看代码:

 

seg001:004528D0                 jmp     short loc_4528E2  //跳到开始位置

seg001:004528D0

seg001:004528D2 ; ---------------------------------------------------------------------------

seg001:004528D2                 nop

seg001:004528D3                 nop

seg001:004528D4                 nop

seg001:004528D5                 nop

seg001:004528D6                 nop

seg001:004528D7                 nop

seg001:004528D7

seg001:004528D8

seg001:004528D8 loc_4528D8:        ; CODE XREF: sub_4528D0:loc_4528E9j

seg001:004528D8                 mov     al, [esi]       ; 1

seg001:004528DA                 inc     esi

seg001:004528DB                 mov     [edi], al

seg001:004528DD                 inc     edi

seg001:004528DD

seg001:004528DE

seg001:004528DE loc_4528DE:      ; CODE XREF: sub_4528D0+BAj

seg001:004528DE                             ; sub_4528D0+D1j

seg001:004528DE                 add     ebx, ebx

seg001:004528E0                 jnz     short loc_4528E9

seg001:004528E0

seg001:004528E2

seg001:004528E2 loc_4528E2:            ; CODE XREF: sub_4528D0j

seg001:004528E2                 mov     ebx, [esi]  //从这里开始

seg001:004528E4

seg001:004528E4 loc_4528E4:

seg001:004528E4                 sub     esi, -4

seg001:004528E7                 adc     ebx, ebx

seg001:004528E7

seg001:004528E9

seg001:004528E9 loc_4528E9:              ; CODE XREF: sub_4528D0+10j

seg001:004528E9                 jb      short loc_4528D8

 

     我们发现开始的地方需要访问ESI指向的内存,往回看发现解密代码需要的参数,在前面说的自修改代码部分(0046206F)已经处理过了。该处代码很容易转成高级语言,现在来看看如何重整代码的流程。跳转向上的时候,代表一个循环。这与高级语言是相通的,值得注意的是向下的跳转。达到某一条件,就绕过一部分代码,向后执行,这跟高级语言中的IF控制语句,即遇到某一条件就执行随后的代码。也就是说,我们得反转比较条件。

       以给出的代码为例子,与自身相加,相当于乘2,实际就是以个向左位移操作。想想十进制中,把1向左移动一位,实际就是将1乘以10。在二进制中也是一样,将一个二进制数向左移动一位,则是乘以2。汇编指令jb仅在进位标记CF=1时跳转,也就是说004528E7处的adc  ebx, ebx及后面的jb short loc_4528D8的意义为,将EBX中的数向左移一位,并检查的最高位是否为1,1则向上跳转,也就是循环,0则继续执行,即终止循环的条件。现在我们可以构造下面循环的框架:

 

auto EBX,HigtBitfla;

 

while (HigtBitflat != 0){

 

HigtBitflat = EBX & 0x80000000;    //与0x80000000进行and运算

                                //最高位不为0则HigtBitflat为0

                                //0x80000000最高位为1,其他位0

                                //不明白的读者看将其展开计算

EBX = EBX + EBX;               //向左位移

 

                   }

 

现在再来看看004528DE处的代码,jnz在ZF=0时产生跳转,即当最高位之外任意一位不为0时产生跳转。正如上面说的,将跳转条件反转,我们便能使用IF语句了。

 

Auto EBX,IsNotZero;

 

IsNotZero = EBX & 0x7FFFFFFF;    //0x7FFFFFFF最高位为0

                                //屏蔽最高位,以检查后面的位

                                //仅当最高位外全为0,IsNotZero为0

If (IsNotZero == 0){

//  此处可以填上004528E2到004528E7的代码

}

EBX = EBX + EBX;               //注意这里与汇编的区别

                                //先判断,然后才移位

 

注意这里与汇编代码的区别,由于我们无法在IDC上访问标记寄存器,也无法使用跳转。这里只能先判断最高位,然后才进行位移。下面让我们来直接看最后得出的IDC脚本:

 

#include "idc.idc"

 

static main() {

  auto MyAddr,DeCodeAddr,HigtBitflat,EBX;

auto EAX,ECX,EBP,ESI,EDX,CF,IsNotZero,Counter;

 

MyAddr = 0x447000;

DeCodeAddr =0x447000 - 0x46000;

ESI=DeCodeAddr;

Counter = 0;       //初始化循环条件

CF = 0;             //代表标志寄存器的CF位

EBX = Dword(MyAddr);

MyAddr = MyAddr + 4;

HigtBitflat = EBX & 0x80000000; 

EBX = EBX + EBX;

EBX++;

//    为了统一循环入口,将部分代码移出循环执行。

while (Counter != 1){

while (HigtBitflat != 0){

       PatchByte (DeCodeAddr,Byte(MyAddr));

       MyAddr++;

       DeCodeAddr++;

       IsNotZero = EBX & 0x7FFFFFFF;

       if (IsNotZero == 0){

              CF=1;   //sub esi, -4与add esi,4的区别就是前者CF=1

              EBX = Dword(MyAddr);

               MyAddr = MyAddr + 4;

                            }

       HigtBitflat = EBX & 0x80000000;

       EBX = EBX + EBX;

        EBX = EBX + CF;      //加上CF,模拟ADC指令

       CF = 0;

                               }

 

EAX = 1;

while (Counter != 1){

//4528F0到45291A,以JMP构成一个循环。因此使用while语句,构造

//一个无限循环。在符合终止循环条件处使用break指令结束循环。

IsNotZero = EBX & 0x7FFFFFFF;

if (IsNotZero == 0){

       CF=1; 

       EBX = Dword(MyAddr);        

        MyAddr = MyAddr + 4;

                     }

HigtBitflat = EBX & 0x80000000;

EBX = EBX + EBX;

EBX = EBX + CF;

CF = 0;

EAX = EAX + EAX;

if (HigtBitflat != 0) EAX++;

HigtBitflat = EBX & 0x80000000;

if (HigtBitflat != 0){

       IsNotZero = EBX & 0x7FFFFFFF;

       if (IsNotZero != 0) {  //00452901   jnz  short loc_45291C

       EBX = EBX + EBX;

       break;

                            }

       EBX = Dword(MyAddr);

       MyAddr = MyAddr + 4;

       HigtBitflat = EBX & 0x80000000;

       if (HigtBitflat != 0) {     //0045290A   jb  short loc_45291C

       EBX = EBX + EBX;

       EBX++;

       break;

                            }

       CF = 1;  

                            }

EBX = EBX + EBX;

EBX = EBX + CF;

CF = 0;

EAX--;  

IsNotZero = EBX & 0x7FFFFFFF;

if (IsNotZero == 0){

       CF=1; 

       EBX = Dword(MyAddr);        

        MyAddr = MyAddr + 4;        

                     }

HigtBitflat = EBX & 0x80000000;

EBX = EBX + EBX;

EBX = EBX + CF;

CF = 0;

EAX = EAX + EAX;

if (HigtBitflat != 0) EAX++;

                                           }

 

ECX = 0;        //xor ecx,ecx常见的为寄存器赋值为0的语句。

//注意00452921  jb  short loc_452934处,程序分开两条路线

//在loc_45293F处汇合。因此这里使用if 。。else语句重整程序流程。

if (EAX < 3){    //此处直接使用减法指令作比较,而不是使用CMP

EAX = EAX - 3;  //因此只能在比较之后再减

IsNotZero = EBX & 0x7FFFFFFF;

if (IsNotZero == 0){

       CF=1; 

       EBX = Dword(MyAddr);

        MyAddr = MyAddr + 4;

                     }

HigtBitflat = EBX & 0x80000000;

EBX = EBX + EBX;

EBX = EBX + CF;

CF = 0;

}

else{

EAX = EAX - 3;

EAX = EAX << 8;

EAX = EAX + Byte(MyAddr);

MyAddr++;

EAX = EAX ^ 0xffffffff;

if (EAX == 0) break;

HigtBitflat = EAX & 1;  //检查sar eax,1是否影响CF位

EAX = EAX >> 1;      //检查结束再执行位移

EBP = EAX;

}

ECX = ECX + ECX;

if (HigtBitflat != 0) ECX++;

IsNotZero = EBX & 0x7FFFFFFF;

if (IsNotZero == 0){

       CF=1; 

       EBX = Dword(MyAddr);

        MyAddr = MyAddr + 4;

                     }

HigtBitflat = EBX & 0x80000000;

EBX = EBX + EBX;

EBX = EBX + CF;

CF = 0;

ECX = ECX + ECX;

if (HigtBitflat != 0) ECX++;

if (ECX == 0 ){

ECX++;

HigtBitflat = 0;

//00452960   jnb     short loc_452951

//0045296B   jnb     short loc_452951

//此处有两个跳转指向循环入口,将00452960处的条件反转,翻译成if

//语句。便可得到下面循环:

while (HigtBitflat == 0){

IsNotZero = EBX & 0x7FFFFFFF;

if (IsNotZero == 0){

       CF=1; 

       EBX = Dword(MyAddr);        

        MyAddr = MyAddr + 4;        

                     }

HigtBitflat = EBX & 0x80000000;

EBX = EBX + EBX;

EBX = EBX + CF;

CF = 0;

ECX = ECX + ECX;

if (HigtBitflat != 0) ECX++;

HigtBitflat = EBX & 0x80000000;

if (HigtBitflat != 0){

IsNotZero = EBX & 0x7FFFFFFF;

if (IsNotZero != 0) {

EBX = EBX + EBX;

break; 

}

EBX = Dword(MyAddr);        

MyAddr = MyAddr + 4;

CF = 1;

HigtBitflat = EBX & 0x80000000;

                     }

EBX = EBX + EBX;

EBX = EBX + CF;

CF = 0;

                             }

ECX = ECX + 2;

                     }

//高级语言的比较为有符号数的比较,而0045297F  jbe  short loc_452990

//是无符数的比较。因此要先比较其最高位,模拟无符号数的比较

HigtBitflat = EBP & 0x80000000;

if (HigtBitflat !=0){

if (EBP < 0xfffffb00) CF =1;

                     }

else{

CF =1;

       }

ECX ++;

ECX = ECX + CF;

CF=0;

EDX = DeCodeAddr + EBP;

if (HigtBitflat !=0){

if (EBP > -4) CF=1;

                     }

//0045297F  jbe  short loc_452990将此处分开两条路线,

//以jmp  loc_4528DE重新汇合。这里同样使用if….else语句。

if (CF==1){

CF=0;

while (ECX !=0){

PatchByte(DeCodeAddr,Byte(EDX));

EDX ++;

DeCodeAddr ++;

ECX --;

              }

              }

else{

while(Counter != 1){

PatchDword(DeCodeAddr,Dword(EDX));

EDX = EDX + 4;

DeCodeAddr = DeCodeAddr + 4;

if (ECX <= 4){

ECX= ECX -4;

break;

}

ECX = ECX - 4;

}

DeCodeAddr = DeCodeAddr + ECX;

       }

//反汇编代码的循环入口(4528DE)与我们转换的循环入口不同(4528E9)

//跟开始的时候一样,入口之前的代码放到循环外面。

IsNotZero = EBX & 0x7FFFFFFF;

if (IsNotZero == 0){

       CF=1; 

       EBX = Dword(MyAddr);        

        MyAddr = MyAddr + 4;        

                     }

HigtBitflat = EBX & 0x80000000;

EBX = EBX + EBX;

EBX = EBX + CF;

CF = 0;

                            }

              }

Patch5.idc

 

至此,我们成功将004528D0到004529A1处的代码转换成C代码。在完成如此复杂的代码还原之后,004529A6到004529D8处的反汇编代码只是小菜一碟。里面的代码也很好理解,将符合E8 01和E9 01的机械码解密。位移指令可以通过借用程序中的一个闲置的Dword,使用IDC提供的Pactch系列指令来模拟,详见Patch6.idc。在完成最后的解密代码后,便是IAT的修复了。现在看看下面代码:

 

004529DA                 lea     edi, [esi+50000h]

004529E0 loc_4529E0:   

004529E0                 mov     eax, [edi]

004529E2                 or      eax, eax

004529E4                 jz      short loc_452A22

004529E4

004529E6                 mov     ebx, [edi+4]

004529E9                 lea     eax, [eax+esi+549B0h]

004529F0                 add     ebx, esi

004529F2                 push    eax

004529F3                 add     edi, 8

004529F6                 call    dword ptr [esi+54A3Ch]

004529FC                 xchg    eax, ebp

004529FD loc_4529FD:   

004529FD                 mov     al, [edi]

004529FF                 inc     edi

00452A00                 or      al, al

00452A02                 jz      short loc_4529E0

00452A02

00452A04                 mov     ecx, edi

00452A06                 push    edi

00452A07                 dec     eax

00452A08                 repne scasb

00452A0A                 push    ebp

00452A0B                 call    dword ptr [esi+54A40h]

00452A11                 or      eax, eax

00452A13                 jz      short loc_452A1C

00452A13

00452A15                 mov     [ebx], eax

00452A17                 add     ebx, 4

00452A1A                 jmp     short loc_4529FD

 

在分析该处代码之前,显然应该先把ESI的值计算出来。鼠标点击ESI,以高亮显示该寄存器,向上滚动反汇编窗口,发现从004529A6  pop  esi处开始,ESI便没有被修改过,而该处对应于:

 

seg005:0046206F                 mov     esi, offset unk_447000

seg005:00462074                 lea     edi, [esi-46000h]

seg005:0046207A                 push    edi

 

       可见ESI=0x401000,容易计算出004529F6和00452A0B处CALL的地址分别为455A3Ch和455A40h。跳转到该地址:

 

 

 

       显然,这里便是壳填充IAT的地方了。那么004529DA  lea  edi,[esi+50000h]中,EDI便是保存API名字的数据表。做脱壳机的任务就留给读者作课后练习,正如前面介绍的那样,只需要API的名字为相关IAT地址重命名,便能分析了。也就是说00452A0B处,调用GetProcAddress,跟踪它的参数lpProcName (00452A06   push    edi),以及它的返回值(00452A15   mov     [ebx], eax),当然这里的跟踪,可以象刚才那样手动确认,也可以通过与调试器配合快速得出结果。不难得出下面脚本:

 

#include "idc.idc"

 

static main() {

   auto ESI,EDI,EAX,EBX,Counter,cBuffer,BufLen,straa;

 

ESI = 0x447000 - 0x46000;

EDI = ESI + 0x50000;

Counter = MaxEA() - MinEA();

MakeUnknown(MinEA(),Counter,1);   //将整个程序标记未分析

AnalyzeArea (MinEA(),MaxEA());     //分析整个程序

Counter = 0;

while (Counter != 1){

EAX = Dword(EDI);

if (EAX == 0) break;

EBX = Dword(EDI+4);

EBX = EBX + ESI;

EDI = EDI + 8;

while (Counter != 1){

EAX = Byte(EDI);

EDI++;

if (EAX == 0) break;

cBuffer = GetString(EDI,-1,ASCSTR_C);

straa = cBuffer + "_";    //IDA不允许重复命名,加上“_”避免重复

MakeNameEx(EBX,straa,SN_AUTO);

EBX = EBX + 4;

EDI = EDI + strlen(cBuffer);

EDI++;

                     }

                     }

}

IATPATCH.idc

 

       注意解密后,必须将整个程序标记为未分析,并重新分析,然后才能进行重命名。

程序的OEP

      

       到此,静态脱壳完毕。从这个例子也可以知道,对于掌握反汇编器的人来说,除非反调试机制与解密KEY关联,否则根本就没有强度可言。然而,IDA博大精深,还有更多强大的功能,本文也只是抛砖引玉而已。下面给出几个链接,方便大家更进一步学习:

 

IDA的官方网站:

www.datarescue.com

 

看雪论坛9月翻译专题:

http://bbs.pediy.com/showthread.php?s=&threadid=31023

 

IDA Pro的插件开发SDK:

http://bbs.pediy.com/showthread.php?s=&threadid=31441

 

IDA逆向工程入门:

http://bbs.pediy.com/showthread.php?s=&threadid=40765

 

IDA简易教程:

www.pediy.com/practise/IDA.htm

 

2 0