千家信息网

如何从零开始写一个加壳器

发表于:2026-11-04 作者:千家信息网编辑
千家信息网最后更新 2026年11月04日,本篇内容介绍了"如何从零开始写一个加壳器"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!加壳原理手工加
千家信息网最后更新 2026年11月04日如何从零开始写一个加壳器

本篇内容介绍了"如何从零开始写一个加壳器"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

加壳原理

手工加壳

即我们向PE文件添加一个区段并将其设置为入口点,这样PE文件最开始执行的命令就是我们添加的区段也就是壳的指令,壳对加密区进行解密,对压缩区进行解压,将原本的EXE文件还原出来,然后跳转至原程序入口,程序照常运行。

首先生成一个打印hello的exe文件。

#include int main() {printf("hello");}

我们目前要干的事情是:以手动的形式向PE文件添加一个壳部分并设为程序入口,并使其能跳转回原入口。 那就来吧

用010editor打开我们的exe文件,启用exe模板分析。 我们首先修改其文件头numverofsection属性,这个属性用来定义当前PE文件存在多少个区段,因为我们要添加一个壳区段,所以我们将其加1变成6

在我们重载模板后我们就会在区段表发现多出来一个空的区段表

从上到下各个比较重要字段的意思是 \1. Name 表示该区段的名字 2.VirtualSize 表示在内存中的大小(一般内存对齐为0x1000) 3.virtualaddress 虚拟地址 即上一个区段的VirtualAddress + 上一个区段经内存对齐粒度对齐后的大小 4.sizeofdata 表示在文件中的大小(一般文件对齐为0x200) 5.pointertorawdata 文件的偏移 即 上一个区段的PointerToRawData + 上一个区段的SizeOfRawData

然后我们通过修改以上各值来定义一个新区段(壳区段)的属性

这里的virtualsize看着填一个就行了。 此时我们只是定义了区段表,但文件中并没有该区段存在,所以我们得创建该区段。 然后还要让区段可编辑,把下列值改为1即可

ctrl+shift+i 向目标文件偏移处插入0x200大小的空间。 这样一来,壳区段就创建好了。 然后我们还要修改 扩展头的SizeofImage 。将他改为最后一个区段的内存地址+内存大小

然后去掉随机基址选项。

找到扩展头的DLL属性字段,去掉随机基址,把40 81改为 00 81

接下来我们把程序入口点设置给壳区段。 使用LORDPE把入口点设为壳区块的虚拟地址

然后我们用OD打开这个文件

真正的加壳流程

刚刚提到的手工加壳,不过是最最基础的加壳原型而已,真正的加壳还涉及了代码加解密等操作.

真正写壳时一般写两个东西,加壳器和stub 所谓加壳器,就是给被加壳文件创造出一个新的区段, 在此同时将程序以某种方式加密,然后把stub放入新区段,并将程序入口点设为新区段的地址,然后在新区段结束后跳转回原程序入口。这个新区段我们叫做壳区段. 那么这个stub就是加壳后程序最先执行的命令了,它执行解密算法,将原程序释放出来。

基于c++的壳编写

https://github.com/ConsT27/PackingEXE/tree/master项目地址

Stub

stub是被植入到PE文件中的代码,它一般会干下面这些事情。

流程如下

0.合并data,rdata到text 1.PEB动态寻址,遍历导出表找到GetProcAddress函数 2.解密 3.修改入口点到原入口点

同时stub一般以dll的形式存在。原因是DLL通常自带重定位表,这在我们的移植过程中的重定位操作中提供了巨大的便利。

合并数据段

我们要移植stub过去,肯定需要移植代码段,也需要移植数据段。不如我们干脆把数据段合并到代码段,一块移植过去。

PEB动态寻址&导出表遍历找函数

为什么会用到这个技术编写stub? 因为我们的stub.dll植入到宿主程序时,只有.text植入过去,没有对应的导入表,所以我们的stub无法直接调用一些API。所以我们需要动态获取各种API。 其中我采用的是PEB动态查询得到GetProcAddress函数,然后用GetProcAddress函数去获取各个API。

那么,什么是PEB? PEB是一个微软还未完全公开作用的一个结构,它叫做 进程环境信息块 ,包含了进程的信息。其结构如下

typedef struct _PEB {BYTE                          Reserved1[2];BYTE                          BeingDebugged; //被调试状态BYTE                          Reserved2[1];PVOID                         Reserved3[2];PPEB_LDR_DATA                 Ldr;PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;BYTE                          Reserved4[104];PVOID                         Reserved5[52];PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;BYTE                          Reserved6[128];PVOID                         Reserved7[1];ULONG                         SessionId;} PEB, *PPEB;复制代码

我们关心的是PEB偏移0c得到的 PPEB_LDR_DATA Ldr; 它是一个指针,指向一个 PPEB_LDR_DATA 结构, 存放着已经被进程装在的动态链接库的信息

typedef struct _PEB_LDR_DATA{ULONG Length; // +0x00BOOLEAN Initialized; // +0x04PVOID SsHandle; // +0x08LIST_ENTRY InLoadOrderModuleList; // +0x0cLIST_ENTRY InMemoryOrderModuleList; // +0x14LIST_ENTRY InInitializationOrderModuleList;// +0x1c} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24

PPEB_LDR_DATA 偏移1c是一个指向LIST_ENTRY InInitializationOrderModuleList结构的指针,这个结构 存放着指向模块初始化链表的头 , 按顺序存放着PE装入运行时初始化模块信息,一般来说第一个链表结点是ntdll.dll,第二个链表结点就是kernel32.dll 。我们就在其中找到kernel32.dll的信息,获取其PE信息,得到导出表,循环遍历得到GetProcAddress函数。 另外,PEB地址再TEB偏移0x30处。用汇编语言表示就是 fs:[0x30]。

以上是PEB寻址的大致流程,另外还有一个比较关键的点是遍历kernel32.dll导出表获得GetProcAddress函数信息。 关于导出表可以看看这个文章https://blog.csdn.net/evileagle/article/details/12176797

首先一个导出表结构体如下

typedef struct _IMAGE_EXPORT_DIRECTORY {DWORD   Characteristics;  //一般为0,没啥用DWORD   TimeDateStamp;  //导出表生成的时间WORD    MajorVersion;  //版本,也是0没啥用WORD    MinorVersion;  //也是没啥用的版本信息一般为0DWORD   Name;  //当前导出表的模块名字DWORD   Base;  //序号表中序号的基数DWORD   NumberOfFunctions;  //导出函数数量DWORD   NumberOfNames;  //按名字导出函数的数量DWORD   AddressOfFunctions;     // 序号表DWORD   AddressOfNames;         // 名称表DWORD   AddressOfNameOrdinals;  // 地址表} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

其中序号表的起始序号是Base属性定义的值。以下是导出表的序号名称地址表的关系

我们的遍历流程是,先遍历名称表找到GetProcAddress在名称数组中的下标,然后根据这个下标去序号数组中找相同下标的序号值,然后以这个序号值为下标去找地址数组中的对应值。我们找到的地址表中的值就是函数入口

下面我把这段程序的汇编代码放出来。我是用内联汇编把这段代码塞进C++的

void GetApis(){HMODULE hKernel32;_asm{pushad;; //获取kernel32.dll的加载基址;mov eax, fs: [0x30] ;  //得到PEB地址mov eax, [eax + 0ch];  //获得LDR_PEB_DATA地址mov eax, [eax + 0ch];  //获得LIST_ENTRY InLoadOrderModuleList;地址mov eax, [eax];  //获得LIST_ENTRY InLoadOrderModuleList下一项的地址mov eax, [eax];  /获得LIST_ENTRY InLoadOrderModuleList下下项即我们需要的LIST_ENTRY InInitializationOrderModuleList的地址mov eax, [eax + 018h]; //获得kernel32.dll地址mov hKernel32, eax;mov ebx, [eax + 03ch];//获得kernel32.dll NT头RVAadd ebx, eax; //NT头的VAadd ebx, 078h; //获得区段表mov ebx, [ebx]; //获得导出表RVAadd ebx, eax;  //导出表VAlea ecx, [ebx + 020h];  mov ecx, [ecx]; // ecx => 名称表的首地址(rva);add ecx, eax; // ecx => 名称表的首地址(va);xor edx, edx; // 作为索引(index)来使用._WHILE:;mov esi, [ecx + edx * 4];//名称数组入口点rva,名称数组单位大小4字节lea esi, [esi + eax];  //入口点VAcmp dword ptr[esi], 050746547h;   //进行名称匹配,050746547h即小端存储的GetPjne _LOOP;//不相等就跳入_LOOP段cmp dword ptr[esi + 4], 041636f72h; //名陈匹配,rocA,以下依次为ddre,ssjne _LOOP;cmp dword ptr[esi + 8], 065726464h;jne _LOOP;cmp word  ptr[esi + 0ch], 07373h;jne _LOOP;mov edi, [ebx + 024h]; add edi, eax;  //获得序号表VAmov di, [edi + edx * 2];  //获得序号数组中对应下标的地址,序号数组单位大小2字节and edi, 0FFFFh;  //给di提位到32位,即给予edi 序号表中对应下标的地址mov edx, [ebx + 01ch];  add edx, eax;  //获得地址表mov edi, [edx + edi * 4];  //获得地址数组中,序号对应的值,地址数组单位大小4字节add edi, eax;   //获得GetProcAddress的入口地址mov MyGetProcAddress, edi;  //赋值jmp _ENDWHILE;  //END_LOOP:;inc edx; // ++index;jmp _WHILE;_ENDWHILE:;popad;}
解密

解密代码段。这段好写。

void Decrypt(){unsigned char* pText = (unsigned char*)g_conf.textScnRVA + 0x400000;//锁定到PE文件的text段(因为加壳时去掉了基址随机化,所以自信的把基址填成0x400000DWORD old = 0;MyVirtualProtect(pText, g_conf.textScnSize, PAGE_READWRITE, &old);//修改代码段的属性,注意我们这里使用了动态获得的//解密代码段for (DWORD i = 0; i < g_conf.textScnSize; i++){pText[i] ^= g_conf.key;}//把属性修改回去MyVirtualProtect(pText, g_conf.textScnSize, old, &old);}
修改入口点
_asm    {mov eax, g_conf.srcOep;  //入口点是g_conf.srcOepadd eax, 0x400000jmp eax}

加壳器

加壳器流程如下

1.打开需要被加壳的PE文件 2.加载stub 3.加密代码段 4.添加新区段 5.stub重定位修复 6.stub移植 7.PE文件入口点修改 8.去随机基址 9.保存文件

以下的各个流程描述中会用到诸多自定义函数,我先贴上来吧。

诸多自定函数&结构体
//****************//对齐处理//time:2020/11/5//****************int AlignMent(_In_ int size, _In_ int alignment) {return (size) % (alignment)==0 ? (size) : ((size) / alignment+1) * (alignment);}//***********************//PE信息获取函数簇//time:2020/11/2//***********************PIMAGE_DOS_HEADER GetDosHeader(_In_ char* pBase) {return PIMAGE_DOS_HEADER(pBase);}PIMAGE_NT_HEADERS GetNtHeader(_In_ char* pBase) {return PIMAGE_NT_HEADERS(GetDosHeader(pBase)->e_lfanew+(SIZE_T)pBase);}PIMAGE_FILE_HEADER GetFileHeader(_In_ char* pBase) {return &(GetNtHeader(pBase)->FileHeader);}PIMAGE_OPTIONAL_HEADER32 GetOptHeader(_In_ char* pBase) {return &(GetNtHeader(pBase)->OptionalHeader);}PIMAGE_SECTION_HEADER GetLastSec(_In_ char* pBase) {DWORD SecNum = GetFileHeader(pBase)->NumberOfSections;PIMAGE_SECTION_HEADER FirstSec = IMAGE_FIRST_SECTION(GetNtHeader(pBase));PIMAGE_SECTION_HEADER LastSec = FirstSec + SecNum - 1;return LastSec;}PIMAGE_SECTION_HEADER GetSecByName(_In_ char* pBase,_In_ const char* name) {DWORD Secnum = GetFileHeader(pBase)->NumberOfSections;PIMAGE_SECTION_HEADER Section = IMAGE_FIRST_SECTION(GetNtHeader(pBase));char buf[10] = { 0 };for (DWORD i = 0; i < Secnum; i++) {memcpy_s(buf, 8, (char*)Section[i].Name, 8);if (!strcmp(buf, name)) {return Section + i;}}return nullptr;}typedef struct _StubConf{DWORD srcOep;       //入口点DWORD textScnRVA;   //代码段RVADWORD textScnSize;  //代码段的大小DWORD key;          //解密密钥}StubConf;struct StubInfo{char* dllbase;          //stub.dll的加载基址DWORD pfnStart;         //stub.dll(start)导出函数的地址StubConf* pStubConf;    //stub.dll(g_conf)导出全局变量的地址};

打开PE文件

这里采用的方法是利用CreateFileA函数。同时这个函数还抛出了一个指向PE文件大小的指针

char* GetFileHmoudle(_In_ const char* path,_Out_opt_ DWORD* nFileSize) {//打开一个文件并获得文件句柄HANDLE hFile = CreateFileA(path,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);//获得文件大小DWORD FileSize = GetFileSize(hFile, NULL);//返回文件大小到变量nFileSizeif(nFileSize)*nFileSize = FileSize;//申请一片大小为FileSize的内存并将指针置于首位char* pFileBuf = new CHAR[FileSize]{ 0 };//给刚刚申请的内存读入数据DWORD dwRead;ReadFile(hFile, pFileBuf, FileSize, &dwRead, NULL);CloseHandle(hFile);return pFileBuf;}

加载STUB

void LoadStub(_In_ StubInfo* pstub) {pstub->dllbase = (char*)LoadLibraryEx(L"F:\\stubdll.dll", NULL, DONT_RESOLVE_DLL_REFERENCES);pstub->pfnStart = (DWORD)GetProcAddress((HMODULE)pstub->dllbase, "Start");  //获得stub的入口函数Start(自己定义在stub中的一个函数pstub->pStubConf = (StubConf*)GetProcAddress((HMODULE)pstub->dllbase, "g_conf");}//不仅加载了stub,还获得了stub抛出的用于收集信息的全局结构体(g_conf,是一个stub抛出的结构体,用于获取信息,结构如下)typedef struct _StubConf{DWORD srcOep;       //入口点DWORD textScnRVA;   //代码段RVADWORD textScnSize;  //代码段的大小DWORD key;          //解密密钥}StubConf;

加密代码段

DWORD textRVA = GetSecByName(PeHmoudle, ".text")->VirtualAddress;DWORD textSize = GetSecByName(PeHmoudle, ".text")->Misc.VirtualSize;Encry(PeHmoudle,pstub);void Encry(_In_ char* hpe,_In_ StubInfo pstub) {//获取代码段首地址BYTE* TargetText = GetSecByName(hpe, ".text")->PointerToRawData + (BYTE*)hpe;//获取代码段大小DWORD TargetTextSize = GetSecByName(hpe, ".text")->Misc.VirtualSize;//加密代码段for (int i = 0; i < TargetTextSize; i++) {TargetText[i] ^= 0x15;}pstub.pStubConf->textScnRVA = GetSecByName(hpe, ".text")->VirtualAddress;pstub.pStubConf->textScnSize = TargetTextSize;pstub.pStubConf->key = 0x15;}//加密代码段,并给予了stub一些信息
添加新区段
char* AddSec(_In_ char*& hpe, _In_ DWORD& filesize, _In_ const char* secname, _In_ const int secsize) {GetFileHeader(hpe)->NumberOfSections++;PIMAGE_SECTION_HEADER pesec = GetLastSec(hpe);//设置区段表属性memcpy(pesec->Name, secname, 8);pesec->Misc.VirtualSize = secsize;pesec->VirtualAddress = (pesec - 1)->VirtualAddress + AlignMent((pesec - 1)->SizeOfRawData,GetOptHeader(hpe)->SectionAlignment);pesec->SizeOfRawData = AlignMent(secsize, GetOptHeader(hpe)->FileAlignment);pesec->PointerToRawData = AlignMent(filesize,GetOptHeader(hpe)->FileAlignment);pesec->Characteristics = 0xE00000E0;//设置OPT头映像大小GetOptHeader(hpe)->SizeOfImage = pesec->VirtualAddress + pesec->SizeOfRawData;//扩充文件数据int newSize = pesec->PointerToRawData + pesec->SizeOfRawData;char* nhpe = new char [newSize] {0};//向新缓冲区录入数据memcpy(nhpe, hpe, filesize);//缓存区更替delete hpe;filesize = newSize;return nhpe;}

stub重定位

好家伙,这个东西稍有不慎就会让整个程序拉跨掉(过来人的忠告 为什么需要stub重定位呢?因为我们的stub最开始是加载在内存中的,它的许多指令如跳转到的地址是按内存为基准确定的,但是我们需要把他移植进文件,所以它的代码里许多地址就是错误的,我们需要对这些地址进行处理,即重定位,使其以宿主程序为标准进行地址修复。 可能我表述的不是很清楚

举个例子吧,比如stub在加载进内存时,有一条跳转指令时jmp 12345678, 如果我们不处理就把这条指令移植进PE文件,那么PE文件执行到此处时就会跳转到12345678,此时的12345678地址可能就已经不是PE文件加载的内存区间了,从而程序会崩溃。所以要修复。根据stub的重定位表进行修复。 重定位表就是记录哪些地址的数据需要被修复的,我们遍历这些地址进行修复即可。 如果以下代码看起来吃力,可以先去了解一下重定位表

void FixStub(DWORD targetDllbase, DWORD stubDllbase,DWORD targetNewScnRva,DWORD stubTextRva ){//找到stub.dll的重定位表DWORD dwRelRva = GetOptHeader((char*)stubDllbase)->DataDirectory[5].VirtualAddress;IMAGE_BASE_RELOCATION* pRel = (IMAGE_BASE_RELOCATION*)(dwRelRva + stubDllbase);//遍历重定位表while (pRel->SizeOfBlock){struct TypeOffset{WORD offset : 12;WORD type : 4;};TypeOffset* pTypeOffset = (TypeOffset*)(pRel + 1);DWORD dwCount = (pRel->SizeOfBlock - 8) / 2;    //需要重定位的数量for (int i = 0; i < dwCount; i++){if (pTypeOffset[i].type != 3){continue;}//需要重定位的地址DWORD* pFixAddr = (DWORD*)(pRel->VirtualAddress + pTypeOffset[i].offset + stubDllbase);DWORD dwOld;//修改属性为可写VirtualProtect(pFixAddr, 4, PAGE_READWRITE, &dwOld);//去掉dll当前加载基址*pFixAddr -= stubDllbase;//去掉默认的段首RVA*pFixAddr -= stubTextRva;//换上目标文件的加载基址*pFixAddr += targetDllbase;//加上新区段的段首RVA*pFixAddr += targetNewScnRva;//把属性修改回去VirtualProtect(pFixAddr, 4, dwOld, &dwOld);}//切换到下一个重定位块pRel = (IMAGE_BASE_RELOCATION*)((DWORD)pRel + pRel->SizeOfBlock);}

stub移植

这个简单,没啥说的

memcpy(GetLastSec(PeNewHmoudle)->PointerToRawData+ PeNewHmoudle,GetSecByName(pstub.dllbase, ".text")->VirtualAddress+pstub.dllbase,GetSecByName(pstub.dllbase,".text")->Misc.VirtualSize);
入口点修改
GetOptHeader(PeNewHmoudle)->AddressOfEntryPoint =pstub.pfnStart-(DWORD)pstub.dllbase-GetSecByName(pstub.dllbase,".text")->VirtualAddress+GetLastSec(PeNewHmoudle)->VirtualAddress;

去随机基址

不去掉随机基址,加载基址就是不固定的,不方便操作

GetOptHeader(PeNewHmoudle)->DllCharacteristics &= (~0x40);
保存文件
void SaveFile(_In_ const char* path, _In_ const char* data, _In_ int FileSize) {HANDLE hFile = CreateFileA(path,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);DWORD Buf = 0;WriteFile(hFile, data, FileSize, &Buf,NULL);CloseHandle(hFile);}

"如何从零开始写一个加壳器"的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!

地址 文件 区段 代码 入口 函数 加壳 大小 序号 程序 定位 信息 基址 内存 属性 结构 名称 就是 数组 移植 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 互联网科技公司简介 互联网科技工作委员会主任 美国互联网科技巨头监管 以房互联网科技 安徽鑫晟辉互联网科技 狮子座互联网科技培训 甘肃拓世互联网科技有限公司 深圳卡尔迅互联网科技有限公司怎么样 海南购全球互联网科技 互联网 科技馆 天速互联网络科技有限公司 速维有品互联网信息科技(苏州)有限公司 青海科技产业互联网发展 徐州市互联网科技有限公司 保险科技服务互联网营销体系 沐思想互联网科技有限公司 广州千川互联网科技有限公司 本届互联网大会上的黑科技 互联网与数字科技关系 山东蛋芒互联网科技有限公司 咖啡豆 百单网互联网科技有限公司怎么样 科技互联网技术 苏州互联网科技贷款公司 融势互联网科技 辽宁互联网新科技 洛阳昕诺飞互联网科技有限公司 工业互联网等科技实现智能化赋能 杭州披云加互联网科技有限公司 科技互联网专业务虚会材料
0