整个文章都是参照
使用OllyDbg从零开始Cracking
这份文档写的,不是什么干货,就是一个学二进制的笔记
破解的程序 Splish.exe
使用软件 OD
首先直接把exe文件拉进去OD
左上角,反汇编窗口,默认自动对主程序分析,并显示附加的信息
下断点,单步执行,调试,很多时候都是在这个窗口
右上角,寄存器窗口
EAX “累加器”,很多加法乘法指令的缺省寄存器
ECX ”计数器”,重复(REP)前缀指令和loop指令的内定计数器
REP 重复执行某条指令,直到ECX=0
Loop 重复执行某个指令块,直到ECX=0
EDX 总是用来存放整数除法产生的余数
EBX “基地址”,在内存寻址是存放基地址
ESP 专门用作堆栈指针,被形象的称为栈顶指针,压入堆栈的数据越多,ESP越小
EBP 基址指针,因为ESP始终指向栈顶,所以EBP主要用来在堆栈中寻址用的
针对ESP EBP 网上有找到一个例子,解释的很容易理解
******************************************************************
-------------------------------------------------------------------------------- esp是堆栈指针 ebp是基址指针
那两条指令的意思是 将栈顶指向 ebp 的地址 ---------------------------------------------------------------
以下摘自网上一篇文章:
pushebp;ebp入栈 movebp,esp;因为esp是堆栈指针,无法暂借使用,所以得用ebp来存取堆栈 subesp,4*5;下面的wsprintf一共使用了5个参数,每个参数占用4个字节,所以要入栈4*5个字节 push1111 push2222 push3333 pushoffsetszFormat pushoffsetszOut callwsprintf;调用wsprintf addesp,4*5;堆栈使用完毕,“还”回4*5个字节给系统 ... movesp,ebp;恢复esp的值 popebp;ebp出栈 ret
明白了吗?主要是用来保存/恢复堆栈,以便传递参数给函数。 在MASM里面,有一条更方便的语句,就是invoke 使用它后,你就不用自己做这些事情了。
---------------------------------------------------------------
esp始终指向栈顶,ebp是在堆栈中寻址用的
我的理解:
调用一个函数时,先将堆栈原先的基址(EBP)入栈,以保存之前任务的信息。然后将栈顶指针的值赋给EBP,将之前的栈顶作为新的基址(栈底),然后再这个基址上开辟相应的空间用作被调用函数的堆栈。函数返回后,从EBP中可取出之前的ESP值,使栈顶恢复函数调用前的位置;再从恢复后的栈顶可弹出之前的EBP值,因为这个值在函数调用前一步被压入堆栈。这样,EBP和ESP就都恢复了调用前的位置,堆栈恢复函数调用前的状态。
*******************************************************************************
ESI
EDI
源/目标索引寄存器”,这个是指在串操作指令里的,如 movs/cmps/stos/lods。这这类指令里,esi 和 edi 的使用是固定的,比如 movs 是由 ds:[esi] 复制到 es:[edi] 处,无可变化(据说 ds: 是可以被段前缀指令改动的),尽管指令表面上没有任何的表示。 此外,它们又作为通用寄存器可以进行任意的常规的操作,如加减移位或普通的内存间接寻址(这时是不和 ds: 或 es: 联动的)。
可以理解为,跟EAX这类的通用寄存器一样,但是在某些指令下的使用情况是固定的,如上例
C 进位标志位
无符号运算是否产生进位或借位。运算结果的最高有效位向更高位进位或者借位,CF置1,否则置0。
P 奇偶校验位
运算结果低8位中'1'的个数。'1'的个数为偶数,PF置1,否则置0。
A 辅助进位标志位
低半字节向高半字节进位或借位。字操作时低字节向高字节进位或借位,AF置1,否则置0。
Z 全零标志位
运算结果为0,ZF置1,否则置0
S 符号标志位
运算结果为负数,SF置1,否则置0
O溢出标志位
有符号运算是否溢出。运算结果超过了8位或者16位有符号数的表示范围,OF置1,否则置0。
左下角,数据窗口
显示的是内存地址以及存放的数据
右下角,堆栈窗口
显示的是ESP指向的地址,调试的时候如果有函数调用,可以在这个窗口看到压栈和出栈的情况。
基本的介绍完了,接下来就是这个程序的破解
运行一下程序
有3个输入框,第一个验证码的比较简单,这次不讲这个。
主要是第2和第3个框。
Name是输入一个名字,serial相当于这个名字对应的序列号
其实这个得破解就是相当于我们平常用的注册机,我们随便输入一个名字,然后注册机产生一个系列号,然后验证成功。
开始
按教程的步骤,先查找API
如下如图
右键单击,查找----选择当前某块中的名称
找到GetWindowsTextA
这个我理解为是一个窗口控件,因为我们是在调用一个可输入的窗口后再输入name和系列号的,所以先在这个位置下一个断点。
然后按F9运行程序
结果发现输入名字和系列号后,程序直接运行,判断错误。没有按我们预想的停止在窗口调用的地方。
因为是新手,只能跟着教程,结果以为是OD有问题,换了好几个版本还是一样,后面突然想到,可能是我们下的断点不对,那我们换一个想法,我们查找一下参考文本字串
同样的,右键---查找---所以文本参考字串
我们看到了几个熟悉的字符串
Good job,now keygen it
Sorry,please try again
这个地方是显示我们输入的系列号是否正确的地方,那我们选中其中一个,双击跳回调试窗口
然后我们往回找,发现两个GetWindowsTextA
地址分别为4015F1 40160D
对比一下我们刚才下断点的那个地方的地址,发现是不一样的
所以,我们把这个断点删掉,把断点断在4025F1
然后按F9运行
我们按教程的一样
名字输入narvaja
系列号输入 988898
然后点击按钮
程序并没有跳出正确或者错误的判断,而是暂停在了我们下断点的地方
我们看下堆栈窗口,看到有个buffer=splish.00403242
这里开辟了一段缓冲区域来存放我们输入的系列号,选中这段,右键--数据窗口跟随。我们看一下数据窗口
里面是空的,数据都为0
点击菜单栏--调试--执行到返回
看下数据窗口,发现数据已经写进去了,看红色箭头
然后按下F7
回到刚才主界面
因为我们输入的是错误的系列号,所以我们试着在缓冲区的内存下一个断点
选中989898,右键--断点---内存访问
运行起来,我们断在了这里,如下图
我们按F7单步执行的话,会发现错误系列号的第一个字节移动到了EAX中
看一下接下来的这段代码
CDQ
IDIV ESI
CDQ指令双字扩展,把EAX中的符号位扩展到EDX中去,然后EDX:EAX对应的值除以ESI,商保存到EAX中,余数保存到EDX中。EAX符号位扩展到EDX中,EDX的值应该变为零,相当于对EDX进行XOR EDX,EDX操作。现在不需要将EDX清零了,因为CDQ指令已经帮我们完成了该操作。
所以当前情况下我们不必每次循环之前将EDX赋值为零,我们只需要在IDIV指令之前加上一个CDQ指令即可。
EDX:EAX除以ECX,商存放在EAX中,余数存放到EDX中。好了,我们现在来看看具体的实现。
第一个字节为39,除以ECX(值为0A)。
F7单步调试一下
上图为执行IDIV前
下图是执行后
商为5保存在EAX 余数为7保存在EDX
接下来是把EDX的低位即DL的值07保存到内存地址指向的内存单元
然后接下来是
Inc ebx
cmp ebx , dword ptr ds:[0x403467]
Jnz short splish.00401669
EBX的值为0,然后递增1,接着与6进行比较,如果不相等,则跳到00401669
这里的6是我们输入的989898的长度,存放在内存地址0x403467指向的内存单元
这里肯定不相等,所以跳回到开始的地方,对第二个字节进行操作
接下来就是跟前面一下,不在重复说明。我们直接单步运行到989898执行完毕
然后跳出循环如图
接下来就是判断系列号的正确和错误并给出提示
那我们先来看看,什么情况下会跳转到正确的情况
但是因为我们输入的是错误的,所以我们看下接下来是怎么执行的
看下ESI和EDI所保存的地址
然后eax,ecx进行比较,相等的话,跳转为实现,然后ebx加1,跳转到4016b6,判断一下是不是整个用户输入的989898是否比较完成,完成并且系列号都是正确的话,整跳转到正确的提醒框。
那这里面,最主要得是,eax和ecx的比较,如果两个不相等,则会直接跳转错误
Ecx保存的是esi指向的内存单元,我们通过前一步计算所求得的余数07
Eax保存的是edi所指向的内存单元的数据是02
也就是说只有ecx为02时,则就会跳转到正确的情况
上面是第一个字节的情况,那么第二个字节呢,我们直接找到EDI所指向的内存地址
所以我们可以得出
第一个字节的算式是这样的
39=5*0A+7
也就是说除法运算的结果是39/0A 商为5,余数为7。所以我们可以通过反向运算5乘以0A然后加上7得到39。
所以只要我们求得的余数为02 08 08 03 05 05
则能破解成功
所以正确的系列号应该是
5*0A+02=34(十六进制)----ascii 4
5*0A+08=40(超出30-39的范围)(因为ascii0-9对应的十六进制就是30-39)
注:因为我们输入的错误系列号为989898,对应的十六进制为39 38 39 38 39 38,而他们除以0A后的商都为5,所以求其正确系列号的时候,都是5*0A+xxxx
所以因为变换为 正确字节值 = 4*0A + 平衡值
第二个数应该为 4*0A+08=30---ascii 0
一次类推得出正确的系列号为 4005775
最后我们再梳理一遍
我们随手输入一个系列号 989898
然后 程序计算出了,长度为6,通过通过循环6次,把989898一个个字节进行除以0A,求得商和余数
最后余数和本身存在的一段数据进行比较,如果相等的话,则系列号正确,如果其中一个错误的话,则系列号错误。