c++编写壳(入门)

作者: const27 分类: All,开发-C,提权/免杀 发布时间: 2020-10-28 05:07

参考:https://blog.csdn.net/qq_31507523/article/details/89438410

学习一下写壳,在以后免杀中使用。

加壳原理

手工加壳

用010editor手工加壳了解一波原理。加壳原理大致如下

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

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

#include <stdio.h>

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打开这个文件

发现入口点确实改变了,然后我们jmp回原程序入口点吧(这里是00400000+12a8)。保存

运行正常,手动加壳成功

真正的加壳流程

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

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

基于c++的壳编写

实现了一个薛定谔的加壳器(雾)
加壳好的程序有一定几率运行不了,原因未知。。真就薛定谔呗。🙃我写你妈

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

很大一部分上是借鉴这个老哥的
https://github.com/TonyChen56/GuiShou_Pack

第一次接触汇编编程,c++编程,上来就是搞这么一个项目,搞了快两个星期,确实有点痛苦,到现在还有很大部分不是很懂的地方(比如许多数据类型以及底层汇编(笑😁
这个项目也存在bug,也就是刚刚说的程序有几率不能运行的问题。😡
但是也学到了挺多,比如PEB动态寻址,PE文件结构等等。😁
苦于网上没有一篇文章详细的交代了技术的细节,所以这篇文章会尽可能的详细。
接下来是各个流程的详细实现方法,至于怎么把各个流程链接起来,师傅们可以通过下载上面提到的两个项目来看一下。

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; // +0x00
 BOOLEAN Initialized; // +0x04
 PVOID SsHandle; // +0x08
 LIST_ENTRY InLoadOrderModuleList; // +0x0c
 LIST_ENTRY InMemoryOrderModuleList; // +0x14
 LIST_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;  //也是没啥用的版本信息一般为0
    DWORD   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头RVA
		add ebx, eax; //NT头的VA
		add ebx, 078h; //获得区段表
		mov ebx, [ebx]; //获得导出表RVA
		add ebx, eax;  //导出表VA
		lea 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];  //入口点VA
		cmp dword ptr[esi], 050746547h;   //进行名称匹配,050746547h即小端存储的GetP
		jne _LOOP;//不相等就跳入_LOOP段
		cmp dword ptr[esi + 4], 041636f72h; //名陈匹配,rocA,以下依次为ddre,ss
		jne _LOOP;
		cmp dword ptr[esi + 8], 065726464h;
		jne _LOOP;
		cmp word  ptr[esi + 0ch], 07373h;
		jne _LOOP;
		mov edi, [ebx + 024h]; 
		add edi, eax;  //获得序号表VA

		mov 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段(因为加壳时去掉了基址随机化,所以自信的把基址填成0x400000

	DWORD 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.srcOep
		add eax, 0x400000
			jmp 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;	//代码段RVA
	DWORD 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);
	//返回文件大小到变量nFileSize
	if(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;	//代码段RVA
	DWORD 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);
}

后记

第一次搞底层的玩意儿,导致调试这东西花了我不少时间。。
不过学的还是?蛮多的?
动态调试,c++,汇编,PE结构,动态寻址都学到了些。(怎么感觉在往逆向走了2333
继续弄吧,这个项目目前还有bug,等以后来了兴趣再继续添加更多更牛逼的机制。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

Leave a Reply

Your email address will not be published. Required fields are marked *

标签云