聊天群里发的一个CM,感觉很适合我的水平,每次都感觉就要弄出来了,就是做不对,来来回回磨了几天终于做出来了。现在总结起来,跟大家分享一下。
我的水平是什么名词都听过,但是OD的单步调试是哪个键都要百度查这种水平....
所以我就尽量写详细一点,按照时间顺序把我踩过的坑都写一下,避免大家再次上当受骗。
首先拿到这个CM我是丢进了win7 32位虚拟机里用OD跑。
先找到关键代码位置,我百度了可以通过字符串查找 消息断点 函数断点之类的,我都试了试,
最后是对messagebox下断点找到了代码区域。
00401103 /$ 55 push ebp ; 从这里开始验证注册码
00401104 |. 8BEC mov ebp,esp
00401106 |. 83C4 FC add esp,-4
00401109 |. 53 push ebx
0040110A |. 56 push esi
0040110B |. 57 push edi
0040110C |. FF75 08 push [arg.1] ; /s
0040110F |. FF15 20304000 call dword ptr ds:[<&msvcrt.strlen>] ; \先计算注册码的长度,结果存储在eax里
00401115 |. 83C4 04 add esp,4
00401118 |. 83F8 2A cmp eax,2A ; 把结果和2A比较,如果不对的话就弹出对话框think again.意思是注册码应该是42位的
0040111B |. 74 17 je short give_a_t.00401134
0040111D |. 6A 00 push 0 ; /Style = MB_OK|MB_APPLMODAL
0040111F |. 6A 00 push 0 ; |Title = NULL
00401121 |. 68 00404000 push give_a_t.00404000 ; |Text = "Think again!"
00401126 |. 6A 00 push 0 ; |hOwner = NULL
00401128 |. E8 BB010000 call <jmp.&user32.MessageBoxA> ; \MessageBoxA
0040112D |. 5F pop edi
0040112E |. 5E pop esi
0040112F |. 5B pop ebx
00401130 |. C9 leave
00401131 |. C2 0400 retn 4
00401134 |> 33FF xor edi,edi
00401136 |. 8B75 08 mov esi,[arg.1] ; 注册码放进esi里
00401139 |. AC lods byte ptr ds:[esi] ; 取注册码第一位放到eax里,同时esi+1
0040113A |. EB 06 jmp short give_a_t.00401142
0040113C |> 0FB6C0 /movzx eax,al
0040113F |. 03F8 |add edi,eax
00401141 |. AC |lods byte ptr ds:[esi]
00401142 |> 0AC0 or al,al ; 判断eax是不是空
00401144 |.^ 75 F6 \jnz short give_a_t.0040113C ; 上面这点代码整个意思就是循环累加43位的注册码,并把结果赋给edi
00401146 |. 333D 6C404000 xor edi,dword ptr ds:[40406C] ; 40406c是个数据段的值,所以就是edi和[40406c]做异或运算
0040114C |. 57 push edi ; /edi 作为随机数种子
0040114D |. FF15 24304000 call dword ptr ds:[<&msvcrt.srand>] ; \调用srand
00401153 |. 83C4 04 add esp,4
00401156 |. 33DB xor ebx,ebx ; ebx清零
00401158 |. 8B75 08 mov esi,[arg.1] ; esi重新获得注册码
0040115B |. 8D3D B4304000 lea edi,dword ptr ds:[4030B4] ; edi存放地址4030B4,这个地址的值是固定的
00401161 |. E9 83000000 jmp give_a_t.004011E9
00401166 |> FF15 28304000 /call dword ptr ds:[<&msvcrt.rand>] ; [rand
0040116C |. 0FB60C33 |movzx ecx,byte ptr ds:[ebx+esi] ; 循环取出注册码放到ecx里
00401170 |. F7E1 |mul ecx ; 随机数*注册码里的一位(循环取),结果存在eax里
00401172 |. B9 2166C9FA |mov ecx,FAC96621
00401177 |. 50 |push eax ; 保存eax
00401178 |. 33D2 |xor edx,edx
0040117A |. F7F1 |div ecx ; eax/ecx ,余数放在edx中,商放在eax中
0040117C |. 58 |pop eax
0040117D |. 52 |push edx ; 保存了第一次的余数
0040117E |. F7E0 |mul eax ; eax=eax*eax
00401180 |. F7F1 |div ecx
00401182 |. 8BC2 |mov eax,edx ; eax=余数
00401184 |. F7E2 |mul edx ; eax=余数*余数
00401186 |. F7F1 |div ecx ; 这里ecx就是固定的值,一直没变
00401188 |. 8BC2 |mov eax,edx ; 再除一遍,余数赋给eax
0040118A |. F7E2 |mul edx ; eax=余数*余数
0040118C |. F7F1 |div ecx ; 再除一遍
0040118E |. 8BC2 |mov eax,edx
00401190 |. F7E2 |mul edx ; eax=余数*余数
00401192 |. F7F1 |div ecx ; 后面重复这个过程
00401194 |. 8BC2 |mov eax,edx
00401196 |. F7E2 |mul edx
00401198 |. F7F1 |div ecx
0040119A |. 8BC2 |mov eax,edx
0040119C |. F7E2 |mul edx
0040119E |. F7F1 |div ecx
004011A0 |. 8BC2 |mov eax,edx
004011A2 |. F7E2 |mul edx
004011A4 |. F7F1 |div ecx
004011A6 |. 8BC2 |mov eax,edx
004011A8 |. F7E2 |mul edx
004011AA |. F7F1 |div ecx
004011AC |. 8BC2 |mov eax,edx
004011AE |. F7E2 |mul edx
004011B0 |. F7F1 |div ecx
004011B2 |. 8BC2 |mov eax,edx
004011B4 |. F7E2 |mul edx
004011B6 |. F7F1 |div ecx
004011B8 |. 8BC2 |mov eax,edx
004011BA |. F7E2 |mul edx
004011BC |. F7F1 |div ecx
004011BE |. 8BC2 |mov eax,edx
004011C0 |. F7E2 |mul edx
004011C2 |. F7F1 |div ecx
004011C4 |. 8BC2 |mov eax,edx
004011C6 |. F7E2 |mul edx
004011C8 |. F7F1 |div ecx
004011CA |. 8BC2 |mov eax,edx
004011CC |. F7E2 |mul edx
004011CE |. F7F1 |div ecx
004011D0 |. 8BC2 |mov eax,edx
004011D2 |. F7E2 |mul edx
004011D4 |. F7F1 |div ecx
004011D6 |. 8BC2 |mov eax,edx
004011D8 |. F7E2 |mul edx
004011DA |. F7F1 |div ecx
004011DC |. 8BC2 |mov eax,edx
004011DE |. 5A |pop edx ; edx=第一次的余数
004011DF |. F7E2 |mul edx ; 乘上最后一次的余数
004011E1 |. F7F1 |div ecx ; 再求一次余数
004011E3 |. 3B149F |cmp edx,dword ptr ds:[edi+ebx*4] ; 最后和4030B4这里的数据作比较,如果不等就比较是不是验证到了
004011E6 |. 75 0A |jnz short give_a_t.004011F2 ; 最后一位,是的话就成功,不是的话就失败。
004011E8 |. 43 |inc ebx ; 如果验证的数据相等的话,就继续验证下一个数据。
004011E9 |> 83FB 2A cmp ebx,2A
004011EC |.^ 0F85 74FFFFFF \jnz give_a_t.00401166 ; 验证42个数据
004011F2 |> 83FB 2A cmp ebx,2A
004011F5 |. 73 12 jnb short give_a_t.00401209
004011F7 |. 6A 00 push 0 ; /Style = MB_OK|MB_APPLMODAL
004011F9 |. 6A 00 push 0 ; |Title = NULL
004011FB |. 68 0D404000 push give_a_t.0040400D ; |Text = "Incorrect!"
00401200 |. 6A 00 push 0 ; |hOwner = NULL
00401202 |. E8 E1000000 call <jmp.&user32.MessageBoxA> ; \MessageBoxA
00401207 |. EB 13 jmp short give_a_t.0040121C
00401209 |> 6A 00 push 0 ; /Style = MB_OK|MB_APPLMODAL
0040120B |. 68 21404000 push give_a_t.00404021 ; |Title = "Congrats"
00401210 |. 68 18404000 push give_a_t.00404018 ; |Text = "Correct!"
00401215 |. 6A 00 push 0 ; |hOwner = NULL
00401217 |. E8 CC000000 call <jmp.&user32.MessageBoxA> ; \MessageBoxA
这里我还傻乎乎的写满了注释,其实用IDA F5一下比这可清晰明了多了。
这里的逻辑基本就看明白了,
///****************************************/
先判断注册码是不是42位
然后用42位注册码求和,与40406c这个地址的数做异或运算,作为随机数的种子
For(42)
{
注册码的每一位*随机数;
求余数
求平方
求余数
秋平方
...
最后和4030b4地址的数比较一下看看对不对
}
//////*************************************************/
这里要特别说明0x40406c的值,因为我每次打开OD载入这个程序都是0x3133335d,所以我以为这是一个固定的值。至于他怎么来的我也没关心。
然后我天真的以为这就完了呢。当时想的找个用户名,然后按这个逻辑算一遍,得到注册码,美滋滋。
后来转念一想,这程序也没有输入用户名的地方啊,只让输入注册码,而且计算完的东西已经写好固定了,就在0x4030B4 这个地方。合着我要把这个结果逆回去啊~~~
这个一直是求余数,对原来的数肯定有损失,相当于多对一的关系,肯定是不可逆的,那就只能是爆破了。
然后就是想想怎么爆破,穷举42位的asc码是不可能,算到明年也算不出来。
那么我只能先去吃顿饭,吃完饭之后果然想出来办法了。
可以先把v6求出来,从0~0xffffffff一个一个试,这个估计用不了多长时间
求出v6之后只要满足 注册码*rand=v6 就可以了。
注册码是不可能知道的,那么只能从随机值来考虑了,看看随机值是不是可以确定。
因为C语言的随机是一种伪随机,只要种子是确定的,那么随机值就是确定 。
而种子是42位asc码的和,因为asc码最大128,那么42位的和也不会超过6000,这个数还是很小的那就肯定可以爆破了。
第一步,求出v6
第二步,求出正确的随机数(其实就是爆破出42位注册码的和)
第三步,相除得到注册码
然后我就把爆破代码写好了,可以看得出,跟IDA里的很像。没错,我就是无脑复制粘贴的。
只要把最后的比较改成一个switch去比较42个就好了。
这里我把结果写入文件了。
这是我得到的部分数据。
接下来就是再写一个爆破注册码的和的程序,也很简单。
验算部分的代码我给弄丢了,我就不贴了,就是循环6000遍,算出来注册码
然后去比较注册码的和,一样了就说明找对了。
然而,这最关键,最激动人心的一步,竟然没有得到结果!!
之后我做了很多无用的尝试,比如说我觉得可能是因为除法不准的问题,所以我只去确定所有的注册码都在1~128这个范围就行了,暗喜一下,结果还是没有结果。
-----------------------------------华丽的分割线---------------------------------------------------------
注册码*rand=v6
注册码是我要求的,不用考虑。
如果有问题,那要么是rand 的问题,要么是v6的问题。
V6可是我无脑粘贴IDA得到的,不可能有问题,而且经过那么复杂的运算还能正好得到一个值跟他对应,所以v6肯定没问题,一定是rand的问题。
Rand是通过注册码的和 跟40406c的值异或的种子得来的。注册码的和肯定是在0~6000不可能错,那么一定就是40406c这个值错了。
之后下班回家了继续干,由于家里没装虚拟机,我就直接在本机上调试了,环境是win10 64位。
按照之前相同的方法下断点,结果断点没断下来,程序竟然直接退出了!!!
我仅有的一点知识面告诉我,这是反调试!但是我对反调试的认识仅限于这个词而已。我一个OD都不知道单步调试是哪个快捷键的人,哪会搞什么反调试~~
但是我就在想啊,既然刚载入的时候回断下来,之后下断点又运行不到,那么一定会有一个地运行完了就退出吧,那么我就要找到这个地方,看看到底是怎么做到的让我不能下断点。
然后我就从程序一开始一路高歌F7+F8,结果没一会儿程序就退了,也没运行多少代码,经过反复几次测试,找到了运行完就退出的位置
就是这个函数,运行完就退了,难怪下断点都断不下来。然后我又百度了这个函数看看是什么作用。果然是一个反调试函数,会除去所有附加的调试器。像我这么粗暴的人,看见这种直接就nop掉了。之后果然是能下断点了,开开心心继续调试。
之后更诡异的事情发生了,等运行到验证注册码的地方,我看了下40406c的值,竟然是0!
有了刚才这个反调试的经验,我很自然的想到应该还有其他的反调试,他虽然没有直接让程序退出,但是可能根本有没有被调试,更改了程序流程。
然后我又开始从头开始调试,nop掉ntsetinformationthread,继续f8。插句话,这里的代码很奇怪,就跟对不齐一样,每次运行到那里才能显示出正常代码,好在这里的代码少,我一句一句跟也能看,要是代码多了话真就看不懂了,还请大佬指教一下怎么解决这个问题。
之后遇到了
想都不想,肯定有问题,直接百度一下。这果然也是一个反调试函数,如果有调试器的话,会给第三个参数就是40406c赋值-1
这里果然是跟我想的差不多,比较了一下这个值,然后多了一个赋值
后面把edi的值赋给了[404036],应该是一个回调函数的地址。
之后会运行到[404036]。
往404036的值 下断点(不是404036是它的值)继续跟。
然后又遇到了这个
想都不想直接nop啊!
......
后来反反复复看了好多遍我才发现这个好像是个坑,因为他有两个参数是40406c,最终的值不是有没有被调试,而是返回值的字节数,也就是4.
所以这个虽然是一个反调试函数,但是nop掉的话就错了!这个是我反反复复很多遍才发现的~~
如果把这个函数nop掉了,最终40406c的值就是3133335d,正确的应该是31333359.
其实在这一段代码里还有几处跟前面很像,比如给回调函数赋值那些,但是这一段代码确确实实没有能赋值的地方,也确实不该赋值,我反反复复看了好多遍才确定。
最终确定了40406c的值,就是0x31333359。
/*/*/**********************************************************///
接着返回验证代码,把生成随机数时,异或的值改成31333359,满怀欣喜的等一个结果!
然而,并没有结果!
我只能继续看上面的代码分析40406c的值,前面是直接把分析全写了,实际是来回看了好遍,最后的状态就是,他就是31333359,要不就是3133335d,要都不是我就真不会了!
我把可能的值都试过了,还是算不出来结果。
/***********************************************/
之后我又想,是不是生成的随机数不对?于是我打开的vs调试,看了下生成的随机数,看起来是有点问题。虽然单看每一个42位随机都挺好,但是这6000个一起看,第一个生成的随机数,明显有一种稳定增大的迹象!
我甚至都觉得根据第一个随机值都能推算出他的种子...
然后我就把目标放在了随机值我就觉得他不对,在网上查了半天,也没有结果,但是我得到了一个很重要的信息。那就是随机数的值最大只能是0x7fff
我的注册码最大128,随机值最大0x7fff,相乘最大也不到16进制的八位那么大啊,而我得到的那些数据全都是8位的,那就说明一个问题,我一开始计算的那些数据,肯定不对!
/*************************************************************/
这下可真难住我了,从IDA直接复制的代码怎么会有错呢,我订正了好几遍,就是没错,但算出来的肯定不对,最后实在没有办法了,干脆我直接用汇编代码写跟od里一模一样的代码看看算出来到底对不对。
大概这样子,基本是从OD复制的...
这是最后得到的结果
这样才对嘛,全都是6位一下的数字,才有可能算的对啊。
看来IDA确实出错了,第一次用IDA体验极差。
最后到原来的程序验证一下,成功得到注册码!
总结一下:
最主要的IDA坑我这一下着实浪费我很长时间
但是要不是坑我这一下,我也不会对其他地方看的那么细心认真,也不会有那么多收获
虽然每一次的努力不一定能让我得到结果
但是真的感觉到每次我都向结果前进了一点
在这个过程中感受到很多乐趣
最终得到了结果体验还是极好的
希望以后能和大家多多交流共同进步
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)