zoukankan      html  css  js  c++  java
  • 软件加壳的原理及实现[转]

    转载自 https://blog.csdn.net/woshigeshusheng/article/details/68489843,侵删

    加壳的实现

    我是个初学者,所知有限,难免会有错误,如果有人发现了错误,还请指正。 
    先大致说一下加壳的原理,即在原PE文件(后面称之为宿主文件)上加一个新的区段(也就是壳),然后从这个新的区段上开始运行;也就算是成功的加上了壳;下面我们就说一下具体的实现。 
    这个工程有两个项目,一个用来生成壳的Win32项目(dll类型),另一个是实现加壳的MFC项目; 
    加壳的项目界面是用MFC实现的,除了原有的类外,添加了两个新类,一个用于PE操作, 
    一个用于加壳。

    下面说下加壳过程的实现: 
    先将原PE文件读取到内存;获取头文件信息,获得.text区段信息,然后对代码段进行加密(简单的异或加密);随后再用LoadLibrary将生成的壳(是一个dll)加载到内存;我们需要在壳的程序里对宿主PE进行解密,并且还要修复重定位,所以要把一些必要的数据存储到加载的壳里面; 
    申请空间将dll(壳)拷贝一份(此处大家可能会疑惑为什么要拷贝一份,因为我在以LoadLibrary加载进来的壳里直接修改需要重定位的地址信息时,程序运行会出错); 
    然后申请内存,大小是原宿主PE文件和壳的大小的和;先将宿主程序拷贝进去; 
    然后修复重定位信息,这个地方应该重点说明一下,我是直接把展开的整个dll拷贝到新PE文件里,并且打算壳的部分利用系统的重定位(每次加载PE文件的时候,如果重定位没有关掉,系统会进行一次重定位),所以在拷贝之前,要把需要重定位的地址修改成在新PE中的虚拟地址,并且我们的壳是通过LoadLibrary的方式加载的,已经被重定位过,我又是直接拷贝过来的,所以修改地址的时候要注意,后面会说到如何修改。 
    然后,合并PE文件和壳;设置新的OEP,既然加壳,当然要从壳的我们规定的开始位置开始执行,需要将其改成相对新PE开始位置的偏移,原理和修复重定位差不多,不过一个是相对虚拟地址,一个是虚拟地址。 
    如图: 
    这里写图片描述 
    相对虚拟地址=1+2; 
    如果修复重定位的话就某一地址的相对虚拟地址再加个默认基址; 
    下面是代码实现部分:

    bool CPack::Pack(WCHAR * szPath)
    {
        CPe objPe;
    
        //读取要被加壳的PE文件
        DWORD dwReadFilSize = 0;
        HANDLE hFile = CreateFile(szPath,GENERIC_READ | GENERIC_WRITE, 0, NULL,
                       OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    
        DWORD dwFileSize = GetFileSize(hFile, NULL);
        char * pFileBuf = new char[dwFileSize];
        memset(pFileBuf, 0, dwFileSize);
        ReadFile(hFile, pFileBuf, dwFileSize, &dwReadFilSize, NULL);
    
        //获取PE头文件信息
        PEHEADERINFO pPeHead = { 0 };
        objPe.GetPeHeaderinfo(pFileBuf, &pPeHead);
    
        //加密
        IMAGE_SECTION_HEADER pTxtSection;
        objPe.GetSectionInfo(pFileBuf, &pTxtSection, ".text");
        objPe.XorCode((LPBYTE)(pTxtSection.PointerToRawData + pFileBuf), pTxtSection.SizeOfRawData);
    
        //用loadLibrary加载壳文件
        HMODULE pLoadStubBuf = LoadLibrary(L"..\Release\Stub.dll");
    
        //存储必要的信息
        PPACKINFO PackInfoAdd = (PPACKINFO)GetProcAddress((HMODULE)pLoadStubBuf, "g_PackInfo");
    
        PackInfoAdd->dwOriStartPoint = pPeHead.pOptionHeader->AddressOfEntryPoint;                                          //需要跳转的OEP
        PackInfoAdd->dwImageBase = pPeHead.pOptionHeader->ImageBase;                                                        //默认加载基址
        PackInfoAdd->dwXorCode = pTxtSection.VirtualAddress;                                                                //加密代码段地址
        PackInfoAdd->dwXorKey = 0xE;                                                                                        //加密密钥
        PackInfoAdd->dwXorSize = pTxtSection.SizeOfRawData;                                                                 //加密大小
        PackInfoAdd->stcPeRelocDir = pPeHead.pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];                 //重定位表信息
        PackInfoAdd->dwSizeOfImage = pPeHead.pOptionHeader->SizeOfImage;                                                    //原PE的大小
    
        //拷贝一份
        MODULEINFO stcModInfo = { 0 };
        GetModuleInformation(GetCurrentProcess(), pLoadStubBuf, &stcModInfo, sizeof(MODULEINFO));
        char * pStubBuf = new char[stcModInfo.SizeOfImage];
        memset(pStubBuf, 0, stcModInfo.SizeOfImage);
        memcpy(pStubBuf, pLoadStubBuf, stcModInfo.SizeOfImage);
    
        //申请新空间存储新PE
        int NewPeSize = objPe.GetAddSectionSize(pFileBuf, (char*)pLoadStubBuf, stcModInfo.SizeOfImage);
        char * pNewPeBuf = new char[NewPeSize];
        memset(pNewPeBuf, 0, NewPeSize);
        memcpy(pNewPeBuf, pFileBuf, dwFileSize);
    
        //修改重定位表
        objPe.FixReloc(pStubBuf, pNewPeBuf);
    
        // 添加一个区段
        objPe.AddSection(pNewPeBuf, pStubBuf, stcModInfo.SizeOfImage);
    
        //设置入口点
        //自己定义的壳的开始位置的原始偏移
        DWORD Offset = PackInfoAdd->dwStartPoint - (DWORD)pLoadStubBuf;
        //相对于新PE的起始位置的偏移
        unsigned int NewOep = Offset + pPeHead.pOptionHeader->SizeOfImage;
        objPe.SetOep(pNewPeBuf, NewOep);
    
        //保存成文件
        SavePackFile(szPath, pNewPeBuf, NewPeSize);
    
        //释放内存
        delete[]pFileBuf;
        delete[]pStubBuf;
        delete[]pNewPeBuf;
    
        return true;
    }
    

    下面我们就重点说一下修复重定位,通过重定位表找到能存储着需要重定位地址的地址,然后我们把这个地址存储的数据给改了就行了;其实修改重定位信息,简而言之就是把需要重定位的地址改成基于新基址的虚拟地址,这样程序在运行时才能根据真正的加载基址进行重定位;像我们这种情况,需要算出目标地址相对于壳的起始位置的偏移,加上宿主PE的默认加载基址,再加上这个壳加到PE之后的作为新区段的相对虚拟地址,也就是基于新PE的虚拟地址了。 
    并且由于相对虚拟地址是从0开始,所以头文件里的sizeofimage就是这个新区段的RVA。 
    比如说一个在内存中展开后大小(sizeofimage)为5000的宿主文件,相对虚拟地址为5000的地方就是新区段的起始位置(vitualaddress)。 
    如图: 
    这里写图片描述

    还有几点应该注意,壳的部分我们要用系统进行重定位,在循环中要修改壳需要重定位的页的起始位置相对虚拟地址(重定位表的每一个PIMAGE_BASE_RELOCATION结构体变量描述的都是某个区段一个页的重定位信息),最后还要将重定位表的指针指向壳的重定位表,并且将重定位表的大小改成壳的重定位表的大小。 
    代码实现如下:

    bool CPe::FixReloc(char* StubBuf, char*PeBuf)
    {
        //获取被加壳PE文件的重定位表指针
        PIMAGE_DOS_HEADER pPeDos = (PIMAGE_DOS_HEADER)PeBuf;
        PIMAGE_NT_HEADERS pPeNt = (PIMAGE_NT_HEADERS)(pPeDos->e_lfanew + PeBuf);
        PIMAGE_DATA_DIRECTORY pPeRelocDir = &(pPeNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
    
    
        //获取壳文件的重定位表指针
        PIMAGE_DOS_HEADER pStuDos = (PIMAGE_DOS_HEADER)StubBuf;
        PIMAGE_NT_HEADERS pStuNt = (PIMAGE_NT_HEADERS)(pStuDos->e_lfanew + StubBuf);
        PIMAGE_DATA_DIRECTORY pStuRelocDir = pStuNt->OptionalHeader.DataDirectory;
        pStuRelocDir = &(pStuRelocDir[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
    
        //获取重定位目录
        PIMAGE_BASE_RELOCATION pStuReloc = (PIMAGE_BASE_RELOCATION)((DWORD)StubBuf + pStuRelocDir->VirtualAddress);
    
        //定义一个存储TypeOffset的结构体
        typedef struct {
            WORD Offset : 12;
            WORD Type : 4;
        }TypeOffset, *PTypeOffset;
    
        //修复重定位信息
        PTypeOffset pTypeOffset = (PTypeOffset)(pStuReloc + 1);
    
        DWORD dwCount = (pStuReloc->SizeOfBlock - 8) / 2;
    
        while (pStuReloc->VirtualAddress)
        {
            for (DWORD i = 0; i < dwCount; i++)
            {
                if (*(PDWORD)(&pTypeOffset[i]) == NULL)
                {
                    break;
                }
    
                //存储重定位地址的相对虚拟地址
                DWORD dwRVA = pStuReloc->VirtualAddress + pTypeOffset[i].Offset; //RVA
    
                //重定位的地址
                DWORD pRelocAddr = *(PDWORD)((DWORD)StubBuf + dwRVA);            
    
                //改掉(PDWORD)((DWORD)StubBuf + dwRVA)存储的值,也就是已经重定位的将来需要重定位的地址
                *(PDWORD)((DWORD)StubBuf + dwRVA) = pRelocAddr - pStuNt->OptionalHeader.ImageBase
                    + pPeNt->OptionalHeader.ImageBase + pPeNt->OptionalHeader.SizeOfImage;
            }
    
            //修改壳重定表中需要重定位的页的起始位置的相对虚拟地址
            pStuReloc->VirtualAddress += pPeNt->OptionalHeader.SizeOfImage;
    
            pStuReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pStuReloc + pStuReloc->SizeOfBlock);
        }
    
        //修改PE文件的重定位表指针
        pPeRelocDir->Size = pStuRelocDir->Size;
        pPeRelocDir->VirtualAddress = pStuRelocDir->VirtualAddress + pPeNt->OptionalHeader.SizeOfImage;
    
        return true;
    }
     

    接下来讲一下添加区段,这个主要要注意更改PE头文件的信息,文件才能正常运行,不用对齐,我是把整个展开的dll拷贝过来的,按0x1000进行对齐展开的代码,按0x200对齐肯定没问题。要注意将新区段的属性得是可读可写可执行(我踩的一个坑),至于为什么后面再说; 
    下面是实现代码:

    void CPe::AddSection(char* pBuf, char*pNewSection, int nSize)
    {
        PEHEADERINFO HeaderInfo = { 0 };
        DWORD NumOfSection = 0;
        if (IsPeFile(pBuf) == false)
        {
            return;
        }
    
        //获得PE的头文件信息
        GetPeHeaderinfo(pBuf,&HeaderInfo);
        //修改区段数量
        NumOfSection = HeaderInfo.pFileHeader->NumberOfSections;
        HeaderInfo.pFileHeader->NumberOfSections += 1;
        //新增区段信息
        PIMAGE_SECTION_HEADER pLastHeader = HeaderInfo.pSectionHeader + NumOfSection - 1;
        PIMAGE_SECTION_HEADER pNewSecHeader = HeaderInfo.pSectionHeader + NumOfSection;
        memcpy(pNewSecHeader->Name, "aStub", 6);
        pNewSecHeader->Misc.VirtualSize = nSize;
        pNewSecHeader->VirtualAddress = HeaderInfo.pOptionHeader->SizeOfImage;
        int a = sizeof(*pBuf);
        pNewSecHeader->PointerToRawData = pLastHeader->PointerToRawData + pLastHeader->SizeOfRawData;
        pNewSecHeader->SizeOfRawData = nSize;
        pNewSecHeader->Characteristics = 0xE0000020;
        //修改镜像大小
        HeaderInfo.pOptionHeader->SizeOfImage += nSize;
        //把区段添加到PE文件中
        memcpy(pBuf + pNewSecHeader->PointerToRawData, pNewSection, nSize);
    }
    

      

    大家应该已经注意到我获取头文件信息 很多地方都是直接用的一个函数GetPeHeaderinfo,我是把获取头文件信息的操作封装成了函数;信息保存在一个自定义结构体类型的变量里;

    typedef struct PEHEADERINFO
    {
        PIMAGE_FILE_HEADER pFileHeader;
        PIMAGE_OPTIONAL_HEADER pOptionHeader;
        PIMAGE_SECTION_HEADER pSectionHeader;
    }PEHEADERINFO, *PPEHEADERINFO;
    

     

    下面就说一下壳的实现: 
    既然前面对宿主PE的代码段进行加密,自然要在壳的代码里进行解密;在执行之前我们还要人工对原PE文件进行一次重定位;要让程序执行原PE文件;自然要跳转到原始的入口点,前面已经将原始入口点的相对虚拟地址保存到了壳里面。只要根据加载基址算出这个入口点的地址,到这个地址去执行就可以了。那么问题来了,我们的壳利用利用入口点直接开始执行了我们的代码,既没加载模块,也没加载资源什么的,无论是人工的进行重定位,还是获得当前的加载基址,都需要函数。所所以我们要人工的获取以下这些函数的地址。 
    首先,要先将可能要用到的函数定义好:

    //Stub部分用到的函数的类型定义
    typedef DWORD(WINAPI *fnGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName);
    typedef HMODULE(WINAPI *fnLoadLibraryA)(_In_ LPCSTR lpLibFileName);
    typedef HMODULE(WINAPI *fnGetModuleHandleA)(_In_opt_ LPCSTR lpModuleName);
    typedef BOOL(WINAPI *fnVirtualProtect)(_In_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flNewProtect, _Out_ PDWORD lpflOldProtect);
    typedef LPVOID(WINAPI *fnVirtualAlloc)(_In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect);
    typedef void(WINAPI *fnExitProcess)(_In_ UINT uExitCode);
    typedef int(WINAPI *fnMessageBox)(HWND hWnd, LPSTR lpText, LPSTR lpCaption, UINT uType);

    定义函数指针,保存函数地址

    //需要获取的函数
    fnGetProcAddress     pfnGetProcAddress = NULL;
    fnLoadLibraryA       pfnLoadLibraryA = NULL;
    fnVirtualProtect     pfnVirtualProtect = NULL;
    fnVirtualAlloc       pfnVirtualAlloc = NULL;
    fnGetModuleHandleA   pfnGetModuleHandleA = NULL;
    fnMessageBox         pfnMessageBox = NULL;
    fnExitProcess        pfnExitProcess = NULL;

    好了该说一下,我上面提到的那个坑了,我开始没有把壳的这个区段保存成可读可写可执行的属性,导致获得的下面这些变量的值都无法保存;后来意识到这个问题,改了属性后果然正常。

    下面是获取Kernel32.dll的代码

    DWORD GetKernel32Addr()
    {
        DWORD dwKernel32Addr = 0;
        _asm
        {
            push eax
                mov eax, dword ptr fs : [0x30]  //eax=PEB的地址
                mov eax, [eax + 0x0C]           //eax=PEB_LDR_DATA的指针
                mov eax, [eax + 0x1C]           //eax=模块初始化链表的指针,InInitializationOrderModuleList
                mov eax, [eax]                  //eax=列表中的第二个条目
                mov eax, [eax]                  //eax=列表中的第二个条目
                mov eax, [eax + 0x08]           //eax=获取到的Kernel32.dll基址(win7下获取的是KernelBase.dll
                mov dwKernel32Addr, eax                           
                pop eax
        }
        return dwKernel32Addr;
    }

    然后用名称遍历导出表得到函数GetProcAddress的地址; 
    然后我们就可以利用GetProcAddress获得所需函数的地址:

    void Init()
    {
        HMODULE hKernel32 = (HMODULE)GetKernel32Addr();
        pfnGetProcAddress = (fnGetProcAddress)MyGetProcessAddress();
        pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress(hKernel32, "LoadLibraryA");
        pfnVirtualProtect = (fnVirtualProtect)pfnGetProcAddress(hKernel32, "VirtualProtect");
        pfnVirtualAlloc = (fnVirtualAlloc)pfnGetProcAddress(hKernel32, "VirtualAlloc");
        pfnGetModuleHandleA = (fnGetModuleHandleA)pfnGetProcAddress(hKernel32, "GetModuleHandleA");
        pfnExitProcess = (fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess");
    
        //获取messagebox
        HMODULE hUser32 = pfnLoadLibraryA("user32.dll");
        pfnMessageBox = (fnMessageBox)pfnGetProcAddress(hUser32, "MessageBoxA");
    
        //加载基址
        LoadBase = (DWORD)pfnGetModuleHandleA(NULL);
    }

    下面就要利用得到的函数,对原宿主PE进行人工的重定位了,就是把需要重定位的虚拟地址改成真正的地址;用保存的地址-默认加载基址(加壳的过程中已保存)+实际的加载基址;在重定位的过程中要修改保护属性。

    bool SelfReloc()
    {
    
        //获取重定位目录
        PIMAGE_BASE_RELOCATION pStuReloc = (PIMAGE_BASE_RELOCATION)(LoadBase + g_PackInfo.stcPeRelocDir.VirtualAddress);
    
        //定义一个存储TypeOffset的结构体
        typedef struct {
            WORD Offset : 12;
            WORD Type : 4;
        }TypeOffset, *PTypeOffset;
    
        DWORD dwOldProtect = 0;               
    
        //修复重定位信息
        while (pStuReloc->VirtualAddress)
        {
            //修改保护属性
            //按道理来修改属性大小设置为0x1000就可以了,调试能力有限,暂时不知道为什么设置成0x2000才可以
            pfnVirtualProtect((LPVOID)(LoadBase + pStuReloc->VirtualAddress), 0X2000, PAGE_EXECUTE_READWRITE, &dwOldProtect);
            PTypeOffset pTypeOffset = (PTypeOffset)(pStuReloc + 1);
    
            DWORD dwCount = (pStuReloc->SizeOfBlock - 8) / 2;
    
            for (DWORD i = 0; i < dwCount; i++)
            {
                if (*(PDWORD)(&pTypeOffset[i]) == NULL)
                {
                    break;
                }
    
                DWORD dwRVA = pStuReloc->VirtualAddress + pTypeOffset[i].Offset;        
    
                DWORD pRelocAddr = *(PDWORD)(LoadBase + dwRVA);                         
                *(PDWORD)(LoadBase + dwRVA) = pRelocAddr - g_PackInfo.dwImageBase
                    + LoadBase;
            }
    
            //恢复保护属性
            pfnVirtualProtect((LPVOID)(LoadBase + pStuReloc->VirtualAddress), 0X2000, dwOldProtect, &dwOldProtect);
    
            //获取描述下一个页重定位信息的结构体变量
            pStuReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pStuReloc + pStuReloc->SizeOfBlock);
        }
    
        return true;
    }

    加壳的时候已经把原PE文件OEP相对虚拟地址保存下来,加上加载基址就是原PE入口点现在的地址,然后从那个地址执行就可以了:

    void _declspec(naked) start()
    {
        Init();
        DeXorCode();
        SelfReloc();
        _asm
        {
            push eax;
            mov eax, LoadBase;
            add eax, g_PackInfo.dwOriStartPoint;
            mov dword ptr[esp], eax;
            ret;
        }
    }
  • 相关阅读:
    js 实现长按效果(类似安卓的)
    Java编程思想学习笔记(一)
    中文自然语言处理(NLP)(五)应用HanLP分词模块进行分词处理
    中文自然语言处理(NLP)(四)运用python二维字典和jieba实现词频的统计
    中文自然语言处理(NLP)(三)运用python jieba模块计算知识点当中关键词的词频
    中文自然语言处理(NLP)(二)python jieba模块的进一步学习和xlrd模块
    中文自然语言处理(NLP)(一)python jieba模块的初步使用
    正则表达式(几个例子)
    用户登陆界面(jquery)
    一个简单的注册页面,基于JS
  • 原文地址:https://www.cnblogs.com/yc-only-blog/p/9154894.html
Copyright © 2011-2022 走看看