前言: 在网上关于ShellCode编写技术的文章已经非常之多,什么理由让我再写这种技术文章呢?本文是我上一篇溢出技术文章<Windows 2000缓冲区溢出技术原理>的姊妹篇,同样的在网上我们经常可以看到一些关于ShelCode编写技术的文章,似乎没有为初学者准备的,在这里我将站在初学者的角度对通用ShellCode进行比较详细的分析,有了上一篇的溢出理论和本篇的通用ShellCode理论,基本上我们就可以根据一些公布的Window溢出漏洞或是自己对一些软件系统进行反汇编分析出的溢出漏洞试着编写一些溢出攻击测试程序. 文章首先简单分析了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程序这么简单:
#include <stdio.h> 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相关的部分,如果你对其它部分也比较感兴趣可以看看台港侯俊杰先生译的<Windows 95系统程序设计大奥秘>中的相关内容以及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 APIGetProcAddress的功能,这种技术对于想使用一些未公开的系统函数也是非常之有用的.
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基址的方法.
二,在动态获得Kernel32.DLL地址方法的分析
1,简析结构化异常处理(SEH,Structred Exception Handling) SEH已经不是很什么新技术了,但是对于我将要讲了非常重要,所以在这里对它做一个简单的分析.Ok,打开VC,让我们来分析一个简单的"除"运算程序,看看它哪里有问题:
#include <stdio.h> #include <conio.h> int main(void) { int x,y,z=y=x=0; printf("Input two integer number:"); scanf("%d %d",&x,&y); z=x/y; printf("%d DIV %d = %d",x,y,z); getch(); return 0; } 编译,运行:输入4 2,程序输出"4 DIV 2 = 2",结果很正确.再运行输入 4 0,问题出来了,Visual Studio弹出了一个信息框: "Unhandled exception in seh.exe:0xC0000094:Integer Divide by Zero",出现了未处理的 "除0异常",传统的方法是我们在z=x/y之前加上判断: #include <stdio.h> #include <conio.h> int main(void) { int x,y,z=y=x=0; printf("Input two integer number:"); scanf("%d %d",&x,&y); if(!y) { printf("Can not Divide by Zero!"); goto LQUIT; } z=x/y; printf("%d DIV %d = %d",x,y,z); LQUIT: getch(); return 0; } 出错处理在这个小程序里这的确很容易看懂,可是想想如果在数千甚至上万行的程序里,这样的错误捕获处理会让程序变的十分凌乱难懂,而且传统方法处理的是我们可以想像(猜测)到的错误,但是某些导到程序出错的情况是很随机的,这样就不能保证程序的健壮性了,而SEH正是为了让正常的处理代码和出错处理代码分开,以使程序结构清淅,并使程序更加 健壮.让我们再把这个小程序改一下: #include <stdio.h> #include <conio.h> #include <windows.h>
int main(void) { int x,y,z=y=x=0; printf("Input Two Integer Number:"); scanf("%d %d",&x,&y); __try {//把可能出错的程序段封装起来 z=x/y; //...... } __except(EXCEPTION_EXECUTE_HANDLER) {//在这里找出出现异常的原因,并进行处理 switch(GetExceptionCode()) { case EXCEPTION_INT_DIVIDE_BY_ZERO://如果除0异常 { printf("Can not Divide by Zero!"); goto LQUIT; } case EXCEPTION_ACCESS_VIOLATION://内存访问违例 { //..... break; } //do other...... default: break; } } printf("%d DIV %d = %d/n",x,y,z); LQUIT: getch(); return 0; } 这样我们就使终都可以捕获到异常了,编译,选择"Disassembly",可以看到这样的代码: push offset __except_handler3 (00401330) mov eax,fs:[00000000] push eax mov dword ptr fs:[0],esp 这是实际上是标准的SEH异常处理函数的注册方法,我们的__except(){}实际在编译时被当成一个线程相关的异常处理函数,实际上这段代码的作用是将我们的异常处理函数加入异常处理结构链表EXCEPTION_REGISTRATION_RECORD,fs:[0]是这个异常处理函数链表的首指针,它的最后一条记录的节点指针指向0xffffffff.它的结构描述是这样的:
typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD * pNext; //指向后面的节点 FARPROC pfnHandler;//指向异常处理函数 } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
你可能会问"你怎么知道fs:[0]是该结构的首指针呢?",当然我没有那么天才,从Windows 95系统程序设计一书中可以得知每当创建一个线程,系统均会为每个线程分配TEB(Thread Environment Block)在Windows 9x中被称为TIB(Thread Information Block),而且TEB永远放在fs段选择器指定的数据段的0偏移处. 再看一下TEB的结构定义你就会明白的: typedef struct _TIB { PEXCEPTION_REGISTRATION_RECORD pvExcept; // 00h Head of exception record list<=---注意这个指针成员
PVOID pvStackUserTop; // 04h Top of user stack PVOID pvStackUserBase; // 08h Base of user stack
union // 0Ch (NT/Win95 differences) { struct // Win95 fields { WORD pvTDB; // 0Ch TDB WORD pvThunkSS; // 0Eh SS selector used for thunking to 16 bits DWORD unknown1; // 10h } WIN95;
struct // WinNT fields { PVOID SubSystemTib; // 0Ch ULONG FiberData; // 10h } WINNT; } TIB_UNION1; <  
说明:本教程来源互联网或网友上传或出版商,仅为学习研究或媒体推广,wanshiok.com不保证资料的完整性。
1/2 1 2 下一页 尾页 |