攻防世界逆向高手题之crackme
继续开启全栈梦想之逆向之旅~
这题是攻防世界逆向高手题的crackme
下载附件,照例扔入exeinfope中查看信息。
(这里积累第一个经验)
nsPack
壳,上网查了查说是北斗压缩壳
(nsPack),下面还提示用Quick unpack
工具去壳:
.
.
(这里积累第二个经验)
然后在上网中一搜就搜到手动脱nspack
壳的办法,是ESP
定律,又搜了好多资料,现在结合别人的内容大致梳理一下我理解的ESP脱壳定律:
狭义ESP定律
ESP定律的原理就是“堆栈平衡”原理。
让我们来到程序的入口处看看吧!
.
涉及的汇编知识:
call命令:
1.向堆栈中压入下一行程序的地址;
2.JMP 到 call 的子程序地址处。
.
例如:
00401029 E8 DA240A00 call 004A3508
0040102E 5A pop edx
//在执行了00401029以后,程序会将0040102E(下一条指令地址)压入堆栈,然后 JMP 到 004A3508 地址处!
.
RETN命令:
1.将当前的 ESP 中指向的地址出栈;
2.JMP 到这个地址。
这个就完成了一次调用子程序的过程。
.
在这里关键的地方是:
如果我们要返回父程序,则当我们在堆栈中进行堆栈的操作的时候,一定要保证在 RETN 这条指令之前,ESP指向的是我们压入栈中的地址。这也就是著名的“堆栈平衡”原理!
.
1.这个是加了 ASPACK 壳程序的入口时各个寄存器的值:(入壳,解压缩)
EAX 00000000
ECX 0012FFB0
EDX 7FFE0304 //堆栈值
EBX 7FFDF000 //堆栈值
ESP 0012FFC4
EBP 0012FFF0
ESI 77F57D70 ntdll.77F57D70
EDI 77F944A8 ntdll.77F944A8
EIP 0040D000 ASPACK.
.
.
2.这个是 ASPACK 壳 JMP 到 OEP(Original Enter Point)后的寄存器的值:(出壳,回主程序 OEP)
EAX 004010CC ASPACK.004010CC //保存了当前OEP的值
ECX 0012FFB0
EDX 7FFE0304 //堆栈值
EBX 7FFDF000 //堆栈值
ESP 0012FFC4
EBP 0012FFF0
ESI 77F57D70 ntdll.77F57D70
EDI 77F944A8 ntdll.77F944A8
EIP 004010CC ASPACK.004010CC
.
呵呵~是不是除了EIP不同以外,eax 保存当前 OEP 值,其他都一模一样啊!
.
.
为什么会这样呢?我们来看看ASPACK加壳程序的起始代码:(压栈、入壳解压缩)
0040D000 A> 60 pushad
//PUSHAD 就是把所有寄存器压栈!注意这里压入寄存器后,原本 ESP=0012FFC4 向上移动到栈顶(减少)位置为 ESP=0012FFA4
0040D001 E8 00000000 call ASPACK.0040D006
//这里压入 32 位地址后 ESP=0012FFA8,压入的地址是 call 语句的下一行地址,存储在栈段中的 A4 段。然后开始调用的是壳内的代码,来给后面压缩的代码解压缩。并且这里 call 的壳内函数中会有平衡堆栈的 pop 指令(不需要 jmp),弹出 ESP=0012FFA8 的数据。
.
.
.
我们在到壳的最后看看:(弹栈、跳出壳外OEP)
0040D558 61 popad
//这里弹出的是 ESP=0012FFA4 存储的值,是前面 call 语句的下一行地址,然后堆栈就和前面平衡了。
.
0040D559 75 08 jnz short ASPACK.0040D563
//注意这里堆栈已经平衡了,这里准备跳出壳外代码,用前面 pop 和 Jmp 操作代替壳内函数的 retn 语句,跳到真正的主代码处。所以前面 ESP=0012FFA4 可以下访问断点,作为回到主程序的依据。
.
.
.
也就是说当我们对 ESP 的 0012FFA4 栈地址处下硬件访问断点之后。当程序要通过堆栈访问这些值,从而恢复原来寄存器的值,准备跳向苦苦寻觅的 OEP 的时候,OD 帮助我们中断下来。
.
硬件断点的大小类型:
.
.
小结:我们可以把壳假设为一个子程序,当壳把代码解压前和解压后,他必须要做的是遵循堆栈平衡的原理。
.
1.ESP定律的原理是什么?
堆栈平衡原理。
.
.
2.ESP定律的适用范围是什么?
几乎全部的压缩壳,部分加密壳。只要是在JMP到OEP后,ESP=0012FFC4的壳,理论上我们都可以使用。但是在何时下断点避开校验,何时下断OD才能断下来,这还需要多多总结和多多积累。
.
.
3.是不是只能下断12FFA4的访问断点?
当然不是,那只是ESP定律的一个体现,我们运用的是ESP定律的原理,而不应该是他的具体数值,不能说12FFA4,或者12FFC0就是ESP定律,他们只是ESP定律的一个应用罢了!
.
.
什么是内存断点?
内存断点等效于命令bpm,他的中断要用到DR0-DR7的调试寄存器,也就是说OD通过这些DR0-DR7的调试寄存器来判断是否断下普通断点(F2下断)等效于bpx,他是在所执行的代码的当前地址的一个字节修改为CC(int3)。当程序运行到int3的时候就会产生一个异常,而这个异常将交给OD 处理,把这个异常给EIP-1(所以才会停在上一条代码处)以后,就正好停在了需要的中断的地方(这个根据系统不同会不一样),同时OD在把上面的int3修改回原来的代码。
.
内存断点分为:
内存访问断点,内存写入断点。
.
我们知道,在程序运行的时候会有3种基本的状态产生:
读取–>写入–>执行。
.
004AE242 A1 00104000 mov eax,dword ptr ds:[004AE24C] //004AE24C处的内存读取
004AE247 A3 00104000 mov dword ptr ds:[004AE24C],eax //004AE24C处的内存写入
004AE24C 83C0 01 add eax,1 //004AE24C处的内存执行
.
1.当对004AE24C下内存访问断点的时候,可以中断在004AE242也可以中断在004AE247和004AE24C。(因为访问包含执行、写入、读取。)
2.当对004AE24C下内存写入断点的时候,只能中断在004AE247。
3.当执行004AE24C的时候,只能中断在004AE24C。
.
.
.
如何在寻找OEP时使用内存断点?(解压为主函数code段写入,跳转为主函数code段执行)
我们要知道壳如果要把原来加密或压缩的代码运行起来就必须要解压和解密原来的代码。而这一个过程我们可以看做是对代码段(code段)的写入。
解压完毕后,我们要从壳代码的区段JMP到原来的代码段的时候,可以看成是对代码段(code段)的执行。
.
.
理清了上面的关系就好办了,那么如果载入OD后,我们直接对code段下内存访问断点的时候,一定会中断在壳对code段的写入的代码的上面,就像上面的 004AE247的这一行。而如果当他把code段的代码全部解压解密完毕了以后,JMP到OEP的时候,我们还会停在OEP的代码上面呢,而且每按下F9都会中断,因为这时code段在执行中,而我们下的是访问断点。
.
如果我们不在JMP OEP上面下断点而是在一开始就下断点,正入我上面所说的,如果你在前面下断很可能壳对code段还没解压完毕呢,这时如果你不停的按F9,你将会看到OD的下方不断的在提示你,“对 401000写入中断”“对401002写入中断”“对401004写入中断”。。。。
.
.
.
更便捷的两次内存断点:
假设我是一个壳的作者,一个EXE文件的有code段,data段,rsrc段…依次排列在你的内存空间中,那么我会怎么解码呢?
我会先将code段解码,然后再将data段解压,接着是rsrc段…那么你不难发现,只要你在data段或者rsrc段下内存访问断点,那么中断的时候code段就已经解压完毕了。这时我们再对code段下内存访问断点,不就可以到达OEP了吗?
.
.
这里注意上面虽然下了两次内存访问断点,但是本质是不一样的,目的也是不一样的:
1.对data段下内存访问断点而中断是因为内存写入中断,目的是断在对data段的解压时,这时壳要对data段写数据,但是code段已经解压完毕。
2.对code段下内存访问断点而中断是因为内存执行中断,目的当然就是寻找OEP了。
.
.
总结一下:
如果我们知道壳在什么地方对code段解压完毕我们就可以使用内存断点,找到OEP。
如果不知道,那么我们就依靠2次内存断点去找,如果还不行就用多次内存断点。总之明白了原理在多次的内存断点其实都一样。从这个过程中我们了解的是壳在对区段解码的顺序!
.
.
(这里积累第三个经验)
然后回到正题,手动脱壳,把程序扔如OD中,照着上面流程来一遍,这里面也可以看到 pushfd 和 pushad 是北斗壳的特征:
.
.
然后就是在ESP处数据窗口跟踪,再在数据窗口中右键设置word级或Dword级的硬件访问断点
.
.
然后确认一下是否下了断点:
.
.
补充一下为什么是word级别,下面是《IDA权威指南中的解释》:
.
.
.
(这里积累第四个经验)
点击运行程序,框中的代码就是顶替了retn语句,这里标识壳函数调用完毕,要返回了。
.
.
(这里积累第五个经验)
jmp
跳出壳后就是解压缩的代码了,右键分析当前代码把.text.
段中数据重新反汇编成汇编代码,同IDA
的热键C
。
.
.
然后就是在这个位置处脱壳,用OD插件OllyDump
来脱壳,生成新的可执行程序,然后IDA伪代码
分析,有main函数看main函数。
.
.
逻辑简单,因为考点是脱壳,所以直接附上脚本:
key1="this_is_not_flag"
key2=[18, 4, 8, 20, 36, 92, 74, 61, 86, 10, 16, 103, 0, 65, 0, 1, 70, 90, 68, 66, 110, 12, 68, 114, 12, 13, 64, 62, 75, 95, 2, 1, 76, 94, 91, 23, 110, 12, 22, 104, 91, 18]
flag=""
for i in range(len(key2)):
flag+=chr(key2[i]^ord(key1[i%16]))
print(flag)
.
.
结果:
.
.
.
(这里积累第六个经验)
然后附上第二种方法,工具脱壳,使用万能脱壳工具
,扔进去,脱壳,生成同名+unpack
程序:
.
.
然后这个程序运行不了,因为导入表
乱了,所以用importREC
修复导入表
。因为ImportREC是内存转储
,所以要在运行
程序中判断导入的库,首先双击运行前面万能脱壳工具脱壳后的程序:(生成同名的多个_的程序)
该程序就是正常的。
.
.
解毕!敬礼!