本文将通过一个实例说明PE结构中的import table及import address table(IAT).
在data directory中有两项:IMAGE_DIRECTORY_ENTRY_IMPORT(1)和IMAGE_DIRECTORY_ENTRY_IAT(12),IMAGE_DIRECTORY_ENTRY_IMPORT指向了该PE文件中所有的输入信息,而IMAGE_DIRECTORY_ENTRY_IAT指向了该PE文件中的导入地址表。现在不必关心这两个表的关系,在之后的分析中将渐渐明朗。
如果用C/C++程序来分析import table或IAT的结构不是很合适,因为CRT会插入太多的代码到最终的PE文件中,所以我们选择用汇编代码作为例子,好处就是所有的导入函数都是自己的代码中确确实实使用到的,一目了然。
我们使用现成的一个例子的分析:(摘自Win32ASM Programming 3rd Edition)
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- ; Sample code for < Win32ASM Programming 3rd Edition>
- ; by 罗云彬, http://www.win32asm.com.cn
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- ; FirstWindow.asm
- ; 窗口程序的模板代码
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- ; 使用 nmake 或下列命令进行编译和链接:
- ; ml /c /coff FirstWindow.asm
- ; Link /subsystem:windows FirstWindow.obj
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- .386
- .model flat,stdcall
- option casemap:none
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- ; Include 文件定义
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- include windows.inc
- include gdi32.inc
- includelib gdi32.lib
- include user32.inc
- includelib user32.lib
- include kernel32.inc
- includelib kernel32.lib
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- ; 数据段
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- .data?
- hInstance dd ?
- hWinMain dd ?
- .const
- szClassName db 'MyClass',0
- szCaptionMain db 'My first Window !',0
- szText db 'Win32 Assembly, Simple and powerful !',0
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- ; 代码段
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- .code
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- ; 窗口过程
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- _ProcWinMain proc uses ebx edi esi hWnd,uMsg,wParam,lParam
- local @stPs:PAINTSTRUCT
- local @stRect:RECT
- local @hDc
- mov eax,uMsg
- ;********************************************************************
- .if eax == WM_PAINT
- invoke BeginPaint,hWnd,addr @stPs
- mov @hDc,eax
- invoke GetClientRect,hWnd,addr @stRect
- invoke DrawText,@hDc,addr szText,-1,/
- addr @stRect,/
- DT_SINGLELINE or DT_CENTER or DT_VCENTER
- invoke EndPaint,hWnd,addr @stPs
- ;********************************************************************
- .elseif eax == WM_CLOSE
- invoke DestroyWindow,hWinMain
- invoke PostQuitMessage,NULL
- ;********************************************************************
- .else
- invoke DefWindowProc,hWnd,uMsg,wParam,lParam
- ret
- .endif
- ;********************************************************************
- xor eax,eax
- ret
- _ProcWinMain endp
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- _WinMain proc
- local @stWndClass:WNDCLASSEX
- local @stMsg:MSG
- invoke GetModuleHandle,NULL
- mov hInstance,eax
- invoke RtlZeroMemory,addr @stWndClass,sizeof @stWndClass
- ;********************************************************************
- ; 注册窗口类
- ;********************************************************************
- invoke LoadCursor,0,IDC_ARROW
- mov @stWndClass.hCursor,eax
- push hInstance
- pop @stWndClass.hInstance
- mov @stWndClass.cbSize,sizeof WNDCLASSEX
- mov @stWndClass.style,CS_HREDRAW or CS_VREDRAW
- mov @stWndClass.lpfnWndProc,offset _ProcWinMain
- mov @stWndClass.hbrBackground,COLOR_WINDOW + 1
- mov @stWndClass.lpszClassName,offset szClassName
- invoke RegisterClassEx,addr @stWndClass
- ;********************************************************************
- ; 建立并显示窗口
- ;********************************************************************
- invoke CreateWindowEx,WS_EX_CLIENTEDGE,offset szClassName,offset szCaptionMain,/
- WS_OVERLAPPEDWINDOW,/
- 100,100,600,400,/
- NULL,NULL,hInstance,NULL
- mov hWinMain,eax
- invoke ShowWindow,hWinMain,SW_SHOWNORMAL
- invoke UpdateWindow,hWinMain
- ;********************************************************************
- ; 消息循环
- ;********************************************************************
- .while TRUE
- invoke GetMessage,addr @stMsg,NULL,0,0
- .break .if eax == 0
- invoke TranslateMessage,addr @stMsg
- invoke DispatchMessage,addr @stMsg
- .endw
- ret
- _WinMain endp
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- start:
- call _WinMain
- invoke ExitProcess,NULL
- ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- end start
通过stud_pe(一个很好的学习PE的工具)我们清楚的看到在这个例子中我们使用了如下导入函数:
user32.dll:
DispatchMessageA
DrawTextA
EndPaint
GetClientRect
GetMessageA
DestroyWindow
PostQuitMessage
RegisterClassExA
ShowWindow
TranslateMessage
UpdateWindow
DefWindowProcA
CreateWindowExA
LoadCursorA
BeginPaint
kernel32.dll:
GetModuleHandleA
ExitProcess
RtlZeroMemory
现在我们从Data Directory开始分析。(以下截图均来自Stud_PE)
IMAGE_DIRECTORY_ENTRY_IMPORT:
IMAGE_DIRECTORY_ENTRY_IAT:
在继续之前我们需要了解两个东西:
1. Data Directory的每一项前四字节表示虚拟地址,后四字节表示大小,本文中不关心。不熟悉的读书可以查阅PE结构获取更多信息。
2. 这里的地址是虚拟地址,也就是载入内存以后的地址。我们如何在文件中也就是物理硬盘上找到对应的内容呢?其实通过Optional Header中的FileAlignment和SectionAlignment可以实现虚拟地址和物理地址的转化。有兴趣的读书可以查阅PE结构获取更多信息,本文将使用stud_pe提供的转化工具完成相应工作。
接下来开始我们逐步分析:
Import table的地址:00002090 通过stud_pe找到物理地址:00000690
IAT的地址:00002000 通过stud_pe找到物理地址:00000600
那么00000690和00000600分别在文件的哪个section呢?通过查看该PE的section table我们发现所有的相关信息都存放在.rdata段了。说到这里简单提一下,data directory中指向的内容都是存在各个section中的,至于存在哪个section主要看section的读写属性,当然也可以另起一个section单独存放某个data entry的内容,比如.reloc就作为一个单独的段存放了所有的重定位信息。
为了方便,我们把整个.rdata段拿出来分析:
在继续之前,我们先大概了解一下import table的知识:
这个图再清晰不过了,不过要彻底搞明白还是要结合例子分析:
输入表是一个 IMAGE_IMPORT_DESCRIPTOR数据结构的数组,这个结构定义如下:
- typedef struct _IMAGE_IMPORT_DESCRIPTOR {
- union {
- DWORD Characteristics;
- DWORD OriginalFirstThunk;
- } DUMMYUNIONNAME;
- DWORD TimeDateStamp;
- DWORD ForwarderChain;
- DWORD Name;
- DWORD FirstThunk;
- } IMAGE_IMPORT_DESCRIPTOR;
这个结构总共20个字节,最重要的有三个:OriginalFirstThunk、Name、FirstThunk。一个IMAGE_IMPORT_DESCRIPTOR包含了从一个dll中导入的信息,在这个例子中我们有两个dll:user32.dll和kernel32.dll,最后用一个全0的IMAGE_IMPORT_DESCRIPTOR表示结束。我们只提取我们关心的东西:
OriginalFirstThunk1: 000020DC --> 000006DC(物理地址)
Name1: 0000220E --> 0000080E
FirstThunk1: 00002010 --> 00000610
OriginalFirstThunk2: 000020CC --> 000006CC(物理地址)
Name2: 0000224C --> 0000084C
FirstThunk2: 00002000 --> 00000600
我们先从简单的开始分析:Name
(前面的地址如00000200不是真实的物理地址,在stud_pe中此地址表示到.rdata的起始地址的偏移,也就是00000600+00000200,因此我们看到user32.dll的物理地址其实是0000080E,也就是Name1的值。kernel32.dll同理)
再继续往下之前,我们先要回到之前PE的import table结构中另外两个结构的定义:
- typedef struct _IMAGE_THUNK_DATA {
- union {
- ULONGLONG ForwarderString; // PBYTE
- ULONGLONG Function; // PDWORD
- ULONGLONG Ordinal;
- ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME
- } u1;
- } IMAGE_THUNK_DATA;
- typedef struct _IMAGE_IMPORT_BY_NAME {
- WORD Hint;
- BYTE Name[1];
- } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
我们看到IMAGE_THUNK_DATA是一个4字节的联合,在本例中其实就是一个IMAGE_IMPORT_BY_NAME的虚拟地址。如果导入函数不是用函数名导入的而是用序号导入的,情况就不一样了,具体是按名字导入还是按序号导入是看IMAGE_THUNK_DATA字段的最高位是否为1决定的。具体可查看PE文件结构。
接下来我们结合例子来看OriginalFirstThunk和FirstThunk两个字段表示的意义(以kernel32.dll为例):
OriginalFirstThunk: 000020CC --> 000006CC
内容如下:(28 22 00 00 1A 22 00 00 3C 22 00 00 00 00 00 00)
FirstThunk: 00002000 --> 00000600
内容如下:(28 22 00 00 1A 22 00 00 3C 22 00 00 00 00 00 00)
我们发现了两个地方:
1. 000006CC和00000600指向的内容是一样的,但是确存放了两个拷贝。
2. 00000600好熟悉啊。。。回头看看原来是data directory中IMAGE_DIRECTORY_ENTRY_IAT的地址!
OriginalFirstThunk和FirstThunk其实是指向了两个不同的表INT和IAT,但是这两个表其实内容是相同的。INT/IAT中的内容在本例中都是虚拟地址,至于该虚拟地址中的数据究竟表示了什么含义,就涉及IMAGE_IMPORT_BY_NAME这个结构体了。我们就取一个例子分析:1A 22 00 00 --> 0000081A(9B 00 45 78 69 74 50 72 6F 63 65 73 73 00)
Hint: 00 9B,这个是用来优化的,具体不展开,在本例中直接可以忽略。
Name: 45 78 69 74 50 72 6F 63 65 73 73 00 == ExitProcess
至于为什么需要用两个相同的表INT/IAT,原因比较复杂,简单的说是为了加快加载速度,能够进行链接时绑定。具体就不展开了,因为跟本文主题没有密切的联系,有兴趣的读者自己查阅相关资料。
对import table的分析基本全部完成了,现在我们再回头来看一下IAT的内容:
除了之前kernel32对应的FirstThunk,还有user32.dll对应的FirstThunk(00000610),也就是说IAT其实包含了该PE文件中所有导入的函数,以00 00 00 00作为一个dll的结尾。IAT对应的data directory的size告诉我们IAT的大小,在本例中50h表示这个IAT总共包含了80字节(20项)。
说到这里其实关于import table跟IAT相关的分析已经结束了,但是既然话题是import相关的,那我们也来分析一下在代码中调用一个输入函数是如何处理的:
比如:invoke ExitProcess,NULL
我们在OD中可以看到对应的机器/汇编指令:E8 5B 00 00 00/CALL 004011CA
004011CA这个地址是做什么用的呢?在OD中我们看到004011CA地址对应的指令如下:
JMP DWORD PTR DS:[402004]
那么402004是什么呢???噢!其实就是文件中对应的物理地址604!402004-400000(exe加载地址)== 2004 --> 604,也就是IAT表中的第二项,那么是1A 22 00 00么?不是!为什么叫IAT(image address table)?因为这个表的内容在加载时会用实在的导入函数地址填充。所以这个时候其实是真正的ExitProcess的地址了,我们可以用OD看到,运行时这个地址是76 0E 73 4E!
其实这个就是调用外部函数通常的做法,先用CALL指令转向一个跳转表:
而这个跳转表其实就是用JMP指令跳转到了IAT中的每一项。但是这个跳转表不是必需的,又是也可以直接用CALL DWORD PTR DS:[XXX]这样的语句实现外部函数的调用,XXX正是IAT中的某一项的地址。具体什么时候系统会创建这个跳转表是个疑问,暂时没有找到规律。