[原创]MagicISO的逆向过程及算法破解
1.缘由。
事情是这样的:一天突然想下载VC++ 2008用,就在本论坛搜索下载地址,找到了这个帖子:http://bbs.pediy.com/showthread.php?t=167366,就用这个帖子里面的下载地址把Visual Studio 2008 Professional下载了,下载后发现文件是虚拟镜像文件,只好再下载工具软件:MagicISO来导出安装文件。下载MagicISO安装文件后发现MagicISO是未注册的,遂产生了要破解意愿,以下为逆向MagicISO的过程。
2.脱壳
MagicISO安装好之后,用PEiD检测之后发现是:UPX 0.89.6 - 1.02 / 1.05 - 1.24 -> Markus & Laszlo,之后再下载了两个脱壳工具(http://tools.pediy.com/unpacker.htm):upxfix_by_DiKeN和upxfix。先用upxf.exe修复MagicISO.exe,然后再输入命令行:UPX –d 文件名,就可以把MagicISO.exe脱壳,脱壳之后发现程序使用Borland C++编译的。
3.软件加密解密原理
本软件在用户名和序列号的处理上采用了RSA和Base64加密两种手段共同完成,加密和解密分为两条路径,一是软件外部的加密,一是软件程序的内部解密,基本逻辑是这个样子的:
●
4.OD逆向 4.1寻找分析断点
程序运行后的第一个界面,点击“register”按钮后是这个输入用户名和序列号的界面,如图:

我随便输入用户名和序列号(序列号长度应>171个字符,程序内有验证要求)之后,想跟踪点击“Register”按钮事件处理,刚开始想对DialogBoxParamA、MessageBoxA、MessageBoxW、MessageBoxExA、MessageBoxExW下断点,发现都徒劳,经过请教别人发现通过对SendMessageA下断点可以跟踪到“Register”按钮事件处理事件处理函数,即断点地址:004AF030。
4.2分析断点内容
经过一段时间对函数:004AF030的分析,发现这个事件处理函数很复杂,一是函数很多,二是函数嵌套很多,跟踪起来相当头疼。随着耐心的分析代码,还是能够找到一些逻辑,大致如下:
4.2.1获取用户名和序列号 获取用户名的地址为:
004AF12A CALL <JMP.&KERNEL32.lstrcpyA> ; \lstrcpyA
获取序列号的地址为:
004AF174 CALL <JMP.&KERNEL32.lstrcpyA> ; \lstrcpyA
这两处地方都是比较明显的代码。
4.2.2 创建注册表项
接着程序的下一步操作是在操作系统的注册表中创建、保存用户名和序列号的注册表项并,保存用户名和序列号的键值(程序的变态之处在于:程序启动时也会再次验证用户名和密码是否正确),代码如下:
004AF18C PUSH ECX ; /Arg2,序列号 004AF18D MOV EAX,DWORD PTR SS:[EBP-8] ; | 004AF190 PUSH EAX ; |Arg1,用户名 004AF191 CALL test.0047B0B0 ; \创建保存用户名和序列号的注册表键值
F7跟踪进入此函数,基本很容易就能发现创建并保存注册表键值的地方,代码如下:
0047B2B2 PUSH EAX ; /BufSize,用户名长度 0047B2B3 PUSH EBX ; |Buffer,用户名地址 0047B2B4 PUSH 1 ; |ValueType = REG_SZ 0047B2B6 PUSH 0 ; |Reserved = 0 0047B2B8 PUSH test.0070063D ; |ValueName = "RegUserName" 0047B2BD MOV EAX,DWORD PTR SS:[EBP-40] ; | 0047B2C0 PUSH EAX ; |hKey 0047B2C1 CALL <JMP.&ADVAPI32.RegSetValueExA> ; \RegSetValueExA
4.2.3 读取注册表项键值 紧接着是读取前面函数已保存过注册表中的用户名和序列号键值,代码为:
004AF199 PUSH test.00755CF4 ; /Arg2 = 00755CF4
004AF19E PUSH test.00755CE8 ; |Arg1 = 00755CE8
004AF1A3 CALL test.0047B61C ; \test.0047B61C
F7进入函数,读取键值的地方为:
0047B697 PUSH EDX ; /读取出来的用户名的长度 0047B698 MOV ECX,DWORD PTR SS:[EBP-8] ; | 0047B69B LEA EAX,DWORD PTR SS:[EBP-38] ; | 0047B69E PUSH ECX ; |读取出来的用户名存储在此地址中 0047B69F PUSH EAX ; |pValueType 0047B6A0 PUSH 0 ; |Reserved = NULL 0047B6A2 PUSH test.00700693 ; |ValueName = "RegUserName" 0047B6A7 MOV EDX,DWORD PTR SS:[EBP-34] ; | 0047B6AA PUSH EDX ; |hKey 0047B6AB CALL <JMP.&ADVAPI32.RegQueryValueExA> ; \RegQueryValueExA
4.2.4 判定程序是否已注册
此处是判断程序是否已注册的标识位,在程序内部有很多地方,其中一处为:
004AF233 MOV BYTE PTR SS:[EBP-6D],0
004AF237 CMP BYTE PTR DS:[6FE908],1
004AF23E JNZ test.004AF3C3
若想爆破此软件,则可以将004AF233 |. C645 93 00 MOV BYTE PTR SS:[EBP-6D],0,修改为:004AF233 |. C645 93 00 MOV BYTE PTR SS:[EBP-6D],1,并且要遍历整个程序,将全部存在类似的地方全部修改完毕即可爆破此软件。
004AF3C3是个很明显的注册错误程序处理分支,后面会出现很多,因此可以借此判断出此为爆破点!
4.2.5 黑名单比对
紧接着下面是从程序中取出一部分黑名单用户名,拿来跟用户输入的用户名进行比对,以判断程序的流向,第一个比对的代码地址为:
004AF24A PUSH ECX ; /String2
004AF24B MOV EAX,DWORD PTR DS:[755CEC] ; |
004AF250 PUSH EAX ; |String1 => "11ccccccccccc9"
004AF251 CALL <JMP.&KERNEL32.lstrcmpA> ; \lstrcmpA
4.2.6 解Base64编码
对用户输入的序列号密文首先进行Base64解码,函数地址为:
004AF37C PUSH EDX ; /Arg3
004AF37D PUSH EDI ; |Arg2
004AF37E PUSH ECX ; |Arg1 => 序列号
004AF37F CALL test.0047BA60 ; \对序列号按照一定的编码规则编码,然后放在Arg3开始的内存区间内
按F7跟踪进入,可以看到具体的解码代码为以下代码:
0047BA9E /TEST EAX,EAX 0047BAA0 |JNZ SHORT test.0047BAC1 0047BAA2 |MOV EAX,DWORD PTR SS:[EBP-4] 0047BAA5 |INC DWORD PTR SS:[EBP-4] 0047BAA8 |MOV EDX,DWORD PTR SS:[EBP+8] 0047BAAB |MOV CL,BYTE PTR DS:[EDX+EAX] ;依次取序列号每个字符 0047BAAE |PUSH ECX 0047BAAF |CALL test.0047BA00 ;返回每个字符在base64码表中的位置 0047BAB4 |MOVSX ESI,BYTE PTR DS:[EAX+755D40] ;取各个字符的对应的码值 0047BABB |MOV EAX,6 ;设定循环6次,每次取最低一位值 0047BAC0 |POP ECX 0047BAC1 |MOV ECX,EBX 0047BAC3 |MOV EDX,ESI 0047BAC5 |AND DL,1 ;取码值的最第一位值 0047BAC8 |AND ECX,7 ;判断循环是否够8位,够就组下一个8位 0047BACB |SHL DL,CL ;设定位置 0047BACD |MOV ECX,EBX 0047BACF |MOV EDI,DWORD PTR SS:[EBP+10] 0047BAD2 |SHR ECX,3 0047BAD5 |INC EBX 0047BAD6 |SHR ESI,1 ;码值向右移动一位, 0047BAD8 |DEC EAX 0047BAD9 |OR BYTE PTR DS:[EDI+ECX],DL ;依次向内存中写入处理好的每一位的值 0047BADC |CMP EBX,DWORD PTR SS:[EBP-8] 0047BADF \JB SHORT test.0047BA9E
假如需用输入的序列号为:012abc……,则会在内存中解码形成数据为:8A BA 80 ……,其处理的大致逻辑如图所示:

4.2.7 RSA解码
此处是处理RSA解码的函数:
004AF393 PUSH EAX ; /Arg5 => 0000008C
004AF394 PUSH test.006FE87C ; |Arg4 = 006FE87C
004AF399 PUSH EDX ; |Arg3
004AF39A PUSH 80 ; |Arg2 = 00000080
004AF39F MOV ECX,DWORD PTR SS:[EBP-28] ; |
004AF3A2 PUSH ECX ; |Arg1
004AF3A3 CALL test.006B4A10 ; \test.006B4A10
RSA解码区程序只给出了N、E,无法获知D,因此我们比较好的处理方法是按照我们给定的N、E、D,配合我们输入的用户名和序列号来完成算法的逆演算,这样所有的数据都是我们编排好的,就可以确保注册是成功的。
首先我找来RsaKit_V1.0版本,随机产生一个1024位的密钥,然后输入我们需要注册的用户名,点击加密,得到加密密文,如图:

此处我们设定的几个关键参数为:
N= 6D2E20A939BAB289A52AC6A80BD954FC497EF69369754262513D364DB624D33D902591E443C55448128647E853D4505FDC885788123EEBAB1660066F0E6C4BF520D0D8870F921E79ED63ED049101309635D0E46CCBDE42C67F29DCEA63CF3ABD67AFD81AFFC053AF6CA51D3745B5E97EDAC3AC0B70E48D2CE862AC5823C5F5DD
E=1001
D= 2849F5537D321FE01B4B05B3CD6B8DA8EB9626778F5AEDFC49EE44B3B89B2E387190EB9D4844F12D0768F5F00DBF3349AC9FC36D3B0E6CF8DD70E1EEE96B9D3B839C43C9EBB4256E6622D1DA9D1EA7E847470643C8C77F3099D384144B653420813FA366F5E0B458EF994FA0D238CA48A0730606767F7B8854958BE926637921
M=11ccccccccccc9= 3131636363636363636363636339
C= 4EE90DA9A2A43376C529DE6EC71B043A07DF321935B1F50CF8DFC0C0E518DCBE9EE6E120A4C19C4358CF416C2C69260AA89EA5BD29F0CE430D060DF1C666E89EE156B8304B2A8C6447DF03DAC83B888C973526358D498951D271A5D6F29624FC62A7A843A3AB8372811FFCB1223461DF4CC2ECEA5D8762B8720A22B46D7B721B
设计好N、E、D、M、C之后,那么就是要程序按照我们设计好的计划来运转,下面一步就是要替换程序中的N 和E,此时本程序的Base64编码相对复杂,我们也可以省去自己去做Base64编码,而可以在程序做完Base64解码后,直接把我们设计的C替换,替换时我们可以在此处替换:
006B4AE6 PUSH ECX ; /解码后的大小 006B4AE7 MOV EAX,DWORD PTR SS:[EBP-14] ; | 006B4AEA PUSH EAX ; |解码后的地址 006B4AEB MOV EDX,DWORD PTR SS:[EBP+C] ; | 006B4AEE PUSH EDX ; |待解码长度 006B4AEF MOV ECX,DWORD PTR SS:[EBP+8] ; | 006B4AF2 PUSH ECX ; |待解码地址 006B4AF3 MOV EAX,DWORD PTR SS:[EBP-8] ; | 006B4AF6 PUSH EAX ; |N 和E存储的地方 006B4AF7 CALL test.0069489C ; \test.0069489C
对于N 和E 的替换,我们可以用WinHEX打开EXE文件(即附件),通过检索既可以找到N的地址为:002FD488(EXE文件偏移地址),然后把上面我们设计好的N进行倒序排列,即变为:
N= DDF5C52358AC62E82C8DE4700BACC3DA7EE9B545371DA56CAF53C0FF1AD8AF67BD3ACF63EADC297FC642DECB6CE4D0359630019104ED63ED791E920F87D8D020F54B6C0E6F066016ABEB3E12885788DC5F50D453E84786124854C543E49125903DD324B64D363D516242756993F67E49FC54D90BA8C62AA589B2BA39A9202E6D,然后进行替换。
E的地址为:002FD488(EXE文件偏移地址),即将:C7406E69,替换为:00010001
待解码地址我们替换为:4EE90DA9A2A43376C529DE6EC71B043A07DF321935B1F50CF8DFC0C0E518DCBE9EE6E120A4C19C4358CF416C2C69260AA89EA5BD29F0CE430D060DF1C666E89EE156B8304B2A8C6447DF03DAC83B888C973526358D498951D271A5D6F29624FC62A7A843A3AB8372811FFCB1223461DF4CC2ECEA5D8762B8720A22B46D7B721B
这样,我们就完成了RSA KEY的替换。
4.2.8 比对用户名
进行完以上的KEY替换之后,我们继续运行程序,来到最后的比对处:
004AF3B0 PUSH EAX ; /String2 => "11ccccccccccc9"
004AF3B1 MOV EDX,DWORD PTR SS:[EBP-34] ; |
004AF3B4 INC EDX
004AF3B5 PUSH EDX ; EDX是解码
004AF3B6 CALL <JMP.&KERNEL32.lstrcmpA> ; \lstrcmpA
在此处,EDX处是存储解码后的最终地址,打开后我们看到:

确实解密出来之后的数据还是11ccccccccccc9,但是在这个解密数据之前有很多的空字节,如果按照这个结果去比对,结果还是注册失败!因此我们可以通过再给函数006B4A10打补丁的方式能够解决此问题!但是为什么在11ccccccccccc9之前会有这么多的空字节呢?我一直没有找到正确的答案,也很想知道其中的缘由,恳请高人指点!
最后感谢MistHill在研究这个软件过程中给予的帮助!
请论坛各位大牛指导!

test.zip
