写在前面:
真正的加壳器应该包括代码压缩、加解密、增删节区、修改PE头、破坏导入/导出表等内容,但受限于笔者目前的技术水平,还无法完全兼顾这些所有内容,仅记录一些比较关键的原理和操作(之后将另择时间完整地实现一遍壳,届时再考虑是否开新贴还是就此处补充吧)
前置知识:
至少要知道PE文件格式有哪些内容(我们当然可以现查,只是在查找的时候不要显得那么手足无措的程度就行)
内容涉及一些基础的API,现查官方文档即可
正文:
壳的实现在本质上似乎与ShellCode注入并没有太多不同,它们是往PE文件里插入额外的数据以附加原文件本身所没有的功能。
大致流程未:将PE中的一部分数据进行加密与压缩,为并其写入新的解密解压代码;将OEP设到解密解压代码处,程序运行时将优先运行这些代码将数据恢复到加壳之前,最后移交控制权给源程序。
从这方面入手,那么就需要往PE文件中插入一些新的数据以实现这些功能。若是往区中添加这些数据,难免会遇上相当多麻以有的节烦的事情(但Shellcode的注入就应该如此,否则将很容易被识破),所以我们应该为文件添加一个额外的节区以及相应的节区头,在这个新的节区中插入代码。
有关PE文件结构的基础知识在这里不再赘述,简短的用一张图片来表示它的全貌吧。
代码实现与分析:(添加节区的实现代码其主要结构引自《反病毒攻防研究》,但似乎原博主已经删稿了,有些可惜.....)
首先是一些基础的变量:
#define _CRT_SECURE_NO_WARNINGS 1
#include
#define FILENAME L"helloword.exe" //文件名
char szSecName[] = ".alice";
//所添加的节区名称
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 hFilecopy = 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;
}
//读取PE文件并创建映射(模拟PE文件映射到内存中的结构)
hFile = CreateFile(FILENAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
hFilecopy = hFile;//创建拷贝
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;//DOS头指针
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;//新节区头的指针
BYTE* content = (BYTE*)lpBase+pSecHeader->PointerToRawData;//指向text段的指针(用于加密)
double sizetext = pSecHeader->SizeOfRawData;//加密的长度
PIMAGE_IMPORT_DESCRIPTOR TABLE = NULL;//用于指向导入表的指针
PIMAGE_SECTION_HEADER pSecHeader2 = pSecHeader;//节区头拷贝
PIMAGE_DATA_DIRECTORY pDir = &pNtHeader->OptionalHeader.DataDirectory[1];//指向导入表的指针
int tmpAddr = pDir->VirtualAddress;//抄出导入表内容
int tmpSize = pDir->Size;
//检验部分,测试PE文件是否被正确读入(通过这里也大致能够发现,如果某些壳修改了PE的文件头,那么其他壳可能就不能再次加壳了)
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBase;
PIMAGE_NT_HEADERS pNtHeader = NULL;
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;
}
//初始化新节区头的数据
//拷贝节区名称
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;
//修正代码长度(只在添加代码时才需修改此项)
pNtHeader->OptionalHeader.SizeOfCode += pTmpSec->SizeOfRawData;
//修正程序的入口地址(只在添加代码并想让ShellCode提前执行时才需修改此项
pNtHeader->OptionalHeader.AddressOfEntryPoint = pTmpSec->VirtualAddress+tmpSize;
pDir->VirtualAddress = pTmpSec->VirtualAddress;//重建导入表的地址
//RVA to FOA
for (int i = 0; i < nSecNum; i++)
{
if (pSecHeader2->VirtualAddress <= tmpAddr && pSecHeader2->VirtualAddress + pSecHeader2->SizeOfRawData >= tmpAddr)
{
tmpAddr = tmpAddr - pSecHeader2->VirtualAddress + pSecHeader2->PointerToRawData;
break;
}
else
{
pSecHeader2++;
}
}
//添加节区的功能函数
void AddSectionData(int nSecSize, LPVOID lpBase,int Address,int Size)
{
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);
//抄写导入表
BYTE* tmpP=new BYTE[Size];
tmpP[Size] = 0;
BYTE* content = (BYTE*)lpBase + Address;
for (int i = 0; i < Size; i++)
{
tmpP[i] = content[i];
}
WriteFile(hFile, tmpP, Size, &dwNum, NULL);
//在文件的末尾写入ShellCode
WriteFile(hFile, shellcode, strlen(shellcode) + 3, &dwNum, NULL);
//在ShellCode的末尾用00补充满
WriteFile(hFile, pByte, nSecSize - Size-(strlen(shellcode) + 3), &dwNum, NULL);
//WriteFile(hFile, pByte, nSecSize - ((ULONG)print_hex + 3 - (ULONG)print_hex_end), &dwNum, NULL);
FlushFileBuffers(hFile);
free(pByte);
}
代码本身并不复杂,也没有什么复杂的逻辑。如果能够通读一遍代码,大致就能理顺实现流程了。
私以为阅读代码是最好的理解方式,只有读不懂的时候才需要额外的解释(指我这个废物一定要人说一遍才能看懂)
但我仍有必要复述一遍其过程以方便阅读:
1. 首先通过读取模块获取PE结构的映射表(lpBase指针指向表头,一切偏移都可以通过这个指针实现)
2. 检测PE文件的读取是否顺利(通过判断 MZ签名 和 PE签名)
3. 获取基本变量。
首先,我们的当前目标是添加一个节区头,并且这个节区头应该被写在已有节区头的末尾。那么我们需要知道"现在有几个节区"、“文件/节区的对齐因数”、“指向节区头的指针”、“指向新节区头的指针”
代码的实现通过Windows.h中已经封装的结构体指针实现,该头文件已经包括了这些固定名称的结构体的声明。(指向新节区头的指针就是通过结构体指针与偏移得到的)
接着便应该为这个新的节区头填入一些必要的数据(这些数据研究代码便可明了),以及获取原本的OEP(解密之后提供跳转回原程序)
在我更加深入的了解PE结构之前,我本以为只要这样便能够实现一个简单的伪壳,但事实并非如此。
拓展:
在PE结构的可选头中存在一个PIMAGE_DATA_DIRECTORY数组,其第二个元素为导入表地址,实例程序的地址将指向TEXT段。
什么是导入表?PE文件都将经过PE装载器后才能运行在操作系统上,而这个PE装载器起到了一个初始化的作用。
简单来说,装载器的工作是“寻址”。
所有的PE结构要想运行在Windwos系统上,都需要调用一些已有的DLL(如Kernel.dll或USER32.dll),但PE文件是不知道这些动态链接库存放在计算机的何处的(如果你写死了它的地址,那么一旦版本变换,这些DLL可能也跟着迁家,那么你的程序很可能就无法在另外一个版本的同一个系统中使用了)
于是装载器将会告诉文件这些DLL在哪里。
但是,前提是文件会告诉装载器,它想要哪些DLL。
也就是说,加密TEXT段之后,你的程序很可能没办法告诉装载器它需要什么,导致程序甚至无法运行,直接损坏。
(可以不妨去看看这些内容放在了哪,有的在TEXT段,有的则在DATA段,也有的在别的段,所以你没办法不做任何准备的直接破坏这些数据)
于是我的解决方法是“抄表”——在加密之前把这张表原封不动地抄写到新节区里,再将PIMAGE_DATA_DIRECTORY数组中的指针指向我们抄好的表,从而实现这项工作
(可能也有人会奇怪,为什么不能直接跳过这些内容,只加密其他内容呢?事实是,这么做将非常繁琐,并且也有悖于壳的初衷;以及这样做的话,将很容易被一些反编译工具识破导入表的内容)