我是个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了,那样可以做到真正的单文件版本。(只是个人构思)希望各位大侠能够指点一下如何使用段模拟文件。
       现在大家可以使用这种方法制作自己喜欢游戏的单文件版本了。
       不足之处还望批评指正。