伪加壳器制作记录(增节区/修改导入表)

写在前面:

    真正的加壳器应该包括代码压缩、加解密、增删节区、修改PE头、破坏导入/导出表等内容,但受限于笔者目前的技术水平,还无法完全兼顾这些所有内容,仅记录一些比较关键的原理和操作(之后将另择时间完整地实现一遍壳,届时再考虑是否开新贴还是就此处补充吧)

前置知识:

    至少要知道PE文件格式有哪些内容(我们当然可以现查,只是在查找的时候不要显得那么手足无措的程度就行)

    内容涉及一些基础的API,现查官方文档即可

正文:

    壳的实现在本质上似乎与ShellCode注入并没有太多不同,它们是往PE文件里插入额外的数据以附加原文件本身所没有的功能。

    大致流程未:将PE中的一部分数据进行加密与压缩,为并其写入新的解密解压代码;将OEP设到解密解压代码处,程序运行时将优先运行这些代码将数据恢复到加壳之前,最后移交控制权给源程序。

    从这方面入手,那么就需要往PE文件中插入一些新的数据以实现这些功能。若是往区中添加这些数据,难免会遇上相当多麻以有的节烦的事情(但Shellcode的注入就应该如此,否则将很容易被识破),所以我们应该为文件添加一个额外的节区以及相应的节区头,在这个新的节区中插入代码。

    有关PE文件结构的基础知识在这里不再赘述,简短的用一张图片来表示它的全貌吧。

伪加壳器制作记录(增节区/修改导入表)_第1张图片

代码实现与分析:(添加节区的实现代码其主要结构引自《反病毒攻防研究》,但似乎原博主已经删稿了,有些可惜.....)

    首先是一些基础的变量:

#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数组中的指针指向我们抄好的表,从而实现这项工作

    (可能也有人会奇怪,为什么不能直接跳过这些内容,只加密其他内容呢?事实是,这么做将非常繁琐,并且也有悖于壳的初衷;以及这样做的话,将很容易被一些反编译工具识破导入表的内容)

    

 

 

    

你可能感兴趣的:(伪加壳器制作记录(增节区/修改导入表))