ESP定律
一.准备知识
在我们开始讨论ESP定律之前,我先给你讲解一下一些简单的汇编知识。
1.call
这个命令是访问子程序的一个汇编基本指令。也许你说,这个我早就知道了!别急请继续看完。call真正的意义是什么呢?我们可以这样来理解:
1.向堆栈中压入下一行程序的地址;
2.JMP到call的子程序地址处。
例如:
00401029.E8 DA240A00 call 004A3508
0040102E.5A pop edx
在执行了00401029以后,程序会将0040102E压入堆栈,然后JMP到004A3508地址处!
2.RETN
与call对应的就是RETN了。对于RETN我们可以这样来理解:
1.将当前的ESP中指向的地址出栈;
2.JMP到这个地址。
这个就完成了一次调用子程序的过程。在这里关键的地方是:如果我们要返回父程序,则当我们在堆栈中进行堆栈的操作的时候,一定要保证在RETN这条指令之前,ESP指向的是我们压入栈中的地址。这也就是著名的“堆栈平衡”原理!
3.狭义ESP定律
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.<ModuleEntryPoint>
2.这个是ASPACK壳JMP到OEP后的寄存器的值!
EAX 004010CC ASPACK.004010CC
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值,其他都一模一样啊!
为什么会这样呢?我们来看看
0040D000 A> 60 pushad //注意这里ESP=0012FFC4
0040D001 E8 00000000 call ASPACK.0040D006 //ESP=0012FFA4
PUSHAD就是把所有寄存器压栈!我们在到壳的最后看看:
0040D558 61 popad //ESP=0012FFA4
0040D559 75 08 jnz short ASPACK.0040D563 //注意这里ESP=0012FFC4
也就是说当我们对ESP的0012FFA4下硬件访问断点之后。当程序要通过堆栈访问这些值
,从而恢复原来寄存器的值,准备跳向苦苦寻觅的OEP的时候,OD帮助我们中断下来。
小结:我们可以把壳假设为一个子程序,当壳把代码解压前和解压后,他必须要做的是遵循堆栈平衡的原理。
因为大家对ESP理解各有异同,但是,大同小异!一般理解可以为:
1、在命令行下断hr esp-4(此时的ESP就是OD载入后当前显示的值)
2、hr ESP(关键标志下一行代码所指示的ESP值(单步通过))
5.总结
现在我们可以轻松的回答一些问题了。
1.ESP定律的原理是什么?
堆栈平衡原理。
2.ESP定律的适用范围是什么?
几乎全部的压缩壳,部分加密壳。只要是在JMP到OEP后,ESP=0012FFC4的壳,理论上我们都可以使用。但是在何时下断点避开校验,何时下断OD才能断下来,这还需要多多总结和多多积累。
3.是不是只能下断12FFA4的访问断点?
当然不是,那只是ESP定律的一个体现,我们运用的是ESP定律的原理,而不应该是他的具体数值,不能说12FFA4,或者12FFC0就是ESP定律,他们只是ESP定律的一个应用罢了!
内存断点
1、要解决的问题是:
1.什么是内存断点?
2.如何在寻找OEP时使用内存断点。
2、内存断点寻找OEP的原理
i. 首先,在OD中内存断点和普通断点(F2下断)是有本质区别的。
内存断点等效于命令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。
2.当对004AE24C下内存写入断点的时候,只能中断在004AE247。
3.当执行004AE24C的时候,只能中断在004AE24C
到这里你可能不明白了,为什么内存访问断点能中断在004AE247这一句对004AE24C的写入,而且还能中断在004AE24C的执行呢?其实很简单,我们只要仔细体会一下“内存访问”这四个字的含义遍可以知道,当我们对004AE24C进行读取的时候需要“访问”他吧,当我对004AE24C进行写入的时候也需要“访问”他吧!!当然我们要执行内存地址004AE24C的代码的时候也是还是要“访问”他的!
所以我们不难得出下面的结论:
1.内存写入中断的地方,一定是也可以用内存访问中断。
2.内存执行的地方,也可以用内存访问中断。
如果这时你认为,那么内存写入岂不是没用了。呵呵~那我要告诉你当然不是,如果你想快速的准确的定位到004AE247这一行的时候,那么他就大有作用了!
总结一下:内存断点不修改原代码,不会像普通断点那样因为修改代码被程序校验而导致中断失败;对于区段的访问只是区域大了一点,其原理和上面分析的三行代码是一样的。
ii.如何使用内存断点来寻找OEP呢?
要回答这个问题首先要回答这一个问题:壳是如何解压代码的?
正如我们知道的,壳如果要把原来加密或压缩的代码运行起来就必须要解压和解密原来的代码。而这一个过程我们难道不能将他看做是对代码段(code段)的写入吗?好了,解压完毕了。我们要从壳代码的区段JMP到原来的代码段的时候,难道不正是对代码段(code段)的执行吗?
理清了上面的关系就好办了,那么如果载入OD后,我们直接对code段下内存访问断点的时候,一定会中断在壳对code段的写入的代码的上面,就像上面的004AE247的这一行。而如果当他把code段的代码全部解压解密完毕了以后,JMP到OEP的时候,我们是不是还可以停在OEP的代码上面呢?而且每按下F9都会中断,因为这时code段在执行中哦!
而如果你还要继续问我为什么一定要到那个地方才可以下断呢?我难道不可以一开始就下断吗?
正入我上面所说的,如果你在前面下断很可能壳对code段还没解压完毕呢,这时如果你不停的按F9,你将会看到OD的下方不断的在提示你,“对401000写入中断”“对401002写入中断”“对401004写入中断”.......如果你不介意按F9到他把正个code段写完的话,我除了同情你的“F9”以外,没什么其他的意见!
那么我们就没有别更快一点的办法了吗?
有的!那就是我们呼之欲出的两次内存断点办法。
怎么理解两次内存断点呢?
让我来做一个假设吧,假设我是一个壳的作者。一个EXE文件的有code段,data段,rsrc段.....依次排列在你的内存空间中,那么我会怎么解码呢?呵呵~我比较笨一点,我会先将code段解码,然后再将data段解压,接着是rsrc段......那么你不难发现,只要你在data断或者rsrc段下内存访问断点,那么中断的时候code段就已经解压完毕了。这时我们再对code段下内存访问断点,不就可以到达OEP了吗?
这里注意上面虽然下了两次内存访问断点,但是本质是不一样的,目的也是不一样的。
1.对data段下内存访问断点而中断是因为内存写入中断,目的是断在对对data段的解压时,这时壳要对data段写数据,但是code段已经解压 完毕。
2.对code段下内存访问断点而中断是因为内存执行中断,目的当然就是寻找OEP了。
总结一下:如果我们知道壳在什么地方对code段解压完毕我们就可以使用内存断点,找到OEP。如果不知道,那么我们就依靠2次内存断点去找,如果还不行就用多次内存断点。总之明白了原理在多次的内存断点其实都一样。从这个过程中我们了解的是壳在对区段解码的顺序!
汇编语言中的整数常量表示
十进制整数
这是汇编器默认的数制。直接用我们熟悉的表示方式表示即可。例如,1234表示十进制的1234。不过,如果你指定了使用其他数制,或者有凡事都进行完整定义的小爱好,也可以写成[十进制数]d或[十进制数]D的形式。
十六进制数
这是汇编程序中最常用的数制,我个人比较偏爱使用十六进制表示数据,至于为什么,以后我会作说明。十六进制数表示为0[十六进制数]h或0[十六进制数]H,其中,如果十六进制数的第一位是数字,则开头的0可以省略。例如,7fffh, 0ffffh,等等。
二进制数
这也是一种常用的数制。二进制数表示为[二进制数]b或[二进制数]B。一般程序中用二进制数表示掩码(mask code)等数据非常的直观,但需要些很长的数据(4位二进制数相当于一位十六进制数)。例如,1010110b。
八进制数
八进制数现在已经不是很常用了(确实还在用,一个典型的例子是Unix的文件属性)。八进制数的形式是[八进制数]q、[八进制数]Q、[八进制数]o、[八进制数]O。例如,777Q。
---------------------------------------------------------------------------------
爱国者黑客动画教程
爱国者黑客www.3800cc.com
专业的黑客安全技术培训基地
多抽出一分钟时间学习.让你的生命更加精彩.
---------------------------------------------------------------------------------
需要说明的是,这些方法是针对宏汇编器(例如,MASM、TASM、NASM)说的,调试器默认使用十六进制表示整数,并且不需要特别的声明(例如,在调试器中直接用FFFF表示十进制的65535,用10表示十进制的16)。
现在我们来写一小段汇编程序,修改EAX、EBX、ECX、EDX的数值。
我们假定程序执行之前,寄存器中的数值是全0:
寄存器 ? X
H L
EAX 0000 00 00
EBX 0000 00 00
ECX 0000 00 00
EDX 0000 00 00
正如前面提到的,EAX的高16bit是没有办法直接访问的,而AX对应它的低16bit,AH(high)、AL(low)分别对应AX的高、低8bit。
mov eax, 012345678h
mov ebx, 0abcdeffeh
mov ecx, 1
mov edx, 2 ; 将012345678h送入eax
; 将0abcdeffeh送入ebx
; 将000000001h送入ecx
; 将000000002h送入edx
则执行上述程序段之后,寄存器的内容变为:
? ?X
?H ?L
EAX 1234/0001,0010,0011,0100 56/0101,0110 78/0111,1000
EBX abcd ef fe
ECX 0000 00 01
EDX 0000 00 02
那么,你已经了解了mov这个指令(mov是move的缩写)的一种用法。它可以将数送到寄存器中。我们来看看下面的代码:
mov eax, ebx
mov ecx, edx ; ebx内容送入eax
; edx内容送入ecx
则寄存器内容变为:
? X
H L
EAX abcd ef fe
EBX abcd ef fe
ECX 0000 00 02
EDX 0000 00 02
我们可以看到,“move”之后,数据依然保存在原来的寄存器中。不妨把mov指令理解为“送入”,或“装入”。
下面我们将介绍一些指令。在介绍指令之前,我们约定:
使用Intel文档中的寄存器表示方式
reg32 32-bit寄存器(表示EAX、EBX等)
reg16 16-bit寄存器(在32位处理器中,这AX、BX等)
reg8 8-bit寄存器(表示AL、BH等)
imm32 32-bit立即数(可以理解为常数)
imm16 16-bit立即数
imm8 8-bit立即数
在寄存器中载入另一寄存器,或立即数的值:
mov reg32, (reg32 | imm8 | imm16 | imm32)
mov reg32, (reg16 | imm8 | imm16)
mov reg8, (reg8 | imm8)
例如,mov eax, 010h表示,在eax中载入00000010h。需要注意的是,如果你希望在寄存器中装入0,则有一种更快的方法,在后面我们将提到。
交换寄存器的内容:
xchg reg32, reg32
xchg reg16, reg16
xchg reg8, reg8
例如,xchg ebx, ecx,则ebx与ecx的数值将被交换。由于系统提供了这个指令,因此,采用其他方法交换时,速度将会较慢,并需要占用更多的存储空间,编程时要避免这种情况,即,尽量利用系统提供的指令,因为多数情况下,这意味着更小、更快的代码,同时也杜绝了错误(如果说Intel的CPU在交换寄存器内容的时候也会出错,那么它就不用卖CPU了。而对于你来说,检查一行代码的正确性也显然比检查更多代码的正确性要容易)刚才的习题的程序用下面的代码将更有效:
mov eax, 0a1234h
mov bx, ax
xchg ah, al ; 将0a1234h送入eax
; 将ax内容送入bx
; 交换ah, al的内容
递增或递减寄存器的值:
我们假定ax的值为8
inc reg(8,16,32) //inc ax
dec reg(8,16,32) //dec ax
这两个指令往往用于循环中对指针的操作。需要说明的是,某些时候我们有更好的方法来处理循环,例如使用loop指令,或rep前缀。这些将在后面的章节中介绍。
将寄存器的数值与另一寄存器,或立即数的值相加,并存回此寄存器:
add reg32, reg32 / imm(8,16,32)
add reg16, reg16 / imm(8,16)
add reg8, reg8 / imm(8)
例如,add eax, edx,将eax+edx的值存入eax。减法指令和加法类似,只是将add换成sub eax, edx。
需要说明的是,与高级语言不同,汇编语言中,如果要计算两数之和(差、积、商,或一般地说,运算结果),那么必然有一个寄存器被用来保存结果。在PASCAL中,我们可以用nA := nB + nC来让nA保存nB+nC的结果,然而,汇编语言并不提供这种方法。如果你希望保持寄存器中的结果,需要用另外的指令。这也从另一个侧面反映了“寄存器”这个名字的意义。数据只是“寄存”在那里。如果你需要保存数据,那么需要将它放到内存或其他地方。
类似的指令还有and、or、xor(与,或,异或)等等。它们进行的是逻辑运算。
我们称add、mov、sub、and等称为为指令助记符(这么叫是因为它比机器语言容易记忆,而起作用就是方便人记忆,某些资料中也称为指令、操作码、opcode[operation code]等);后面的参数成为操作数,一个指令可以没有操作数,也可以有一两个操作数,通常有一个操作数的指令,这个操作数就是它的操作对象;而两个参数的指令,前一个操作数一般是保存操作结果的地方,而后一个是附加的参数。
使用sub eax, eax,或者xor eax, eax,可以得到与mov eax, 0类似的效果。在高级语言中,你大概不会选择用a=a-a来给a赋值,因为测试会告诉你这么做更慢,简直就是在自找麻烦,然而在汇编语言中,你会得到相反的结论,多数情况下,以由快到慢的速度排列,这三条指令将是xor eax, eax、sub eax, eax和mov eax, 0。
我们反复强调,寄存器是CPU的一部分。从寄存器取数,其速度很显然要比从内存中取数快。那么,不难理解,xor eax, eax要比mov eax, 0更快一些。
那么,为什么a=a-a通常要比a=0慢一些呢?这和编译器的优化有一定关系。多数编译器会把a=a-a翻译成类似下面的代码(通常,高级语言通过ebp和偏移量来访问局部变量;程序中,x为a相对于本地堆的偏移量,在只包含一个32-bit整形变量的程序中,这个值通常是4):
mov eax, dword ptr [ebp-x]
sub eax, dword ptr [ebp-x]
mov dword ptr [ebp-x],eax
而把a=0翻译成
mov dword ptr [ebp-x], 0
上面的翻译只是示意性的,略去了很多必要的步骤,如保护寄存器内容、恢复等等。如果你对与编译程序的实现过程感兴趣,可以参考相应的书籍。多数编译器(特别是C/C++编译器,如Microsoft Visual C++)都提供了从源代码到宏汇编语言程序的附加编译输出选项。这种情况下,你可以很方便地了解编译程序执行的输出结果;如果编译程序没有提供这样的功能也没有关系,调试器会让你看到编译器的编译结果。
例子
1: int myTransform(int nInput){
00401000 push ebp ; 保护现场原先的EBP指针
00401001 mov ebp,esp
2: return (nInput*2 + 3) % 7;
00401003 mov eax,dword ptr [nInput] ; 取参数
00401006 lea eax,[eax*2+3] ; LEA比ADD加法更快
0040100A cdq ; DWORD->QWORD(扩展字长)
0040100B mov ecx,7 ; 除数
00401010 idiv eax,ecx ; 除
00401012 mov eax,edx ; 商->eax(eax中保存返回值)
3: }
00401014 pop ebp ; 恢复现场的ebp指针
00401015 ret ; 返回
; 此处删除10条int 3指令,它们是方便调试用的,并不影响程序行为。
4:
5: int main(int argc, char* argv[])
6: {
00401020 push ebp ; 保护现场原先的EBP指针
00401021 mov ebp,esp
00401023 sub esp,10h ; 为取argc, argv修正堆栈指针。
7: int a[3];
8: for(register int i=0; i<3; i++){
00401026 mov dword ptr [i],0 ; 0->i
0040102D jmp main+18h (00401038) ; 判断循环条件
0040102F mov eax,dword ptr [i] ; i->eax
00401032 add eax,1 ; eax ++
00401035 mov dword ptr [i],eax ; eax->i
00401038 cmp dword ptr [i],3 ; 循环条件: i与3比较
0040103C jge main+33h (00401053) ; 如果不符合条件,则应结束循环
9: a[i] = myTransform(i);
0040103E mov ecx,dword ptr [i] ; i->ecx
00401041 push ecx ; ecx (i) -> 堆栈
00401042 call myTransform (00401000); 调用myTransform
00401047 add esp,4 ; esp+=4: 在堆中的新单元
; 准备存放返回结果
0040104A mov edx,dword ptr [i] ; i->edx
0040104D mov dword ptr a[edx*4],eax ; 将eax(myTransform返回值)
; 放回a[i]
10: }
00401051 jmp main+0Fh (0040102f) ; 计算i++,并继续循环
11: return 0;
00401053 xor eax,eax ; 返回值应该是0
12: }
00401055 mov esp,ebp ; 恢复堆栈指针
00401057 pop ebp ; 恢复BP
00401058 ret ; 返回调用者(C++运行环境)