大家好,其实是前几天写简历的时候写了自己能手动脱壳,(手动脱壳,从寻找OEP->dump->IAT修复,最后成功拆除)
然而在脱壳方面的文章,一直没时间写,因为本来打算写逆向C++的,但是c++的内容绝不是三几句话能包含的,我打算自己
整理好以后会一并发出来;还忘大家谅解; 由于前不久瑞星电话面试的时候问我是不是能手动脱壳,遇到不熟悉的壳怎么办,
答: OEP-》DUMP—》IAT修复,最后问了找OEP的方法,为了证明下自己能做这个事,所以打算写篇脱壳的文章;当然也是为
了学习。技术方面的东西,我是报着学习的态度面对的;因为我知道我还是只小菜; 嘿嘿。。。废话不说;开始:
1:PE文件的装载
我们知道,一个普通的PE文件存放在磁盘中,在你不点击它之前,它其实和你电脑里的一张图片一样,形象的说来,
它成了个摆设,当鼠标双击它之后,shell调用CreateProcess函数打开一个有效的windows可执行文件,并且创建了
一个内存区对象,为的是稍后将它映射到内存空间中;然后通过调用WIndows的内部函数NtCreateProcess函数,创建了一个
windows执行体对象,以运行该映像;而创建执行体对象涉及以下几步(由创建线程来完成的):
1) 建立EPROCESS块
2)创建初始的进程地址空间
3)初始化内核进程块(KPROCESS)
4)结束地址空间的创建过程
5)建立PEB
6)完成执行体对象的创建过程
由于,本文写的是脱壳,更多详细,请参考 Windows.Internals.Fourth.Edition
PE文件装载过程:(Undocumented Windows)
我们来看一下 loader 是如何解释 PE 文件,又是如何为执行准备内存 image 的。 loader 需要找到空闲的虚拟地址空间来将文件映射到内存。
loader 尝试着将 image 加载在 preferred base address。成功后,loader 将 sections 映射入内存。loader 扫描 section table,
用每一个 section 的 RVA 加上基地址算出 section 的加载地址,然后将 sections 加载在相应的地址上。页属性是根据 section 的特征要求设定的。
将 section 映射入内存后,若基地址不等于 preferred base address,则 loader 开始进行基址重定位。之后检查 import table 并加载所需的 DLLs。
加载 DLL 与加载可执行文件的过程一样——映射 sections,基址重定位,解析
imports等等。所有的 DLL 都加载了之后,就修改 IAT 使之指向实际的 imported 函数的地址。
成了! image 已准备好执行了。关于Load的文章,What Goes On Inside Windows 2000: Solving the Mysteries of the Loader 我放附件里,
由于时间爱你关系,只翻译了一小点,大家凑合看吧;
2 :壳以及壳的加载过程:
1)什么是壳?
我们可以把壳看成一个子程序,由它处理后的Pe文件在磁盘中一般是以加密后的形式存在的,有的壳还带有压缩功能,使得exe文件更加小巧,加壳在一定程度上可以防止破解者对程序文件的非法修改,同时可以防止程序被反编译;壳附加在原程序上通过Load载入内存后,却抢先于原程序执行,也就是在PE文件代码段执行之前抢先得到控制权; 然后在执行过程中对原PE文件加密,还原,还原后在把控制权交还给原程序;
2)壳的加载过程
a:保存入口参数,加壳程序初始化时保存各寄存器的值,其实对windows来说,在每个子程序执行之前,总要保存ebx,edi,esi,ebp寄存器的值,而ecx,edx的值是不固定的,不能在返回时应用。特别注意:从 Windows API 函数中返回后,eax,ecx,edx 中的值和调用前不一定相同。当函数返回时,返回值放在eax中。如果您应用程序中的函数提供给 Windows 调用时,也必须尊守这一点,即在函数入口处保存段寄存器和 ebx,esp,esi,edi 的值并在函数返回时恢复。如果不这样一来的话,您的应用程序很快会崩溃。而通常,我们都用pushad/popad pushfd/popfd指令保存和恢复现场环境,注意:上面说过,壳可以看成一子程序,它只是比没加壳的代码提前获得了控制权,所以基本在没一个壳的开头,总能看到这个指令:
00413000 > 60 pushad
b:处理多次进入
c:模拟PE加载器完成相应的功能,处理完后将控制权交还给原程序;将控制权交给程序原入口点就是大家熟悉的(OEP)了; 在这一步中,还包括对输入表的处理,重定位表的处理,等; 由于加密加密三书上写的很详细,所以不在重复;
关于壳的加载过程,网上有篇文章:
http://blog.chinaunix.net/u1/51827/showart_1757935.html 我转在我的blog上,有兴趣的可以看看;
3:手动脱壳三部曲
1) 查找程序的真正入口(oep)
2) 抓取内存映像文件(dump)
3) Pe文件重建
到这里,我们可以开始练手了,注意: 不要在不熟悉pe文件格式的情况下,就想着手动脱壳,至少不要在还没搞清楚导入表和导出表,重定位表的情况下就想着更进一步,如果这样,无疑,你是在自找苦吃;
我用的加密解密三的例子:RebPe.exe
好了废话不说,开始三部曲第一部: 寻找OEP
这一部分涉及几种下断的方法和原理,如果不清楚的,赶紧翻开加密解密三,第二章,看吧,最近在看深入浅出MFC,记住一句话:勿在浮沙上筑高台;
寻找OEP的几种方法:
要是你对壳很熟悉,你当然可以一条指令一条指令的来,一直跟踪到代码段,兄弟我除了佩服你的技术精湛之余,还有向你学习的冲动; 但是,我们还是来用用书上的剩下的一些方法看看;
1:内存二次访问断点找OEP
原理:外壳首先要将原来压缩的代码解压,并放到对应的区块上,处理完毕,将跳到代码段执行。这种方法的关键,要等到代码段解压完毕,再对代码段设置内存访问断点,而一般的壳,会依次对.code .data .rsrc区块进行解压处理,所以,可以现在非代码段上下内存访问断点,此时代码段已经解压,在对代码段设内存访问断点;操作如下:
a:)OD载入RebPe.exe 看到代码如下:
00413000 > 60 pushad
00413001 E8 C2000000 call 004130C8
00413006 2E:3001 xor byte ptr cs:[ecx], al
00413009 0000 add byte ptr [eax], al
0041300B 0000 add byte ptr [eax], al
0041300D 0000 add byte ptr [eax], al
0041300F 0000 add byte ptr [eax], al
00413011 003E add byte ptr [esi], bh
00413013 3001 xor byte ptr [ecx], al
00413015 002E add byte ptr [esi], ch
b:)Alt+M 打开内存窗口,对rdata设内存访问断点 : F2(,你也可以对其它的非代码段设断;)F9运行,暂停在:
00413145 A4 movs byte ptr es:[edi], byte ptr [esi>
00413146 B3 02 mov bl, 2
00413148 E8 6D000000 call 004131BA
0041314D ^ 73 F6 jnb short 00413145
在Alt+M ,对.Text设断 F2后,F9
003A0282 61 popad
003A0283 68 30114000 push 401130 ; OEP嘿嘿,看到了吧;
003A0288 C3 retn
003A0289 3011 xor byte ptr [ecx], dl
我们跟踪,会发现,指令可读性较差,不用担心: ctrl +a ,OD会帮你: 此时,来到如下;也就是我们Pe的真正入口处了:
00401130 /. 55 push ebp
00401131 |. 8BEC mov ebp, esp
00401133 |. 6A FF push -1
00401135 |. 68 B8504000 push 004050B8
0040113A |. 68 FC1D4000 push 00401DFC ; SE 处理程序安装
0040113F |. 64:A1 0000000>mov eax, dword ptr fs:[0]
00401145 |. 50 push eax
00401146 |. 64:8925 00000>mov dword ptr fs:[0], esp
0040114D |. 83EC 58 sub esp, 58
00401150 |. 53 push ebx
00401151 |. 56 push esi
00401152 |. 57 push edi
00401153 |. 8965 E8 mov dword ptr [ebp-18], esp
00401156 |. FF15 28504000 call dword ptr [405028] ; kernel32.GetVersion ;这个函数熟悉吧
0040115C |. 33D2 xor edx, edx
随便用PE查看器,看下入口点:00400000
00401130 – 00400000 = 1130 (入口RVA)
2:堆栈平衡原理找oep
我们说过,在windows中调用子程序前,必须保存现场环境,而当子程序调用之前,必须恢复现场; 这一部分,可能你要,至少要对堆栈有个基本的了解,你可以参考我前面写的逆向C++中,函数,那一节,有一小部分介绍;而要继续深入,推荐arhat的 the shellcode handbook 一书。
我们知道,PUSHAD(Push All 32-bit General Registers)
指令格式:PUSHAD ;80386+
其功能是把寄存器EAX、ECX、EDX、EBX、ESP、EBP、ESI和EDI等压栈。
POPAD(Pop All 32-bit General Registers)
指令格式:POPAD ;80386+
其功能是依次把寄存器EDI、ESI、EBP、ESP、EBX、EDX、ECX和EAX等弹出栈,它与PUSHAD对称使用即可。
我们看看堆栈的变化:
Popad未执行前:
0012FFC4 7C817067 返回到 kernel32.7C817067 ; 当前esp指向
0012FFC8 0012BBC4
0012FFCC 73FB49E4
0012FFD0 7FFD8000
0012FFD4 8054C6B8
执行后:
0012FFA4 0012BBC4 ;edi
0012FFA8 73FB49E4 ;esi
0012FFAC 0012FFF0 ;ebp
0012FFB0 0012FFC4 ;esp
0012FFB4 7FFD8000 ;ebx
0012FFB8 7C92E4F4 ntdll.KiFastSystemCallRet ;edx
0012FFBC 0012FFB0 ;ecx
0012FFC0 00000000 ;eax
0012FFC4 7C817067 返回到 kernel32.7C817067
对0012FFA4下硬件断点: hr 0012FFA4
F9 ,程序中断在:
003A0282 61 popad
003A0283 68 30114000 push 401130 ;熟悉吧,OEP
003A0288 C3 retn
其实堆栈平衡原理,是找第一个pushad 配对的popad,因为,在很多壳中,可能在处理数据的时候,还会调用其它子程序,这样,如果用观察法找配对 pushad 和popad
指令就会让初学者,眼花缭乱,相信我,我有过这种经历;所以,用一个硬件断点,方便了很多吧;
脱壳二部曲: dump 抓取内存映像
如果用OD里的dump插件,哈哈,那么IAT你都不用修复拉; OD真的很好用,偶喜欢;好期待有天我也能弄个这NX的插件出来,奉献给大家;不过,有什么用呢; 大牛们都写好了摆好了; 你会发现,脱壳后运行的程序,运行的非常好;
在这里,有一点补充下,有可能你加壳后,会发现文件执行的时候有错误;kanxue补充如下:
将记事本和计算器 中Directory Table 中的LoadConfig值清零,即可加。外壳程序处理时没有考虑LoadConfig。
下面用loadPE来: 右键—》完全转存; dump.Exe 双击,55555.。。。 不能用了吧;
笑不出来了吧;没关系,今天宿舍就停电了,本来打算连IAT修复也写完的,555.。。。我不想在宿舍住拉,每次干得兴起就断电; 那就明天在来吧;今天先到这;
IAT修复:
1:)先来回顾下基础知识:
首先,PE文件中的数据按照装入内存后的页面属性被分成多个节,并由节表中的数据来描述这些节;一个节中的数据仅仅是属性相同而已,并不一定是同一种用途;
其次,由于不同用途的数据可能被放在同一个节中,仅仅靠节表是无法确定它的存放的位置的,PE文件中依靠可选头中的数据目录表来指出它们的位置;结构如下:typedef struct _IMAGE_OPTIONAL_HEADER {
//
//标准域
//
USHORT Magic; //魔数
UCHAR MajorLinkerVersion; //链接器主版本号
UCHAR MinorLinkerVersion; //链接器小版本号
ULONG SizeOfCode; //代码大小
ULONG SizeOfInitializedData; //已初始化数据大小
ULONG SizeOfUninitializedData; //未初始化数据大小
ULONG AddressOfEntryPoint; //入口点地址
ULONG BaseOfCode; //代码基址
ULONG BaseOfData; //数据基址
//
//NT增加的域
//
ULONG ImageBase; //映像文件基址
ULONG SectionAlignment; //节对齐
ULONG FileAlignment; //文件对齐
USHORT MajorOperatingSystemVersion;//操作系统主版本号
USHORT MinorOperatingSystemVersion;//操作系统小版本号
USHORT MajorImageVersion; //映像文件主版本号
USHORT MinorImageVersion; //映像文件小版本号
USHORT MajorSubsystemVersion; //子系统主版本号
USHORT MinorSubsystemVersion; //子系统小版本号
ULONG Reserved1; //保留项1
ULONG SizeOfImage; //映像文件大小
ULONG SizeOfHeaders; //所有头的大小
ULONG CheckSum; //校验和
USHORT Subsystem; //子系统
USHORT DllCharacteristics; //DLL特性
ULONG SizeOfStackReserve; //保留栈的大小
ULONG SizeOfStackCommit; //指定栈的大小
ULONG SizeOfHeapReserve; //保留堆的大小
ULONG SizeOfHeapCommit; //指定堆的大小
ULONG LoaderFlags; //加载器标志
ULONG NumberOfRvaAndSizes; //RVA的数量和大小
IMAGE_DATA_DIRECTORY DataDirectory [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录数组
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
typedef struct _IMAGE_DATA_DIRECTORY {
ULONG VirtualAddress; //虚拟地址
ULONG Size; //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY
// 各个目录项
// 输出目录
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
// 输入目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
// 资源目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
// 异常目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
// 安全目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
// 基址重定位表
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
// 调试目录
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
// 描述字符串
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
// 机器值(MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
// TLS(线程本地存储)⑥目录
#define IMAGE_DIRECTORY_ENTRY_TLS 9
// 载入配置目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
我们知道,从数据目录表引出的,仅仅是这些数据的RVA和数据块的尺寸,很明显,不同数据块的数据组织方式是不同的,比如导入表和导出表,描述它们的数据结构是不同的;如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
ULONG Characteristics; //特征
ULONG TimeDateStamp; //时间日期戳
USHORT MajorVersion; //主版本号
USHORT MinorVersion; //小版本号
ULONG Name; //名字
ULONG Base; //基址
ULONG NumberOfFunctions; //函数数
ULONG NumberOfNames; //名字数
PULONG *AddressOfFunctions; //函数的地址
PULONG *AddressOfNames; //名字的地址
PUSHORT *AddressOfNameOrdinals; //名字序数的地址
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
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;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
我们知道,PE文件在没有装入内存之前,导入表结构中由OriginalFirstThunk和FirstThunk指向的
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
结构数组中的值是相同的,注意,这个结构定义为一个联合,实际上就是一双字,这个结构用来指定一个导入函数,但双字的最高位是1时,表示函数是以序号的方式导入的,这时双字的低位字就是函数的序号,但双字的高位为0时,表示函数以字符类型的函数名方式导入,这时的双字的值就是一个RVA,指向一个结构:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
其中Hint表示函数的序号,name【】定义了导入函数的名称;
但是为什么要两个一模一样的IMAGE_THUNK_DATA数组呢,让我们先来回顾下调用导入函数的指令:
在RebPE中找到oep后,F8跟踪,你能看到这个函数:
00401150 |. 53 push ebx
00401151 |. 56 push esi
00401152 |. 57 push edi
00401153 |. 8965 E8 mov dword ptr [ebp-18], es>
00401156 |. FF15 28504000 call dword ptr [405028] ; kernel32.GetVersion
Call dword ptr[405028], 直接寻址方式,将405028处的数据内存属性转换为Dword后作为函数的地址调用;
我们可以在提示窗口中:
ds:[00405028]=7C81126A (kernel32.GetVersion)
可见,在这个地址中存放的是(kernel32.GetVersion)导入函数的入口地址;这样,call 调用将Eip压入堆栈后,程序执行流就到kernel32中了;
还有一种调用方式:
就是call aaaaaaaa
Aaaaaaaa jmp dword ptr 【xxxxxxxx】,这个指令是一个间接寻址的跳转指令,如果你在16进制下查看这个ptr[xxxxxxx]的xxxxxxxx,你会发现它存放的是一个指向欲调用的导入函数名的字符串的RVA地址;当PE文件被装载时,windows装载器根据这个xxxxxxxx处的Rva得到函数名,然后用GetProcAddress函数找到内存中此导入函数的地址,并将xxxxxxxx处的内容替换成真正的函数地址;此时firstthunk指向的DWOWD数组中原本指向函数名的RVA被替换成导入函数的真正入口地址;
在PE文件中,所有DLL对应的导入地址数组在位置上是被排列在一起的,全部这写数组的组合也被称为导入地址表,IAT;导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的FirstThunk字段指向的就是IAT的起始地址;还有就是数据目录表中的13项,可以直接用来定位IAT的地址; 不过一般还是以上一种方法为准;
到这里,我们可以来思考手动查找IAT的方法了,由于所有DLL模块的firstThunk数组一般都被放在一块,组成了输入地址表,注意: 这里是一般,不是完全,加密解密三上就有IAT被分开存放的例子,大家可以看下;
所以我们只要在找到OEP后,进入到原PE文件的代码块后,找到一个API调用的地址;如:
004011A4 |. FF15 24504000 call dword ptr [405024] ; [GetCommandLineA
004011AA |. A3 F8894000 mov dword ptr [4089F8], ea>
在[405024]右键-》数据窗口中跟随,内存地址;在数据窗口中数据如下:
00405024 AD 2F 81 7C 6A 12 81 7C FA CA 81 7C 1A 1E 80 7C ?亅j 亅亅 €|
00405034 85 DE 80 7C 6A 3E 86 7C 5F B5 80 7C D7 D6 81 7C 呣€|j>唡_祤|字亅
00405044 77 4B 81 7C 64 A1 80 7C 7B CC 81 7C 98 2F 81 7C wK亅d|{虂|?亅
00405054 27 CD 80 7C C9 2F 81 7C 82 4B 81 7C 6E 2B 81 7C '蛝|?亅侹亅n+亅
00405064 88 0F 81 7C 46 2C 81 7C ?亅F,亅t泙|
我们再在数据窗口中右键-》长型,地址,可以看到如下:
00405000 7C810EE1 kernel32.GetFileType
00405004 7C80A520 kernel32.GetStringTypeW
00405008 7C838A24 kernel32.GetStringTypeA
0040500C 7C80CD38 kernel32.LCMapStringW
00405010 7C838E00 kernel32.LCMapStringA
00405014 7C809C88 kernel32.MultiByteToWideChar
00405018 7C801D7B kernel32.LoadLibraryA
0040501C 7C80B731 kernel32.GetModuleHandleA
00405020 7C801EF2 kernel32.GetStartupInfoA
00405024 7C812FAD kernel32.GetCommandLineA
00405028 7C81126A kernel32.GetVersion
0040502C 7C81CAFA kernel32.ExitProcess
00405030 7C801E1A kernel32.TerminateProcess
00405034 7C80DE85 kernel32.GetCurrentProcess
00405038 7C863E6A kernel32.UnhandledExceptionFilter
0040503C 7C80B55F kernel32.GetModuleFileNameA
00405040 7C81D6D7 kernel32.FreeEnvironmentStringsA
00405044 7C814B77 kernel32.FreeEnvironmentStringsW
00405048 7C80A164 kernel32.WideCharToMultiByte
0040504C 7C81CC7B kernel32.GetEnvironmentStringsA
00405050 7C812F98 kernel32.GetEnvironmentStringsW
00405054 7C80CD27 kernel32.SetHandleCount
00405058 7C812FC9 kernel32.GetStdHandle
0040505C 7C814B82 kernel32.GetEnvironmentVariableA
00405060 7C812B6E kernel32.GetVersionExA
00405064 7C810F88 kernel32.HeapDestroy
00405068 7C812C46 kernel32.HeapCreate
0040506C 7C809B74 kernel32.VirtualFree
00405070 7C92FF0D ntdll.RtlFreeHeap
00405074 7C94ABA5 ntdll.RtlUnwind
00405078 7C810E17 kernel32.WriteFile
0040507C 7C812F06 kernel32.GetCPInfo
00405080 7C8099A5 kernel32.GetACP
00405084 7C812837 kernel32.GetOEMCP
00405088 7C9300A4 ntdll.RtlAllocateHeap
0040508C 7C809AE1 kernel32.VirtualAlloc
00405090 7C939B80 ntdll.RtlReAllocateHeap
00405094 7C80AE30 kernel32.GetProcAddress
00405098 00000000
0040509C 77D2F3C2 USER32.SendMessageA
004050A0 77D2E8F6 USER32.LoadIconA
004050A4 77D2B19C USER32.DestroyWindow
004050A8 77D2AAFD USER32.PostMessageA
004050AC 77D24A4E USER32.EndDialog
004050B0 77D3B144 USER32.DialogBoxParamA
004050B4 00000000
上下拉拉看,找出IAT的起始地址,和结束地址: 这里是:00405000 和004050B8 大小: B8
下面我们就用Import REC来修复:
1)载入RebPE,定位到OEP处,00401130处,打开ImportREC这个软件,从进程下拉列表中找到RebPE后,在OEP中填入正确的OEP这里是; 1130
2)我们在上面已经找出了IAT的起始地址和大小,我们在这里填入: 5000 和B8;
3)单击Get Import按钮,分析IAT得到的基本信息,如果都能正确识别,选择Add new section ,单击FixDump按钮,选择我们抓取的映像,(注意,此例抓取内存映像的时候要修正ImageSize),修复完成后,得到:dump_.exe文件; 运行,
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: