【文章标题】: 某五子棋软件分析
【文章作者】: CCDebuger
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
写这个文章的原因是来自论坛的这篇求助帖:http://bbs.pediy.com/showthread.php?s=&threadid=31752,是个五子棋的小游戏,没事就下来玩了玩,顺便写一篇文章给新手参考一下,没什么技术含量,权当灌水,呵呵。
先用PEiD来扫描一下这个程序,显示是Microsoft Visual C++ 6.0,没壳。运行一下,帮助菜单下有个注册,点击一下进去,随便输个注册码,点确定,弹出一对话框:注册失败!既然是有地方输入注册码的,我们就在程序的输入函数中找找看有没有 GetDlgItemTextA 或 GetWindowTextA 这样的函数,这两个函数都是用来获取文本内容的。用OD载入这个程序,利用函数参考,找到了这个函数:GetWindowTextA。就用它来试试吧。在 GetWindowTextA 的每个参考上都设上断点,F9运行程序,点击程序菜单 帮助->注册,随便输入注册码,点确定,被OD拦下:
004069A5 |. FF15 28D14000 CALL DWORD PTR DS:[<&USER32.GetWindowTextA>] ; \断在这里
004069AB |. 56 PUSH ESI
004069AC |. E8 3F0D0000 CALL 5_termin.004076F0 ; 关键CALL,要跟进去
004069B1 |. 59 POP ECX
004069B2 |. 85C0 TEST EAX,EAX
004069B4 |. 6A 00 PUSH 0
004069B6 |. 74 0C JE SHORT 5_termin.004069C4 ; 跳就完蛋
004069B8 |. 68 48E74000 PUSH 5_termin.0040E748 ; ASCII "SUCCEED"
004069BD |. 68 3CE74000 PUSH 5_termin.0040E73C
004069C2 |. EB 0A JMP SHORT 5_termin.004069CE
004069C4 |> 68 34E74000 PUSH 5_termin.0040E734 ; ASCII "FAILED"
为什么说004069AC地址处是关键CALL,大家看一下下面的代码和注释就清楚了。这个CALL的运算影响到004069B2地址处那条指令中EAX的值,要搞清楚这里EAX的值是怎么来的,就需要跟进那个CALL看看。我们到004069AC地址处的那个CALL时按F7跟进去:
004076F0 /$ 55 PUSH EBP ; 来到这里
004076F1 |. 8BEC MOV EBP,ESP
004076F3 |. 51 PUSH ECX
004076F4 |. 53 PUSH EBX
004076F5 |. 56 PUSH ESI
004076F6 |. 57 PUSH EDI
004076F7 |. FF75 08 PUSH DWORD PTR SS:[EBP+8]
004076FA |. E8 41040000 CALL 5_termin.00407B40
004076FF |. 83F8 10 CMP EAX,10 ; 注册码为16位
00407702 |. 59 POP ECX
00407703 |. 74 04 JE SHORT 5_termin.00407709
00407705 |. 33C0 XOR EAX,EAX
00407707 |. EB 62 JMP SHORT 5_termin.0040776B
00407709 |> 8D45 FC LEA EAX,DWORD PTR SS:[EBP-4]
0040770C |. BB 88EC4000 MOV EBX,5_termin.0040EC88 ; ASCII "%8lX"
00407711 |. 50 PUSH EAX
00407712 |. 53 PUSH EBX
00407713 |. FF75 08 PUSH DWORD PTR SS:[EBP+8]
00407716 |. E8 030A0000 CALL 5_termin.0040811E
0040771B |. 8B45 FC MOV EAX,DWORD PTR SS:[EBP-4] ; 取注册码前8位
0040771E |. BF 888888F8 MOV EDI,F8888888
00407723 |. F7D0 NOT EAX ; 前8位取反
00407725 |. 33C7 XOR EAX,EDI ; 与F8888888异或
00407727 |. BE 000000F0 MOV ESI,F0000000
0040772C |. 0BC6 OR EAX,ESI ; 再与F0000000或
0040772E |. 83C4 0C ADD ESP,0C
00407731 |. 3B05 300A4100 CMP EAX,DWORD PTR DS:[410A30] ; 与机器码的前8位比较
00407737 |. 8945 FC MOV DWORD PTR SS:[EBP-4],EAX
0040773A |. 75 05 JNZ SHORT 5_termin.00407741 ; 不等则跳到后面的计算步骤,等于则成功
0040773C |. 6A 01 PUSH 1
0040773E |. 58 POP EAX
0040773F |. EB 2A JMP SHORT 5_termin.0040776B
00407741 |> 8D45 FC LEA EAX,DWORD PTR SS:[EBP-4] ; 前面不等则跳到这
00407744 |. 50 PUSH EAX
00407745 |. 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8]
00407748 |. 83C0 08 ADD EAX,8
0040774B |. 53 PUSH EBX
0040774C |. 50 PUSH EAX
0040774D |. E8 CC090000 CALL 5_termin.0040811E
00407752 |. 8B45 FC MOV EAX,DWORD PTR SS:[EBP-4] ; 取注册码后8位
00407755 |. 83C4 0C ADD ESP,0C
00407758 |. F7D0 NOT EAX ; 取反
0040775A |. 33C7 XOR EAX,EDI ; 与F8888888异或
0040775C |. 33C9 XOR ECX,ECX ; 把ECX清零,为下面的设置值做准备
0040775E |. 0BC6 OR EAX,ESI ; 再与F0000000或
00407760 |. 3B05 340A4100 CMP EAX,DWORD PTR DS:[410A34] ; 与机器码后8位比较
00407766 |. 0F94C1 SETE CL ; 根据比较结果设CL的值,上面比较相等时,CL置1
00407769 |. 8BC1 MOV EAX,ECX ; 把ECX中的值送到EAX中,如果EAX中得到的是0,则注册失败
注册算法很简单,就是把16位的注册码分别取8位先和F0000000进行或运算,再和F8888888进行异或,然后取反,再与机器码的前8位和后8位比较,相等则注册成功。不过好像这个程序的注册是说着玩的,根本没用。看了作者的主页,说这个软件根本不提供注册。而这个软件的注册对话框中竟然还说正确注册后所有菜单都会生效,晕倒。所以这里只是让你玩玩,可用不用管了。
2、启用禁用的菜单
这个程序把很多菜单都禁用了,我们现在想让它的菜单都可用。在打算启用它所有菜单前,我们先考虑一下怎么才能在OD中断到程序设置菜单是否可用的地方。查一下API手册,我们知道 CheckMenuItem 函数有设置菜单是否可用或灰色等的功能。那我们先来试一下这个函数。用OD载入程序,下bp CheckMenuItem,会断在这里:
77D31A8E > 8BFF MOV EDI,EDI ; 断在这
77D31A90 55 PUSH EBP
取消断点,ALT+F9返回,到这:
0040533B |. 53 PUSH EBX ; /Flags = MF_BYCOMMAND|MF_ENABLED|MF_CHECKED|MF_STRING
0040533C |. 68 D6000000 PUSH 0D6 ; |ItemId = D6 (214.)
00405341 |. 57 PUSH EDI ; |hMenu
00405342 |. FFD6 CALL ESI ; \CheckMenuItem
00405344 |. 53 PUSH EBX ; /Flags => MF_BYCOMMAND|MF_ENABLED|MF_CHECKED|MF_STRING
00405345 |. 68 D3000000 PUSH 0D3 ; |ItemId = D3 (211.)
0040534A |. 57 PUSH EDI ; |hMenu
0040534B |. FFD6 CALL ESI ; \CheckMenuItem
0040534D |. 53 PUSH EBX ; /Flags => MF_BYCOMMAND|MF_ENABLED|MF_CHECKED|MF_STRING
0040534E |. 68 D4000000 PUSH 0D4 ; |ItemId = D4 (212.)
00405353 |. 57 PUSH EDI ; |hMenu
00405354 |. FFD6 CALL ESI ; \CheckMenuItem
00405356 |. 53 PUSH EBX ; /Flags => MF_BYCOMMAND|MF_ENABLED|MF_CHECKED|MF_STRING
00405357 |. 68 D5000000 PUSH 0D5 ; |ItemId = D5 (213.)
0040535C |. 57 PUSH EDI ; |hMenu
0040535D |. FFD6 CALL ESI ; \CheckMenuItem
0040535F |. 8B35 5CD14000 MOV ESI,DWORD PTR DS:[<&USER32.EnableMenuItem>] ; USER32.EnableMenuItem
00405365 6A 01 PUSH 1 ; 把这句改成PUSH 0
00405367 |. 5B POP EBX
00405368 |. 53 PUSH EBX ; /Flags => MF_BYCOMMAND|MF_GRAYED|MF_STRING
00405369 |. 68 D0000000 PUSH 0D0 ; |ItemID = D0 (208.)
0040536E |. 57 PUSH EDI ; |hMenu
0040536F |. FFD6 CALL ESI ; \EnableMenuItem
注意这一句:
00405365 6A 01 PUSH 1
查API参数我们知道如果要启用菜单的话 EnableMenuItem 的第三个参数就是 MF_ENABLED,其值为0,而MF_GRAYED是使菜单变成灰色并禁用,其值是1。我们要启用菜单的话应该让这里的标志是 MF_ENABLED,就是把传递给 EnableMenuItem 的第三个参数改成0,所以这里改成 PUSH 0,现在所有的菜单都启用了。把修改后的文件另存为5_terminator1.exe,再用OD载入这个5_terminator1.exe,现在要去自校验。
3、去自校验
启动我们修改后的5_terminator1.exe,会出现一个出错对话框,点确定就退出了。所以我们就从这个对话框入手。对话框一般都是MessageBoxA,我们先来拦这个API:
OD载入5_terminator1.exe,命令行中输入 bp MessageBoxA,回车,F9运行程序,会断在这里:
77D504EA > 8BFF MOV EDI,EDI ; 断在这
77D504EC 55 PUSH EBP
77D504ED 8BEC MOV EBP,ESP
取消断点,ALT+F9返回,会出来一个出错对话框,点确定,断在这里:
004054CA |. FF15 60D14000 CALL DWORD PTR DS:[<&USER32.SetTimer>] ; \SetTimer
004054D0 |. E8 4D230000 CALL 5_termin.00407822
004054D5 | 3B05 D8F74000 CMP EAX,DWORD PTR DS:[40F7D8] ; 改这一句
004054DB |. A3 380A4100 MOV DWORD PTR DS:[410A38],EAX
004054E0 |. 74 28 JE SHORT 5_termin.0040550A
004054E2 |. 50 PUSH EAX
004054E3 |. BE 580A4100 MOV ESI,5_termin.00410A58 ; ASCII " no 3228
"
004054E8 |. 68 04E54000 PUSH 5_termin.0040E504 ; ASCII " no %lu
"
004054ED |. 56 PUSH ESI
004054EE |. E8 8C240000 CALL 5_termin.0040797F
004054F3 |. 83C4 0C ADD ESP,0C
004054F6 |. 55 PUSH EBP ; /Style
004054F7 |. 68 F4E44000 PUSH 5_termin.0040E4F4 ; |Title = " fatal error"
004054FC |. 56 PUSH ESI ; |Text
004054FD |. 55 PUSH EBP ; |hOwner
004054FE |. FF15 68D14000 CALL DWORD PTR DS:[<&USER32.MessageBoxA>>; \MessageBoxA
00405504 |. 55 PUSH EBP ; 断在这里
把代码往上翻翻,从上面的代码可以看出控制是否弹出出错对话框的是004054E0处的那条JE指令。我们可以直接把这里改成JMP就不会再出现出错对话框了。不过我们看这两句:
004054D5 3B05 D8F74000 CMP EAX,DWORD PTR DS:[40F7D8] ; 改这一句
004054DB |. A3 380A4100 MOV DWORD PTR DS:[410A38],EAX
004054D5处的指令是把EAX的值与内存地址40F7D8中的值作比较,后面还把EAX中的值保存到内存中的一个地方了。如果你想知道EAX中的值是从哪来的,你可以跟进004054D0地址处的那个CALL看看,我就懒得去看了。到004054D5处时看一下寄存器中的标志位,正好符合我们的要求,就是说只要不执行CMP命令(因为我们现在要执行这条指令的话,肯定是不同的,也就是说执行完这条指令标志位都会变化,下面004054E0处的那条JE指令肯定就不跳了),我们把这里改成个不影响标志位的指令,正好下面那条指令还要保存EAX的值,我们就给它个正确的吧,所以把004054D5地址处的那条指令中的CMP改成MOV:
004054D5 A1 D8F74000 MOV EAX,DWORD PTR DS:[40F7D8] ; 改这一句
004054DA 90 NOP
004054DB |. A3 380A4100 MOV DWORD PTR DS:[410A38],EAX
这样的话我们修改后的程序就可以运行了。
4、去未注册对话框,使选项子菜单中的各个项都可用
上面的第三步完成后F9运行程序,在 游戏->选项 下面的子菜单中找第二个菜单点一下,会跳出一个出错对话框“未注册版本不提供此功能!”。呵呵,又是一个对话框,就从这个对话框入手,还是拦 MessageBoxA 函数。现在在那个出错对话框上点确定,转到OD中,在命令行中输入 bp MessageBoxA,回车。现在转到程序中,再在 游戏->选项 下面的子菜单中找第二个菜单点一下,被OD拦下:
77D504EA > 8BFF MOV EDI,EDI ; 断在这
77D504EC 55 PUSH EBP
77D504ED 8BEC MOV EBP,ESP
取消断点,ALT+F9返回,出来出错对话框,点确定,再次被OD拦下(注意这个程序有窗口总在最前功能,断下后会挡住OD窗口。我们只需在OD中按ALT+F5启用OD的窗口总在最前功能就可以了,再按一次ALT+F5就是取消OD的总在最前功能):
0040779B /$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
0040779D |. 68 B8EC4000 PUSH 5_termin.0040ECB8 ; |Title = "unregisted version don't provide this function"
004077A2 |. 68 90EC4000 PUSH 5_termin.0040EC90 ; |Text = "未 注 册 版 本 不 提 供 此 功 能 !"
004077A7 |. FF35 0C1E4100 PUSH DWORD PTR DS:[411E0C] ; |hOwner = 00150552 ('五子棋终结者,机器执黑必胜',class='FIVE')
004077AD |. FF15 68D14000 CALL DWORD PTR DS:[<&USER32.MessageBoxA>] ; \MessageBoxA
004077B3 \. C3 RETN ; 返回到这
鼠标左击0040779B处的那条指令,会在信息窗口中看到如下内容:
本地调用来自 00404893
在信息窗口中的这条内容上右击,选择“转到 CALL 来自 00404893”,我们会来到这里:
00404893 |> \E8 032F0000 CALL 5_termin.0040779B ; 到这
同样再看一下信息窗口:
0040779B=5_termin.0040779B
跳转来自 004047AF
在那条“跳转来自 004047AF”上右击一下,选右键菜单“转到 JE 来自 004047AF”,我们来到这里:
004047A7 |> \33DB XOR EBX,EBX
004047A9 |. 391D D0F74000 CMP DWORD PTR DS:[40F7D0],EBX
004047AF | 0F84 DE000000 JE 5_termin.00404893 ; 来到这
没啥好说的,就是004047AF处的JE干的好事,NOP掉这句就行了。把我们所做的修改都保存一下,另存为5_terminator2.exe。
5、测试 游戏->开发测试工具 子菜单功能及其它
现在我们运行修改过的5_terminator2.exe,点一下菜单 游戏->开发测试工具 中的子菜单第一项,程序自动退出了。我们现在想知道在我们点了这个子菜单后,程序干了什么。还是菜单的问题,我们先看看程序中所用到的API函数,再翻翻API手册,这个 GetMenu 好像不错,呵呵。我们就拿它来试试。OD载入5_terminator2.exe,在函数参考中把 GetMenu 的每个参考上都设上断点,F9运行程序,中间会断几次,不用管,直接F9到主程序运行。现在我们再点一下菜单 游戏->开发测试工具 中的子菜单第一项,被OD拦下。因为这个程序有窗口置前功能,挡住了OD窗口,很不方便。现在我们点任务栏的OD图标,按ALT+F5组合键,这样OD中看起来就方便了:
0040460D |. FF15 78D14000 CALL DWORD PTR DS:[<&USER32.GetMenu>] ; \断在这里
00404613 |. 0FB74D 10 MOVZX ECX,WORD PTR SS:[EBP+10] ; 把菜单项ID(用资源编辑工具可以看到)送到ECX,现在是012D,即10进制301
00404617 |. BA 30010000 MOV EDX,130
0040461C |. 8945 08 MOV DWORD PTR SS:[EBP+8],EAX
0040461F |. 3BCA CMP ECX,EDX ; 进行菜单项ID比较,看看是点了那个菜单,再跳到相应位置处理
00404621 |. 0F8F E6030000 JG 5_termin.00404A0D
00404627 |. 0F84 C5030000 JE 5_termin.004049F2
0040462D |. BF CF000000 MOV EDI,0CF
00404632 |. 3BCF CMP ECX,EDI
一路F8,到这:
00404B37 |> \68 324F4000 PUSH 5_termin.00404F32 ; |ThreadFunction = 5_termin.00404F32
00404B3C |. 68 80841E00 PUSH 1E8480 ; |StackSize = 1E8480 (2000000.)
00404B41 |. 53 PUSH EBX ; |pSecurity
00404B42 |. FF15 04D14000 CALL DWORD PTR DS:[<&KERNEL32.CreateThread>] ; \CreateThread
00404B48 |. A3 001E4100 MOV DWORD PTR DS:[411E00],EAX
00404B4D |. E9 8A030000 JMP 5_termin.00404EDC
程序创建一个线程就退出了。根据菜单项的字面意思和实际跟踪情况,这里是用来在开发时测试用的,跟我们没什么关系,不用管它,最好还是把这里的菜单禁用掉。如何禁用这个 开发测试工具 菜单项下的子菜单我就不说了,可以参考前面的启用子菜单的分析,再做相应处理。
最后,我们再来点一下菜单 选项->窗口总在最前面 的菜单,同样还被我们前面设的那个 GetMenu 函数断点断下。分析一下,可以知道程序根本没做处理,所以这个程序窗口是长期总在最前面。这东西在启动的时候老是挡住OD的窗口,很不爽,直接把它设成窗口不用总在最前吧。设置窗口是否总在最前的我们可以结合程序中的函数参考和API参考手册,决定断 SetWindowPos 这个API。OD载入程序,命令行输入bp SetWindowPos,回车,F9运行程序,断在这里:
77D1C01B > B8 22120000 MOV EAX,1222 ; 断在这
77D1C020 BA 0003FE7F MOV EDX,7FFE0300
77D1C025 FF12 CALL DWORD PTR DS:[EDX]
取消断点,ALT+F9返回,断在地址00405323处:
0040530D |. 6A 03 PUSH 3 ; /Flags = SWP_NOSIZE|SWP_NOMOVE
0040530F |. 68 F4010000 PUSH 1F4 ; |Height = 1F4 (500.)
00405314 |. 68 90010000 PUSH 190 ; |Width = 190 (400.)
00405319 |. 6A 32 PUSH 32 ; |Y = 32 (50.)
0040531B |. 6A 64 PUSH 64 ; |X = 64 (100.)
0040531D |. 6A FF PUSH -1 ; |InsertAfter = HWND_TOPMOST
0040531F |. FF7424 2C PUSH DWORD PTR SS:[ESP+2C] ; |hWnd
00405323 |. FF15 58D14000 CALL DWORD PTR DS:[<&USER32.SetWindowPos>] ; \断在这里
往上翻翻代码,就是这里了。我们再查一下API手册,知道 SetWindowPos 的第二个参数可用来设置窗口是否置前显示,HWND_TOPMOST(= -1)是置前显示,不让它置前显示的话可用把参数设为 HWND_NOTOPMOST(= -2)。所以我们把0040531D地址处的那句 PUSH -1 改成 PUSH -2,就可用让程序的窗口不再具有总在最前功能了。
--------------------------------------------------------------------------------
6、后记
看了作者的主页,应该说这个程序是免费的,作者禁用的那些菜单都是些容易引起问题的。所以这个程序根本没必要来修改
它,让它保持原样就行了。基本上上面所做的都是无用功。有人要问为什么还要写这篇文章呢?答:写这篇文章只是想给新
手一个思路,就是如何着手来分析一个程序。在我看来,调试程序最重要的是如何找到关键的地方,找到位置后分析代码那
是体力活。所以一个好的思路很重要,不要一上来就一通瞎撞,很多时候只会浪费时间。希望这篇文章能给新手一点帮助,
也不浪费我打这么多字,呵呵。
--------------------------------------------------------------------------------
【版权声明】: 本文纯属技术交流, 转载请注明作者并保持文章的完整, 谢谢!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)