为Sega Mega Drive解决一个简单的Crackme

大家好



尽管我在Sega Mega Drive倒车游戏方面拥有丰富的经验,但我从未决定要破解它,并且它们并没有在Internet上出现。 但是,前几天有一个有趣的饼干想解决。 我同意你的决定...


内容描述


任务说明和朗姆酒本身可以在此处下载


尽管资源清单上有Hydra的事实,但Sega Ida ToolsSega上用于调试和反转游戏的工具中的标准事实。 它具有解决此奶油所需的一切:


  • 朗达朗姆酒装载机
  • 调试器
  • 查看和更改RAM / VDP内存
  • 在VDP上显示几乎完整的信息

我们将最新版本放到Ide的插件中,然后开始看看我们拥有什么。


解决方案


任何Shogi游戏的发布都始于执行Reset向量。 从朗姆酒开头的第二个DWORD中可以找到指向它的指针。




我们看到了一些从地址0x27A开始的身份不明的函数。 让我们看看那里有什么。


sub_2EA()



根据我自己的经验,我会说这通常看起来像等待VBLANK中断完成的功能。 让我们看看在哪里还有对byte_FF0026变量的调用:



我们看到零位只是在VBLANK中断中设置的。 因此,我们将变量vblank_ready ,检查该变量的函数为wait_for_vblank


sub_60E()


接下来,通过代码调用sub_60E函数。 让我们看看那里是什么:



第一条命令写入VDP_CTRL就是VDP控制命令。 要了解她在做什么,我们站在此命令上并按J键:



我们看到CRAM (存储调色板的位置)中的条目已初始化。 这意味着所有后续功能代码都仅设置了某种初始调色板。 因此,该函数可以称为init_cram


sub_71A()



我们看到一些命令再次传输到VDP_CTRL ,然后再次按J并发现此命令初始化了视频内存中的记录:



此外,要了解在那里传输到视频存储器的内容没有任何意义。 因此,我们只需调用函数load_vdp_data


sub_C60()


这里几乎发生与上一个函数相同的事情,因此,在不赘述的情况下,我们仅调用load_vdp_data2函数。


sub_8DA()


已经有更多的代码。 此外,该函数还调用了另一个函数。 让我们看一下-在sub_D08


sub_D08()



我们看到在寄存器D0VDP_CTRL D0命令,在D1 VDP_CTRL将填充的值,在D2D3 -填充的宽度和高度(因为结果是两个周期:内部和外部)。 调用fill_vram_by_addr函数。


sub_8DA()


我们返回上一个功能。 将D0寄存器中的值作为VDP_CTRL的命令发送VDP_CTRL ,请按该值上的J键。 我们得到:



再次,从将游戏转换为Sega的经验来看,我可以说此命令初始化了映射图块的记录。 在90%的情况下, $Dxxx $Fxxx$Exxx$Dxxx$Dxxx地址将是具有这些相同映射的区域的地址。 什么是映射:
这些是您可以指定在屏幕上的何处显示或该图块的值(图块是8x8像素的正方形)。


因此该函数可以称为init_tile_mappings


sub_CDC()



第一条命令在地址$F000处初始化记录。 一个注意事项:在“ 映射 ”的地址中,仍然存在一个存储精灵表的区域(这些区域是它们的位置,它们指向的图块等)。找出哪个区域负责可调试的内容。 但是现在,我们不需要此功能,因此我们只需调用函数init_other_mappings


此外,我们看到在此函数中初始化了两个变量: word_FF000Aword_FF000C 。 根据我的经验(是的,他决定),我会说,如果在地址空间中有两个变量与映射相关联,那么在大多数情况下,它们将是某个对象(例如,精灵)的坐标。 因此,我建议称它们为sprite_pos_xsprite_pos_yxy的误差y允许的,因为 进一步调试,将很容易修复。


VBLANK


由于循环在代码中进行得更远,因此我们可以假设我们已经完成了基本初始化。 现在您可以查看VBLANK中断。



我们看到两个变量正在递增(这很奇怪,在每个变量的链接列表中,它绝对是空的)。 但是,由于它们每帧更新一次,因此可以将它们称为timer1timer2


接下来, sub_2FE函数。 让我们看看那里是什么:


sub_2FE()



在那里-使用IO_CT1_DATA端口(负责第一个操纵杆)。 端口地址被加载到寄存器A0 ,并传递给sub_310函数。 我们去那里:


sub_310()



我的经验再次帮助了我。 如果您看到适用于操纵杆的代码以及内存中的两个变量,则其中一个会存储pressed keys ,第二个会存储held keys ,即 只需按住键即可。 因此,我们将这些变量称为: pressed_keysheld_keys 。 然后该函数可以称为update_joypad_state


sub_2FE()


将该函数称为read_joypad


处理程序循环


现在,一切看起来都更加清晰了:



因此,此循环响应所按下的键,并执行相应的操作。 让我们遍历循环中调用的每个函数。


sub_4D4()



有很多代码。 让我们从第一个函数sub_60C


sub_60C()


她什么也没做-乍看起来似乎是这样。 只是从当前函数返回的是rts 。 但是,因为 仅在其上发生跳转( bsr ),这意味着rts将使我们返回到处理程序循环。 我将此函数称为retn_to_loop


sub_4D4()


接下来,我们看到对word_FF000E变量的调用。 除了当前功能以外,没有在其他任何地方使用它,起初,我的目的并不明确。 但是,如果您仔细观察,我们可以假设仅在按键处理之间有很小的延迟才需要此变量。 ( 在本次朗姆酒中,它的实施效果很差,但是,我认为,如果没有此变量,情况将会更糟 )。



接下来,我们有大量的代码以某种方式处理sprite_pos_xsprite_pos_y ,这些sprite_pos_y只能说一句话-这对于在字母中所选字符周围显示选择精灵是必须的。


因此,现在您可以安全地将函数命名为update_selection 。 让我们继续前进。



该代码检查某些按键的位是否已设置,并调用某些功能。 让我们看看它们。


sub_D28()



某种萨满魔术。 首先,从word_FF0018变量中提取WORD ,然后执行一条有趣的指令:


 bsr.w *+4 

该命令只是跳转到其后的指令。


接下来是另一个魔术:


 move.l d0,(sp) rts 

寄存器D0的值位于堆栈的顶部。 值得注意的是,对于Shogi以及某些x86 ,函数调用时的返回地址都放在堆栈的顶部。 因此,第一个指令将某个地址放在顶部,第二个指令将其从堆栈中移出并沿其过渡。 好招


现在,您需要了解此值在变量中的含义,然后进行检查。 但是首先,我们将其称为jmp_addr变量。


这些函数将称为:


  • sub_D38goto_to_d0
  • sub_D28jump_to_var_addr

jmp_addr


找出该变量的填充位置。 我们看一下参考文献清单:



只有一个地方可以写入此变量。 让我们看看他。


sub_3A4()



在此,根据子画面的坐标(请记住,这很可能是所选字符的地址),将输入该值或该值。 我们看到以下代码部分:



现有值向右移动4位,新值放入低字节,然后将结果再次输入到变量中。 从理论上讲,我们的jmp_addr变量存储可以在键输入屏幕上输入的字符。 另请注意,变量的大小为WORD


实际上, sub_3A4函数可以称为update_jmp_addr


sub_414()


现在,循环中只剩下一个函数,该函数无法识别。 它被称为sub_414



它的代码类似于update_jmp_addr函数的代码,仅在最后我们有一个sub_45E函数sub_45E 。 让我们看看那里。


sub_45E()



我们看到在D0寄存器中输入了数字#$4B1E2003 ,然后将其发送到VDP_CTRL ,这意味着我们正在处理另一个VDP控制命令。 我们按J ,我们会收到一个映射$Cxxx区域中的记录命令。


接下来,代码使用变量byte_FF0014 ,除了当前函数外,该变量未在其他任何地方使用。 如果仔细看一下它的用法,您会注意到它可以安装的最大数量为4 。 我假设这是输入密钥的当前长度。 让我们来看看。


运行调试器


我将使用Smd Ida Tools的调试器,但实际上,某些Gens KModGens ReRecording就足够了。 最主要的是,有一个功能可以在内存中显示地址。



我的理论已经得到证实。 因此,变量byte_FF0014现在可以key_length


还有另一个变量: dword_FF0010 ,它也仅在当前函数中使用,并且其内容在添加到D0的初始命令(回想起来,它是编号#$4B1E2003 )之后,被发送到VDP_CTRL 。 我add_to_vdp_cmd ,将其命名为变量add_to_vdp_cmd


那么这个功能做什么呢? 我假设她会绘制输入的字符。 检查这很简单-通过启动调试器并在调用sub_45E函数之前和之后比较状态:


至:



之后:



我是对的-此函数绘制输入的字符。 我们称它为do_draw_input_char ,调用它的函数( sub_414 )为draw_input_char


现在呢


现在让我们检查一下我们称为jmp_addr的变量jmp_addr确实存储了输入的密钥。 我们将使用相同的Memory Watch



如您所见,这个猜想是正确的。 这给了我们什么? 我们可以跳转到任何地址。 但是哪一个呢? 在功能列表中,所有内容归根到底是:



然后,我开始滚动浏览代码,直到找到:



训练有素的眼睛看到了$4E, $75的序列$4E, $75在未分配字节的末尾。 这是rts指令的操作码,即 从函数返回。 因此,这些未分配的字节可以是某些功能的代码。 让我们尝试将它们指定为代码,按C



显然,这是一个功能代码。 您也可以按其上的P以使代码起作用。 记住这个名字: sub_D3C


然后想到了:如果您跳到sub_D3C怎么sub_D3C ? 听起来不错,尽管在这里单跳显然是不够的,因为 没有更多到word_FF0020变量的链接。


然后另一个想法浮现在我头:如果我们寻找另一个这样的未分配代码怎么办? 打开Binary search对话框(Alt + B),在其中输入序列4E 75 ,选中“ Find all occurrences ”框:



单击开始搜索,我们得到以下结果。



朗姆酒中至少还有两个地方可能包含功能代码,您需要检查它们。 我们单击第一个选项,向上滚动一点,然后再次看到一系列未定义的字节。 将它们表示为功能吗? 是的 在字节开始处打P



好酷! 现在我们有了sub_34C函数。 我们尝试对找到的最后一个选项重复同样的事情,并且...我们感到非常遗憾。 4E 75之前有太多字节,因此不清楚函数从何处开始。 而且,显然,并非以上所有这些字节都是代码,因为 很多重复的字节。


确定函数的开始


如果我们找到数据的结束位置,那么最容易找到函数的开始。 怎么做? 实际上一点也不复杂:


  1. 我们在数据开始之前进行扭曲(代码中将存在指向它们的链接)
  2. 我们点击链接并寻找一个循环,在该循环中应显示此数据的大小
  3. 标记阵列

因此,我们执行第一段...:



...,我们立即看到,在一个循环中,从数组中一次将4个字节的数据(由于move.l )复制到了VDP_DATA 。 接下来,我们看到数字2047 。 起初,似乎数组的最终大小为2047 * 4 ,但是基于dbf的循环执行的次数更多,而+1迭代 最后比较的值不是0 ,而是-1


总计:数组的大小为2048 * 4 = 8192 。 将字节表示为数组。 为此,请单击*并指定大小:



我们扭曲到数组的末尾,然后看到字节,它们恰好是代码的字节:




现在我们有了sub_2D86函数,并且我们拥有解决此sub_2D86一切! 让我们看看新创建的函数的作用。


sub_2D86()


它将值#$4147放入寄存器D1并调用sub_34C函数。 看看她。


sub_34C()



我们看到这里word_FF0020变量的值被word_FF0020 。 如果查看它的链接,我们将看到在此变量中进行记录的另一个地方,而这正是我要跳过jmp_addr变量的地方。 这确认了sub_D3C肯定需要跳转到sub_D3C


但是接下来发生的事情让我无法理解,因此我把朗姆酒扔进了GHIDRA ,找到了这个功能,然后看了反编译的代码:


 void FUN_0000034c(void) { ushort in_D1w; short sVar1; ushort *puVar2; if (((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,in_D1w ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; } 

我们看到使用了具有奇怪名称in_D1w的变量,还使用了变量DAT_00ff0020 ,其地址类似于word_FF0020提到的word_FF0020


in_D1w告诉我们,该值是从寄存器D1in_D1w的WORD一半中获取的,并将寄存器D1设置为传递该值D1函数。 还记得#$4147吗? 因此,您需要将此寄存器指定为函数的输入参数。


为此,在带有反编译代码的窗口中,右键单击函数名称,然后选择“ Edit Function Signature菜单项:



为了指示该函数通过特定的寄存器(而不是通过当前调用约定的标准方法)接受参数,您需要选中Use Custom Storage ,然后单击带有绿色加号的图标:



出现新输入参数的位置。 双击它,将出现一个对话框,指示参数的类型和媒介:



在反编译的代码中,我们看到in_D1wushort类型,这意味着我们将在type字段中指定它。 然后单击Add按钮:



将出现一个位置来指示参数的D1w Location ,我们需要在Location指定D1w寄存器,然后单击OK



反编译的代码将采用以下形式:


 void FUN_0000034c(ushort param_1) { short sVar1; ushort *puVar2; if (((ushort)(param_1 ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(param_1 ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,param_1 ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; } 

我们param_1我们的param_1值是常量,由调用函数传递,并且等于#$4147 。 那么DAT_00ff0020的值应该是DAT_00ff0020 ? 我们考虑:


 0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50 

因为 xor该操作是可逆的,所有常数可以相互争吵,并获得变量DAT_00ff0020的所需值。


 DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553 

事实证明,变量的值应为0x4553 。 看来我已经看到了设置了这样一个值的地方...



结论与决定


我们得出以下结果:


  1. 首先,您需要跳转到地址0x0D3C ,为此,您需要输入代码0D3C
  2. 跳转到函数0x2D86 ,该函数将D1的值设置为寄存器#$4147 ,为此,您需要输入代码2D86

通过实验,我们找出需要按下的键来检查输入的键: B 我们尝试:



谢谢你

Source: https://habr.com/ru/post/zh-CN448500/


All Articles