史上最全最完整,最详细,软件保护技术-程序脱壳篇-逆向工程学习记录
历史:壳是最早出现的专用加密软件技术!作用:可以是开发者未来保护自己的代码不被借鉴、破解、逆向;也可用来病毒、木马、蠕虫隐藏恶意代码,不被杀毒如软件查杀!预习:vmp纯虚拟机壳,目前公认认为公认最强。温馨提示:脱壳上瘾,可不要随意传播哦!
title: 程序脱壳篇
date: {{ date }}
tags: [‘程序壳’,‘程序脱壳篇’]
categories: [‘程序壳’,‘程序脱壳篇’]
excerpt: 壳是最早出现的专用加密软件技术!程序的壳可以是开发者未来保护自己的代码不被借鉴、破解、逆向;也可用来病毒、木马、蠕虫隐藏恶意代码,不被杀毒如软件查杀!所以我们每次逆向破解的时候遇到带壳程序会非常头疼,这篇文章主要是有关程序脱壳的技术的讲解,内容详细,深入学习者一定不能错过,带你深入研究程序脱壳技术,一定有所收获!
声明
首先要感谢的是我在2022年为了进阶逆向工程学习这门技术的老师,是鱼C工作室的老师小甲鱼,先学会感恩,是为人处世的基本准则。这是我看了它的脱壳篇后的课后自己实践过一遍的笔记总结。
其次这篇文章我最早发布于我临时搭建的博客网站,随后发布现在才搭建的正式网站
最后,抄一百篇不如自己实践操作写一篇,尊重知识产权,写笔记辛苦,禁止转载!!!引用注明出处!!!写了几个月,因为都是实践过的,如今分享给大家学习,但是尊重劳动成果。
我是同一作者!
一· 什么是壳?
1 . 初步认识壳
历史:壳是最早出现的专用加密软件技术!
作用:可以是开发者未来保护自己的代码不被借鉴、破解、逆向;也可用来病毒、木马、蠕虫隐藏恶意代码,不被杀毒如软件查杀!
预习:vmp纯虚拟机壳,目前公认认为公认最强。
温馨提示:脱壳上瘾,可不要随意传播哦!
2 . 壳的执行过程
加壳压缩,壳先执行,还原源程序,执行源程序
3 . OEP(Original Entry Point)
1·EP(Entry Point)
EP(Entry Point),意即程序的入口点。而OEP是程序的原始入口点,一个正常的程序只有EP,
例如:C语言main函数、winAPI函数、VC编译的入口地址0x40000000
只有入口点被修改的程序(加壳等),才会拥有OEP。
2·OEP
OEP:(Original Entry Point),程序的原始入口点,软件加壳就是隐藏了EP, 只要我们找到程序的OEP,就可以立刻脱壳。
PUSHAD (压栈) 代表程序的入口点,POPAD (出栈) 代表程序的出口点,与PUSHAD相对应,一般找到这个OEP就在附近啦。
4 . 壳的装载过程
模拟windows加载器:
壳获取自己所需要使用的API地址、解压或解密源程序的各个区块、进行必要的重定位、跳转到程序原入口点(OPE)
5 . 压缩引擎
1·aPLib
低于64kb的文件压缩速度较快
2·JCALG1
对大文件效果较好
3·LZMA
使用目的:开源,稳定
特点:保证压缩比的情况下解压速度一定要快
6 . 壳的分类
1·压缩壳
- ASPack
- UPX
- PECompact
2·加密壳
- ASProtect
- Armadillo
- EXECryptor
- Themida
- VMProtect 缺点慢
二· 实践是检验真理的唯一标准
接下来我们尝试破解这样一个老程序壳:
1 . 查看壳
1·PEiD查看
2·Exeinfo PE查看
咳咳咳,咋就是说,这找不到这个程序壳的特征码,咋们看不出什么来,试试别的,是个老古董壳!
3·脱壳后的程序查看
可见它是由VC5.0编译的程序!
可见无壳!
2 . OD脱壳
用od把程序打开,刚进来情况是这样的:
刚进来的时候是要提示是壳的EP,如果不是,要在选项/调制选项/事件中设置第一次暂停于主模块入口:
点击OD里面的M查看进程内存:
试着分析对比,可见代码的入口地址是0x00401000,按C回到查看汇编代码,我们现在的入口地址是0x46B6DE,可见它在包含SFX的地址0x46B00000中。这壳有这三个区块(资源、SFX、重定向)。.text段就是程序的代码段块!
接下来单步执行F8,到jmp的时候跳转到地址0x004271B0:
来到这大概这个样子,右键分析/分分析代码:
再看进程内存,可知0x004271B0是在包含代码地址0x00401000中,所以它开头是55/8BEC,我们就找到了OEP
在OD的plugin文件夹中加入插件重新打开:
右键选择插件dump debugged process把程序dump出来
如下设置:
确保勾选Fix Raw Size是修复壳进行的数据偏移,不勾选Rebuil lmport输入表不进行重定位!
点击dump,保存文件命名dumo1,回到上面脱壳后的查看!
3 . 删除壳,减小程序大小
目前查看dump1.exe文件属性/大小:
打开软件lordPE,然后点击PE Editor按钮,选择dump1,对照弹出窗口里面显示的出数据和OD里的进程内存对比,然后点击Sections按钮,发现多余的壳区块,通通右键选择wipe section header, 如果是灰色的,那么请确保程序先不处于运行状态。后点击Save按钮保存OK。
最后点击Rebuild PE再选择dump1打开:
咋就知道,程序大小最后变成了原本91%!
此时再用OD打开就发现壳已经无了:
4 . 根据堆栈平衡原理寻找OEP
1·示例:zip压缩壳
实例:看文件名可猜测这是一个压缩壳
用OD打开,发现刚进来第一句就是一堆跳转之中的一个jmp:
这里就是壳的模块入口了,F8执行,可见这格式像是VC程序的入口点:
可是这并不是OEP,这是为什么呢?原因我们来到进程内存来看:
程序目前运行地址在0x004682BC,这大概在进程内存的0x00465000到0x0046A000之间,并不是我们所想的在0x401000里面,所以它其实是一个伪造程序区块OEP的壳!
现在我们重新打开,从最开始就要注意观察ESP寄存器的变化:
刚开始是0012FFC4,当运行到
时ESP发生第一次变化,变成0012FFC0,我们要把鼠标放在ESP显示的数据上右键选择数据窗口中跟随:
然后就可以在数窗口看到0012FFC0,这是ESP所在的内存的位置,在该位置右键/断点/硬件访问/word都可以:
此时就设置好了硬件断点,此断点不一般,只能在/调试/硬件断点/查看到哦,也可在此修改,也可以看到一般它只有四个硬件断点,使用的CPU特殊寄存器下断:
F9直接运行到断点的位置
此时ESP的值是0012FFC4,刚好是刚才断点的位置!
这里可以记录一下,一般大部分的壳出现jmp eax都是跳转到OEP,但是这会出现一个明显的分界点,所以现在一些的强壳不会这样做!
可以看到EAX的值是004271B0,我们继续看到进程内存,它在0x00401000到0x0044B000之间的代码区块!
然后F8就进到了OEP:
此时就可以删除硬件断点,并进行dump操作!
也是先不勾选重定向,然后保存dump2!
此时打开lordPE,点击PE Editor按钮,选择dump2打开:
先点击Sections按钮把壳的区块删除。然后可以看到ImageBase:00040000正确。但是代码段的偏移Base0fCode:00065000指向的还是伪装壳的代码段,所以我们要把他修改偏移成1000,Base0fData:改成4B000。,Save,OK保存!
会发现它脱完壳之后竟然变大了原来的85%,因为这是压缩壳!
接下来就可以根据retn所指向链接的黑线找到该区块的入口:
根据这个内存地址和连接指向的红线我们可以再往上寻找,发现就是回到了我们刚进来的一堆jmp的地方:
也就是这样通过一连串的代码块的跳转对文件不同区块的压缩和加密!
2·作业
1.eXPressor壳
程序:
查看PEiD:
可见这是一个exPressor的壳!
OD打开程序,刚进来就在这个地址:
点击M查看进程内存:
根据经验,程序本来的.text代码块被壳包含在了0x0046B000开始的区段,达到了“壳里有肉,肉里有壳”的效果!
根据堆栈平衡原理(80%有效)找到OEP,这次进行dump操作的时候,一点要勾选上重定向输入表!
这样插件就会搜索重建输入表,搜索里面的jmp跳转到一些API,然后再把这些API依次组成一个输入表!保存为dump3
再用PEiD查看dump3.exe的时候,就原型毕露了,知道它是VC++5.0写的一个程序!
再用OD打开dump3.exe点击M查看进程内存:
发现它多了一个输入表!
我们在进行LordPE的时候修改偏移Base0fcode的时候,首先要看到dump3程序入口点地址:
对比上面的进程内存,可见他在0x401000开始的区段,所以Base0fcode要修改成1000!Save
在删除多余的区块的时候,备份一份,尝试后发现,只有.ex_cod可以wipe掉!
最后Rebuild PE选择dump3!
完成!
2.MEW压缩壳
程序:
查看壳:
这题只需要注意一点,像MEW系列壳除了可以使用堆栈平衡原理寻找OEP,记得分析代码;还可以在第一个区段网下滑找到retn加断点,F9运行到这F8,就到了OEP。应为这种壳,就是有个大区段,对软件进行解压缩,没有对它进行什么防护措施!
其他的都很简单,直接ReBulid PE就行,不用删除什么区块!
5 . .text区块加断点寻找OEP
程序:
查看壳:
该程序直接点击运行是没有任何问题的,但是一用OD运行就只能运行出一个空窗口,而且这时使用堆栈平衡法寻找OEP也是不管用,所以这个程序是具有反调试系统的,所以我们之能先一步一步走,看能不能找到关于OD的API函数,直到运行到:
IsDebuggerPresent是windows提供的对调试器判断的一个API
还有一种更快捷的办法,右键/查找/所有模块中的名称/往下滑就能看到IsDebuggerPresent:
双击就进了它的动态链接库:
在这里我们加断点,运行到这,然后按Alt+F9也可以来到差不多我们刚才单步走到的位置!
注意寄存器EAX,当能探测到调试器的时候eax=1,反之不能探测到则为eax=0
因此可见,该程序跳转就不能是实现了:
所以我们可以先修改,标准寄存器ZF位的值吧,也可以把它改为jmp,让其实现跳转,此时再按F9运行可见程序就能正常执行了!
此时就是去掉他的反调试功能了,在修改jmp的位置此时右键/复制到可执行文件/选择,然后保存文件,命名2。
继续用OD打开去掉反调试后的该文件2:
查看进程内存:
但是!后面发现,用堆栈平衡方式寻找OEP并没有用,甚至停不下!所以这里我们要用另外一种方法就是在.text区块加断点!因为壳不论怎么运行终究还是要回到源程序的代码段的,所以我们就直接在原程序代码段加断点:
00401000右键/在访问上设置中断,运行:
但是这并不是我们想要的地址,那由于运行过了,断点取消了,那我们再继续断继续走,多次后发现并不可以,但是我们看到了loopd:
可以看到这里灰线的箭头其实是多次循环回去了,所以我们先直接先让它跳出循环再说,在循环外的下一个地址加断/运行/取段,就来到了这:
此时再次00401000下断点/运行的时候,又是多次来到了这里:
可见这是又有一个循环的,那总是进去很多个循环怎么办,一个一个像上面那样吗?不!我们重新开始,可以Ctrl+F8,或者长按F8,先观察循环都在哪里,再直接找到循环的末端,通过上面的方式下断:
看就是这个灰色循环,目前我们已经断在外了,取断后,断00401000执行,然后我们就来到真实的OEP啦,右键/分析/分析代码:
然后我们就把它dump5吧:
接下来备份dump5就可以开始老的方法wipe去除多余的区块.Prt啦!
图片 p120.png:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qtOxDMhv-1687064225509)(https://www.52ying.top/usr/uploads/2023/ketu/图片p120.png)]
成功啦!
三· 重建IAT输入表(Import Address Table)
1 . 啥是输入表?
在各种不同版本的Windows系统中,DLL的版本各不相同,同一函数在不同版本DLL中的位置也可能不同;另外一个原因在于DLL的重定位,DLL文件的ImageBase值一般为10000000,但当两个DLL尝试装载到同一个地址时会发生冲突,因此有一个DLL得寻找另一个地址装载,这就是所谓的DLL重定位。
因此当我们要调用某个DLL中的函数时,需要借助IAT(Import Address Table,导入地址表)作为中间桥梁。PE文件在装载时由PE装载器将导入函数的真实地址写入到IAT时,函数调用时则需要从IAT中获取函数的真实地址。
对于每一个引入的可执行文件(例如dll),有一个镜像引入描述符(IMAGE_IMPORT_DESCRIPTOR)。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; // RVA,指向字符串,是这个可执行文件的名字。例如"ACE.dll"
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
IAT是一个IMAGE_THUNK_DATA类型的数组。有多少个函数被导入,这个数组就有多少个成员。该数组以0结尾。
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // 一个RVA地址,指向forwarder string
DWORD Function; // PDWORD,被导入的函数的入口地址
DWORD Ordinal; // 该函数的序数
DWORD AddressOfData; // 一个RVA地址,指向IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
PIMAGE_IMPORT_BY_NAME是一个非常简单的结构,就两个成员。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 该函数的导出序数
BYTE Name[1]; // 该函数的名字
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
2 . OD跟踪输入表
例程序:
这是个简单的MessgeBoxA对话框程序,用OD打开:
虽然OD注释可以识别到这是MessgeBox,但点开这条指令是call 004011EA,那为什么它知道内存地址0x004011EA地址是一个MessgeBoxA的API函数呢?
那我们就在这行按下回车Enter键:
可以看到这是个跳转,其实周围还很多都是跳转,查看下面按是跳转到哪里:
上面显示的是取[00402048]内存地址里面的值=77D507EA跳转,为了验证一下,我们在内存窗口Ctrl+G输入00402048,就来到了这个地址:
根据小端存储,从右往左读,发现正是77D507EA!
所以我们可以知道在这整个程序无论到在个位置调用MessgeBoxA,它都相当于call 004011EA,在这里集中跳转到MessgeBoxA的动态链接库!
可以看到上面这些都是OD分析给我的,但是OD为什么会知道呢,其实就是根据IAT得出的!
接下来我们继续跟踪输入表,Alt+M到内存双击PE文件头,也就是00400000区块:
然后我们就可以看到标准PE头文件格式:标志:Image Base:偏移地址,这是输入表的地址Import Table address:
回到内存窗口,其实输入表所在地差不多就在这个位置:
接下来我们在代码窗口Ctrl+G搜索00402050,我们就来到了这里,但是发现没办法分析,因为它认为我们这不是在代码段:
所以我们要像之前一样安装一个新的插件:
安装好插件后重新打开,就可以点击Analyze This!
可见左边黑色数据是小端存储,右边绿色是OD简化成了我们方便阅读的字节形式,都是相对400000的偏移地址,可见总共有两个IMAGE_IMPORT_DESCRIPTOR,最后一个全是0是作为结束的标识。我们先尝试Ctrl+G搜第一个参数OriginalFilrstThunk地址00402098:
然后就走到了INT的第一个参数IMACE_THUNK_DATA又是一个偏移地址00402122,继续搜:
可见我们就来到了IMAGE_IMPORT_BY_NAME内,可找到函数的名字(都是由ASCII码转换翻译的)!
然后看到IMAGE_IMPORT_DESCRIPTOR第五个参数FirstThunk,这才是真正指向IAT的偏移地址:
搜索到:
可见其实这就是IAT输入表!
3 . HOOK-API
壳为了防止输入表被还原,强化壳自身的保护功能,会在IAT大量加密。
例如:源程序要调用一个函数的时候需要找IAT,然后却进到了壳程序进行监控般的跳转才能最终回到IAT!
四· FSG压缩壳和ImportREC的使用
程序可以正常运行:
查看壳:
可见这是用Delphi7.0写的程序,API比较多,所以IAT修复比较麻烦!
安装壳程序,点击install.exe,或者自行解压fsg.rar:
给程序FishC加壳,拖拽即可!这是一个压缩壳,压缩44%加壳后程序也可正常运行!
现在查看壳就可看到壳了:
把加FSG壳后的程序用OD打开:
此时用老方法较为复杂,目前使用一种新方法,步骤如上图1234,选项/调试选项/SFX/字节方式跟踪真正入口处/重新运行!
在这之后,还是回到了原来的地方,然后再带点击选项/调试选项/SFX/停在自解压器的入口/重新运行/,就可以看见左下角在疯狂的运行,这是在模拟程序的运行,自解压:
然后就来到了OEP,接下来就是右键/分析/从模块中删除分析:
接下来就可以dump操作了。由于SFG壳破坏了IAT,所以不要勾选重建输入表,因为勾选了也没用!我们接下来还要借助ImpREC.exe神奇工具修复IAT!
如图,要确保要修改的程序被OD打开中或者与运行中,ImpREC才能检测到,然后根据我们刚才跟踪到了SFX代码真正入口点的地址,提取偏移地址,修改上面红色框起来的OEP,然后点击IAT AutoSearch就弹出一个提示框:
ImportREC会自动搜索所有的IAT!
翻译:好像找到了一些东西,但也有可能不不正确,如果不正确,建议把RAV改成:00001000 Size改成:00094000
接下俩我们就来验证一下,在内存搜索RAV地址,看它是不是IAT的入口点:
就看到这里:
继续往下滑就可以看见熟悉的API函数:
可见00461BEC上面还包含一些数据其实也是,所以我们还要是要把RVA改成:0046B12C,把这些数据包含进去,这才能确保我们的IAT修复的是完整的,才能正常打开程序!
可见IAT最后到了这里:
所以Size也要修改,范围大了可以,范围小了就不可以,只要把全部IAT包含到了就可以!我们修改成大概10000吧:
然后我们就可以点击Get Imports按钮了!
加载一会儿,可以看到很多的valid:NO这些就都是不合法的,总共有6642条都是不合法:
所以我们点击Show Invalid按钮把不合法的都展示出来,可见这些不合法的数据穿插在各个IAT里面,只要程序加载到这里的时候,加载不到,就会报错!所以我们右键/Cut thunk(s),把这些不合法的东西全部都给砍掉!
然后就可见我们见到的都是valid:YES的了,然后我们就点击Fix Dump按钮,选择覆盖刚才OD的dump文件:
然后它就帮我们多生成了一个文件dump_可以正常运行!
然后再用PEiD查看壳,如果有时候查看不出来可以选择选项/深度扫描:
就出来了!也可以观察到,脱壳后的文件变大了!
五 . UPX和WinUpacx压缩壳
UPX壳是目前最流行的,最稳定的壳,更新也较块!
1 . upx309w命令行加\脱壳
win+R打开控制台,cd到壳程序目录下,upx.exe加壳,后面的才参数是我们待加壳地程序,加壳后可以看到,压缩到了原来的51.93%,大小从582656变成了302592字节!
其实也可以不用命令行,直接拖拽到upx.exe加壳!
然后我们就能查到壳:
然后查阅文档可知,命令加上-d参数,即可脱壳:
如下:
2 . Free UPX
点击Add files按钮选择程序,COMPRESS是加壳,DECOMPRESS脱壳,下面的Backup file可以选择create可以创建一个副本,备份原程序!
3 . OD脱UPX壳
OD打开加UPX壳后的fishc程序:
通过堆栈平衡原理寻找OEP过程中,硬件断点来到这里:
由于这是3.09的版本壳,在之前的版本有可能是jmp,但这里jmp在下面,所以我们先取消硬件断点,然后在下面的jmp加断点/运行至此/去掉断点/F8/分析/分析代码,就来到了OEP:
然后dump操作,注意不要勾选重建输入表,接下来我们要用ImportRE恢复!
5 . 使用ImportREC修复IAT
- 先点击小三角选择用OD正在运行的原程序文件
- 更具OD找到的OEP,修改ImportREC的OEP里面的偏移地址
- 点击IAT AutoSearch按钮确定
- 根据给出的RAV偏移地址去OD内存搜索验证,保证输入表没被修改,如果被修改了,就要把RAV的偏移地址调整过来,把IAT包含进去!这里可见上滑上面没有了,是可以的准确的,下面的Size也把IAT包含了,OK
- 点击Get Imports按钮,点击Show Invalid按钮,可见没有错误的IAT
- 就可以直接Fix Dump选择刚才OD的dumpupx文件了!
6 . WinUpacx壳
该壳压缩效率更高,但已不更新!
界面中文,比较友好!建议勾选3.清除导出表/4.清除重定位表
然后压缩之后会出现一个文件:这个文件去掉.bak后缀之后就是我们的原程序备份!
然后OD打开:
入过一开始不是这里,那也可以Alt+M,双击fishc在数据窗口找到偏移地址,即可搜索:
接下来就是根据堆栈平衡寻找OEP:
就可进行dump操作,由于输入表已被破坏所以记得不用勾选重建,重建也是错误的!
然后其实和上一个程序一样操作ImportREC,不多赘述!
然后如果还不满意可以再进行LordPE操作:
最后这就就完美啦!恭喜结业!!!芜湖!!!
链接欢迎大家关注我的Bilibli
感谢一键三连哦~
更多推荐
所有评论(0)