男人系列超级内存补丁点睛教程
作者: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