边看边翻译,其中敲字错误或表述有误再所难免。如果您发现错误,而且也有时间,敬请留言告之,以便更正。 请尊重作者及译者工作之艰辛,若转摘,请务必注明出处如下:
------------------------------------------------------------------
《代码反汇编:IDA Pro与Soft ICE之应用》
英文名称:Disassembling Code IDA Pro and Soft ICE
作者:Vlad Pirogov
译者:罗祥勇 <E-mail:solo_lxy@126.com>
出自:优快云 Blog <背你走天涯>专栏
------------------------------------------------------------------
本文来自优快云博客,转载请标明出处:http://blog.youkuaiyun.com/solo_lxy/archive/2009/10/13/4665143.aspx
1.3 Windows系统编程的专有特征
本节主要介绍Windows编程。我不打算写个学习教程;那将需要一本单独的书的容量。我只提醒你关于Windows编程的主要原则,这些将对可执行文件的分析非常有用。
1.3.1 通用概念
Windows编程基于应用程序编程接口(API)的使用。通过API,应用程序可以直接和Windows系统通信。基于这种交互动作构建的应用程序将和操作系统集成的更加紧密,并且较之其他程序有更强大的功能。某些情况下,API函数又称系统调用。但这种叫法实际上并不正确。系统调用(例如在UNIX系统中)会调用操作系统内核中的系统过程。操作系统提供一定范围的过程以简化应用程序对系统资源的管理。API函数是包含在系统过程和应用程序之间的一种接口。当调用API的时候,你并不知道它调用的是加载到应用程序地址空间中的动态连接库(DLL)函数,还是直接调用了操作系统内核代码。Windows操作系统一直在改变、进化,新的版本也一直在发布,但是API却保持不变(尽管还是有新的函数会加入进来)。因此,要保持应用程序和操作系统最大程度的兼容的可能,就只能使用最基本的系统API函数。
API函数通过保存在系统目录(windows/system32)中的DLL提供支持。编译器确保程序和这些库的准确连接(又称之为隐式绑定)。API函数的树木非常庞大;超过3000个。大多的API函数包含在以下几个DLL中:
■ Kernel32.dll – 本库包含了主要的系统控制和管理函数(包含内存、应用程序、资源、和文件等控制的函数)。
■ User32.dll – 本库包含大量的用户接口函数(包含窗口消息,定时器,菜单等处理函数)。
■ GDi32.dll - 本库为图形设备接口GDI库,包含大量的窗口图形操作函数。
■ Comctl32.dll – 本库提供大量的通用控件。特别需要注意的是,本库提供新的控件样式(Windows XP 接口样式)。
如果API函数接收一个字符串的指针做为输入参数,则这个函数就有两个版本:一个ANSI版本带A后缀;一个Unicode版本带W后缀。例如:MessageBox的两个版本分别为:MessageBoxA, MessageBoxW。高级编程语言,如C++,必须首先决定在程序中使用哪种字符串进行操作。然后,编译器会自动选择合适的函数版本。当使用汇编语言时,有必要显式指定你要使用函数的什么版本。
在Windows中有两中不同类型的应用程序:控制台(console)应用程序和图形界面(GUI)应用程序。控制台应用程序其转有的特征,在执行它的时候,系统为这样的应用程序(或者变体,应用程序从其父进程中继承了控制台)创建一个文本窗口,称之为控制台。GUI应用程序和图形窗口一起工作,可以包含图形和众多的控件。GUI应用程序也被称之为图形或窗口应用程序。Windows可以运行其他类型的应用程序-服务和驱动程序,这些都是系统程序。除此以外,Windows还可以运行Posix和OS/2子系统,但是还是有一定程度的限制。这些应用程序的细节将不包含在本书介绍的内容中。
通常情况下,Windows编程使用库函数(C/C++)或类库(在Delphi中,被称之为组件components)。这种情况下,和操作系统的交互就被隐藏在了库这层下了。这就导致对可执行代码的分析变的更加复杂,因为我们得知道哪个库或类被使用了。这些可以通过分析库代码加以确定使用了哪个API函数,借以了解这些调用的目的。但这并是件简单的工作。本节的目的是解释Windows编程的通用结构,以是你了解分析API调用的方法。
本质上说,控制台应用程序和GUI应用程序的区别在与其PE文件头中的Subsystem标志字段(见1.5节)。这个标志在应用程序连接的时候被设置。当连接GUI应用程序的时候,使用linkexe连接应用的时候,应该选择下面的命令行选项:/SUBSYSTEM:WINDOWS,而控制台应用程序为:/SUBSYSTEMl:CONSOLE。因此,在使用高级语言的时候,编译器必须提供选择GUI还是console的选项。同时,console和GUI访问操作系统资源的权限是一样的。任何控制台应用程序都可以创建图形窗口并和这些窗口一起工作,同时,GUI应用程序也可以也console应用程序一起工作。
1.3.2 控制台应用程序
不管是编译后的形式还是源代码,控制台应用程序都显得紧凑一些。控制台本身值得特殊关注。可能就想你推测的那样,一个控制台就是一个文本模式的窗口。和控制台应用程序或这样的窗口交互步骤可简化如下:
■ 如果控制台程序被另外一个控制台程序启动,则子程序继承父程序的控制台。
■ 如果父程序没有控制台,则系统创建一个新的控制台。
■ 每个控制台应用程序有且只有一个控制台。
■ 假设已经删除了存在的控制台,一个控制台应用程序可以通过调用AllocConsole API函数创建新的控制台。
Windows系统起先倾向于图形程序,存在控制台的原因主要是为了可以运行老的MS-DOS程序。而MS-DOS程序一般运行于字符模式。当运行这样的程序时,Windows自动的为其分配一个控制台,自动将其输入和输出重定向到控制台。
典型的控制台应用程序的结构被称之为批处理结构(列表 l.4) 这样的程序包含一系列需要执行的动作。例如,程序可能打开一些文件,实施某些动作,然后关闭文件,结束操作。
列表 l.4 典型的控制台应用程序
列表 l.4显示一个典型的控制台应用程序,它输出字符串到屏幕。这个程序的专有特征是创建了它自己的控制台,不管它是从控制台启动的还是其他情况启动的。FreeConsole()和AllocConsole()函数序列调用释放存在的程序控制台,然后创建一个新的。被继承的控制台什么都没发生;程序很方便的就获取了自己创建控制台的能力。如果你删除程序开始处的FreeConsole函数调用,然后从控制台启动本程序,则没有新的控制台将会被创建。尽管存在对AllocConsole函数的调用,程序还是会将所有的输入/输出都重定向到存在的控制台。
列表 l.4中的程序基于API函数。甚至用来获取字符串长度的函数lstrlen实际上也是个API函数。现在,我们来看看IDA Pro是如何识别可执行文件的(列表 l.5)。
列表 l.5 可执行文件的反汇编代码
即使无经验的用户,第一眼看来,IDA Pro对此可执行文件反汇编出来的代码也非常清楚,它解决了反汇编中可能遇到的大部分困难。然而,本章,我不打算描述这里反汇编出来的代码;接下来的各章将对这个问题做更加详细的阐述。当前,我只想引起你对这方面的注意,仅使用API函数写的程序的可执行代码,经过反汇编后看起来透明而且清晰。当涉及和表1.4类似的程序时,大多数的程序员使用C++库函数代替API函数。列表1.6提供类似的程序。
列表 l.6 可执行文件的反汇编代码
#include <stdio.h> char *s = "Example of console program./n/0"; char buf[100]; void main() { puts(s); gets(buf); } |
有必要提示一下,列表1.6中的程序并不重新创建一个新的控制台窗口,而是使用操作系统提供的。因此,通常情况下,它的特征也行为和列表1.4中的程序是一样的。为了使用控制台进行工作,此程序使用puts和gets函数。这里值得注意的特征是,IDA Pro对使用标准C++写的程序反汇编的结果也很不错。例如,如果深入的研究一下puts函数,你会很容易的发现该函数最终还是调用了API函数WriteFile,这个函数和WriteConsole类似。因此,应用程序开发者通常会使用非标准的库,使用了这些库中的函数调用使得类似函数的识别就困难些了。特别是,当你想反汇编Delphi写的程序的话,问题就很明显。例如,在Delphi环境中,执行写入控制台的操作需要你调用两个过程,这样IDA Pro就无法识别了。
线形(批处理)结构的控制台程序相当的简单。尽管类似的操作可能很复杂,但是通过代码挖掘后其调用序列的次序其实相当的简单。但是,如果你想写一个和用户交互密切的程序的话,你将不得不处理键盘和鼠标消息。这种情况下,程序的结构将变的相当的复杂。这需要你引入处理控制台事件的主循环,用来处理键盘和鼠标事件。列表1.7类似这样程序的可能设计。
列表 l.7 和用户交互的控制台程序示例
我不会对此程序做详细的介绍,而且希望你是你一个有经验的程序员。如果你对编写控制台应用程度感兴趣,我推荐你读我写的关于Windows编程的书。[3]
当分析列表1.7中的程序时,你会发现一个非常让人感兴趣的细节:那个处理器函数并没有被直接调用。它的地址被传递给了SetConsoleCtrlHandler API函数。自然地,获取此段重要代码的唯一方法是分析对SetConsoleCtrlHandler API函数的调用。IDA Pro反汇编器在这方面做的相当地好。考虑列表1.8中的代码片段。
列表 l.8 和用户交互的控制台程序示例
.text:00401453 mov edi, ds:SetConsoleCtrlHandler .text:00401459 push 1 ; Add .text:0040145B push offset loc_401000 ; HandlerRoutine .text:00401460 mov hConsoleInput, eax .text:00401465 call edi ; SetConsoleCtrlHandler |
汇编器不单识别了SetConsoleCtrlHandler函数,而且还识别了其参数。不要被mov hConsoleInput, eax指令给弄糊涂了,这条指令和SetConsoleCtrlHandler函数调用没有关系。相反,它和上一个函数调用GetStdHandle有关。这是因为代码优化造成的。
注意:必须承认现代编译器比专业的汇编程序员还要能更好的优化代码。程序员总是关注于诸多的惯例,如编程规范和代码可读性。这些惯例对编译器没有影响。诸多的优化方法将在本书中提及。
回想一下之前描述的代码片段。因为SetConsoleCtrlHandler函数,反汇编器正确的确定了处理器handler函数。
注意以下inputcons函数。原则上说,它并不包含任何不正常的代码。循环中对函数ReadConsoleInput的调用允许你去探测handler函数不能处理的事件。这里的循环也可称之为控制台的消息处理循环。这样的循环对Windows应用程序来说很典型,因此这样的循环对控制台程序来说也是允许的。自然地,这两种循环之间有值得注意的地方。每个应用程序只能有一个控制台窗口,所以没必要考虑消息是关于哪个窗口的。GUI应用程序可以拥有很多窗口,而只有一个消息循环(参见1.3.3)。这种情况下,每个窗口的handler标识消息。这会儿,你可能会产生一些疑问,接下来的我继续解释。
1.3.3 窗口应用程序
窗口应用程序,即大家熟知的GUI应用程序,都基于事件驱动机制。换句话说,也就是,类似这样程序的主要部分都聚焦在当指定事件发生后对一些特殊的函数的调用,这些函数同前一节我们讲的handler一样。除此之外,这内程序还有一个典型的消息处理循环。消息处理循环用来向适当的处理器handler函数分发消息(列表1.9)。
列表 l.9 一个典型的GUI应用程序
列表1.9给出了一个拥有最少功能最小的GUI应用程序。通常情况下,Windows应用程序构建在基础的主窗口之上。剩下的所有窗口都像太阳系的行星围绕着太阳一样,“盘旋”在主窗口之上。因此,分别此类程序的三个主要部件就很容易:
■ 定义和注册主窗口所属类别。
■ 消息处理循环,其重要任务就是接收应用程序消息,分发这些消息到指定的窗口函数(不仅仅是主窗口)。
■ 主窗口或其他窗口的消息处理函数。
注意这些相关的特征,就有可能有目的的找到GUI应用程序的各个元素。
DispatchMessage是消息循环中使用的主要API函数。这个函数将新到来的消息重定向到指定的窗口函数。消息的结构如列表1.10所示。
列表 l.10 消息结构
typedef struct { HWND hwnd; UINT message; WPARAM wParam; LPARAM 1Param; DWORD time; POINT pt; } MSG |
上面结构体的各个字段描述如下:
■ hwnd – 窗口句柄,标识当前消息要发往哪里。
■ message – 当前消息代码。
■ wParam – 带有补充信息的可选参数。
■ lParam – 带有补充信息的可选参数。
■ time – 消息到达时的时间。
■ pt – 消息被发送时当前鼠标的坐标。低位的字表示X坐标,高位的字表示Y坐标。
Hwnd的值定义了消息要发送的窗口。对于没个窗口-更精确的说,对于每个窗口类-都有指定的消息处理函数(见列表1.9)。系统知道消息要发往哪里,但用户不知道。顺便说一下,对于这段程序代码我们可以关注消息处理函数,也可以关注对它们的调用。这是如何解决我们的疑问的呢?回想一下,大多数的窗口函数都必须注册。而且函数和窗口类都要被注册。例如,考虑列表1.9的程序:消息循环函数的地址被赋给了lpfnWndProc字段。换句话说,通过查看反汇编代码,你可以发现函数的地址。例如,下面(列表1.11)是IDA Pro给出的反汇编代码片段。
列表 l.11 IDA Pro针对GUI应用程序反汇编出来的代码片段
.text:00401077 mov [esp + 80h + WndClass.lpfnWndProc], offset loc_401000 |
这里,loc_401000为窗口函数的地址。反汇编器认识RegisterClass函数和它接受的参数。列表1.12显示了使用W32Dasm v.10(也拥有很好的名声)反汇编出来的代码片段。
列表 l.12 使用W32Dasm v.10反汇编出来的代码片段
:00401077 C744241800104000 mov [esp+18], 00401000 :0040107F 896C241C mov dword ptr [esp+1C], ebp :00401083 896C2420 mov dword ptr [esp+20], ebp :00401087 89742424 mov dword ptr [esp+24], esi
* Reference To: USER32.LoadIconA, Ord:01BDh
| :0040108B FF15C4504000 Call dword ptr [004050C4] :00401091 68007F0000 push 00007FOO :00401096 55 push ebp :00401097 89442428 mov dword ptr [esp+28], eax
* Reference To: USER32.LoadCursorA, Ord:01B9h | :0040109B FF15C8504000 Call dword ptr [004050C8] :004010A1 89442424 mov dword ptr [esp+24], eax :004010A5 8D44240C lea eax, dword ptr [esp+OC] :004010A9 8D542450 lea edx, dword ptr [esp+50] :004010AD 50 push eax :004010AE C744242C06000000 mov [esp+2C], 00000006 :004010B6 896C2430 mov dword ptr [esp+30], ebp :004010BA 89542434 mov dword ptr [esp+34], edx
* Reference To: USER32.RegisterClassA, Ord:0216h | :004010BE FF15CC504000 Call dword ptr [004050CC] :004010C4 6685C0 test ax, ax |
仔细研究以上W32Dasm给出的代码,你会发现它没有IDA Pro提供的信息丰富。然而,大多数情况下,它可以正确的识别API函数。因此,我们很容易的找到RegisterClass函数。通过RegisterClass之前的其他函数,我们就有可能知道mov [esp + 18], 00401000指令将窗口函数的地址赋给了lpfnWndProc字段。一旦找到了窗口函数,代码挖掘者就可以通过它们找到自己期望找到的内容了。
窗口函数用于处理发送到它的消息。窗口和一些控件可以形成并发送到为数众多的消息到消息处理函数。而且,可以发送用户自定义的消息。正因为如此,这里就有一个特殊的WM_USER常量,所有程序自定义的消息都必须大于或等于这个常量。通过窗口函数的反汇编代码我们可以找到特定事件的处理动作,从而理解指定GUI应用程序的运行机制。
但问题是,窗口函数不属于某个窗口,而是属于指定的整个窗口类。当使用API函数编程时,一个函数可以和一个窗口相关。对每个消息的处理并不需要做特别的努力,因为每个消息都包含有窗口句柄。但是,在分析可执行文件代码的时候,还是有困难的,因为在静态分析代码的时候,很难决定当前代码的代码属于哪个窗口的函数。在这点上,调试器就很有用了,它可以在窗口函数上设置断点,特别是在使用Soft ICE调试器的时候甚至可以在消息上设置断点。
自然地,消息处理循环在GUI应用程序中地位相当重要。在反汇编代码中定位了它,你就可以定位先前的循环代码块 – 换句话说就是,就可以知道主程序在哪里创建的,主窗口的类是在哪里注册的了。利用像GetMessagem,PeekMessage,TranslateMessge和DispatheMessage这样的API,就可以找到消息处理循环。
1.3.4 基于对话框的应用程序
列表1.3显示了是使用模式对话框做为主窗口的应用程序。
列表 l.13 使用模式对话框的应用程序例子
// Resource identifiers // Definitions of style constants #define WS_VISIBLE 0x0l0000000L #define WS_SYSMENU 0x000S0000L #define WS_MINIMIZEBOX 0x00020000L #define WS_MAXIMIZEBOX 0x000l0000L
// Modal dialog definition DIALOG DIALOGEX 10, 10, 150, 100 STYLE WS_VISIBLE | WS_SYSMENU | WS_MINIMIZEBOX |WS_MAXIMIZEBOX CAPTION "Modal dialog" FONT 12, "Arial" }
// Program module #include <windows.h>
int DWndProc(HWND, UINT, WPARAM, LPARAM);
__stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { // Create a modal dialog. DialogBoxParam(hInstance, "DIALOG", NULL, (DLGPROC)DWndProc, 0);
// Close the application. ExitProcess(0); };
// Message-handling function of the modal dialog int DWndProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM 1Param) { switch(uMsg) { // Message that arrives when the dialog is created case WM_INITDIALOG: break; // Message that arrives in case of an attempt at closing the window case WM_CLOSE: EndDialog(hwndDlg, 0); // Message from window controls case WM_COMMAND: break; }; return FALSE; }; |
相对于正常窗口,模式对话框有以下特征:
■ 模式对话框基于存储于程序或内存中的资源模板创建。列表1.13的例子中,对话框的资源保存在资源文件中。
■ 模式对话框使用DialogBoxParam函数创建。这个函数的第四个参数,指定处理窗口消息的函数地址。DialogBoxParam在未调用EndDialog函数执行永不返回。
■ 相对于一样的窗口,对话框窗口没有消息处理循环。更准确的说是,是有一个,但是操作系统创建的,用于处理和分发消息。因此,你可能会遇到没有显式消息处理循环的应用程序。
■ 处理模式对话框最主要的是处理WM_CLOSE消息和调用EndDlalog函数(用于从内存中移除模式对话框)。
图l.13对话框的例子(列表1.13)
顺便说一下,使用API MessageBox函数调用生成的窗口也是一种典型的模式对话框。在这种情况下,系统不仅处理消息,同时也创建窗口模板和管理窗口的消息函数。
注意:在资源文件中(列表1.13),窗口样式常量直接定义了。但这并不是必须的。你可以简单的插入下面的代码#include <windows.h>。 做为可选项,如果不关注资源文件的内容,你可以使用Visual Studio .NET。
还是一样,我们来看看IDA Pro的反汇编代码(列表1.14)。DialogBoxParam可以帮助我们找到对话框的消息处理函数。
列表 l.14 列表1.13的反汇编代码
最后,有必要介绍另外一种类型的窗口:无模式对话框。这种类型的窗口需要显式的消息循环。列表1.15提供了这样例子,这里无模式对话框作为程序的主要窗口。
列表 l.15 无模式对话框作为程序的主要窗口
正如你从列表1.15中看到的那样,它和一般窗口应用程序是一样的。但是,还是有些专有的特征:
■ 非常显著的特征就是没有窗口的注册。
■ 消息循环处理做了小幅的修改。使用IsDialogMessage函数代替TranslateMessage和DispatchMessage函数。之所以这样,是为了处理使用<Tab>键在各个控件之间切换。IsDialogMessage函数用来无模式对话框正常工作。一般情况下,应用程序包含模式和无模式对话框的时候,消息处理循环通常如列表1.16所示。
列表 l.16 包含模式和无模式对话框时的消息处理循环
while (GetMessage(&msg, NULL, 0, 0)) { if(!IsDialogMessage(hw, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } |
这里,hw为无模式对话框窗口句柄。因此,IsDialogMessage函数也可以在一般窗口函数中使用。
在对窗口关闭事件(点击窗口右上角的关闭按钮)的处理上的不同也值得关注。一般的窗口实际上被系统关闭,同时发送WM_DESTORY 消息,用于退出消息处理循环。而无模式对话框并不是自动关闭;因此,有必要处理WM_CLOSE消息,并使用DestoryWindow函数关闭窗口。这些都不是什么秘密了。DefWindowProc函数处理WM_CLOSE消息,并隐式地调用DestoryWindow函数。
[5] 除非特别说明,所有的程序都使用Visual Studio .NET环境编译本书中的程序。
[6] IDA Pro反汇编器在全书都将不断地被使用。