New Microsoft 通用ShellCode深入剖析文档格式.docx
《New Microsoft 通用ShellCode深入剖析文档格式.docx》由会员分享,可在线阅读,更多相关《New Microsoft 通用ShellCode深入剖析文档格式.docx(37页珍藏版)》请在冰点文库上搜索。
[目录]
1,PE文件结构的简介,及PE引出表的分析.
1.1PE文件简介
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(PortableExecutable,移植的执行体),是微软Win32环境可执行文件的标准格式
(所谓可执行文件不光是.EXE文件,还包?
DLL/.VXD/.SYS/.VDM等)
PE文件结构(简化):
-----------------
│1,DOSMZheader│
│2,DOSstub│
│3,PEheader│
│4,Sectiontable│
│5,Section1│
│6,Section2│
│Section...│
│n,Sectionn│
记得在我还没有接确Win32编程时,我曾在Dos下运行过一个Win32可执行文件,程序只输出
了一行"
ThisprogramcannotberuninDOSmode."
我觉得很有意思,它是怎么识别自
己不在Win32平台下的呢?
其实它并没有进行识别,它可能简单到只输入这一行文字就退出
了,可能源码就像下面的C程序这么简单:
#include<
stdio.h>
voidmain(void)
{
printf("
ThisprogramcannotberuninDOSmode.\n"
);
}
你可能会问"
我在写Win32程序时并没有写过这样的语句啊?
"
其实这是由连接器(linker)
为你构建的一个16位DOS程序,当在16位系统(DOS/Windows3.x)下运行Win32程序时它才会
被执行用来输出一串字符提示用户"
这个程序不能在DOS模式下运行"
.
我们先来看看DOSMZheader到底是什么东西,下面是它在Winnt.h中的结构描述:
typedefstruct_IMAGE_DOS_HEADER{//DOS.EXEheader
WORDe_magic;
//0x00Magicnumber
WORDe_cblp;
//0x02Bytesonlastpageoffile
WORDe_cp;
//0x04Pagesinfile
WORDe_crlc;
//0x06Relocations
WORDe_cparhdr;
//0x08Sizeofheaderinparagraphs
WORDe_minalloc;
//0x0aMinimumextraparagraphsneeded
WORDe_maxalloc;
//0x0cMaximumextraparagraphsneeded
WORDe_ss;
//0x0eInitial(relative)SSvalue
WORDe_sp;
//0x10InitialSPvalue
WORDe_csum;
//0x12Checksum
WORDe_ip;
//0x14InitialIPvalue
WORDe_cs;
//0x16Initial(relative)CSvalue
WORDe_lfarlc;
//0x18Fileaddressofrelocationtable
WORDe_ovno;
//0x1aOverlaynumber
WORDe_res[4];
//0x1cReservedwords
WORDe_oemid;
//0x24OEMidentifier(fore_oeminfo)
WORDe_oeminfo;
//0x26OEMinformation;
e_oemidspecific
WORDe_res2[10];
//0x28Reservedwords
LONGe_lfanew;
//0x3cFileaddressofnewexeheader
}IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
DOSMZheader中包括了一些16位DOS程序的初使化值如果IP(指令指针),cs(代码段寄存
器),需要分配的内存大小,checksum(校验和)等,当DOS准备为可执行文件建立进程时会读取其
中的值来完成初使化工作.
留意到最后一个结构成员了吗?
微软的人对它的描述是Fileaddressofnewexeheader
意义是"
新的exe文件头部地址"
它是一个相对偏移值,我想文件偏移量你一定知道是什么吧!
e_lfanew就是一个文件偏移值,它指向PEheader,它对我们来说非常重要.紧跟着DOSMZheader
的是DOSstub它是linker为我们建立的这个16位DOS程序的代码实体部分,就是它输出了
.再后面就是PEheader了,有人曾问过我PE头部
相对于.exe文件的偏移是不是固定的?
这个可不好说,不同的编译器生成的stub长度可能不一样
(比如:
它可能存储了这样一个字串来提示用户"
TheCurrnetOSisnotWin32,Iwanttorun
inWin32Mode."
那么这个stub的长度将比前面的那个长),所以用一个固定值来定位PEheader
是不科学的,这个时候我们就用到了e_lfanew,它指向真正的PEheader,它总是正确吗?
那是当然
的!
linker总是会它赋予一个正确的值.所以我们要它精确定位PEheader,同样的Win32PELoader
也根据e_lfanew来定位真正的PEheader,并使用PEheader中的不同的成员值进行初使化,PE还
包涵了很多个"
节"
(Section),有用来存储数据的,有用来存可执行代码的,还有的是用来存资源
的(如:
程序图标,位图,声音,对话框模板等)
下面我只简单分析一下PE结构与编写ShellCode相关的部分,如果你对其它部分也比较感兴趣
可以看看台港侯俊杰先生译的<
Windows95系统程序设计大奥秘>
中的相关内容以及Iczelion的经
典PE教程,我个人觉得将两者结合起来看要好一点.
2,引出表分析
在PEheader结构(你可以Winnt.h中找到它)中包括一个DataDirectory结构成员数组,可以通
过这样的方法来找到它的位置:
PE头部偏移=可执行文件内存映象基址+0x3c(e_lfanew)
PE基址=可执行文件内存映象基址+PE头部偏移
引出表目录指针(IMAGE_EXPORT_DIRECTORY*)=PE基址+0x78<
=---DataDirectory
引出函数名称表首指针(char**)=引出表目录基址+0x20
引出函数地址表首指针(DWORD**)=引出表目录指针+0x1c
它的结构定义是这样的:
typedefstruct_Image_Data_Directory{
DWORDVirtualAddress;
DWORDisize;
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
该结构数组共包括16成员,第一个成员的VirtualAddress存储了一个相对偏移量,它指向一个
IMAGE_EXPORT_DIRECTORY结构,它的定义是这样的:
typedefstruct_IMAGE_EXPORT_DIRECTORY{
DWORDCharacteristics;
//0x00
DWORDTimeDateStamp;
//0x04
WORDMajorVersion;
//0x08
WORDMinorVersion;
//0x0a
DWORDName;
//0x0c
DWORDBase;
//0x10
DWORDNumberOfFunctions;
//0x14
DWORDNumberOfNames;
//0x18
DWORDAddressOfFunctions;
//0x1cRVAfrombaseofimage
DWORDAddressOfNames;
//0x20RVAfrombaseofimage
DWORDAddressOfNameOrdinals;
//0x24RVAfrombaseofimage
}IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
其中AddressOfFunctions里又存储了一个二级指针,它指向一个DWORD型指针数组该数
组成员所指就是函数地址值,但其中的值是函数相对于可执行文件在内存映象中基地址的一
个相对偏移值,真正的函数地址等于这个相对偏移值+可执行文件在内存映象中的基地址,我
们可以Call这个计算后的真实地址来调用函数.AddressOfNames是一个二级字符指针,该数组
成员所指就是函数名称字符串相对于可执行文件在内存映象中的基地址的一个偏移值,同样
可以通过相对偏移值+可执行文件在内存映象中的基地址来引用函数名称字串.Name也是一个
字符指针,它也只存储了相对偏移值,如果是kernel32的IMAGE_EXPORT_DIRECTORY那么它指向
的字串就为"
KERNEL32.dll"
3,本节应用实例
关于PE和引出表我们已经分析了与编写ShellCode密切相关的部分,这一部分的确有点难,
但一定要把它搞清楚,只有把它搞懂我们才能进行下一节的学习,在本节的最后附上一个小程序,
在内联汇编代码中大量使用了"
间接引用"
如果你对指针很熟悉基本上它很好理解,在程序里我
们实现了WindowsAPIGetProcAddress的功能,这种技术对于想使用一些未公开的系统函数也是
非常之有用的.
-----------------------------------------------------
GetFunctionByName函数可以从一个PE执行文件中以函数名查找引出表并返回引出函数地址,只
需要知道KERNEL32.DLL的基地址值,使用它在本程序中我们不包括头文件也可以使用任何一个
WindowsAPI.在我的机器上它是0x77e60000程序如下:
//GetFunctionByName.c
//原型:
DWORDGetFunctionByName(DWORDImageBase,constchar*FuncName,intflen);
//参数:
//ImageBase:
可执行文件的内存映象基址
//FuncName:
函数名称指针
//flen:
函数名称长度
//返回值:
//函数成功时返回有效的函数地址,失败时返回0.
//最终在写ShellCode时,应该给该函数加上__inline声明,因为它要与ShellCode融为一体.
//注意,在本例中我们没有包括任何一个.h文件
unsignedintGetFunctionByName(unsignedintImageBase,constchar*FuncName,intflen)
unsignedintFunNameArray,PE,Count=0,*IED;
__asm
moveax,ImageBase
addeax,0x3c//指向PE头部偏移值e_lfanew
moveax,[eax]//取得e_lfanew值
addeax,ImageBase//指向PEheader
cmp[eax],0x00004550
jneNotFound//如果ImageBase句柄有错
movPE,eax
moveax,[eax+0x78]
addeax,ImageBase
mov[IED],eax//指向IMAGE_EXPORT_DIRECTORY
//moveax,[eax+0x0c]
//addeax,ImageBase//指向引出模块名,如果在查找KERNEL32.DLL的引出函数那么它将指向"
//moveax,[IED]
moveax,[eax+0x20]
movFunNameArray,eax//保存函数名称指针数组的指针值
movecx,[IED]
movecx,[ecx+0x14]//根据引出函数个数NumberOfFunctions设置最大查找次数
FindLoop:
pushecx//使用一个小技巧,使用程序循环更简单
moveax,[eax]
movesi,FuncName
movedi,eax
movecx,flen//逐个字符比较,如果相同则为找到函数,注意这里的ecx值
cld
repcmpsb
jneFindNext//如果当前函数不是指定的函数则查找下一个
addesp,4//如果查找成功,则清除用于控制外层循环而压入的Ecx,准备返回
moveax,[IED]
moveax,[eax+0x1c]
addeax,ImageBase//获得函数地址表
shlCount,2//根据函数索引计算函数地址指针=函数地址表基址+(函数索引*4)
addeax,Count
moveax,[eax]//获得函数地址相对偏移量
addeax,ImageBase//计算函数真实地址,并通过Eax返回给调用者
jmpFound
FindNext:
incCount//记录函数索引
add[FunNameArray],4//下一个函数名指针
moveax,FunNameArray
popecx//恢复压入的ecx(NumberOfFunctions),进行计数循环
loopFindLoop//如果ecx不为0则递减并回到FindLoop,往后查找
NotFound:
xoreax,eax//如果没有找到,则返回0
Found:
/*
让我们来测试一下,先用GetFunctionByName获得kernel32.dll中LoadLibraryA
的地址,再用它装载user32.dll,再用GetFunctionByName获得MessageBoxA的地址,call
它一下
*/
intmain(void)
chartitle[]="
test"
user32[]="
user32"
msgf[]="
MessageBoxA"
;
unsignedintloadlibfun;
loadlibfun=GetFunctionByName(0x77e60000,"
LoadLibraryA"
12);
//0x77e60000是我机器上的kernel32.dll的基址,不同机器上的值可能不同
leaeax,user32
pusheax
calldwordptrloadlibfun//相当于执行LoadLibrary("
leaebx,msgf
push0x0b//"
的长度
pushebx
callGetFunctionByName
movebx,eax
addesp,0x0c//GetFunctionByName使用C调用约定,由调用者调整堆栈
push0
leaeax,title
callebx//相当于执行MessageBox(NULL,"
"
MB_OK)
return1;
函数的内联汇编代码有很多这样的语句:
moveax,[somewhere]
moveax,[eax+0x?
?
]
我试过使用moveax,[ImageBase+eax+0x?
]之类的语法,因为用到很多多级指针,而它们指向
的又是相对偏移量所以要不断的"
获取和计算"
否则很容易导致"
访问违例"
.编译运行,弹出了
一个MessageBox标题和内容都是"
看到了吗?
你可能会问这个程序拿到其它机器上也可能
运行吗?
在整个程序里我们唯一依赖的就是0x77e60000这个kernel32.dll基址,其它机器上的
可能不是这个值,如果这个地址值可以在程序运行时动态的计算出来,那么这个程序将非常通
用,它可以动态计算出来吗?
答案是肯定的!
下一节我们将来分析一种并不很流行但很通用的动
态计算获得kernel32.dll基址的方法.
---------------------------------------------------------------------------------
二,在动态获得Kernel32.DLL地址方法的分析
1,简析结构化异常处理(SEH,StructredExceptionHandling)
SEH已经不是很什么新技术了,但是对于我将要讲了非常重要,所以在这里对它做一个简单的
分析.Ok,打开VC,让我们来分析一个简单的"
除"
运算程序,看看它哪里有问题:
conio.h>
intx,y,z=y=x=0;
Inputtwointegernumber:
scanf("
%d%d"
&
x,&
y);
z=x/y;
%dDIV%d=%d"
x,y,z);
getch();
return0;
编译,运行:
输入42,程序输出"
4DIV2=2"
结果很正确.再运行输入40,问题出来了,
VisualStudio弹出了一个信息框:
Unhandledexceptioninseh.exe:
0xC0000094:
IntegerDividebyZero"
出现了未处理的
除0异常"
传统的方法是我们在z=x/y之前加上判断:
if(!
y)
CannotDividebyZero!
gotoLQUIT;
LQUIT:
出错处理在这个小程序里这的确很容易看懂,可是想想如果在数千甚至上万行的程序里,这样的
错误捕获处理会让程序变的十分凌乱难懂,而且传统方法处理的是我们可以想像(猜测)到的错误,
但是某些导到程序出错的情况是很随机的,这样就不能保证程序的健壮性了,而SEH正是为了让正
常的处理代码和出错处理代码分开,以使程序结构清淅,并使程序更加
健壮.让我们再把这个小程序改一下:
windows.h>
InputTwoIntegerNumber:
__try
{//把可能出错的程序段封装起来
//......
__except(EXCEPTION_EXECUTE_HANDLER)
{//在这里找出出现异常的原因,并进行处理
switch(GetExceptionCode())
caseEXCEPTION_INT_DIVIDE_BY_ZERO:
//如果除0异常
caseEXCEPT