我是个KOF迷,自从上初中以来就对KOF情有独钟,(没少被老爸K)尤其是KOF97版本更是喜欢的很,从有了电脑开始,WinKawaks模拟器就一直常驻我的硬盘,模拟最多的游戏就是KOF97,自己的键盘也快不行了。虽然WinKawaks模拟器功能强大,但是衍生的文件和文件夹就一大堆,我想到同学家搞把拳皇还要打包压缩解包,实在受不了,一直以来都想在网上看到有单文件版的KOF97出现,可是就是没有人做,我想这么一个经典的游戏应该有个好版本,并且以前也试过制作,但是一直都没有成功,这与我是个菜鸟有很大的关系。难得现在放寒假有时间,于是花了两天的时间终于把它搞定(我实在是太菜了)。
我使用飞雪汉化的1.45典藏版进行制作,并且文件以前用工具脱过壳,好像是Aspack的壳(还好能够自动脱),不过现在使用LordPE查看段名有UPX,还有ASPACK等,有可能是我以前修改过又加了一次壳。使用OD加载,查找字符串发现一大堆有用的,%s\%s.zip等就是fprintf格式化后加载的ROM文件,.\winkawaks.ini用来初始化INI文件使用,这些现不管他。我想原程序它肯定要从外部加载ROM文件。要制作单文件版本的程序,就必须将文件整合到EXE文件中,而且还要找到原程序ROM文件加载的地方,将其修改成从内存中加载。于是使用OD查找-当前模块中的名称(Ctrl+N),一大堆的都是个MFC函数的调用,这个MFC我也没有学过,我想大概就是个跳转的意思,不然OD也不会将它们识别为函数。仔细一看发现竟然没有CreateFile,OpenFile等函数,当时吓了我一跳,我在想Windows下编程不使用这几个文件函数那还算Windows应用程序吗?在往下都是些fopen,fprintf,fputc,fread等一些C语言下的文件读写函数,是不是这些函数速度快或者是在Windows中不常用作者才使用它?设置断点fopen,在仔细监视%s\%s.zip等字符串的走向,随便加载一个游戏,找到了调用ROM文件的地方:
004E69A2 8B8424 A0000000 MOV EAX,DWORD PTR SS:[ESP+A0]
004E69A9 68 94ED5200 PUSH WinKawak.0052ED94 ; ASCII "rb"
004E69AE 50 PUSH EAX
004E69AF E8 1C870000 CALL <JMP.&MSVCRT.fopen>
上面的文件操作类型为”rb“其中的b是将文件看作二进制文件读写,这有可能是ROM只能当作二进制文件读写,所以作者使用C下面的fopen, fread 函数等;
找到关键ROM调用后,只要进行跳转到自己的代码处执行就可以进行修改,但是这个ROM文件到底怎么整合到EXE文件中去呢?
我先使用LordPE将KOF97.ZIP作为一个段从磁盘中载入。这样EXE文件不仅有了ROM文件还可以正常运行。可是如何加载这段ROM呢,并且返回的是个FILE指针。这时我傻了,打开MSDEV和TurboC下的stduo.h文件发现说明如下:
TurboC中:
typedef struct {
short level; /* fill/empty level of buffer */
unsigned flags; /* File status flags */
char fd; /* File descriptor */
unsigned char hold; /* Ungetc char if no buffer */
short bsize; /* Buffer size */
unsigned char *buffer; /* Data transfer buffer */
unsigned char *curp; /* Current active pointer */
unsigned istemp; /* Temporary file indicator */
short token; /* Used for validity checking */
} FILE; /* This is the FILE object */
MSDEV中:
#ifndef _FILE_DEFINED
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
于是我按照MSDEV中自己的理解 在OD运行时并且EXE文件已经按原来程序正确从磁盘中加载了ROM文件后,我找到FILE指针的结构,将base指向ROM段地址,ptr也指向ROM段地址。可相而知,程序以非法运行中止。
经过无数次的非法中止,我发现这种直接从内存读取数据的方法行不通,当时就这个问题也发了贴子问别人,我的电脑没有联网,在网吧呆了一天(2007.2.5那天),我还是回到的自己的电脑桌慢慢研究。
既然自己的创意不行,我就看看别人的单文件版本是这么做的(这也是我学习Crack的主要原因)。我记得以前的《网友世界》上有一个三国游戏叫”霸王的大陆“,是单文件版。终于给我从宝贝CD盒里找了出来,是2005年4月第8期里的(这些杂志光盘可是我菜鸟的最爱)。
使用资源编辑器ExeScope发现它是将ROM做出资源保存在EXE文件中有”ROM“和”SAN2FONT“两种不常见的资源类型,可以肯定这时作者加的了。在使用OD加载,既然使用到了资源肯定有LoadResource调用,设断后找到ROM资源加载位置:
00431590 |> \68 A0EE4400 PUSH 霸王的大.0044EEA0 ; /ResourceType = "ROM"
00431595 |. 68 98EE4400 PUSH 霸王的大.0044EE98 ; |ResourceName = "#125"
0043159A |. 56 PUSH ESI ; |hModule
0043159B |. FF15 7C214400 CALL DWORD PTR DS:[<&KERNEL32.FindResour>; \FindResourceA
004315A1 |. 8BF8 MOV EDI,EAX
004315A3 |. 57 PUSH EDI ; /hResource
004315A4 |. 56 PUSH ESI ; |hModule
004315A5 |. FF15 78214400 CALL DWORD PTR DS:[<&KERNEL32.LoadResour>; \LoadResource
004315AB |. 57 PUSH EDI ; /hResource
004315AC |. 56 PUSH ESI ; |hModule
004315AD |. 8945 D8 MOV DWORD PTR SS:[EBP-28],EAX ; |
004315B0 |. FF15 68204400 CALL DWORD PTR DS:[<&KERNEL32.SizeofReso>; \SizeofResource
004315B6 |. 8945 E4 MOV DWORD PTR SS:[EBP-1C],EAX
004315B9 |. 8B45 D8 MOV EAX,DWORD PTR SS:[EBP-28]
004315BC |. 50 PUSH EAX ; /nHandles
004315BD |. FF15 74214400 CALL DWORD PTR DS:[<&KERNEL32.LockResour>; \SetHandleCount
注意00431595处ResourceName = "#125",而ExeScope中是直接的125,查找Win32sdk,FindResource就是这么要求的。
这个里面使用了CreateFile,WriteFile等函数。但是加载资源后好相并没有将句柄作为pFileName传送。而是作者直接对资源内容举行了处理,似乎是加载资源LoadResource返回的句柄就直接和CreateFile函数返回的句柄功能相同,这个我没有仔细的研究,因为我现在的程序使用的fopen函数,根本一点关系都扯不上。那是不是可以将LoadResource返回的句柄作为fopen函数的第一个参数进行调用呢。
我在DevC++中使用了一小段C程序进行上面猜测的模拟,结果以失败告终。
没办法,只好操起OD继续看流程。
这个时候OD给了我提示:OD在堆栈段中显示PUSH进堆栈中的两个fopen参数是,
0012F5C8 0012F6F4 |path = ".\2020bb.zip"
0012F5CC 0052ED94 \mode = "rb"
注意第一个参数OD解释的是 ”path“,也就是说,必须是一个路径fopen才能执行成功。其它的什么地址参数fopen本来就不是很牛擦,当让不吊你。(也难怪,以前DOS下几十K的内存,哪还能考虑到这些,磁盘中能放下几个文件供调用就不错了。)
于是我就有了一个笨方法,相信大家都想到了。现将ROM文件做成资源,然后EXE将资源文件转存到硬盘,在修改fopen的第一个参数为刚才资源文件的路径,这样不就OK了吗?
于是统计我们需要的函数如下:
<KERNEL32.DLL>
GetSystemDirectory (将文件存储到系统目录)
FindResource (资源函数)
LoadResource
SizeofResource
LockResource
FreeResource
CreateFile (文件函数)
WriteFile
SetEndOfFile
CloseHandle
<MSVCRT.dll>
strcat (连接字符串,生成路径使用) 使用LoadPE分析EXE文件,添加两个新的输入表。
这样就要到刚才分析到访问ROM文件的地方进行代码修改了。
将004E69A2处修改成 JMP 004F0440 (这块有好多0,好像是脱壳留下来的,不然这地方0也太大了)
004F0440 8925 75044F00 MOV DWORD PTR DS:[4F0475],ESP ; 保存原来的ESP,用来修改原始ROM路径使用
004F0446 60 PUSHAD
004F0447 E8 39000000 CALL Kof97DIY.004F0485 ; 调用路径生成
004F044C 90 NOP ; 按理所这里是POPAD,但是我一加就有错
004F044D 8B8424 A0000000 MOV EAX,DWORD PTR SS:[ESP+A0] ; ROM路径所存放的位置
004F0454 68 94ED5200 PUSH Kof97DIY.0052ED94 ; rb
004F0459 50 PUSH EAX
004F045A FF15 EC134F00 CALL DWORD PTR DS:[<&MSVCRT.fopen>] ; msvcrt.fopen
004F0460 83F8 00 CMP EAX,0 ; 是否成功
004F0463 ^ 0F85 4B65FFFF JNZ Kof97DIY.004E69B4 ; 调回原来的代码
004F0469 /EB 71 JMP SHORT Kof97DIY.004F04DC ; 不成功,生成ROM文件
上面004F044C处应该是POPAD的,但是我手动跟踪(惨啊,用笔记堆栈)发现不能加。我对堆栈不熟悉,好像是下面调用的由便由编译器自己生成的”资源文件转存“代码,那段程序自己恢复堆栈。 004F0485 BE 20064F00 MOV ESI,Kof97DIY.004F0620 ; 用于保存路径
004F048A 6A 50 PUSH 50 ; 获取系统路径
004F048C 56 PUSH ESI
004F048D FF15 BC307900 CALL DWORD PTR DS:[<&KERNEL32.GetSystemDirectoryA>; kernel32.GetSystemDirectoryA
004F0493 68 3B044F00 PUSH Kof97DIY.004F043B ; \
004F0498 56 PUSH ESI
004F0499 FF15 10307900 CALL DWORD PTR DS:[<&MSVCRT.strcat>] ; msvcrt.strcat
004F049F 68 33044F00 PUSH Kof97DIY.004F0433 ; 97
004F04A4 56 PUSH ESI
004F04A5 FF15 10307900 CALL DWORD PTR DS:[<&MSVCRT.strcat>] ; msvcrt.strcat
004F04AB 68 3D044F00 PUSH Kof97DIY.004F043D ; .
004F04B0 56 PUSH ESI
004F04B1 FF15 10307900 CALL DWORD PTR DS:[<&MSVCRT.strcat>] ; msvcrt.strcat
004F04B7 68 36044F00 PUSH Kof97DIY.004F0436 ; roms
004F04BC 56 PUSH ESI
004F04BD FF15 10307900 CALL DWORD PTR DS:[<&MSVCRT.strcat>] ; msvcrt.strcat
004F04C3 8B25 75044F00 MOV ESP,DWORD PTR DS:[4F0475]
004F04C9 B8 20064F00 MOV EAX,Kof97DIY.004F0620 ; 生成路径为 C:\windows\system32\97.roms
004F04CE 898424 A0000000 MOV DWORD PTR SS:[ESP+A0],EAX
004F04D5 ^ E9 72FFFFFF JMP Kof97DIY.004F044C ; 先看看路径下有没有ROM文件,如果有调用成功返回,如果没有成功,生成文件
004F04DA 0000 ADD BYTE PTR DS:[EAX],AL
004F04DC B9 20064F00 MOV ECX,Kof97DIY.004F0620
004F04E1 BA 32044F00 MOV EDX,Kof97DIY.004F0432 ; #97(注意)
004F04E6 B8 36044F00 MOV EAX,Kof97DIY.004F0436 ; roms
004F04EB 60 PUSHAD
004F04EC E8 2C000000 CALL Kof97DIY.004F051D ; 调用ROM文件生成
004F04F1 61 POPAD ; 堆栈平衡
004F04F2 ^ EB CF JMP SHORT Kof97DIY.004F04C3 ; ROM文件生成后在修改路径调用fopen函数 下面这个资源文件生成函数是我以前从”小凤居“(现在好像不能访问了)下载的一个键盘记录器(Delphi版)的函数库中拷贝回来的,修改跳转和函数地址为LordPE中添加的函数地址
004F051D 53 PUSH EBX ; 资源生成函数
004F051E 56 PUSH ESI
004F051F 57 PUSH EDI
004F0520 55 PUSH EBP
004F0521 83C4 F8 ADD ESP,-8
004F0524 8BF1 MOV ESI,ECX
004F0526 33DB XOR EBX,EBX
004F0528 50 PUSH EAX
004F0529 52 PUSH EDX
004F052A B8 00004000 MOV EAX,Kof97DIY.00400000
004F052F 50 PUSH EAX
004F0530 FF15 C0307900 CALL DWORD PTR DS:[<&KERNEL32.FindResourceA>] ; kernel32.FindResourceA
004F0536 8BF8 MOV EDI,EAX
004F0538 85FF TEST EDI,EDI
004F053A 0F84 BB000000 JE Kof97DIY.004F05FB
004F0540 57 PUSH EDI
004F0541 B8 00004000 MOV EAX,Kof97DIY.00400000
004F0546 50 PUSH EAX
004F0547 FF15 C4307900 CALL DWORD PTR DS:[<&KERNEL32.LoadResource>] ; kernel32.LoadResource
004F054D 8BE8 MOV EBP,EAX
004F054F 85ED TEST EBP,EBP
004F0551 0F84 A4000000 JE Kof97DIY.004F05FB
004F0557 55 PUSH EBP
004F0558 FF15 CC307900 CALL DWORD PTR DS:[<&KERNEL32.LockResource>] ; kernel32.SetHandleCount
004F055E 894424 04 MOV DWORD PTR SS:[ESP+4],EAX
004F0562 837C24 04 00 CMP DWORD PTR SS:[ESP+4],0
004F0567 75 0C JNZ SHORT Kof97DIY.004F0575
004F0569 55 PUSH EBP
004F056A FF15 D0307900 CALL DWORD PTR DS:[<&KERNEL32.FreeResource>] ; kernel32.FreeResource
004F0570 E9 86000000 JMP Kof97DIY.004F05FB
004F0575 6A 00 PUSH 0
004F0577 68 80000000 PUSH 80
004F057C 6A 02 PUSH 2
004F057E 6A 00 PUSH 0
004F0580 6A 00 PUSH 0
004F0582 68 00000040 PUSH 40000000
004F0587 56 PUSH ESI
004F0588 FF15 D4307900 CALL DWORD PTR DS:[<&KERNEL32.CreateFileA>] ; kernel32.CreateFileA
004F058E 8BF0 MOV ESI,EAX
004F0590 83FE FF CMP ESI,-1
004F0593 75 10 JNZ SHORT Kof97DIY.004F05A5
004F0595 8BC5 MOV EAX,EBP
004F0597 E8 7C000000 CALL Kof97DIY.004F0618 原来的就是XOR EAX,EAX
004F059C 55 PUSH EBP
004F059D FF15 D0307900 CALL DWORD PTR DS:[<&KERNEL32.FreeResource>] ; kernel32.FreeResource
004F05A3 EB 56 JMP SHORT Kof97DIY.004F05FB
004F05A5 57 PUSH EDI
004F05A6 B8 00004000 MOV EAX,Kof97DIY.00400000
004F05AB 50 PUSH EAX
004F05AC FF15 C8307900 CALL DWORD PTR DS:[<&KERNEL32.SizeofResource>] ; kernel32.SizeofResource
004F05B2 8BF8 MOV EDI,EAX
004F05B4 6A 00 PUSH 0
004F05B6 8D4424 04 LEA EAX,DWORD PTR SS:[ESP+4]
004F05BA 50 PUSH EAX
004F05BB 57 PUSH EDI
004F05BC 8B4424 10 MOV EAX,DWORD PTR SS:[ESP+10]
004F05C0 50 PUSH EAX
004F05C1 56 PUSH ESI
004F05C2 FF15 D8307900 CALL DWORD PTR DS:[<&KERNEL32.WriteFile>] ; kernel32.WriteFile
004F05C8 3B3C24 CMP EDI,DWORD PTR SS:[ESP]
004F05CB 74 10 JE SHORT Kof97DIY.004F05DD
004F05CD 8BC5 MOV EAX,EBP
004F05CF E8 44000000 CALL Kof97DIY.004F0618 好像可以防止函数返回搞不清,将EAX清零
004F05D4 55 PUSH EBP
004F05D5 FF15 D0307900 CALL DWORD PTR DS:[<&KERNEL32.FreeResource>] ; kernel32.FreeResource
004F05DB EB 1E JMP SHORT Kof97DIY.004F05FB
004F05DD 56 PUSH ESI
004F05DE FF15 DC307900 CALL DWORD PTR DS:[<&KERNEL32.SetEndOfFile>] ; kernel32.SetEndOfFile
004F05E4 56 PUSH ESI
004F05E5 FF15 E0307900 CALL DWORD PTR DS:[<&KERNEL32.CloseHandle>] ; kernel32.CloseHandle
004F05EB 8BC5 MOV EAX,EBP
004F05ED E8 26000000 CALL Kof97DIY.004F0618
004F05F2 55 PUSH EBP
004F05F3 FF15 D0307900 CALL DWORD PTR DS:[<&KERNEL32.FreeResource>] ; kernel32.FreeResource
004F05F9 B3 01 MOV BL,1
004F05FB 8BC3 MOV EAX,EBX
004F05FD 59 POP ECX
004F05FE 5A POP EDX
004F05FF 5D POP EBP
004F0600 5F POP EDI
004F0601 5E POP ESI
004F0602 5B POP EBX
004F0603 C3 RETN
004F0618 33C0 XOR EAX,EAX
004F061A C3 RETN
上面这些代码,我复制粘贴备份修改了N遍才让它没有问题,我只能说我是菜鸟。令我感触颇深的就是调用LoadPE添加的函数,我一直
CALL 7930D0 这样就是异常,没有错啊,不就是函数在LordPE中的地址加基址吗?后来才发现要加个括号CALL 【7930D0】才正确。我真是郁闷了好半天。(我这就是典型的浮沙上筑高台)。
上面这些代码已经能够正确的生成ROM文件了,并且在Windows系统目录下可以找到97.ROMS文件,与原来的KOF97.ZIP文件一摸一样。但是程序确还是不能正确运行,游戏打不了。提示 ”232-p1.bin (7db81ad9) 未找到, 错误“ 我找到错误提示的地方进行跟踪。先使用原文件打开一个不存在的游戏进行跟踪,发现程序在遍历过程序设置的文件路径后,先是本身ROM查找 如KOF97.ZIP,然后是NEOGEO.ZIP查找都不存在就报错。我现在的程序已经将所用的文件打开都改成 C:\WINDOWS\SYSTEM32\97.ROMS 调用,并且监视文件打开函数发现也能正确打开啊。那肯定是后续打开文件后进行的某些检测错误。于是我跟进: 004E6AB2 /74 03 JE SHORT Kof97DIY.004E6AB7 ; 这一大堆也不知道是什么比较
004E6AB4 |83CE FF OR ESI,FFFFFFFF ; 反正要跳过这步,如果没调就GAME OVER
004E6AB7 \8B4424 3C MOV EAX,DWORD PTR SS:[ESP+3C]
004E6ABB 8B4C24 40 MOV ECX,DWORD PTR SS:[ESP+40]
004E6ABF 8D1408 LEA EDX,DWORD PTR DS:[EAX+ECX]
004E6AC2 3BDA CMP EBX,EDX
004E6AC4 72 04 JB SHORT Kof97DIY.004E6ACA
004E6AC6 85F6 TEST ESI,ESI ; ESI中存放的是我们ROM的地址 C:\Windows...
004E6AC8 EB 17 JMP SHORT Kof97DIY.004E6AE1 ; 这里没有跳转的话就返回 错误 一定要跳
004E6ACA 57 PUSH EDI
004E6ACB E8 06860000 CALL <JMP.&MSVCRT.fclose>
004E6AD0 83C4 04 ADD ESP,4
004E6AD3 33C0 XOR EAX,EAX
004E6AD5 5F POP EDI
004E6AD6 5E POP ESI
004E6AD7 5B POP EBX
004E6AD8 81C4 90000000 ADD ESP,90
004E6ADE C2 0400 RETN 4
004E6AE1 8BD3 MOV EDX,EBX
004E6AE3 68 80000000 PUSH 80
004E6AE8 2BD0 SUB EDX,EAX
004E6AEA 897C24 20 MOV DWORD PTR SS:[ESP+20],EDI
004E6AEE 2BD1 SUB EDX,ECX
004E6AF0 895C24 3C MOV DWORD PTR SS:[ESP+3C],EBX
004E6AF4 895424 2C MOV DWORD PTR SS:[ESP+2C],EDX
004E6AF8 C78424 9C000000>MOV DWORD PTR SS:[ESP+9C],0
004E6B03 E8 80850000 CALL <JMP.&MSVCRT.malloc> ; 到这里应该对了,分配内存举行文件读取
将 004E6AC8改为JMP 跳转,运行程序,OK 了,可以正常进行游戏了。
剩下的就是一些后续工作了, 编辑资源,添加头像,太爽了,有了这个单文件版本,到哪都HAPPY,不过这个还是比较猥琐,只是使用了资源转存。现在回头看看,如果将ROM编辑成一个段,在将fopen,fread函数全部重写,只要返回值正确就OK了,那样可以做到真正的单文件版本。(只是个人构思)希望各位大侠能够指点一下如何使用段模拟文件。
现在大家可以使用这种方法制作自己喜欢游戏的单文件版本了。
不足之处还望批评指正。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)