加壳与脱壳

 

目录

一、实验目的

二、实验环境

三、实验内容

四、实验要求

五、实验步骤

1.使用加壳工具Aspack、UPX

2. 分析代码流程

1)判断文件格式是否为PE格式

2)分配并载入内存

3)创建新的文件名和文件句柄

4)修改PE文件头

4)进行异或,对OEP处加密

5)生成壳代码段

6)输出加壳后的文件

3.修改弹框信息的内容

1)源代码修改字符串

2)使用IDA查看加壳后的程序

4.对整个代码节进行加密

1)原有代码加密情况

2)修改源代码中加密循环次数

3)修改shell中解密循环次数

5.增加加壳程序的适用性

1)额外数据介绍

2)修改加壳源代码

3)验证加壳代码

 

 

 

一、实验目的

掌握代码防护的基础知识,并熟练使用相关代码防护的相关方法,实现对代码的保护。

二、实验环境

(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字节进行异或加密,要实现对于整个代码节进行加密,需要对以下位置进行修改:

  1. 修改加壳代码中的加密循环中的次数为OEP所在代码段的长度
  2. 修改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文件导入内存时,判断附加数据是否存在,如果附加数据存在,则为附加数据分配内存,并在写入文件时,将附加数据一同写入,同时要修改原有写入节的大小为真实节的大小。

  1. 为附加数据分配内存

首先计算最后一个节的尾部地址,并按照文件对齐,获取其地址,而后通过文件的总大小减去其最后节尾部对齐地址,获得附加数据长度,当存在附加数据时,为附加数据分配内存,并读取文件中附加数据到内存中。

 

       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;

       }

 

 

  1. 将附加数据写入文件

 

       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)验证加壳代码

如上图所示,在最后一个节后,附加数据成功写入,同时验证加壳程序成功执行

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值