用C++实现的壳

1.读取PE文件的信息并保存

//PE.h
public:
	HANDLE				m_hFile;	        //PE文件句柄
	LPBYTE				m_pFileBuf;	        //PE文件缓冲区
	DWORD				m_dwFileSize;	        //文件大小
	DWORD				m_dwImageSize;	        //镜像大小
	PIMAGE_DOS_HEADER	        m_pDosHeader;	        //Dos头
	PIMAGE_NT_HEADERS		m_pNtHeader;	        //NT头
	PIMAGE_SECTION_HEADER	        m_pSecHeader;	        //第一个SECTION结构体指针
	DWORD				m_dwImageBase;	        //镜像基址
	DWORD				m_dwCodeBase;	        //代码基址
	DWORD				m_dwCodeSize;	        //代码大小
	DWORD				m_dwPEOEP;	        //OEP地址
	DWORD				m_dwShellOEP;	        //新OEP地址
	DWORD				m_dwSizeOfHeader;	//文件头大小
	DWORD				m_dwSectionNum;		//区段数量
	DWORD				m_dwFileAlign;		//文件对齐
	DWORD				m_dwMemAlign;		//内存对齐
	DWORD				m_IATSectionBase;	//IAT所在段基址
	DWORD				m_IATSectionSize;	//IAT所在段大小
	IMAGE_DATA_DIRECTORY	        m_PERelocDir;		//重定位表信息
	IMAGE_DATA_DIRECTORY	        m_PEImportDir;		//导入表信息

同时在PE.h头文件中声明了一些函数

InitPE

首先通过句柄及其他一些参数读取文件 并判断是否为PE文件 然后以内存分布的格式将文件读取到内存 修正一些信息

拷贝文件头信息和区段 然后调用GetPEInfo获取PE信息

这个函数主要就是获取PE头的一些信息 NT头、 OEP等 然后重要的要保存重定位目录和IAT目录的信息 且要保存IAT所在区段的地址和大小 这样一个PE文件就读取完成了 在主程序中只需要声明一个PE对象 即可读取并保存PE文件的信息

2.将必要的信息保存到shell (PACK部分和shell部分的数据交换)

接下来是操作PE文件的部分 一共会有两次对PE文件进行操作 一部分是在加壳前PACK中,另一部分是shell在加完壳 让程序运行的时候 所以在这两次对PE文件的操作都需要读取PE文件的信息 壳的作者采用的是让PACK部分直接包含PE的类 定义一个PE的对象 即可直接获取PE文件的信息 而让shell部分导出一个包含PE信息的结构体 让PACK把需要传递的信息保存进这个结构体中 这样shell部分就能直接调用这些变量了

导出的结构体如下:

 Pack部分要做的就是载入Shell.dll这个文件,然后获取这个结构体信息,往里面保存数据

3.将shell部分附加到PE文件

3.1读取shell部分的代码 保存到pShellBuf中

3.2设置shell的重定位信息

一共有两处需要修改重定位信息的地方 一是导入的shell.dll 而是原程序的重定位信息(因为新导入了一个dll)

由于在加壳后 shell部分是最先执行的 所以这部分代码让系统的PE加载器进行重定位的修复即可

然后再手动修复原程序的重定位信息 因为shell.dll在PACK中是用LoadLibrary(L”Shell.dll”)导入的

所以系统已经自动将shell.dll的重定位信息修复了 而要手动修复原程序的重定位信息 就必须要知道系统修复前的重定位信息是怎样的 因为这里导入的shell.dll修复用的是我们shell程序镜像基地址 而我们需要得到新的重定位地址就需要将重定位后的地址减去加载的镜像基地址再加上PE文件的镜像基地址 参考博客中用了一个例子比较清晰的描述了这种修正

拿一条重定位数据举例来说:
        ①重定位原始地址=重定位后的地址-加载时的镜像基址
        重定位原始地址是一个内存相对偏移(RVA),需要把这个RVA加上PE文件默认的加载基址,然后再写回重定位表中的数据,才能让系统正确的进行重定位。
        ②新的重定位地址=重定位原始地址+新的镜像基址
        由①②可得:③新的重定位地址=重定位后的地址-加载时的镜像基址+新的镜像基址
        但由于我们的Shell部分是加载到PE文件的末尾,所以RVA地址还需要加上那个PE文件的镜像大小
        最终得出④新的重定位地址=重定位后的地址-加载时的镜像基址+新的镜像基址+代码基址(PE文件镜像大小)
        只需要把所得出的地址信息,写到加壳后的的PE文件中,系统就可以帮你正确的进行重定位了,当然,由于要重定位的信息我们自己添加的,需要通过修改PE文件目录表中的重定位信息来告诉系统应该去哪找重定位表。

3.3修改被加壳程序的OEP,指向Shell

我们在Shell中到处的结构体的第一个变量即是shell.dll第一个执行的函数 也是我们需要跳转到的新的OEP 将PE文件的OEP直接指向这个函数的地址即可

3.4合并PE文件和Shell的代码到新的缓冲区

现在内存中是有两个缓冲区 一个是原PE文件的缓冲区 另一个是shell.dll的缓冲区 那么要合并它们 就需要在内存申请一块两个缓冲区大小的空间 然后将两个缓冲区的内容拷贝进去 但是由于我们是多了一个区段用来存放shell.dll 在PE文件头中记录的区段数目还是原来的大小 还有新的区段的地址等信息也需要写入PE文件中

MergeBuf:

3.5保存文件

保存文件就是保存上个函数中所合并的缓冲区 由于缓冲区是直接在内存中dump下来的 所以是按内存对齐的 所以在保存成文件时修改了文件对齐大小与内存对齐大小一致 然后再删除目录表一些非必要的信息

3.6释放资源

最后将一些没用的资源释放 InitValue是将对象的所有属性都置为空或0

4.Shell部分代码

4.1获取Shell部分所需要的函数

在壳程序中 为了隐藏自己的行为 往往没有导入表 那么要如何获取所需要的函数呢 首先需要知道 不管一个PE文件有没有导入表 系统都会为其加载两个模块 ntdll.dll和Kernel32.dll 而Kernel32.dll有个函数是GetProcAddress() 通过它来获取我们需要的函数 在参考博客中 分别说了三种方法获取这个函数

常用的方法有三种:①通过特征匹配的暴力搜索。②利用系统的SEH机制找到Kernel32.dll的加载基址。③通过线程环境块TEB的信息逐步找到Kernel32.dll的加载基址。我在这里用的是第三种,详细的代码请见Shell部分的MyGetProcAddress()函数

但是用第三种方法在不同的操作系统可能存在兼容性问题 原因就是每个操作系统的Kernel32.dll的基地址可能不同

而在获取Kernel32.dll后 就可以通过遍历它的导出表来获取我们所要的函数GetProcAddress

这里只实现了通过函数名导出方式的搜索 如果是按序号导出的 那么就需要去查一下对应操作系统版本下的Kernel32.dll导出表的序号所对应的函数了

有了GetProcAddress函数指针后 需要什么函数直接用这个函数获取就行了 还有一个是LoadLibraryA函数 可以导入任意的模块

可以看到在shell的代码中 首先是获取Kernel32的地址 然后通过Kernel32的导出表获取GetProcAddress 再通过GetProcAddress 获取一些我们需要的函数如LoadLibraryA 然后通过LoadLibraryA获取user32.dll 这样一来我们需要什么函数都可以获得

4.2修复重定位信息

由于在加壳程序中 将原程序的重定位指针指向了shell部分的重定位信息 所以在shell中要修复原程序的重定位信息 原程序的重定位表指针在PACK过程中保存了 所以可以直接用 修复参考博客中

重定位表最终指向的是一个需要重定位的地址,这个地址是基于原PE文件默认基址(一般为0x00400000)的地址,原PE文件的默认基址我们也有保存过,所以修复起来还是比较方便的,只需要遍历原PE文件的重定位表,然后通过一个公式计算出重定位后的地址再填充回去就可以了。

计算公式:重定位后的地址=需要重定位的地址-默认加载基址+当前真实的加载基址。
还有一点需要注意的是,在修复的时候你所修复的地址的内存属性不一定是可写的,所以最好在修复之前用VirtualProtect()修改内存属性为可写,修复完以后再将原来的属性设置回去。

4.3修复原程序的IAT(导入表)

通过导入表指针遍历导入表信息 里面保存着需要导入的函数的名称和所在模块 我们所要做的就是加载这些模块 并从中获取函数地址 然后填到正确的IAT位置即可

4.4跳转到程序的原始入口

参考链接:https://bbs.pediy.com/thread-206804.htm

发表评论

您的电子邮箱地址不会被公开。