- 主题
- 0
- 积分
- 7
- 贝壳
- 7 个
- 来自
- 黑色海岸线
- 注册时间
- 2006-11-27
- 最后登录
- 2011-2-10
|
[其他] “是男人就上一百层”系列游戏内存补丁的实现
相信玩过游戏的朋友或多或少地都接触过一些游戏补丁或修改器,在修改器的帮助下主人公可以获得无限的生命和武器,从而轻而易举地战胜敌方的各种人物,避开各种陷阱——这些都是游戏的另一种乐趣。事实上,这类补丁或修改器与软件的逆向工程密切相关,可以说它们是Crack技术的一种衍生。这里我将以曾一度成为男生恶梦的“做男人很难系列”游戏为例,选取其中的“是男人就下一百层”、“是男人就上一百二十层”两个经典小游戏,详细讲解分析游戏并编写内存补丁的原理。
在这个内存补丁中,一共实现了如下几种功能(如图1所示)。
图1
其中“无限生命值”、“蓄力加速”、“弹跳力无限”实现的方法是类似的,我将以实现比较困难的“弹跳力无限”为例讲解这类功能的分析和实现。“窗口始终在最上方”和“自动弹跳闯关”功能只是Windows API的简单应用,大家可以参考源代码明白其实现的原理。
本文需要用到以下工具,这些工具在网上都是随处可见的,而且使用也不难,如果你没有基础但是又想学习,不妨和大家一起来操作,其实没有你想象的那么难!
SoftICE
金山游侠
W32Dasm
OllyDBG
EXEscope
程序里最突出的功能就是“弹跳力无限”了。所谓“弹跳力无限”,就是指按住空格键不放,弹跳力就会不断增加,只要蓄力时间足够长,一跳跳至几百甚至几千层不是问题。在原来的游戏中,弹跳力是以循环方式改变数值,增大到最大值以后又会重新返回到最小值,这一数值的大小我们可以从能量槽(如图2所示)中看到。可以确定弹跳力的当前数值必定以某种形式存放在内存中,因此我们修改的基本思想就是改变游戏对这一数值的操作方式。
图2
为了实现这个功能,我们首先要找到游戏存放弹跳力数值的内存地址,这无疑是各类游戏修改器的拿手好戏。我使用的是《金山游侠》,启动游戏后,进行一些操作,使能量槽显示值(即弹跳力数值)发生变化,然后用《金山游侠》搜索相应的数值(比如当能量槽如图2所示时,我们搜索11),最终我们定位在内存地址“00138078”上。
好了,让我们用SoftICE重新载入游戏(这样做的目的是让SoftICE直接进入游戏的地址空间),下内存写入断点:
bpmd 138078 w
然后按F5进入游戏。奇怪的是SoftICE并没有弹出。这时你应该想到了:这个地址是动态分配的,也就是说游戏每次启动以后都分配一个不同的地址来存放这个数值,因此我们找到的内存地址只在一次运行过程中有效。现在的问题是我们必须先找出这个地址,然后在不重新启动游戏的前提下使SoftICE中断在游戏的地址空间,以便我们设内存访问断点。
这时我们的Crack技术将发挥作用了,还记得游戏开始前的那个注册对话框么?那里就是我们的突破口。在注册对话框中输入一点信息,然后在SoftICE里设断点:
bpx GetWindowTextA
点击注册按钮……BOOM!几次按动F12后我们来到了游戏领空,在这里我们设内存访问断点:
bpmd [找到的地址] w
然后按F5进入游戏。终于,我们中断在下面的地方:
(icefire:事实上,SoftICE有一个功能强大的命令Addr,可以在任何SoftICE激活的时候直接来到指定程序的地址空间,有兴趣的朋友可以尝试一下。)
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]
……
……
[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
如果你以为这样就完工了,那么试着运行一下修改后的游戏,怎么样?发现问题了吧?当蓄力超过能量槽所能显示的范围时,能量槽这块实际上显示成了一块跳板;此外,小人的行动会暂停一段时间,在这期间播放一系列杂乱的声音(icefire:我在调试的时候也出现了这样的情况,包括那声“绝望的尖叫”也出来了——在下文中我将继续使用这个词来表示这段众所周知的声音,因为这个声音将我从睡梦中直接拖到了地上!),然后其动作会继续,但声音就此消失。毫无疑问,这个BUG是在我们修改了偏移量为11A8处的值后产生的,因此必然与该值的存取有关。我们已经找到了游戏中所有与该值的存取有关的代码,但是并不清楚它们分别实现什么功能。所以我们的第一步是定位出现问题的这处代码。
首先我们从声音的BUG入手:能量槽饱和以后,发出一系列杂乱的声音——游戏如何播放声音呢?我在学习VB的时候使用过一个Windows多媒体函数PlaySound,该函数可以播放资源中的波形声音(.wav),我想我们的游戏很可能调用这个函数来播放声音,所以让我们在W32dDasm中观察一下程序的输入表。
..................
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并没有指出该函数可以播放资源中的声音,但输入表告诉我们它是我们的唯一选择。我们试着在反汇编代码中寻找对它的调用(仍然是W32Dasm,双击函数WINMM.sndPlaySoundA):
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 ? 参数fuSound
0040427C PUSH 0 ? 参数lpszSound
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 ?lpszSound=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的时候了。选中资源160,点击“wave”按钮——这正是我们所熟悉的“绝望的尖叫”。我想大家一定对“绝望的尖叫”印象深刻,那么我们就从它入手。160D,转换为16进制就是A0h,等会儿我们会用到它。
这次换OllyDbg,我们在上面代码的RET处设下断点,然后开始游戏。不出所料,一旦我们按下空格键蓄力,游戏就被中断了。但是在这之前并没有播放我们所需要的声音。于是我们不断地按F9返回游戏,并不断中断在这里,直到我们听到″绝望的尖叫″!这次要小心了,按下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(能量槽的饱和值)求余,就可以播放有效的声音了。真的是这样吗?我做了这样的尝试,不幸的是,一旦我在空白代码区写入代码,以修改可执行文件,就会产生非法访问错误,而在原有的代码区修改却不会出错。但是我想即使写入合法,那也不是最高效的修改方法,或许我们真的可以放弃那种做法,在原有的代码上实现我们的功能。
在修改之前,我们先看看能量槽显示的位图问题吧。先看一下程序的位图资源,下面是我用EXEscope在程序资源中找到的相关位图(如图3所示):
图3
是不是很有想法?有了前面的经验,我们大致可以猜测位图也是以某种查表方式“贴”上去的:每一个弹跳力值都对应有一块位图块,根据弹跳力的值可以确定相应的位图块。查表的前提就是读取弹跳力的值,也就是偏移量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 ; | ?加上一个基值0A0h
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上大致意思就是说把一个矩形区域里的位(象素)从一个设备传递到另一个设备,这里的功能我们可以理解为把一块矩形位图复制到视频显示器上输出。它的原型是:
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保存了两个貌似地址的东东,还是不动为妙。上面这段代码修改后,我们的bug就被修复了,在实际弹跳力不断增加的同时,仍然用有效的索引取得位图和声音资源,画面的显示和声音的播放与原来完全一致(详细修改后的代码请看光盘)!
“无限生命值”和“蓄力加速”功能的实现方法与此类似,但要简单得多,其中“蓄力加速”功能只要把实现弹跳力递增的代码改为直接赋值为12(能量槽的饱和值)就可以了,限于篇幅,不再赘述。“窗口始终在最上方”和“自动弹跳闯关”的实现也比较简单,要实现窗口始终在最上方,只要用ShowWindow函数就可以了,为了取得ShowWindow函数的Hwnd参数,可以使用FindWindow来查找。自动弹跳闯关这个功能看起来很神奇,其实就是给游戏定时发送WM_KEYDOWN消息和KEY_UP消息,模拟键盘空格键的功能,以上两部分的具体实现请参考源代码。
总体看来,编写修改器的方法并不是很复杂,用到的技术大致与Crack相同,但是分析的思路却具有相当的灵活性,不同于Crack中的一些思维定势,有兴趣的朋友可以找一些简单的游戏举一反三。学过了Crack再尝试一下编写游戏修改器,应该是别有一番趣味的!
[ 本帖最后由 fcts1230 于 2007-5-19 16:09 编辑 ] |
|