0x00 序言
本学习系列(二)将分析并总结160个CM之中难度中等的题目.
今天的这个程序是上一篇文章中同系列CM的2.0版本相对于上一个CM来说难度有所增加,掌握了核心思想后能使逆向思维得到蛮大的锻炼.
0x01 程序分析正文
拿到程序后先查壳,程序无壳因此还是直接拖入OD进行动态分析.按照一贯套路,先搜索下字符串,看看有无成功或失败提示
可以看到有明显提示,因此我们跳过去看看代码:
可以看到失败提示的上面有一个je跳转,可能是回避失败提示前往成功提示的跳转,但是成功提示的代码段是没有任何跳转访问的,也就是说按照目前的汇编代码来看,成功提示的代码块是永远都不会被执行的,这就有点意思了!
因此我们先在失败提示前面下断点跑一下程序看看.这里用的测试name:"g2uc123",测试serial:"123456"
经过简单的单步调试后,发现以下特点:
一是该CM如上篇文章中的CM一样,serial是纯数字输入,否则失败提示这里的代码块将不会被断点断下.
二是这部分代码,其中有一段是在运行时会变化的,并且跳离失败提示的je判断跳转向了会变动的代码区块,如下图所示:
可以发现地址:0x4012D9到0x4012DC的代码段内容发生了变化,而致使其变化的原因也很容易看见:
004012BB 3105 D9124000 xor dword ptr [0x4012D9], eax
004012C4 66:2905 D912400>sub word ptr [0x4012D9], ax
以上两句代码,对地址0x4012D9开头的四个字节内容进行了读写操作,那么为什么会进行这样的操作?其实这里脑中已经可以形成一个猜测:
也许程序的验证就是通过name和serial运算出0x4012D9地址处正确的四个字节,也许这四个字节运算正确后是个jmp跳转,能跳到成功提示!
为了验证我们的猜想是否符合程序真正的原理,我们得分析下他这长串的汇编代码到底是在做些什么.
首先通过从失败提示开始向上,在每个判断的下一条语句下断,然后测试输入name和serial,发现只有地址0x4012DC是最先断下的,说明此处开始才是serial和name的验证环节,而再往上则可能是判断serial是否为纯数字.
接下来详细分析汇编代码:
0040128C 50 push eax
0040128D 6A 14 push 0x14
0040128F 68 6C314000 push 0040316C ; ASCII "g2uc123"
00401294 FF35 54314000 push dword ptr [0x403154]
0040129A E8 AF010000 call <jmp.&USER32.GetWindowTextA>
0040129F 85C0 test eax, eax
004012A1 74 48 je short 004012EB
========以上部分是保存栈数据,然后从编辑框获取name和serial,就不作详细分析了========
004012A3 A1 0B304000 mov eax, dword ptr [0x40300B] ; 将右边地址指向的字符串"CTEX"作为整型赋值给eax,因此eax=0x58455443
004012A8 BB 6C314000 mov ebx, 0040316C ; 将输入的name字符串的指针赋值给ebx
004012AD 0303 add eax, dword ptr [ebx] ; 循环开始,将ebx指针指向的四个字节作为整型相加给eax
004012AF 43 inc ebx ; ebx自增1,相当于字符串指针右移1位
004012B0 81FB 7C314000 cmp ebx, 0040317C ; 判断指针是否已总共移动了16位,相当于循环十六次
004012B6 ^ 75 F5 jnz short 004012AD ; 循环结束
004012B8 5B pop ebx ; 弹出栈数据到ebx,这里ebx会变成serial的整型数据
004012B9 03C3 add eax, ebx ; eax+=ebx
004012BB 3105 D9124000 xor dword ptr [0x4012D9], eax ; 将eax与[0x4012D9]的数据异或然后保存在[0x4012D9]
004012C1 C1E8 10 shr eax, 0x10 ; eax右移16,此时eax高4位归零,低四位为之前高四位的值
004012C4 66:2905 D912400>sub word ptr [0x4012D9], ax ; [0x4012D9]的低四位-=eax低四位
004012CB BE EC114000 mov esi, 004011EC ; esi=0x004011EC 后面的循环有用
004012D0 B9 3E000000 mov ecx, 0x3E ; ecx置为0x3E,后面的循环有用
004012D5 33DB xor ebx, ebx ; 清零ebx
004012D7 EB 04 jmp short 004012DD ; 跳过会变动的代码块
004012DD AD lods dword ptr [esi] ; 循环开始,从刚才的0x004011EC地址开始,将四个字节写给eax,每次向后移动四个字节
004012DE 33D8 xor ebx, eax ; ebx^=eax
004012E0 49 dec ecx ; ecx-=1 这里ecx记录循环次数,为0x3E次
004012E1 ^ 75 FA jnz short 004012DD ; 循环结束,若ecx归零后则不再循环
004012E3 81FB FBCFFCAF cmp ebx, 0xAFFCCFFB ; 判断ebx的最后结果是否为0xAFFCCFFB
004012E9 ^ 74 EE je short 004012D9 ; 如果结果正确则跳转到会变动的代码块,也应该就是成功验证的意思
算法的主体代码就是这样,其中循环读取的3E*4个字节的块的位置如下图所示:
其中,紫色高亮部分是代表会变动的4个字节(即[0x4012D9]地址指向的4个字节),最终则是将上图红框区域按4个字节为一组划分,然后连续位异或,判断结果是否符合0xAFFCCFFB.
这里需要特别注意的一点是,红框选中部分其实就是包括了我们下断分析的函数,但是OD的普通断点会使每条汇编命令头字节变成CC,因此我们下断后运行,程序即使输入正确的name和serial也无法正常提示成功!
因此这里需要把断点全部取消(可以使用硬件断点,硬断不会修改内存数据)!
根据异或的运算规则:若a^b^c=d,则1. d^c=a^b 2.d^b=a^c 以此类推.
因此除去紫色高亮所涉及的2个四字节区块,其他剩下的四字节区块都是固定不变的,可以先算出来这些固定区块的异或值!
经过计算,除去紫色涉及的两个区块,剩余区块异或结果为:0x23E989A7(Hex: A7 89 E9 23)
此时用该值与最终判断结果异或则得结果等于紫色涉及的两个区块相异或的值:0x23E989A7 Xor 0xAFFCCFFB=ptr:[0x4012D8] Xor ptr:[0x4012DC]=0x8C15465C(Hex: 5C 46 15 8C)
此时可以如此假设:ptr:[0x4012D8]为Hex: 04 ?? ?? ??,ptr:[0x4012DC]为Hex: ?? AD 33 D8,则可通过如下计算得知各??的内容:
ptr:[0x4012D8] Xor ptr:[0x4012DC]=Hex: 5C 46 15 8C
04 Xor ?? =5C
?? Xor AD =46
?? Xor 33 =15
?? Xor D8 =8C
最终带入??得:ptr:[0x4012D8]=(Hex: 04 EB 26 54),ptr:[0x4012DC]=(Hex: 58 AD 33 D8)
此时可知,变动的四个字节正确数据是Hex: EB 26 54 58,我们测试写到内存里看看汇编代码会变成什么样子:
可以看到,这的确是个跳转,jmp跳转到成功提示处,说明我们分析的没错!
这时可以尝试写出相同的验证算法,以下是该算法的易语言源码:
现在来研究下如何写出注册机算法,逆向算法的关键在于利用我们刚才算出的变动的四个字节Hex: EB 26 54 58(0x585426EB)
假设name是"g2uctest123",则经过16次对根据name数据的循环相加操作后:
eax=0x1476B80A,eax=eax+serial的int型
然后此时我们把eax当作未知数:0xAABBCCDD(四个未知数代表4个字节).此时:变动的四字节初始值ptr:[4012D9]=0x00584554
根据上图算法:
ptr:[0x4012D9]^= eax(0xAABBCCDD)
eax右移16位(0xAABB)
ptr:[0x4012D9]-=eax(0xAABB)=0x585426EB //此时ptr:[0x4012D9]高两位(0x5854)并未变化,这是突破点
因此,0x5854(最终高两位)^0x0058(四字节初始高两位)=0xAABB=0x580C
将AABB带回去(怎么像是在做数学题一样哈哈):
ptr:[0x4012D9]^= eax(0x580CCCDD) //别搞混了最右边的CCDD是未知值
eax右移16位(0x580C)
ptr:[0x4012D9]-=eax(0x580C)=0x585426EB //这里根据低位相减来算CCDD的值
所以0x26EB(最终低两位)+0x580C(eax初始高两位0xAABB)=0xCCDD^0x4554(四字节初始低两位)=0x7EF7,因此0xCCDD=0x4554^0x7EF7=0x3BA3
现在就齐全了:eax初始值=0x580C3BA3
所以serial=eax-0x1476B80A(根据name进行16次循环的结果)=0x1476FD5E=343342430 ,这就是对应name:"g2uctest123"的正确serial了!
根据以上原理写出注册机的易语言代码:
本次的CM就分析完啦!
0x02 总结
首先说一点,目前我也是刚开始写博客,很多专业词汇可能表达不准确,例如高2位,左移2位之类,我有时说的2位是指例如FF这样算两位,有时候可能是指FF FF,而实际程序中移动位数写的可能是16,这是因为实际命令移动位数是根据2进制来算的,因此对于我经常对位数的描述比较模糊还请多包涵(其实就是发现了后懒啊实在不想改了233).
另外,这次的分析文章可能比较复杂,尤其是到最后算法逆向的部分,如果你看不很明白,最好自己找到题目文件来实际操作感受一下,其实我在写解析的时候也经常忘了接下来一步是怎么算出未知数,足以看出这个算法的确有点绕人(也许是我太菜).