• 标 题:[原创]男人系列修改补丁点睛教程
  • 作 者:cyclotron
  • 时 间:2004-1-31 周六, 下午1:39
  • 链 接:http://bbs.pediy.com

男人系列超级内存补丁点睛教程
作者:cyclotron[BCG][DFCG][FCG][OCN]

软件破解多了,难免有些乏味——成天在SoftICE里下一些常规的断点拦截,或者反汇编以后查找字符串,然后分析算法……这一系列几近公式化的操作给我们带来的乐趣已经远远不及第一个cracker凭借其深厚的WinSDK和汇编功力破解软件时所享受到的那种快感,我已经很少为自己的名字出现在ABOUT对话框中而激动了。
一年多来我常常在网上搜寻合适的猎物,但破解后很少数能为我所用,我只是花费大量的时间编写和优化注册机,逐渐地,我开始觉得编程能给我带来更多的乐趣——而不是注册本身。

差不多该是结束漫无目的的破解的时候了。

我翻出了这个游戏——是男人就XXXX。让我们从这里开始新的旅程吧!

选中这个游戏主要基于以下两个原因:
1.  拜读了RoBa兄弟的一篇修改“是男人就下100层”的文章,很受启发。
2.  这几个软件没有加壳,对应的资源也很容易找到。

在这里要说明的是,我并没有打算全面讲解修改器制作的全过程,而是将选择我认为最不易实现的一个功能进行详细的讲解;文中将涉及一些WinSDK编程和汇编编程的内容,尽管我将尽可能具体地说明它们的功能,但是对WinSDK和汇编的一定了解无疑将有助于你的理解,确切的说,我个人不认为本文将适合于那些刚开始套用常规的公式捣拨软件但却对编程一无所知的朋友。

我将要实现的功能是“是男人就上120层”中的弹跳力无限功能。我想试用过我的内存补丁的朋友都知道这个功能意味这什么:按住空格键不放,弹跳力就会不断增加,只要蓄力时间足够长,一跳跳至几百甚至几千层不是问题。但是这个功能存在如下缺陷:

当蓄力超过能量槽所能显示的范围时,能量槽这块实际上显示成了一块跳板;此外,小人的行动会暂停一段时间,在这期间播放一系列杂乱的声音(包括那声“绝望的尖叫”——在下文中我将继续使用这个词来表示这段众所周知的声音),然后其动作会继续,但声音就此消失。

我花了一个晚上的时间完全修复了这个bug,而这段修复过程将成为本文最主要的部分。

为了实现这个功能,我们首先要找到游戏存放弹跳力数值的内存地址,这无疑是各类游戏修改器的拿手好戏。我使用的是《金山游侠》。
启动游戏以后,我们设法进行一些操作,使能量槽显示值(即弹跳力数值)发生变化,然后用《金山游侠》搜索相应的数值。最终我定位在下面的内存地址上:
00138078

好了,让我们用SoftICE重新载入游戏,下内存写入断点:
bpmd 138078 w
然后进入游戏。

奇怪的是,SoftICE并没有弹出。这是你应该想到了:这个地址是动态分配的。
现在的问题是我们必须先找出这个地址,然后在不重新启动游戏的前提下使SoftICE中断在游戏的地址空间,以便我们设内存访问断点。
这时我们的crack技术将发挥作用了,还记得重新游戏前的那个注册对话框么?那里就是我们的突破口。在注册对话框中输入一点信息,然后在SoftICE里设断:
bpx GetWindowTextA
点击注册按钮……BOOM,几下F12后我们来到了游戏领空,在这里我们设内存访问断点:
bpmd [找到的地址] w
然后进入游戏。终于,我们中断在下面的地方:

00406F32  CALL 是男人就.00404DBD
00406F37  ADD ESP,4
00406F3A  MOV EAX,DWORD PTR SS:[EBP+8]
00406F3D  MOV DWORD PTR DS:[EAX+1194],0
00406F47  MOV EAX,DWORD PTR SS:[EBP+8]
00406F4A  MOV DWORD PTR DS:[EAX+1198],120
00406F54  MOV EAX,DWORD PTR SS:[EBP+8]
00406F57  MOV DWORD PTR DS:[EAX+119C],1
00406F61  MOV EAX,DWORD PTR SS:[EBP+8]
00406F64  MOV DWORD PTR DS:[EAX+11A4],0B
00406F6E  MOV EAX,DWORD PTR SS:[EBP+8]
00406F71  MOV DWORD PTR DS:[EAX+11A8],0         这里对弹跳力数值做初始化
00406F7B  MOV EAX,DWORD PTR SS:[EBP+8]          光标落在这里
00406F7E  MOV DWORD PTR DS:[EAX+11B0],0
00406F88  MOV EAX,DWORD PTR SS:[EBP+8]
00406F8B  MOV EAX,DWORD PTR DS:[EAX+11B0]
00406F91  MOV ECX,DWORD PTR SS:[EBP+8]

你也许已经猜到了:[寄存器+11A8]这样的形式正是这个游戏寻址动态数据的方式,寄存器保存着基址,后面那个是偏移量。在这里我们只关心弹跳力的数值,但是这个形式对我们后面的处理非常有用。对于这样一个小游戏,我们几乎可以断定弹跳力数值都是通过这个形式进行访问的,打开心爱的W32Dasm来确定这些位置吧:

[A区]:
00403CF3  PUSH EAX
00403CF4  CALL 是男人就.00401A40
00403CF9  ADD ESP,4
00403CFC  MOV EAX,DWORD PTR SS:[EBP+8]
00403CFF  MOV DWORD PTR DS:[EAX+1190],1
00403D09  MOV EAX,DWORD PTR SS:[EBP+8]
00403D0C  MOV DWORD PTR DS:[EAX+1198],160
00403D16  MOV EAX,DWORD PTR SS:[EBP+8]
00403D19  MOV DWORD PTR DS:[EAX+11A8],0          这里
00403D23  MOV EAX,DWORD PTR SS:[EBP+8]
00403D26  MOV DWORD PTR DS:[EAX+1304],0
00403D30  MOV EAX,DWORD PTR SS:[EBP+8]
00403D33  MOV DWORD PTR DS:[EAX+1310],0
00403D3D  MOV EAX,DWORD PTR SS:[EBP+8]


[B区]:
00405AA1  MOV EAX,DWORD PTR DS:[EAX+C]             ; |
00405AA4  PUSH EAX                                 ; |hPalette
00405AA5  MOV EAX,DWORD PTR SS:[EBP+8]             ; |
00405AA8  MOV EAX,DWORD PTR DS:[EAX+4]             ; |
00405AAB  PUSH EAX                                 ; |hDC
00405AAC  CALL DWORD PTR DS:[<&GDI32.SelectPalette>; \SelectPalette
00405AB2  MOV DWORD PTR SS:[EBP-4],EAX
00405AB5  PUSH 0CC0020                             ; /ROP = SRCCOPY
00405ABA  MOV EAX,DWORD PTR SS:[EBP+8]             ; |
00405ABD  MOV EAX,DWORD PTR DS:[EAX+11A8]          这里
00405AC3  SHL EAX,4                                ; |
00405AC6  ADD EAX,0A0                              ; |

[C区]:
004068C6  MOV DWORD PTR DS:[EAX+11AC],1
004068D0  MOV EAX,DWORD PTR SS:[EBP+8]
004068D3  MOV ECX,0C
004068D8  MOV EAX,DWORD PTR DS:[EAX+11A8]         这里
004068DE  CDQ
004068DF  IDIV ECX
004068E1  LEA EAX,DWORD PTR DS:[EDX+1]
004068E4  MOV ECX,DWORD PTR SS:[EBP+8]
004068E7  MOV DWORD PTR DS:[ECX+11A8],EAX         这里
004068ED  MOV EAX,DWORD PTR SS:[EBP+8]
004068F0  PUSH EAX
004068F1  CALL 是男人就.00405A93

[D区]:
    004068F0  PUSH EAX
004068F1  CALL 是男人就.00405A93
004068F6  ADD ESP,4
004068F9  MOV EAX,DWORD PTR SS:[EBP+8]
004068FC  CMP DWORD PTR DS:[EAX+1338],0
00406903  JE 是男人就.00406924
00406909  MOV EAX,DWORD PTR SS:[EBP+8]
0040690C  MOV EAX,DWORD PTR DS:[EAX+11A8]         这里
00406912  ADD EAX,8E
00406917  PUSH EAX
00406918  MOV EAX,DWORD PTR SS:[EBP+8]
0040691B  PUSH EAX
0040691C  CALL 是男人就.00406E3D
00406921  ADD ESP,8
00406924  JMP 是男人就.00406A12

[E区]:
0040692C  CMP DWORD PTR DS:[EAX+11AC],1
00406933  JNZ 是男人就.00406A05
00406939  MOV EAX,DWORD PTR SS:[EBP+8]
0040693C  MOV DWORD PTR DS:[EAX+11B0],1
00406946  MOV EAX,DWORD PTR SS:[EBP+8]
00406949  MOV EAX,DWORD PTR DS:[EAX+11A8]         这里
0040694F  MOV ECX,DWORD PTR SS:[EBP+8]
00406952  MOV DWORD PTR DS:[ECX+11A0],EAX
00406958  MOV EAX,DWORD PTR SS:[EBP+8]
0040695B  INC DWORD PTR DS:[EAX+1320]

[F区]:
00406AFF  CMP DWORD PTR DS:[EAX+11A0],0
00406B06  JGE 是男人就.00406C3F
00406B0C  MOV EAX,DWORD PTR SS:[EBP+8]
00406B0F  MOV DWORD PTR DS:[EAX+11B0],0
00406B19  MOV EAX,DWORD PTR SS:[EBP+8]
00406B1C  MOV DWORD PTR DS:[EAX+11A8],0         这里
00406B26  MOV EAX,DWORD PTR SS:[EBP+8]
00406B29  MOV DWORD PTR DS:[EAX+11A0],0
00406B33  MOV EAX,DWORD PTR SS:[EBP-4]
00406B36  LEA EAX,DWORD PTR DS:[EAX+EAX*2]

[G区]:
00406F4A  MOV DWORD PTR DS:[EAX+1198],120
00406F54  MOV EAX,DWORD PTR SS:[EBP+8]
00406F57  MOV DWORD PTR DS:[EAX+119C],1
00406F61  MOV EAX,DWORD PTR SS:[EBP+8]
00406F64  MOV DWORD PTR DS:[EAX+11A4],0B 
00406F6E  MOV EAX,DWORD PTR SS:[EBP+8]
00406F71  MOV DWORD PTR DS:[EAX+11A8],0         这里
00406F7B  MOV EAX,DWORD PTR SS:[EBP+8]
00406F7E  MOV DWORD PTR DS:[EAX+11B0],0
00406F88  MOV EAX,DWORD PTR SS:[EBP+8]
00406F8B  MOV EAX,DWORD PTR DS:[EAX+11B0]


我们已经找到所有对弹跳力数值的访问,除了G区是对其值进行初始化以外,其他的操作都是未知的。但是通过观察以后,我们发现C区的操作尤其可疑:

004068D3  MOV ECX,0C                             ecx赋值12(12恰为能量槽的饱和值)
004068D8  MOV EAX,DWORD PTR DS:[EAX+11A8]         把弹跳力数值读至eax
004068DE  CDQ
004068DF  IDIV ECX                                除以12
004068E1  LEA EAX,DWORD PTR DS:[EDX+1]            加上1
004068E4  MOV ECX,DWORD PTR SS:[EBP+8]            取得动态数据的基址
004068E7  MOV DWORD PTR DS:[ECX+11A8],EAX         保存回去

联想到游戏的动作(能量槽总是饱和以后再回零递增),我猜这就是我们要找的地方了。如果要使弹跳力无限增加,我们只要去掉那个求余操作就可以了。看看我修改的代码:

004068D0  MOV EAX,DWORD PTR SS:[EBP+8]
004068D3  MOV ECX,0C
004068D8  MOV EAX,DWORD PTR DS:[EAX+11A8]
004068DE  INC EAX                                  仅仅是递增
004068DF  NOP
004068E0  NOP
004068E1  NOP
004068E2  NOP
004068E3  NOP
004068E4  MOV ECX,DWORD PTR SS:[EBP+8]
004068E7  MOV DWORD PTR DS:[ECX+11A8],EAX
004068ED  MOV EAX,DWORD PTR SS:[EBP+8]
004068F0  PUSH EAX

事实上,这正是我的内存补丁所做的一切——就这么多。但是不要以为这样完工了,如果是那样,我前面的许多话就白说了,你一定觉得我前面所描述的那个bug很恼人,上面的修改结果就是这样。接下来的工作才是最重要的——修正这个bug。
毫无疑问,这个bug是在我们修改了偏移量为11A8处的值后产生的,因此必然与该值的存取有关。我们已经找到了游戏中所有与该值的存取有关的代码,但是并不清楚它们分别实现什么功能。所以我们的第一步是定位这处代码。
首先我们从声音入手:能量槽饱和以后,发出一系列杂乱的声音,游戏如何播放声音呢?我在学习VB的时候使用过一个Windows多媒体函数PlaySound,该函数可以播放资源中的波形声音(.wav),我想我们的游戏很可能调用这个函数来播放声音,所以让我们观察一下程序的输入表。

………………
USER32.MessageBoxA
USER32.ModifyMenuA
USER32.MoveWindow
USER32.PostQuitMessage
USER32.PtInRect
USER32.RegisterClassA
USER32.ReleaseCapture
…………
USER32.WinHelpA
WINMM.mciSendCommandA
WINMM.sndPlaySoundA

很遗憾,游戏并没有调用这个函数。
但是我们并非一无所获,WINMM.sndPlaySoundA引起了我们的注意,其函数原型如下:

BOOL sndPlaySound ( LPCSTR lpszSound , UINT fuSound );
LpszSound 指定将要播放的声音,它通常是系统声音标识或者波形文件名;
FuSound   指定播放参数;

MSDN并没有指出该函数可以播放资源中的声音,但输入表告诉我们它是我们的唯一选择。
我们试着在反汇编代码中寻找对它的调用:

00404267  CALL DWORD PTR DS:[<&GDI32.DeleteObject>]    ; \DeleteObject
0040426D  MOV EAX,DWORD PTR SS:[EBP+8]
00404270  MOV EAX,DWORD PTR DS:[EAX+C]
00404273  PUSH EAX                                     ; /hObject
00404274  CALL DWORD PTR DS:[<&GDI32.DeleteObject>]    ; \DeleteObject
0040427A  PUSH 0
0040427C  PUSH 0
0040427E  CALL DWORD PTR DS:[<&WINMM.sndPlaySoundA>]   ;  WINMM.sndPlaySoundA
00404284  MOV DWORD PTR SS:[EBP-8],8D

这显然不是我们需要的。MSDN指出,当lpszSound参数为NULL时,函数将终止异步播放的声音。
下面是第二处调用(也是最后一次),我想这次我们找对地方了:)

00406E3D  PUSH EBP
00406E3E  MOV EBP,ESP
00406E40  PUSH EBX
00406E41  PUSH ESI
00406E42  PUSH EDI
00406E43  PUSH 0
00406E45  PUSH 0                                       同样是为了终止任何正在播放的声音
00406E47  CALL DWORD PTR DS:[<&WINMM.sndPlaySoundA>]   ;  WINMM.sndPlaySoundA
00406E4D  CMP DWORD PTR SS:[EBP+C],9D
00406E54  JA 是男人就.00406E75
00406E5A  PUSH 5
00406E5C  MOV EAX,DWORD PTR SS:[EBP+C]
00406E5F  MOV ECX,DWORD PTR SS:[EBP+8]
00406E62  MOV EAX,DWORD PTR DS:[ECX+EAX*4+1110]        这是一个查表操作
00406E69  PUSH EAX                                     这个参数标识了将要播放的声音
00406E6A  CALL DWORD PTR DS:[<&WINMM.sndPlaySoundA>]   ;  WINMM.sndPlaySoundA
00406E70  JMP 是男人就.00406EAB
00406E75  CMP DWORD PTR SS:[EBP+C],9E
00406E7C  JNZ 是男人就.00406E97
00406E82  PUSH 40005
00406E87  PUSH 9E                                     这是什么?
00406E8C  CALL DWORD PTR DS:[<&WINMM.sndPlaySoundA>]   ;  WINMM.sndPlaySoundA
00406E92  JMP 是男人就.00406EAB
00406E97  PUSH 40004
00406E9C  MOV EAX,DWORD PTR SS:[EBP+C]
00406E9F  AND EAX,0FFFF
00406EA4  PUSH EAX                                    又一次调用
00406EA5  CALL DWORD PTR DS:[<&WINMM.sndPlaySoundA>]   ;  WINMM.sndPlaySoundA
00406EAB  POP EDI
00406EAC  POP ESI
00406EAD  POP EBX
00406EAE  LEAVE
00406EAF  RETN


我在本文的开头提到过,这个游戏的资源很容易找到,现在是动用EXEscope的时候了:


 


我想大家一定对“绝望的尖叫”印象深刻,那么我们就从它入手。
160D,转换为16进制就是A0h,等会儿我们会用到它。
这次换OllyDbg,我们在上面代码的RET处设下断点,然后开始游戏。不出所料,一旦我们按下空格键蓄力,游戏就被中断了。但是在这之前并没有播放我们所需要的声音。于是我们不断地返回游戏,并不断中断在这里,直到我们听到“绝望的尖叫”!这次要小心了,按下F8以后,我们来到了调用这段代码的地方:

004068F1  CALL 是男人就.00405A93
004068F6  ADD ESP,4
004068F9  MOV EAX,DWORD PTR SS:[EBP+8]
004068FC  CMP DWORD PTR DS:[EAX+1338],0
00406903  JE 是男人就.00406924
00406909  MOV EAX,DWORD PTR SS:[EBP+8]
0040690C  MOV EAX,DWORD PTR DS:[EAX+11A8]                  注意这里
00406912  ADD EAX,8E                                       加上一个基址8E
00406917  PUSH EAX                                         参数2
00406918  MOV EAX,DWORD PTR SS:[EBP+8]
0040691B  PUSH EAX                                         参数1,动态数据存储区的基址
0040691C  CALL 是男人就.00406E3D                           我们从这里出来
00406921  ADD ESP,8                                        返回到这里
00406924  JMP 是男人就.00406A12
00406929  MOV EAX,DWORD PTR SS:[EBP+8]

这不就是我们前面找到的D区么?!果然与弹跳力的数值有关!我们再重复这一过程,会发现播放“绝望的尖叫”之前,参数2的值为A0h,正是“绝望的尖叫”的资源标识!8Eh=142D,而143D正是弹跳力为1时播放的蓄力声音!
现在一切都清楚了,游戏是通过弹跳力的值计算出声音资源的标识,然后调用WINMM.sndPlaySoundA播放相应的声音!如果弹跳力不断增加而且不回零,那么参数2标识的资源是无效的声音,当参数2的值达到A0h时——“绝望的尖叫”!如果该值继续增加,那么标识的就根本不是声音资源了,所以继续蓄力就不会播放声音。
把原理弄明白了,打补丁就方便了,我们只要在代码区的空白处添加一些代码,让参数2在传入之前先对12D求余,就可以播放有效的声音了。真的是这样吗?我做了这样的尝试,不幸的是,一旦我在空白代码区写入代码,以修改可执行文件,就会产生非法访问错误,而在原有的代码区修改却不会出错。但是我想即使写入合法,那也不是最高效的修改方法,或许我们真的可以放弃那种做法,在原有的代码上实现我们的功能。

在修改之前,我们先看看能量槽显示的位图问题吧。

先看一下程序的位图资源,下面是我在程序资源中找到的相关位图:

 

是不是很有想法?有了前面的经验,我们大致可以猜测位图也是以某种查表方式“贴”上去的:每一个弹跳力值都对应有一块位图块,根据弹跳力的值可以确定相应的位图块。

查表的前提就是读取弹跳力的值,也就是偏移量11A8处的值。对了,它在我们前面找到的一些代码区里!
除了C区和D区以外,只有B区和E区有对弹跳力的读取。到底是哪里呢?我的结论是B区。为什么?看我的注释就明白了。

00405A93  PUSH EBP
00405A94  MOV EBP,ESP
00405A96  SUB ESP,4
00405A99  PUSH EBX
00405A9A  PUSH ESI
00405A9B  PUSH EDI
00405A9C  PUSH 0                                       ; /ForceBackground = FALSE
00405A9E  MOV EAX,DWORD PTR SS:[EBP+8]                 ; |
00405AA1  MOV EAX,DWORD PTR DS:[EAX+C]                 ; |
00405AA4  PUSH EAX                                     ; |hPalette
00405AA5  MOV EAX,DWORD PTR SS:[EBP+8]                 ; |
00405AA8  MOV EAX,DWORD PTR DS:[EAX+4]                 ; |
00405AAB  PUSH EAX                                     ; |hDC
00405AAC  CALL DWORD PTR DS:[<&GDI32.SelectPalette>]   ; \SelectPalette
00405AB2  MOV DWORD PTR SS:[EBP-4],EAX
00405AB5  PUSH 0CC0020                                 ; /ROP = SRCCOPY
00405ABA  MOV EAX,DWORD PTR SS:[EBP+8]                 ; |
00405ABD  MOV EAX,DWORD PTR DS:[EAX+11A8]                        读取弹跳力的值
00405AC3  SHL EAX,4                                    ; |       乘以16(能量槽位图的高度)
00405AC6  ADD EAX,0A0                                  ; |       加上一个基址
00405ACB  PUSH EAX                                     ; |Ysrc   作为BitBlt的参数传入
00405ACC  PUSH 0C0                                     ; |XSrc = C0 (192.)
00405AD1  MOV EAX,DWORD PTR SS:[EBP+8]                 ; |
00405AD4  MOV EAX,DWORD PTR DS:[EAX+13A4]              ; |
00405ADA  PUSH EAX                                     ; |hSrcDC
00405ADB  PUSH 10                                      ; |Height = 10 (16.)
00405ADD  PUSH 60                                      ; |Width = 60 (96.)
00405ADF  PUSH 20                                      ; |YDest = 20 (32.)
00405AE1  PUSH 58                                      ; |XDest = 58 (88.)
00405AE3  MOV EAX,DWORD PTR SS:[EBP+8]                 ; |
00405AE6  MOV EAX,DWORD PTR DS:[EAX+4]                 ; |
00405AE9  PUSH EAX                                     ; |hDestDC
00405AEA  CALL DWORD PTR DS:[<&GDI32.BitBlt>]          ; \BitBlt 这个函数是关键,一经过这里,能
量槽的位图就发生变化了
00405AF0  PUSH 0                                       ; /ForceBackground = FALSE
00405AF2  MOV EAX,DWORD PTR SS:[EBP-4]                 ; |
00405AF5  PUSH EAX                                     ; |hPalette
00405AF6  MOV EAX,DWORD PTR SS:[EBP+8]                 ; |
00405AF9  MOV EAX,DWORD PTR DS:[EAX+4]                 ; |
00405AFC  PUSH EAX                                     ; |hDC
00405AFD  CALL DWORD PTR DS:[<&GDI32.SelectPalette>]   ; \SelectPalette
00405B03  POP EDI
00405B04  POP ESI
00405B05  POP EBX
00405B06  LEAVE
00405B07  RETN

BitBlt函数是干什么的?下面是MSDN对它的描述:
Transfers bits from a rectangle on a source device to a rectangle that has the same dimensions on a destination device. The transfer is controlled by a ternary raster operation value that specifies how corresponding bits from the source, destination, and pattern in a brush are combined to form the final bits in the destination.
大致意思就是说把一个矩形区域里的位(象素)从一个设备传递到另一个设备,这里的功能我们可以理解为把一块矩形位图复制到视频显示器上输出。它的原型是:

BOOL BitBlt (LPPDEVICE lpDestDev, WORD wDestX, WORD wDestY, 
    LPPDEVICE lpSrcDev, WORD wSrcX, WORD wSrcY, WORD wXext,
    WORD wYext, long Rop3, LPBRUSH lpPBrush, LPDRAWMODE lpDrawMode);
这个函数的参数看起来很多,其实并不复杂。第一个参数是目的设备描述表的句柄,第二、第三个参数指定目标位置,第四个参数是源设备描述表的句柄,第五到第八个参数指定源位图块的位置,剩下一些标志性参数没必要知道。OllyDbg已经把对应的参数都注释出来了,值得注意的是Ysrc,它是通过弹跳力的值计算出来的。很明显,0A0,所谓的基址,就是弹跳力为零时的位图块的左上角的Y坐标。
现在这个bug可以得到解释了:程序以弹跳力的值为索引计算出源位图块的Y坐标位置,这个值如果超过了12,那么得到的位图块自然就是无效的。
从上面这个RET返回后(处理完能量槽的显示后),我们来到了:

004068C3  MOV EAX,DWORD PTR SS:[EBP+8]
004068C6  MOV DWORD PTR DS:[EAX+11AC],1
004068D0  MOV EAX,DWORD PTR SS:[EBP+8]
004068D3  MOV ECX,0C
004068D8  MOV EAX,DWORD PTR DS:[EAX+11A8]
004068DE  CDQ
004068DF  IDIV ECX                                    这里不就是弹跳力的递增么?
004068E1  LEA EAX,DWORD PTR DS:[EDX+1]
004068E4  MOV ECX,DWORD PTR SS:[EBP+8]
004068E7  MOV DWORD PTR DS:[ECX+11A8],EAX
004068ED  MOV EAX,DWORD PTR SS:[EBP+8]
004068F0  PUSH EAX
004068F1  CALL 是男人就.00405A93                      我们从这里出来
004068F6  ADD ESP,4                                   停在这里
004068F9  MOV EAX,DWORD PTR SS:[EBP+8]
004068FC  CMP DWORD PTR DS:[EAX+1338],0
00406903  JE 是男人就.00406924:[EA
00406909  MOV EAX,DWORD PTR SS:[EBP+8]
0040690C  MOV EAX,DWORD PTR DS:[EAX+11A8]
00406912  ADD EAX,8E                                  计算声音资源的索引
00406917  PUSH EAX
00406918  MOV EAX,DWORD PTR SS:[EBP+8]
0040691B  PUSH EAX
0040691C  CALL 是男人就.00406E3D                      这就是播放声音的那个call
00406921  ADD ESP,8
00406924  JMP 是男人就.00406A12
00406929  MOV EAX,DWORD PTR SS:[EBP+8]
0040692C  CMP DWORD PTR DS:[EAX+11AC],1

我们回到了C区的代码,注意到这个事实非常重要,这有利于我们后面对代码的修改。
最后一个疑问:E区代码对弹跳力的读取是干什么的呢?我想是程序开始用牛顿定律开始计算小人能跳多高了吧——这正是我们所希望的。那么A区和F区呢,它们对弹跳力值赋0,我想可能与小人从一块跳板掉到(或者跳到)另一块跳板时弹跳力要重新计数有关吧,当然如果你觉得这个规则不利于你“做男人”,也可以发挥一下,nop掉这句代码。无论如何,这些同我们所要实现的弹跳力无限功能已经没什么关系了:)

现在我们已经完全明白程序的处理了,如何修改呢?要想通过修改代码(而不是添加代码)来增加功能,我们必须做一件事:对原来的代码进行优化,籍此腾出空间增加我们自己的代码。当然这两步可以同时实现,就看各位各显神通了。我的思路是这样的:11A8处还是必须存放无限增加的弹跳力值(因为后面要用它计算弹跳高度),但是在索引位图和声音资源时来个“偷天换日”,把有效的索引值赋给相应的参数。
幸运的是,程序是用VC4编译的,我们要修改的那部分代码似乎效率颇低,有些寄存器根本就没有使用,还有不少重复的工作量:

004068C3  MOV EAX,DWORD PTR SS:[EBP+8]
004068C6  MOV DWORD PTR DS:[EAX+11AC],1
004068D0  MOV EAX,DWORD PTR SS:[EBP+8]         一遍又一遍地读取基址,而且每回只用一次
004068D3  MOV ECX,0C                           这是除数,不能省略的,但不一定要在这里赋值
004068D8  MOV EAX,DWORD PTR DS:[EAX+11A8]
004068DE  CDQ
004068DF  IDIV ECX
004068E1  LEA EAX,DWORD PTR DS:[EDX+1]
004068E4  MOV ECX,DWORD PTR SS:[EBP+8]         一遍又一遍地读取基址,而且每回只用一次
004068E7  MOV DWORD PTR DS:[ECX+11A8],EAX
004068ED  MOV EAX,DWORD PTR SS:[EBP+8]         一遍又一遍地读取基址,而且每回只用一次
004068F0  PUSH EAX                             这里传入基址值——早就可以做了!
004068F1  CALL 是男人就.00405A93 
004068F6  ADD ESP,4

程序始终没有用到ebx、esi、edi,我经过确认以后决定用ebx来保存反复回零的弹跳力值,作为索引位图块和声音资源只用。esi和edi保存了两个貌似地址的东东,还是不动为妙。
上面这段代码修改如下:

004068C3  MOV EAX,DWORD PTR SS:[EBP+8]
004068C6  MOV DWORD PTR DS:[EAX+11AC],1
004068D0  MOV EAX,DWORD PTR SS:[EBP+8]
004068D3  PUSH EAX                              这里就把基址作为参数传入
004068D4  INC DWORD PTR DS:[EAX+11A8]           弹跳力直接递增
004068DA  MOV EAX,DWORD PTR DS:[EAX+11A8]       读取弹跳力的值
004068E0  DEC EAX
004068E1  MOV ECX,0C                            除数
004068E6  CDQ
004068E7  IDIV ECX                              求余,余数送edx
004068E9  INC EDX                               余数加1
004068EA  MOV EBX,EDX                           送ebx保存
004068EC  NOP
004068ED  NOP
004068EE  NOP
004068EF  NOP
004068F0  NOP                                   还空出5个字节
004068F1  CALL 是男人就.00405A93
004068F6  ADD ESP,4
实现了新功能,也把有效的索引值保存下来了。当然下面的地方也要做修改:

004068F9  MOV EAX,DWORD PTR SS:[EBP+8]
004068FC  CMP DWORD PTR DS:[EAX+1338],0
00406903  JE 是男人就.00406924:[EA
00406909  MOV EAX,DWORD PTR SS:[EBP+8]
0040690C  MOV EAX,EBX                          声音的索引值从ebx取得
0040690E  NOP
0040690F  NOP
00406910  NOP
00406911  NOP
00406912  ADD EAX,8E
00406917  PUSH EAX
00406918  MOV EAX,DWORD PTR SS:[EBP+8]
0040691B  PUSH EAX
0040691C  CALL 是男人就.00406E3D               播放声音
00406921  ADD ESP,8

另一处:

00405AB2  MOV DWORD PTR SS:[EBP-4],EAX
00405AB5  PUSH 0CC0020                                 ; /ROP = SRCCOPY
00405ABA  MOV EAX,DWORD PTR SS:[EBP+8]                 ; |
00405ABD  MOV EAX,EBX                                      位图块的索引值从ebx取得
00405ABF  NOP
00405AC0  NOP
00405AC1  NOP
00405AC2  NOP
00405AC3  SHL EAX,4                                    ; |
00405AC6  ADD EAX,0A0                                  ; |
00405ACB  PUSH EAX                                     ; |YSrc
00405ACC  PUSH 0C0                                     ; |XSrc = C0 (192.)
00405AD1  MOV EAX,DWORD PTR SS:[EBP+8]                 ; |
00405AD4  MOV EAX,DWORD PTR DS:[EAX+13A4]              ; |
00405ADA  PUSH EAX                                     ; |hSrcDC
00405ADB  PUSH 10                                      ; |Height = 10 (16.)
00405ADD  PUSH 60                                      ; |Width = 60 (96.)
00405ADF  PUSH 20                                      ; |YDest = 20 (32.)
00405AE1  PUSH 58                                      ; |XDest = 58 (88.)
00405AE3  MOV EAX,DWORD PTR SS:[EBP+8]                 ; |
00405AE6  MOV EAX,DWORD PTR DS:[EAX+4]                 ; |
00405AE9  PUSH EAX                                     ; |hDestDC
00405AEA  CALL DWORD PTR DS:[<&GDI32.BitBlt>]          ; \BitBlt    显示相应的位图块
00405AF0  PUSH 0                                       ; /ForceBackground = FALSE
经过上面的修改,我们的bug就被修复了,在实际弹跳力不断增加的同时,仍然用有效的索引取得位图和声音资源,画面的显示和声音的播放与原来完全一致^O^

又是一个不眠之夜……



Author:cyclotron[BCG][DFCG][FCG][OCN]
2004.1