羽夏壳世界—— PE 解析的实现

写在前面

  此系列是本人一个字一个字码出来的,包括代码实现和效果截图。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏壳世界——序 ,方便学习本教程。

PE 解析实现

  既然要做一个加密保护壳,就首先得会解析它。在C++中面向对象的编程的思想体现的比较明显,那么我就建一个类,名为CWingProtect(一看就是老MFC了)。然后头文件文件内容如下:

#pragma once

class CWingProtect
{
};

  里面空空如也,啥也没有。为了保证别人用我的东西并且表示是我写的东西,我加了一个命名空间,它就成了这样:

#pragma once

namespace WingProtect
{
    class CWingProtect
    {
    };
}

  好,我们需要写一个构造函数,用来解析PE文件。然后调用指定函数来进行保护,那么加上构造函数就这样了:

#pragma once

namespace WingProtect
{
    class CWingProtect
    {
    public:
        CWingProtect(const TCHAR* filename, UINT pagecount = 10);
    };
}

  有构造函数就必然析构函数,加上:

#pragma once

namespace WingProtect
{
    class CWingProtect
    {
    public:
        CWingProtect(const TCHAR* filename, UINT pagecount = 10);
        ~CWingProtect();
    };
}

  下面我们就开始实现这个构造函数了,思考我们要干什么?
  我们首先得解析PE文件,并把相应的处理信息存储起来,放到结构体是再好不过的选择。不过一切的一切前提你得把该文件打开,一种方式是调用文件读写相关的函数,不过这样效率比较低,且不方便。另一种方式直接内存映射的方式,这样读取数据就和在自己读取自己程序的变量一样容易。好,下面我们开始:

CWingProtect::CWingProtect(const TCHAR* filename,UINT pagecount)
{
    //初始化分析需要用到的内存
    auto alloc = AllocPageSizeMemory();
    if (!alloc)
    {
        _lasterror = ParserError::CannotAllocMemory;
        return;
    }
    memset(alloc, 0, PageSize);
    peinfo.AnalysisInfo.ImportDllName = (DllImportName*)alloc;

    alloc = AllocReadWriteMem(PageSize * 10);
    if (!alloc)
    {
        _lasterror = ParserError::CannotAllocMemory;
        return;
    }
    memset(alloc, 0, PageSize * 10);
    peinfo.AnalysisInfo.ImportFunNameTable = (char*)alloc;

    alloc = AllocPageSizeMemory();
    if (!alloc)
    {
        _lasterror = ParserError::CannotAllocMemory;
        return;
    }
    memset(alloc, 0, PageSize);
    peinfo.AnalysisInfo.DllFirstThunks = (UINT*)alloc;

    //进入分析正题

    _lasterror = ParserError::LoadingFile;
    if (wcscpy_s(_filename, filename))
    {
        _lasterror = ParserError::InvalidFileName;
        return;
    }

    _lasterror = ParsePE();

    //如果出错的话,清理相关资源
    switch (_lasterror)
    {
    case ParserError::InvalidPE:
        UnmapViewOfFile(hmap);
        mapping = NULL;
    case ParserError::MapViewOfFileError:
        if (hmap) CloseHandle(hmap);
    case ParserError::FileMappingError:
        CloseHandle(hfile);
        ::memset(&peinfo, 0, sizeof(PEInfo));
        break;
    case ParserError::OpenFileError:
        break;
    case ParserError::TooManyImportDlls:
    case ParserError::TooManyImportFunctions:
        EnableIATEncrypt = FALSE;
        break;
    default:
        break;
    }

    //解析完毕后创建一个节赋值
    peinfo.WingSection = new IMAGE_SECTION_HEADER{};
    peinfo.WingSection->VirtualAddress = peinfo.AnalysisInfo.MinAvailableVirtualAddress;

    alloc = AllocReadWriteMem(PageSize * pagecount);
    if (!alloc)
    {
        _lasterror = ParserError::CannotAllocMemory;
        return;
    }
    memset(alloc, 0, PageSize * pagecount);
    peinfo.WingSecitonBuffer = alloc;

    auto filesize = peinfo.FileSize.QuadPart;
    alloc = AllocReadWriteMem(filesize);
    if (!alloc)
    { 
        _lasterror = ParserError::CannotAllocMemory;
        return;
    }
    packedPE = alloc;
    memcpy_s(packedPE, filesize, mapping, filesize);
}

  结果上来一看,一堆乱七八糟的代码,什么嘛,猛地一看也没有啥解析PE相关的函数。在解析PE前,我们要做一些初始化操作,比如里面调用分配内存相关的函数。并且你解析的PE不一定是合法的,难免会有错误,最后需要一个返回值表示出错原因(当然最好是加一个异常处理try-catch,当然我懒就没有加,如果以后做这方面的工作,一定要加上)。
  其他细节请自行参考我的代码,因为这些代码都已成体系,拆解起来比较麻烦,我们就介绍我们本节比较重要的函数ParsePE
  如下是其代码:

//
// GNU AFFERO GENERAL PUBLIC LICENSE
//Version 3, 19 November 2007
//
//Copyright(C) 2007 Free Software Foundation, Inc.
//Everyone is permitted to copyand distribute verbatim copies
//of this license document, but changing it is not allowed.
// Author : WingSummer (寂静的羽夏)
// 
//Warning: You can not use it for any commerical use,except you get 
// my AUTHORIZED FORM ME!This project is used for tutorial to teach
// the beginners what is the PE structure and how the packer of the PE files works.
// 
// 注意:你不能将该项目用于任何商业用途,除非你获得了我的授权!该项目用来
// 教初学者什么是 PE 结构和 PE 文件加壳程序是如何工作的。
//

ParserError CWingProtect::ParsePE()
{
    if (!PathFileExists(_filename))
    {
        return ParserError::FileNotFound;
    }
    if (PathIsDirectory(_filename))
    {
        return ParserError::InvalidFile;
    }
    hfile = CreateFile(_filename, FILE_READ_ACCESS, FILE_SHARE_WRITE | FILE_SHARE_READ,
        NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hfile == INVALID_HANDLE_VALUE)
    {
        return ParserError::OpenFileError;
    }
    GetFileSizeEx(hfile, &peinfo.FileSize);
    hmap = CreateFileMapping(hfile, NULL, PAGE_READONLY, NULL, NULL, NULL);
    if (!hmap)
    {
        return ParserError::FileMappingError;
    }
    mapping = MapViewOfFile(hmap, FILE_MAP_READ, 0, 0, 0);
    if (!mapping)
    {
        return ParserError::MapViewOfFileError;
    }
    auto dosHeader = (PIMAGE_DOS_HEADER)mapping;
    if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
    {
        return ParserError::InvalidPE;
    }
    peinfo.ntHeaderOffset = dosHeader->e_lfanew;
    auto ntHeader = (PIMAGE_NT_HEADERS)OFFSET(mapping, dosHeader->e_lfanew);
    if (ntHeader->Signature != IMAGE_NT_SIGNATURE)
    {
        return ParserError::InvalidPE;
    }
    auto bits = *(WORD*)OFFSET(ntHeader, sizeof(DWORD) + IMAGE_SIZEOF_FILE_HEADER);
    switch (bits)
    {
    case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
        return Parse32(ntHeader);
    case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
        return Parse64(ntHeader);
    default:
        return ParserError::InvalidPE;
    }
    return ParserError::Success;
}

  直到这行代码开始,之前都是进行校验和加载文件到内存的操作:

auto dosHeader = (PIMAGE_DOS_HEADER)mapping;

  拿到一个PE文件,首先就要检验它的合法性,比如上来这几行代码:

if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
    return ParserError::InvalidPE;
}
peinfo.ntHeaderOffset = dosHeader->e_lfanew;
auto ntHeader = (PIMAGE_NT_HEADERS)OFFSET(mapping, dosHeader->e_lfanew);
if (ntHeader->Signature != IMAGE_NT_SIGNATURE)
{
    return ParserError::InvalidPE;
}

  初步校验PE文件合法之后,我们就开始解析文件了,由于PE文件有32位和64位的,我们需要通过一个标识进行获取,如下代码所示:

auto bits = *(WORD*)OFFSET(ntHeader, sizeof(DWORD) +  IMAGE_SIZEOF_FILE_HEADER);
switch (bits)
{
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
    return Parse32(ntHeader);
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
    return Parse64(ntHeader);
default:
    return ParserError::InvalidPE;
}

  这个代码非指针的初学者可能做不出来,这个就是获取IMAGE_OPTIONAL_HEADERMagic成员,由于IMAGE_FILE_HEADER大小是固定的,再加上Signature成员的大小,就是我们所谓判定位数的成员。如果是32位,就调用Parse32;如果是64位的,就调用Parse64,否则就是非法文件。
  Parse32Parse64代码几乎是一样的,所以我只以Parse64为例开始介绍,如下是该函数的代码:

ParserError CWingProtect::Parse32(PIMAGE_NT_HEADERS ntHeader)
{
    is64bit = FALSE;
    auto nt = (PIMAGE_NT_HEADERS32)ntHeader;
    peinfo.PNumberOfSections = (INT3264)&nt->FileHeader.NumberOfSections;
    auto f = nt->FileHeader;
    peinfo.NumberOfSections = f.NumberOfSections;

    peinfo.PAddressOfEntryPoint = (INT3264)&nt->OptionalHeader.AddressOfEntryPoint;
    peinfo.PSizeOfImage = (INT3264)&nt->OptionalHeader.SizeOfImage;
    peinfo.PSizeOfHeaders = (INT3264)&nt->OptionalHeader.SizeOfHeaders;
    auto o = nt->OptionalHeader;
    peinfo.AddressOfEntryPoint = o.AddressOfEntryPoint;
    peinfo.Subsystem = o.Subsystem;

    if (o.Subsystem == 1)
        return ParserError::DriverUnsupport;

    peinfo.FileAlignment = o.FileAlignment;
    peinfo.SectionAlignment = o.SectionAlignment;
    peinfo.ImageBase = o.ImageBase;
    peinfo.SizeOfImage = o.SizeOfImage;
    peinfo.SizeOfHeaders = o.SizeOfHeaders;
    peinfo.POptionalHeaderDllCharacteristics = &nt->OptionalHeader.DllCharacteristics;
    return ParserDir(ntHeader);
}

  这个函数只是在收集一些必要的PE文件信息,方便我们加密使用,最后调用了比较关键的函数ParserDir,下面是其代码:

//
// GNU AFFERO GENERAL PUBLIC LICENSE
//Version 3, 19 November 2007
//
//Copyright(C) 2007 Free Software Foundation, Inc.
//Everyone is permitted to copyand distribute verbatim copies
//of this license document, but changing it is not allowed.
// Author : WingSummer (寂静的羽夏)
// 
//Warning: You can not use it for any commerical use,except you get 
// my AUTHORIZED FORM ME!This project is used for tutorial to teach
// the beginners what is the PE structure and how the packer of the PE files works.
// 
// 注意:你不能将该项目用于任何商业用途,除非你获得了我的授权!该项目用来
// 教初学者什么是 PE 结构和 PE 文件加壳程序是如何工作的。
//

ParserError CWingProtect::ParserDir(PIMAGE_NT_HEADERS ntHeader)
{
    auto s = IMAGE_FIRST_SECTION(ntHeader);
    peinfo.PSectionHeaders = s;
    auto dd = (INT3264)s - (IMAGE_NUMBEROF_DIRECTORY_ENTRIES * sizeof(IMAGE_DATA_DIRECTORY));
    peinfo.PDataDirectory = (PIMAGE_DATA_DIRECTORY)dd;

    auto pdtls = peinfo.PDataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];
    if (pdtls.VirtualAddress && pdtls.Size)
        HasTLS = TRUE;

    auto pos = peinfo.AddressOfEntryPoint;
    auto pis = encryptInfo.OldImportDataAddr = peinfo.PDataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;

    DWORD maxVirual = 0;
    DWORD sizeraw = 0;
    for (UINT i = 0; i < peinfo.NumberOfSections; i++)
    {
        auto psh = s[i];
        if (psh.VirtualAddress <= pos && pos <= psh.VirtualAddress + psh.SizeOfRawData)
        {
            peinfo.PCodeSection = &s[i];
        }

        if (psh.VirtualAddress <= pis && pis <= psh.VirtualAddress + psh.SizeOfRawData)
        {
            peinfo.PImportSection = &s[i];
        }

        if (psh.VirtualAddress > maxVirual)
        {
            maxVirual = psh.VirtualAddress;
            sizeraw = psh.SizeOfRawData;
        }
    }

    auto d = div((INT3264)(maxVirual + sizeraw), peinfo.SectionAlignment);
    peinfo.AnalysisInfo.MinAvailableVirtualAddress = (UINT)(GetBiggerQuot(d) * peinfo.SectionAlignment);
    peinfo.AnalysisInfo.SectionsCanAddCount = (peinfo.SizeOfHeaders - (UINT)GETOFFSET(mapping, s)) / IMAGE_SIZEOF_SECTION_HEADER - peinfo.NumberOfSections - 1;
    auto pdd = (PIMAGE_DATA_DIRECTORY)dd;
    auto idd = pdd[IMAGE_DIRECTORY_ENTRY_IMPORT];

    auto pidd = (PIMAGE_IMPORT_DESCRIPTOR)GetPointerByRVA(mapping, idd.VirtualAddress);
    peinfo.PImportDescriptor = pidd;
    IMAGE_IMPORT_DESCRIPTOR iid = pidd[0];

    auto pdll = peinfo.AnalysisInfo.ImportDllName;
    auto pfunhint = peinfo.AnalysisInfo.ImportFunNameTable;
    auto pdiat = peinfo.AnalysisInfo.DllFirstThunks;
    if (is64bit)
    {
        int i = 0, ii = 0, itotal = 0;

        if (itotal >= MAXImportFunHintCount)
            return ParserError::TooManyImportFunctions;

        if (i >= MAXDllNameCount)
            return ParserError::TooManyImportDlls;

        while (iid.Characteristics)
        {
            pdll[i].Name = iid.Name;
            pdiat[i] = iid.FirstThunk;

            PIMAGE_THUNK_DATA64 pitd32 =
                (PIMAGE_THUNK_DATA64)GetPointerByRVA(mapping, iid.FirstThunk);
            IMAGE_THUNK_DATA64 itd32 = pitd32[0];
            PIMAGE_IMPORT_BY_NAME iibn;

            ii = 0;
            while (itd32.u1.AddressOfData)
            {
                if (!IMAGE_SNAP_BY_ORDINAL64(itd32.u1.AddressOfData))
                {
                    iibn = (PIMAGE_IMPORT_BY_NAME)GetPointerByRVA(mapping, itd32.u1.AddressOfData);

                    auto c = (char*)&iibn->Name;
                    auto le = strlen(c);

                    strcpy_s(pfunhint, PageSize, c);        //偷懒写法
                    pfunhint += (le + 1);
                }
                ii++;
                itd32 = pitd32[ii];
            }
            pdll[i].FunCount = ii;
            itotal += ii;
            i++;
            iid = pidd[i];
        }

        peinfo.AnalysisInfo.ImportDllCount = i;
        peinfo.AnalysisInfo.ImportFunCount = itotal;
        peinfo.AnalysisInfo.PointerofImportFunNameTable =
            (UINT)GETOFFSET(peinfo.AnalysisInfo.ImportFunNameTable, pfunhint);
    }
    else
    {
        int i = 0, ii = 0, itotal = 0;

        while (iid.Characteristics)
        {
            pdll[i].Name = iid.Name;
            pdiat[i] = iid.FirstThunk;

            PIMAGE_THUNK_DATA32 pitd32 =
                (PIMAGE_THUNK_DATA32)GetPointerByRVA(mapping, iid.FirstThunk);
            IMAGE_THUNK_DATA32 itd32 = pitd32[0];
            PIMAGE_IMPORT_BY_NAME iibn;

            ii = 0;
            while (itd32.u1.AddressOfData)
            {
                if (!IMAGE_SNAP_BY_ORDINAL64(itd32.u1.AddressOfData))
                {
                    iibn = (PIMAGE_IMPORT_BY_NAME)GetPointerByRVA(mapping, itd32.u1.AddressOfData);

                    auto c = (char*)&iibn->Name;
                    auto le = strlen(c);

                    strcpy_s(pfunhint, PageSize, c);        //偷懒写法
                    pfunhint += (le + 1);
                }

                ii++;
                itd32 = pitd32[ii];
            }
            pdll[i].FunCount = ii;
            itotal += ii;
            i++;
            iid = pidd[i];
        }

        peinfo.AnalysisInfo.ImportDllCount = i;
        peinfo.AnalysisInfo.ImportFunCount = itotal;
        peinfo.AnalysisInfo.PointerofImportFunNameTable =
            (UINT)GETOFFSET(peinfo.AnalysisInfo.ImportFunNameTable, pfunhint);
    }

    return  ParserError::Success;
}

  上面的代码对执行的代码区进行确认和定位,记录做后续的加密,于此同时我们记录了必要的数据,接下来就是解析导入表,我们以64位为例进行介绍:

int i = 0, ii = 0, itotal = 0;

while (iid.Characteristics)
{
    pdll[i].Name = iid.Name;
    pdiat[i] = iid.FirstThunk;

    PIMAGE_THUNK_DATA32 pitd32 =
        (PIMAGE_THUNK_DATA32)GetPointerByRVA(mapping, iid.FirstThunk);
    IMAGE_THUNK_DATA32 itd32 = pitd32[0];
    PIMAGE_IMPORT_BY_NAME iibn;

    ii = 0;
    while (itd32.u1.AddressOfData)
    {
        if (!IMAGE_SNAP_BY_ORDINAL64(itd32.u1.AddressOfData))
        {
            iibn = (PIMAGE_IMPORT_BY_NAME)GetPointerByRVA(mapping, itd32.u1.AddressOfData);

            auto c = (char*)&iibn->Name;
            auto le = strlen(c);

            strcpy_s(pfunhint, PageSize, c);        //偷懒写法
            pfunhint += (le + 1);
        }

        ii++;
        itd32 = pitd32[ii];
    }
    pdll[i].FunCount = ii;
    itotal += ii;
    i++;
    iid = pidd[i];
}

peinfo.AnalysisInfo.ImportDllCount = i;
peinfo.AnalysisInfo.ImportFunCount = itotal;
peinfo.AnalysisInfo.PointerofImportFunNameTable =
    (UINT)GETOFFSET(peinfo.AnalysisInfo.ImportFunNameTable, pfunhint);

  while (iid.Characteristics)while (itd32.u1.AddressOfData)这个几个代码就是判断是否为空,为空就中断循环。pdll是导入名称不定长字符串数组,pfunhint是导入函数名称不定长字符串数组,pdiat是每一个IMAGE_IMPORT_DESCRIPTOR中的FirstThunk拷贝来作为的数组,而这些都是我进行IAT加密所需要的数据,这到后面的文章进行介绍,这里就不多说了。
  经历过一系列的解析,如果成功,就会执行return ParserError::Success;表示解析成功,构造函数继续执行后续加密所需资源的申请和处理,这些并不是本篇博文的关注点,就不赘述了。

下一篇

  羽夏壳世界——异或加密的实现

posted @ 2022-04-10 22:29  寂静的羽夏  阅读(266)  评论(0编辑  收藏  举报