【文章标题】: 五子棋终结者软件分析http://liudaocan.googlepages.com
【文章作者】: 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、后记
  看了作者的主页,应该说这个程序是免费的,作者禁用的那些菜单都是些容易引起问题的。所以这个程序根本没必要来修改
  它,让它保持原样就行了。基本上上面所做的都是无用功。有人要问为什么还要写这篇文章呢?答:写这篇文章只是想给新
  手一个思路,就是如何着手来分析一个程序。在我看来,调试程序最重要的是如何找到关键的地方,找到位置后分析代码那
  是体力活。所以一个好的思路很重要,不要一上来就一通瞎撞,很多时候只会浪费时间。希望这篇文章能给新手一点帮助,
  也不浪费我打这么多字,呵呵。
  
--------------------------------------------------------------------------------
【版权声明】: 本文纯属技术交流, 转载请注明作者并保持文章的完整, 谢谢!