目录
一、实验目的
掌握代码防护的基础知识,并熟练使用相关代码防护的相关方法,实现对代码的保护。
二、实验环境
(1) VS2019
(2) Aspack、UPX
(3)样本加壳代码
三、实验内容
1、学习加壳工具ASPack、UPX的用法。
2、自己编写一款可用的加壳软件。
3、学习一种Shellcode代码的高级语言编写框架。
四、实验要求
1、修改弹框信息的内容(MessageBoxA的参数)。
2、对整个代码节进行加密,v1.0只是对代码节头部的1000个字节进行加密处理。
3、完善加壳代码,增加代码的普适性,v1.0对存在结构外数据的PE文件不适用。
五、实验步骤
1.使用加壳工具Aspack、UPX
1)Aspack使用
AsPack是高效的Win32可执行程序压缩工具,能对程序员开发的32位Windows可执行程序进行压缩,使最终文件减小达70%!针对ASPACk所开发的脱壳工具软件也有许多,包括ASPACK ATRIPPER 、ASPACKDIE 、ASPROTECT等。
Aspack界面如下所示
使用Aspack打开待加壳程序
由图可知Aspack压缩后软件为原有规模的33%,压缩效果良好
Aspack相关选项如下所示
可以实现压缩资源、创建备份、使用windows dll加载器、加载后立即执行等功能
2)upx使用
UPX(the Ultimate Packer for eXecutables)是一个非常全面的可执行文件压缩软件,支持 dos/exe、dos/com、dos/sys、djgpp2/coff、 watcom/le、win32/pe、rtm32/pe、tmt/adam、atari/tos、linux/i386 等几乎所有平台上的可执行文件,具有极佳的压缩比,还可以对未压缩的文件和压缩完后进行比较。
UPX展示
UPX进行压缩
UPX解压缩
2. 分析代码流程
通过分析可知整个加壳代码主要分为以下部分,入口函数在OnBnClickedButton1中,当点击开始加壳时,执行相关函数。
1)判断文件格式是否为PE格式,函数为IsPE()
2)分配并载入内存,函数为MemAlloc()
3)修改PE文件头,函数为EditHeader()
4)进行异或,对OEP处加密,函数为xorOEP()
5)生成壳代码段,函数为MakeShell()
6)输出加壳后的文件,函数为MakePacking()
重要的全局变量
pDosHeader:Dos头部地址
pNtHeader:NT头部地址
pOptionalHeader:选项头部地址
pSectionHeader:节头部地址
SizeOfShell:Shell大小
SizeOfImage:文件映像大小
dwFileSize:文件大小
dwOEP:OEP地址
dwNumOfSections:节数量
NewRelocalRVA:新重定位偏移地址
textVA:代码段的RVA
1)判断文件格式是否为PE格式
//创建文件句柄 HANDLE hFile = CreateFile(my_file_name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { MessageBox(TEXT("打开文件失败!"), TEXT("错误提示"), MB_OK); return; } |
首先创建文件句柄
//判断文件格式 if (!IsPE(hFile)) { MessageBox(TEXT("文件不是PE可执行文件"), TEXT("错误提示!"), MB_OK); return; } |
而后使用IsPE()函数判断是否为PE格式
判断一个文件是否为exe格式主要从以下方面判断:
1、首先检验文件头部第一个字的值是否等于 IMAGE_DOS_SIGNATURE(MZ),是则 DOS MZ header 有效。
2、一旦证明文件的 DOS header 有效后,就可用e_lfanew来定位 PE header 了。
3、比较 PE header 的第一个字的值是否等于IMAGE_NT_HEADER(PE),是则有效。
4、判断OEP的值是否为0,否则有效
5、判断文件是否为exe格式
如果前后两个值都匹配,那我们就认为该文件是一个有效的exe文件。
读取MZ标志
使用
SetFilePointer(hFile, 0, NULL, FILE_BEGIN); ReadFile(hFile, &wTemp, 2, &dwBufferRead, NULL); if (wTemp != 'ZM') { return FALSE; } |
010editor打开可知
//读取PE头位置 SetFilePointer(hFile, 0x3C, NULL, FILE_BEGIN); ReadFile(hFile, &dwOffset, 4, &dwBufferRead, NULL); |
读取PE头位置
用e_lfanew指针定位pe头,e_lfanew一般位于0X3C,PE头位置为0x0108
SetFilePointer(hFile, dwOffset, NULL, FILE_BEGIN); ReadFile(hFile, &wTemp, 2, &dwBufferRead, NULL); //判断是否为PE,同样要反着判断。 if (wTemp != 'EP') { return FALSE; } |
读取PE头信息
定位PE头后,判断是否为PE
获取文件OEP
//获取文件OEP SetFilePointer(hFile,dwOffset+0x28,NULL,FILE_BEGIN); ReadFile(hFile,&dwOEP,4,&dwBufferRead,NULL); //如果OEP为0。 if (!dwOEP) { return FALSE; } |
可知OEP值为034256H
//获取文件特征,判断是exe还是dll文件。 SetFilePointer(hFile, dwOffset + 0x16, NULL, FILE_BEGIN); ReadFile(hFile, &wTemp, 2, &dwBufferRead, NULL); if (wTemp & 0x2000 != 0) { return FALSE; } |
获取文件特征,判断是exe还是dll文件
通过相应字段与0x2000相与判断是否为0判断是否为dll格式
2)分配并载入内存
代码调用如下所示
//分配并载入内存 if (!MemAlloc(hFile)) { MessageBox(TEXT("文件加载到内存失败!"), TEXT("错误提示!"), MB_OK); return; } |
在执行一个PE文件的时候,Windows并不在一开始就将整个文件读入内存,而是采用与内存映射的机制,也就是说,Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存。PE文件与内存映像如下所示:
根据内存映像大小,分配内存
//获取文件的映像大小,从PE头中读取。 SetFilePointer(hFile, 0x3C, NULL, FILE_BEGIN); ReadFile(hFile, &dwOffset, 4, &dwBufferRead, NULL); //读取PE头位置 SetFilePointer(hFile, dwOffset + 0x50, NULL, FILE_BEGIN); ReadFile(hFile, &dwSizeOfImage, 4, &dwBufferRead, NULL);//读取文件映像大小 SizeOfImage = dwSizeOfImage; SetFilePointer(hFile, dwOffset + 0x54, NULL, FILE_BEGIN); ReadFile(hFile, &dwSizeOfHeaders, 4, &dwBufferRead, NULL);//读取文件头大小
lpVirtualtAlloc = VirtualAlloc(NULL, dwSizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); //如果分配失败 if (lpVirtualtAlloc == NULL) { return FALSE; } |
根据PE结构获取内存映像大小并进行分配
读取文件头到分配的内存中
//首先读取文件头 SetFilePointer(hFile, 0, NULL, FILE_BEGIN); ReadFile(hFile, lpVirtualtAlloc, dwSizeOfHeaders, &dwBufferRead, NULL);
//获取PE文件头相关指针 pDosHeader = (PIMAGE_DOS_HEADER)lpVirtualtAlloc; pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + (DWORD)pDosHeader); pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)(&pNtHeader->OptionalHeader); //IMAGE_FIRST_SECTION是VC下定义的一个宏,用来获取区段表的头指针 pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader); |
而后分区块进行读入
//然后分区块进行读入 dwNumOfSections = pNtHeader->FileHeader.NumberOfSections; for (int i = 0; i < dwNumOfSections; i++) { //将指针设定到每个区块的开始 SetFilePointer(hFile, (pSectionHeader + i)->PointerToRawData, NULL, FILE_BEGIN); //根据每个区块的原始大小读入到相应的虚拟地址中去。 ReadFile(hFile, (LPVOID)((DWORD)lpVirtualtAlloc + (pSectionHeader + i) ->PointerToRawData), (pSectionHeader + i)->SizeOfRawData, &dwBufferRead, NULL); } |
3)创建新的文件名和文件句柄
//创建新的文件名 CString new_name = my_file_name.Left(my_file_name.GetLength() - 4) + TEXT("_packed.exe");
CloseHandle(hFile); |
在原有文件名后加入_packed作为加壳后代码
创建加壳后文件句柄
//创建加壳后的文件句柄 hFile = CreateFile(new_name, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { MessageBox(TEXT("生成文件失败!"), TEXT("错误提示!"), MB_OK); return; } |
4)修改PE文件头
初始化新的节
IMAGE_SECTION_HEADER SectionHeaderOfShell; //壳代码段的区块表信息 SizeOfShell = 0x100; //大小暂时定为256字节 //以下为修改区段表信息功能,主要是增加壳区段表 //初始化区段表结构 memset(&SectionHeaderOfShell, 0, sizeof(SectionHeaderOfShell)); //获取对齐大小数据 |
对新的节的属性进行填充
SectionHeaderOfShell.Misc.PhysicalAddress = SizeOfShell; //原始大小 //在文件中对齐后的大小,除以文件粒度,如果余数为零,就直接使用;否则,就扩充对齐。 SectionHeaderOfShell.SizeOfRawData = (SizeOfShell%dwFileAlign) ? (dwFileAlign*(SizeOfShell / dwFileAlign + 1)) : SizeOfShell; //区块特征 SectionHeaderOfShell.Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE; memcpy(&SectionHeaderOfShell.Name, ".sun", 7); //区块名称 SectionHeaderOfShell.PointerToRelocations = 0; //重定位偏移 SectionHeaderOfShell.NumberOfRelocations = 0; //重定位表数目 SectionHeaderOfShell.PointerToLinenumbers = 0; //行号表偏移 SectionHeaderOfShell.NumberOfLinenumbers = 0; //行号表中行号数目 |
而后对NT头和选项头进行填充,其中重点是区段表修改、文件镜像增加、修改程序OEP,使得程序初始跳转到新增代码段中。
//计算壳区段表在PE头中的位置 lpShellSecTab = (LPVOID)((DWORD)pSectionHeader + sizeof(IMAGE_SECTION_HEADER)*dwNumOfSections); //将壳区段信息拷贝到文件头中 //此方法并不严密,因为没有考虑到PE文件头中是否还有多余的空间,为简化,暂如此操作。 memcpy(lpShellSecTab, &SectionHeaderOfShell, sizeof(SectionHeaderOfShell));
//区段表修改完毕,下面修改PE头 //区段表个数加1。 pNtHeader->FileHeader.NumberOfSections++;
//文件镜像增加 pOptionalHeader->SizeOfImage = SizeOfImage + ((SizeOfShell % dwSectionAlign) ? (dwSectionAlign*(SizeOfShell / dwSectionAlign + 1)) : SizeOfShell);
//修改程序OEP pOptionalHeader->AddressOfEntryPoint = (pSectionHeader + dwNumOfSections)->VirtualAddress; //由于原先dwNumOfSections未加1 |
4)进行异或,对OEP处加密
DWORD C壳的编写Dlg::GetSectionNumber(DWORD VirtualAddress) { PIMAGE_SECTION_HEADER pSectionTry = pSectionHeader; for (DWORD i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++) { DWORD dwBeginVA = pSectionHeader[i].VirtualAddress; DWORD dwEndVA = pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData; if ((dwBeginVA <= VirtualAddress) && (VirtualAddress < dwEndVA)) return i; } return -1; } |
首先获得OEP所在代码节的序号,通过遍历PE文件中各个节,通过比较OEP的地址与各个节的起始地址和终止地址,得到所在节的序号
而后记录代码段的RVA,并修改代码段的属性,设置为可写,而后对该代码段进行异或加密
//记录代码段的RVA textVA = pSectionHeader[j].VirtualAddress; //修改代码段,让代码段可写,使其可以解密 pSectionHeader[j].Characteristics |= IMAGE_SCN_MEM_WRITE;
byte *oep; oep = (byte*)((DWORD)lpVirtualtAlloc + GetPhyAddress(pSectionHeader[GetSectionNumber(dwOEP)].VirtualAddress));
for (int i = 0; i < 1000; i++)oep[i] ^= 0xAB; |
5)生成壳代码段
lpVirtualShell = VirtualAlloc(NULL, SizeOfShell, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
dwOEP += pOptionalHeader->ImageBase; textVA += pOptionalHeader->ImageBase; memcpy(Code_two ,&dwOEP, sizeof(DWORD)); memcpy(Code_three, &textVA, sizeof(DWORD)); memcpy(lpVirtualShell, Code_one, sizeof(Code_one)); memcpy((LPVOID)((DWORD)lpVirtualShell + sizeof(Code_one)), Code_two, sizeof(Code_two)); memcpy((LPVOID)((DWORD)lpVirtualShell + sizeof(Code_one)+sizeof(Code_two)), Code_three, sizeof(Code_three)); NewRelocalRVA = sizeof(Code_one); |
将shellcode代码、OEP地址以及原有第一个代码段的起始地址合并创建内存空间
其中OEP地址和第一个代码节地址在此前已经给出,通过使用010editor打开可得,OEP地址和第一个节起始地址与基地址相加后地址如下图所示
6)输出加壳后的文件
首先进行重定位操作
而后写入原始文件
if (!WriteFile(hFile, lpVirtualtAlloc, dwFileSize, &dwBufferRead, NULL)) { return FALSE; } |
最后写入壳代码文件
//写入壳代码段文件 if (!WriteFile(hFile, lpVirtualShell, (pSectionHeader + dwNumOfSection - 1)->SizeOfRawData, &dwBufferRead, NULL)) { return FALSE; } |
3.修改弹框信息的内容
1)源代码修改字符串
通过查看弹框内容可知存在字符串
使用IDA查看可知字符串所在位置
在源代码中的shell进行相关修改,由于设计重定位,未简化起见,仅作替换,不做大小的变化。
将小麦的gbk编码0xD0, 0xA1, 0xB1, 0xED替换为,0xCD,0xF5,0xCE,0xE5,即可实现源码修改字符串。
在线中文gbk编码网站链接:http://www.dwenzhao.cn/cal/php/hexhanzi.php
2)使用IDA查看加壳后的程序
修改成功
弹框如下所示:
4.对整个代码节进行加密
参考链接:https://bbs.pediy.com/thread-250960.htm
基础知识:
1.区段名(可选)
2.区段数据的实际字节数Misc.VirtualSize
3.区段的VirtualAddress(区段数据在内存中的RVA),此值必须是: 上一个区段的VirtualAddress + 上一个区段经内存对齐粒度对齐后的大小(内存对齐大小是0x1000的整数倍)
4.区段以文件对齐粒度对齐后的大小SizeOfRawData(文件对齐大小是0x200的整数倍)
5.区段的PointerToRawData(区段数据在文件中的偏移),此值必须是:上一个区段的PointerToRawData + 上一个区段的SizeOfRawData
原有代码只实现了对于OEP所在代码段的前1000字节进行异或加密,要实现对于整个代码节进行加密,需要对以下位置进行修改:
- 修改加壳代码中的加密循环中的次数为OEP所在代码段的长度
- 修改shell中解密循环中的次数为OEP所在代码段的长度
1)原有代码加密情况
加密前
加密后
通过计算可知1000字节以前的代码进行了与0xAB异或加密,1000字节以后的代码未进行加密
2)修改源代码中加密循环次数
通过获取OEP所在节的序号,并利用头部信息获取节的长度,进行加密
bool C壳的编写Dlg::xorOEP() {
DWORD j = GetSectionNumber(dwOEP);
//记录代码段的RVA textVA = pSectionHeader[j].VirtualAddress; //修改代码段,让代码段可写,使其可以解密 pSectionHeader[j].Characteristics |= IMAGE_SCN_MEM_WRITE; byte *oep; oep = (byte*)((DWORD)lpVirtualtAlloc + GetPhyAddress(pSectionHeader[GetSectionNumber(dwOEP)].VirtualAddress)); DWORD OEP_Section_Length = pSectionHeader[GetSectionNumber(dwOEP)].SizeOfRawData; for (int i = 0; i < OEP_Section_Length; i++)oep[i] ^= 0xAB; return false; } |
3)修改shell中解密循环次数
在shell中查找1000的16进制编码0x03EB,将shell中十六进制分为三段,其中中间一段为1000的十六进制表示,用OEP所在代码节的长度进行填充,而后将所有二进制代码进行复制,实现相应次数解密。
代码段未加密
代码段部分加密
代码段全部加密
可知成功修改实现对整个代码节进行加解密
5.增加加壳程序的适用性
1)额外数据介绍
参考链接:https://www.cnblogs.com/huqingyu/archive/2010/04/12/1710638.html
其实,overlay虽然大家在脱壳当中觉得很陌生,但是他离我们并不遥远。在我们平时使用的软件当中,有一些软件要处理一些数据流文件,比如winamp。当我们下载了mp3文件(数据文件),没有播放器是不可能播放的,与此相关的还有很多,比如txt文件和notepad的关系也差不多。而这些数据文件被单独的保存在硬盘上,当我们使用notepad的打开功能的时候,就可以去读取数据文件里面的东西了。
overlay又是什么意思呢?他其实真正的意思就是取消打开功能,将这些需要读取的数据放到pe文件的后面,让程序自动的运行打开的功能。这样的功能就变成了一个notepad的程序对应只能打开一个文件。
最典型的就是一些软件可以把一些数据流文件生成exe文件,比如一些mp3生成器,flash生成器,以及我们用来做动画的S-demo。他们的作用就是将数据对pe进行捆绑。
1.overlay只是数据他是不映射到内存的,他将被程序以打开自己的方式来读取数据
2.只要不是区段里面包括的文件的大小,将被视为overlay
额外数据的两种处理方式
有的附加数据的读取方式是固定的位置,如固定从3800h处开始读.
有的附加数据的读取方式是从文件尾开始向前定位, 比如以前的某些木马的配置信息.
对于后者, 我们原封不动移动附加数据在文件尾即可.
对于前者, 根据情况,可不去更改附加数据的位置, 而将区块数据放到附加数据之后即可
通过查看everything.exe可知:
在各个节之后仍存在数据,这部分数据称为额外数据,供PE文件使用
如下图所示,在正常的节之后,存在额外数据
原有加壳方法下,附加数据丢失:
Everything的额外数据处理方式是从文件尾进行,即将额外数据重新复制在文件的尾部即可使用。
2)修改加壳源代码
主要思路,在将PE文件导入内存时,判断附加数据是否存在,如果附加数据存在,则为附加数据分配内存,并在写入文件时,将附加数据一同写入,同时要修改原有写入节的大小为真实节的大小。
- 为附加数据分配内存
首先计算最后一个节的尾部地址,并按照文件对齐,获取其地址,而后通过文件的总大小减去其最后节尾部对齐地址,获得附加数据长度,当存在附加数据时,为附加数据分配内存,并读取文件中附加数据到内存中。
PIMAGE_SECTION_HEADER pLastSectionHeader = pSectionHeader + dwNumOfSections - 1; DWORD dwRtn; PUCHAR lpUselessBuf = NULL; DWORD file_end = ALIGN_PAGE((pLastSectionHeader->PointerToRawData + pLastSectionHeader->SizeOfRawData), pOptionalHeader->FileAlignment); DWORD useless_size = GetFileSize(hFile, NULL) - file_end; DWORD dwUselessBufLen = useless_size; if (useless_size > 0) { lpUselessBuf = (PUCHAR)VirtualAlloc(NULL, useless_size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);;
if (lpUselessBuf == NULL) return FALSE;
ReadFile(hFile, lpUselessBuf, useless_size, &dwRtn, NULL); if (dwRtn != useless_size) return FALSE; } |
- 将附加数据写入文件
DWORD dwUseLen = dwFileSize - dwUselessBufLen; //if (!WriteFile(hFile, lpVirtualtAlloc, dwFileSize, &dwBufferRead, NULL)) if (!WriteFile(hFile, lpVirtualtAlloc, dwUseLen, &dwBufferRead, NULL)) { return FALSE; } |
修改写入原始文件的大小
在MakePacking函数中将附加数据写入文件
//写入PE描述范围外的数据 if (dwUselessBufLen > 0) { if (!WriteFile(hFile, lpUselessBuf, dwUselessBufLen, &dwBufferRead, NULL)) { return FALSE; } } |
3)验证加壳代码
如上图所示,在最后一个节后,附加数据成功写入,同时验证加壳程序成功执行