【已更新】《WinXP空当接龙》加入无限撤销和存档功能
本帖最后由 klise 于 2021-12-30 12:11 编辑【前言】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
首先说明:该版本在 WinXP/Win7/Win10 都能玩,绿色软件,不用辛辛苦苦去找XP电脑,也不用辛辛苦苦装一套虚拟机。
另外,网友反映的游戏bug已经修复,详见【游戏下载】的说明。
这是小弟第一次在“脱壳破解区”发帖,如有不妥之处,或者无意中违反了规定,务请及时指出,我立即改正。
我一直喜欢玩《空当接龙》,特别是对 WinXP 自带的版本情有独钟,相信这个版本也是很多朋友的童年回忆。
不过,WinXP的《空当接龙》有个很麻烦的问题,就是只能撤销一次,有时候一步不慎,就要从头开始。
尽管 Win7 以后的《空当接龙》已经可以无限撤销,但是我希望自己动手,给《WinXP空当接龙》加上“无限撤销”的功能,一来算是自己练练手,二来我还是喜欢 WinXP 这个版本,也是给自己一种便利。
网上也有很多《空当接龙》的升级版,基本上都是用高级语言(源代码)重新编写的,比如VC、VB、Delphi,属于全新开发。“全新开发”的好处是,可以任意增加功能,不受原版exe的局限。不过,这与“破解逆向”的关系不大。
我决定用WinXP原版《空当接龙》进行改进,加入无限撤销、存档读档功能。改进后的《空当接龙》加强版取名《JF接龙》,JF是 Just Fun 的意思,所谓“世事无绝对,只有真情趣”,也就是 No Worry, Just Fun ...
今年(2020年)即将完结,这一年大家见证了太多历史,这里顺祝各位朋友在新的一年里 No Worry, Just Fun ...
以下是我开发《JF接龙》的过程记述,由于开发的步骤互相穿插,所以次序上并非严格的顺序,想到哪写到哪。
【增加区段 —— 开垦自留地】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
“增加区段”这个操作,通常大家都用不上,这是因为常规的破解(爆破)需要的代码量并不大,有时候把je 改成 jmp 就可以了 :)
但是,这次的任务与通常的“破解”不同,这次并不是单纯的破解,而是要给《空当接龙》加入新的功能。这就必然要加入额外的代码,说不定还要储存一些数据什么的。
可以想象,原版exe是 不会 给我们预留多余的空间的,因此最好的办法是给exe增加一个区段,作为自用的自留地”,把新增的代码、数据放在新区段里。
至于加入的区段要多大,这个一开始只是一种估算,不过以后可以调整,不够再加嘛,所以先加个 4KB 的区段,边走边看吧。
【诡异的 freecell.exe 】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
WinXP 的《空当接龙》执行文件位于Windows\system32 目录中,由 freecell.exe 和 cards.dll 构成:
freecell.exe 54KB 主程序,负责开局、移牌、判断胜负等
cards.dll 352KB 资源文件,包含52张纸牌的图像、绘制等
这个 freecell.exe 就是我们动手的目标,它只有 54KB,没有加壳(微软官方的东西,是不会加壳的)。
加入区段的工具很多,我用 Stud_PE (用国内的 StudyPE 也可以)。
我曾经给 N个 exe 加过区段(比如《盟军敢死队》系列),不过这次却被这个只有54K的小家伙难倒了。加了区段之后,竟然无法运行?
用 OD 打开一看,里面的 API 函数全乱了,疑似是 IAT 的问题?
这个问题困扰了我几天,我用过几种流行工具,包括 pe-explorer, ImportRE 等,都无法解决,还在『脱壳破解讨论求助区』发帖请教。
后来我忽然想到,还有 LordPE 没试过,死马当作活马医,点击【重建PE】,显示“存在重定位”,清除成功。
然后双击重建后的 exe,顺利运行了!
搞了半天,原来这是传说中的“exe 重定位”!
我们一直都知道,DLL 是有重定位的,至于exe,理论上也有重定位,不过我们真的从来没有遇到过,没想到在这个只有54KB的小家伙身上遇到了!
这次真是开眼了,2020年,真是见证历史呀!
这次最终用 LordPE 搞定了,看来关键时候,还是要靠老前辈啊!
【寻找撤销功能】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
在LordPE重建exe后,就顺利加入新区段了,自留地开垦成功了。
接下来开始“无限撤销”的功能。关于《空当接龙》的内部情况,看雪的前辈有一篇文章进行了详细说明,让我节省了不少时间,少走了很多弯路。在此向所有前辈致敬!
[原创]空当接龙逆向算法分析
这篇文章没有提到“撤销”功能,但是从中可知,牌局中的52张纸牌存放在 0x01007500 数据区,那么所有纸牌的变动(移动、撤销),必然在这里反映出来。
这里先说明“中转站”和“终点站”的概念,如图,不必赘述。
我们选一局 #11 ,把 梅花A 弹上去,同时对 0x01007500 数据区 进行截图对比。
我们知道, 在 0x01007500 数据区中,“开头”就是中转站、终点站的数据,而“开头”的数据是最直观、最容易观察的。
我们只要“故意”把纸牌打上中转站、终点站,就能看到数据变化。
下面把 梅花A 弹上去的前、后数据进行截图对比:
我们看到,0x01007510 单元的数据从 -1 变成 0 了!
这就简单了,对 0x01007510 下“硬件写入”断点,然后按一下 F10 撤销,梅花A 回退,它又从 0 变回 -1,并且被 OD 断下来了。
仔细观察,发现两个情况:
1、 这是一个循环过程。这个容易理解,回退嘛,有可能是几张牌一起回去,所以是个循环执行
2、 梅花A 并不是直接回去的,而是先跑到中转站,然后再回去原位
这个过程就有意思了,一张牌也分成几步走?
既然是一个循环,往上追溯,看到几个很有意思的地方。对了,现在不用硬件断点了,直接 F2 下断点就行了。
上图有4个要点,这是仔细分析后得到的,直接说结论:
1、 地址 是回退的步数,比如 梅花A 是 2步,先到中转站,再回到原位
2、 取得 edi= 之后,乘以16,也就是步数*16,每一步占用16个字节
3、 加上固定地址 0x010079CC,可见 0x010079CC 应该是起始地址,但是 ……
4、 但是,下面还要 、 、 ,显然数据区还要往前推,
留意到 是最前方,也就是说, 0x010079CC减去 0xC 是最开端的地址,也就是0x010079C0
捋一下这个循环过程,大意就是:
1、取 的回退步数,比如 步数=5
2、每一步占用16字节
3、从 0x010079C0 开始,依次存放着每一步的16字节,依次读取5遍,然后解释执行
至此,我们找到了“回退步骤”最重要的数据区,也就是数据源,接下来的工作相对简单了,我就不用写得太啰嗦了。
接下来以步骤、思路为主,至于实现方法,大家可以各自发挥,不必局限于我的做法。
【监测“回退数据”】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
原版exe只能保留一次回撤,你点击一下鼠标,这个回退数据就会消失了。
为了实现“无限撤销”功能,我们自然会想到,需要把原版exe每次生成的“回退数据”截留下来。如果没有这些数据,就谈不上“无限撤销”了。
既然知道了回退数据保存在 和 0x010079C0 ,只要下“硬件写入”断点,就能监测到什么时候写入(生成)回退数据。
这是因为,既然有回退,那么步数一定大于0,一定要写入 地址。
通过检测 地址,找到了代码所在,原始就在纸牌发生移动之后, 紧接着就写入回退步数。
【截留“回退数据”】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
紧接上一步,既然找到了“回退数据”生成的源头,就要赶紧把这些数据拷贝出来,另外找地方保存。
否则玩家随便点一下鼠标,这个回退数据就会消失了。
保存的数据很简单,地址从 0x010079C0 开始,长度(字节数)就是的步数乘以16。
熟悉汇编的朋友一下就想到了,无非就是:
-
mov esi, 0x010079C0
mov edi, 我的自留地
mov ecx,
shl ecx,4
rep movSB
-
那么保存到哪里呢?(啥?上传到百度网盘?……)
这就要用到另外开辟的内存了,有几种做法:
1、标准方法是申请一块内存,所有数据全部放进去
2、我们把新加的区段搞大一些,比如放个100KB,直接往里面写数据
具体到 freecell.exe 而言, 第1种方法不是那么方便,因为exe里面没有引入关于内存的API,你要自己找 malloc 或者 calloc 的接口( freecell 里面有 GetProcAddress,因为它自己要调入 cards.dll ),还要申请内存、释放内存,看着有点头大。
因此建议用第2种方法,简单直接,不过,切记用 LordPE 重建一下,然后加区段,切记!
比如用 stud_pe 加入区段,注意“实际大小”和“虚拟大小”必须一致,然后“用零填充区段”,这样就行了。这里 0x8000 就是 32KB 空间。
【“回退”调度】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
我们设想一下,比如用户玩了5步,我们储存了5次回退数据,称为 D1、D2……D5,来看两种情况:
1、用户按一下 F10,应该回退 D5。如果用户继续按F10,那么继续回退 D4。
2、用户按一下 F10,应该回退 D5。这时用户继续玩下一步,那么就产生新的回退数据,截留出来保存在D5(之前的数据已经回退,作废了)
这个过程就是“后进先出”,类似于堆栈的原理。
因此,在保存“回退数据”时,要设计一个“调度”方式,而这个调度方式又直接影响到你复制(拷贝)数据的做法。
说到这里,已经不是“破解技巧”的问题,而是思路、方法的问题了。
【修改快捷键】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
原版exe通过F10进行撤销,通常我们用右手操作鼠标,左手按快捷键,那么左手按F10不太方便,如果换成F1,那就方便多了。
修改快捷键,最简单的方法是修改exe的“资源”。用 Resource Hacker 打开 exe,看到“快捷键”的定义。
容易理解,VK_F1 就是指 F1 键,那么 106 是什么呢?由于 windows 采用“消息机制”,这个 106 是“窗体消息”编号,你按下F1,游戏窗口就接收到 “106号消息”。
类似的,VK_F10 的消息编号是 115,先拿出小本子,记下来。
我把 F1 和 F10 等同起来,用户按下 F1 或者 F10 都可以“无限撤销”,这样就兼顾了 老用户 的习惯。
如下图,VK_F1 和 VK_F10 都设置为 1024,顺便把两个 Shift 组合键删除,避免发生干扰。
如上图,F1 和 F10 都统一为“1024消息”,用户按下这两个键,都是产生相同的消息,执行相同的动作。
为什么是 1024 呢? 这是因为 1024 以下是Windows系统自带的消息,比如滚动条、按钮之类,而 1024 以上就是给你编程使用的,比如 1997、2046 这些都是。
【拦截窗体(快捷键)消息】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
现在要处理 F1/F10 了,也就是“窗体消息1024”。这就必须找到游戏窗口的消息处理过程。
找消息过程,最简单的办法是拦截 DefWindowProcA 或者 DefWindowProcW,因为处理完消息之后,一定会执行一次 DefWindowProc 过程 。
在 OD 中,右键->查找->所有模块中的名称:
然后,按照“区段”排列,在 text 区段找到 USER32.DefWindowProcW,看到地址是 0x01001124 :
在OD的数据区,转到 0x01001124 地址,右键->查找参考:
就看到执行的地址了,下去几行就是 retn :
这里就是消息处理了, 一直往上翻,就是所有消息(包括F10)的处理方法,这可是个大宝藏啊!
当你一直往上翻,凡是看到Switch (cases 65..73) 之类的语句,就下个 F2 断点 ……
然后转到游戏窗口,按一下 F1,在这里断下:
上面我们修改exe的资源,设定了 F1 是 1024,现在 果然出现了 0x400,就是 1024。
温馨提示:这里是整个《空当接龙》的核心,是个大宝藏,整个游戏所有快捷键(菜单项)都在这里实现,包括F2开局、F3选局 ……
你只要拦截这里,就能看到《空当接龙》所有核心过程。
【原版F10的秘密】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
现在用户按下了 F1,我们站在大宝藏里,要实现“无限撤销”。但是问题来了,具体怎么实现呢?
比如那些“回退数据”,到底是表示 梅花5 还是 黑桃A 呢?是第几行第几列呢?
说实在,这个过程太复杂了,我们自己是无法实现的,毕竟手上没有“源代码”。
但是我们这样想,“原版F10”不就是现成的功能吗?直接利用它不就行了吗?
好,刚才我们用小本本记下了原版 VK_F10 的消息是115 (十进制),也就是0x73
现在把 的数值改成 0x73,骗过exe,让它以为是原版F10,继续执行,来到这里:
你会惊奇地看到,这里就两句,一个 push,一个 call,然后就结束了,jmp 到 retn 那里去了。
这么神奇?现在跟进到 01003FCD 里面,然后就看到熟悉的画面:
上面说到,回退数据保存在 和 0x010079C0 ,而在整个“回退”过程中,就是用到这两个关键数据。
也就是说,我们 事先 在这两个地方 填写好回退数据,然后 一个 push 一个 call,就搞定了!
简直了!这年薪百万的程序员 也太滋味了吧?(黑人问号)
啥?回退数据从哪里来? ……前面不是说了嘛,百度网盘呀 ……(黑人晕菜)
现在就差这个 push ,还是有必要搞清楚的,万一它包含着什么数据结构呢?
给 下一个“硬件读取”断点,结果发现,原来它就是游戏窗口的 Handle !
说白了,它就是一个整型数值,直接使用就行了。
如果非要追根究底(这种精神值得大力表扬),可以拦截CreateWindowExW,返回 eax 就是窗口句柄。而且你会发现这个 Handle 保存在 ,可以随时拿去用,比如修改窗口标题……
【实现回退】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
回退的过程大致如下,用示意性的代码来表述如下:
-
01001E91movzx eax,word ptr
01001E95add eax,-0x65 ;Switch (cases 65..73)
01001E98cmp eax,0xE ;拦截下句,处理扩展功能
01001E9Bja <F1回退> ;这里跳转到扩充代码
<F1回退>
; 把回退数据拷贝到exe
mov esi, 我的自留地
mov edi, 0x010079C0
mov ecx,
shl ecx,4
rep movSB
; 执行原版F10回退
push
call 01003FCD
; 继续回到原来的流程
jmp 010020F1
-
【败局处理】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
败局就是“死局”,没法再移动了,这时原版会弹出“你已经走投无路了”提示框。虽然你可以重新开始(同一局),但是你只能从头开始了。
但是,我们既然实现了“无限撤销”,就应该给用户“回退”的机会,这样才是“友好”的,才是人性化的。
所以,现在的处理方式是:
1、在失败提示出来之前,拦截下来
2、给一个提示,告诉用户可以回退,继续挑战
3、回到游戏,让用户继续玩
在文章《[原创]空当接龙逆向算法分析》中谈到“死局”的判定,容易找到相应的代码,进行修改即可。
这里不能放链接,大家网搜就行。
【细节处理】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
还有一些细节,在处理的时候需要注意,比如:
1、开局的时候,也就是新的一局开始时,应该把“自留地”初始化,删除“上一局”的回退数据
2、胜局的时候(通关之后),牌局会自动清除,这时用户通常都 不会 故意按下 F1/F10,但是万一按了,就可能出现不可预料的情况。作为严谨性,我们要进行处理,比如在回退之前判断一下牌局是否已经结束
3、回退之后,要清除“当前点击纸牌”。意思是,用户可能点了一下纸牌(第3列第5张),没有移走,就按下 F1,这样回退之后,(第3列第5张)纸牌还是“被点击”的(变成反色),这是不对的,所以要处理
类似这些细节都是实践中发现的,做事严谨认真的人,在生活和工作中都是受欢迎的。
【存档与读档】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
说实话,对于《空当接龙》这个游戏而言,我认为“存档”功能有点鸡肋,尤其是现在加入了无限撤销功能之后,“存档”的迫切性更加降低了。
不过,既然难得搞一次,就做得完善一些吧,而且“有”总比“没有”要好,可以应付不时之需。比如玩到一半,老妈喊“吃饭喽”,老婆喊“收快递”,也不怕了。
基于这种考虑,我决定不搞那种“N多存档”的功能了,就搞单一存档,保存当前正在进行的牌局,下次读取后继续玩。
【存档的数据】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
存档就是为了下次恢复盘面,要做到100%复原,而且我希望复员后,用户照样可以无限撤销,那就是“回退数据”都要保存。
为了方便用户,在菜单上加了“保存当前牌局(F6)”快捷键:
经过仔细跟踪,以下数据都要保存:
-
//当前牌局ID
fs.Write(PChar($0100834C), 4);
//剩余纸牌数量
fs.Write(PChar($01007800), 4);
//中转站
fs.Write(PChar($01008330), 16);
//终点站
fs.Write(PChar($01008360), 16);
//当前牌面52张
fs.Write(PChar($01007500), 189*4);
//保存回退数据
//……
//……
//……
-
【读档恢复】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
读档过程基本等于存档过程,只不过“源地址”和“目的地”反过来了。
但是,读档要处理的细节很多,包括:
1、当前牌局怎么办?是否提醒用户结束当前正在玩的牌局?
2、如果是,那么还要做一遍清理工作
3、读档后,相当于开始新的一局,有很多地方需要初始化
对于这些繁琐的步骤,如果全部搞一遍,那就太不划算了。但是,这3步不就是玩家“选局(F3)”的过程吗?
所以,我决定把“读档”功能放进”选局(F3)”过程,正好原版exe规定不能输入0,必须 1 ~100万,那么就用0表示“读档”。
这样过程就理顺了,你点“选局(F3)”,首先问你是否放弃当前牌局,然后自动清理内存,再选局,恢复盘面,时间刚刚好,不迟又不早。
【运用DLL】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
存档和读档牵涉到文件操作,包括:创建、打开、写入、读取、关闭,这些过程有很多参数,用汇编来写的话非常吃力,而且容易出错,也不便于维护。
但是对于高级语言(VC、Delphi)来说,这都是小菜一碟。代码一打,参数自动蹦出来,还帮你“自动完成”。
因此《JF接龙》引入了一个辅助DLL,用来处理文件操作。
另外,既然有了DLL,还有更多的工作可以交给它,毕竟用高级语言可以实现更强大的功能。
其实DLL的编写并不难,用VC、Delphi、甚至VB都行(VB就不推荐了),对于exe这边(汇编语言),在初始化时调入DLL,取得功能指针,就是一个地址,以后 call 这个地址就行了。
在 freecell.exe 里就有现成的 LoadLibraryA 和 GetProcAddress,因为它本身要调用 cards.dll 。
搭建一个DLL并不需要多少时间,但是磨刀不误砍柴工,一旦搭建完成,你就拥有无穷的扩展空间。
比如,下面这个游戏,右上角显示敌人的数量。这些图片是原版游戏没有的,要自己加上去。
为了美观起见,图片具有半透明,让背景的游戏画面可以“透出来”。这就需要png图片。
但是你想想,用 汇编代码 怎么可能解码png图片?这不是天方夜谭吗?
但是,用高级语言编写DLL,这个优势就显示出来。解码png的lib都是现成的(VC、Delphi……),拿来用就是了。
另外,像图片、音乐这些资源,总不能全部塞进exe里面吧?而且还是破解方式。
但是你可以放进DLL里,或者通过DLL读取外部资源 ……
这就是所说的,一旦你引进了DLL,就打开了一片广阔无垠的天地。
【游戏下载】
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
说了半天,差点忘了把游戏放上来。
下载后请看《使用说明》。
游戏目录中还包含 WinXP 原版《空当接龙》,以及帮助文件(*.chm),希望完整保留昔日的面貌。在可见的将来,WinXP只会在虚拟机中出现了。
感谢大家的支持,有空回复各位的询问。
更新说明
自从《JF接龙》发布后,有网友反映,在某些情况下《JF接龙》会出现“丢牌”现象。我们立即进行核查,但是由于每个人玩牌的习惯不同,即使是“同一牌局”,所经历的步骤(包括撤销)都不相同,所以我们一直无法“重现”网友反映的故障情况。因而,也就无法进行下一步“除错”工作。
虽然这样,我们相信网友反映的情况是真实的,所以一直没有放弃查找故障。
一次很偶然的机会,我们终于碰到了这种“丢牌”现象,这样就很快找到了原因,并修正了这个隐患。
请各位下载这个更新版本,感谢大家的支持!
【2021-01-02】《JF接龙》更新版:
备用下载:
https://wwi.lanzouw.com/i9L2Yjyfdpg
--------------------------------------------------------------------------------
《JF接龙》首发版(有bug):
备用下载:
https://www.lanzoui.com/iLLUAist13e
大佬厉害,QQ斗地主能方便添加个悔牌功能么 你们的电脑真好,还能安装XP。 厉害了,感谢分享 楼主厉害。 俺一直在玩呢XP的空当接龙才是真经典 只能撤一次算是挑战了win7那种无限撤回的没一点益智了
win95 98的关数又太少 所以还是xp好玩{:301_1001:}
期待更新,学习高手经验 为了玩上空当接龙硬装上了虚拟机 本帖最后由 涛之雨 于 2020-11-26 11:47 编辑
前排追更{:301_1001:}
学一下,完结了尝试一波(建议附上附件,方便学习)
此外推荐用md写,排版看起来很舒服 请问大神,如何更换KDJL的底色呢?