PlayStation 2 Game Reverse Engineering: Ape Escape 3
游戏简介
捉猴啦 3(Ape Escape 3)是一款动作类游戏,发布时间为 2005 年 7 月,游戏厂商为 SCE,代理厂商为 SCEH。其游戏平台为 PlayStation 2。
游戏解包
首先下载游戏文件 Ape Escape 3 (USA).iso
,直接解压缩可以得到四个文件:
DATA.BIN:打包的数据
IOPRP300.IMG:IOP Realtime Kernel 内核文件
SCUS_975.01:游戏主程序,MIPS 32 位可执行文件
SYSTEM.CNF:系统引导文件
其中 DATA.BIN
的前四个字节为 VFI\0
,查一下可以找到对应的 QuickBMS 解压脚本。
解压之后有两个目录,debug
文件夹下存放的是游戏的资源文件,irx
文件夹下存放的是运行时加载的动态库文件。
其中图像文件以 tim2
文件格式储存,可以使用 Rainbow 工具进行读取。
玩家状态
使用 IDA
载入游戏主程序 SCUS_975.01
:
首先从存档入手,简单看一下可以找到一个有趣的字符串 bool <unnamed>::McAccess::decodeData(int)
,通过交叉引用可以找到对应的函数位于 0x531F18
。
看一下可以在里面找到几个比较关键的调用。
第一个调用目标是 0x36CC44
,通过特征可以看出来这里是 zlib
的解压函数。
第二个调用目标是 0x270144
,这个函数对解压的数据进行解析,随后将玩家信息存储到位于 0x649910
的 Data
结构体中:
// local variable allocation has failed, the output may be wrong!
int __fastcall McAccess::decodeDataInner(Data *out, int mc_temp)
{
int *out_; // $s2
int v17; // $s3
_DWORD *v18; // $s0
int v19; // $s3
int *v20; // $s0
int *v21; // $s0
int i; // $s1
int v23; // $s3
int *v24; // $s0
int *v25; // $s0
int j; // $s1
int v27; // $s3
int *v28; // $s0
int *v29; // $s0
int v30; // $s3
int v31; // $s3
char *v32; // $s0
int k; // $s1
char *v34; // $s0
_BYTE v39[32]; // [sp+0h] [-70h] BYREF
int v40[4]; // [sp+20h] [-50h] BYREF
__asm
{
sd $s1, 0x70+var_38($sp)
sd $s2, 0x70+var_30($sp)
sd $s0, 0x70+var_40($sp)
sd $s5, 0x70+var_18($sp)
}
LODWORD(_$S2) = out;
__asm
{
sd $ra, 0x70+var_10($sp)
sd $s3, 0x70+var_28($sp)
sd $s4, 0x70+var_20($sp)
}
sub_371290(v40);
if ( sub_3713E0(v40, (char *)mc_temp) >= 0 )
{
v17 = 0;
strcpy(v39, "player_type"); // d
*out_ = sub_3713F8(v40, (int)v39);
strcpy(v39, "player_left"); // d
out_[1] = sub_3713F8(v40, (int)v39);
strcpy(v39, "player_life"); // f
out_[2] = sub_3713F8(v40, (int)v39);
strcpy(v39, "player_costume"); // d
out_[3] = sub_3713F8(v40, (int)v39);
strcpy(v39, "player_energy"); // f
out_[5] = sub_3713F8(v40, (int)v39);
strcpy(v39, "player_energy_capacity"); // f
out_[6] = sub_3713F8(v40, (int)v39);
strcpy(v39, "costume_timer"); // f
out_[4] = sub_3713F8(v40, (int)v39);
do
{
sub_26F9A0(v39, (int)"costume_possess");
v18 = &out_[v17++ + 3];
v18[4] = sub_3713F8(v40, (int)v39);
}
while ( v17 < 8 );
v19 = 0;
out_[7] = 2;
v20 = out_ + 15;
do
{
sub_26F9A0(v39, (int)"gmecha_possess");
++v19;
*v20++ = sub_3713F8(v40, (int)v39);
}
while ( v19 < 12 );
v21 = out_ + 27;
for ( i = 0; i < 4; ++i )
{
sub_26F9A0(v39, (int)"gmecha_assign");
*v21++ = sub_3713F8(v40, (int)v39);
}
strcpy(v39, "gmecha_currentkey");
v23 = 0;
out_[31] = sub_3713F8(v40, (int)v39);
strcpy(v39, "ammo_type");
out_[32] = sub_3713F8(v40, (int)v39);
do
{
sub_26F9A0(v39, (int)"ammo_count");
v24 = &out_[++v23];
v24[32] = sub_3713F8(v40, (int)v39);
}
while ( v23 < 3 );
v25 = out_ + 36;
for ( j = 0; j < 3; ++j )
{
sub_26F9A0(v39, (int)"ammo_max");
*v25++ = sub_3713F8(v40, (int)v39);
}
strcpy(v39, "rccar_type");
v27 = 0;
v28 = out_ + 40;
out_[39] = sub_3713F8(v40, (int)v39);
do
{
sub_26F9A0(v39, (int)"rccar_poss");
++v27;
*v28++ = sub_3713F8(v40, (int)v39);
}
while ( v27 < 4 );
v29 = out_ + 45;
strcpy(v39, "dance_type");
v30 = 0;
out_[44] = sub_3713F8(v40, (int)v39);
do
{
sub_26F9A0(v39, (int)"dance_poss");
++v30;
*v29++ = sub_3713F8(v40, (int)v39);
}
while ( v30 < 4 );
v31 = 0;
strcpy(v39, "chip_count");
out_[49] = sub_3713F8(v40, (int)v39);
strcpy(v39, "max_chip_count");
out_[50] = sub_3713F8(v40, (int)v39);
do
{
sub_26F9A0(v39, (int)"capture_flag");
v32 = (char *)out_ + v31++;
v32[204] = sub_3713F8(v40, (int)v39);
}
while ( v31 < 570 );
for ( k = 0; k < 259; ++k )
{
sub_26F9A0(v39, (int)"shop_item");
v34 = (char *)out_ + k;
v34[774] = sub_3713F8(v40, (int)v39);
}
}
sub_37132C(v40);
__asm
{
ld $s0, 0x70+var_40($sp)
ld $s1, 0x70+var_38($sp)
ld $s2, 0x70+var_30($sp)
ld $s3, 0x70+var_28($sp)
ld $s4, 0x70+var_20($sp)
ld $s5, 0x70+var_18($sp)
ld $ra, 0x70+var_10($sp)
}
return ((int (*)(void))_$RA)();
}
通过这段代码可以恢复出 Data
结构体定义:
struct __attribute__((packed)) __attribute__((aligned(1))) Data
{
int player_type;
int player_left;
float player_life;
int player_costume;
float costume_timer;
float player_energy;
float player_energy_capacity;
int costume_possess[8];
int gmecha_possess[12];
int gmecha_assign[4];
int gmecha_currentkey;
int ammo_type;
int ammo_count[3];
int ammo_max[3];
int rccar_type;
int rccar_poss[4];
int dance_type;
int dance_poss[4];
int chip_count;
int max_chip_count;
char capture_flag[570];
char shop_item[259];
};
把结构体定义应用到 0x649910
,接下来根据交叉引用修改相关函数的声明(比如静态成员函数),手动进行类型传播。
随后按 Ctrl+Alt+X
查看 Data::player_life
的 Global cross references
,可以找到一个关键的 Write
位置:
函数内容如下:
void __fastcall ChangeHP(Data *this)
{
_$F1 = 0.0;
_$F2 = 100.0;
__asm
{
max.s $f0, $f1
min.s $f0, $f2
}
this->player_life = _$F0;
}
这里的等价形式是:
void ChangeHP(Data *this, float delta)
{
this->player_life = max(min(this->player_life + delta, 100.0), 0.0);
}
但是 IDA
对这里的传参识别有点问题,需要手动指定传参时使用的寄存器,按 Y
修改函数声明:
void __usercall ChangeHP(Data *this@<$a0>, float@<$f12>)
之后再按 X
查看 ChangeHP
的交叉引用,IDA
就可以辅助分析第二个参数的内容了。
通过交叉引用可以找到位于 0x33DB70
的关键函数,这里传入的 delta
正好是我们在游戏中被攻击时受到的伤害:
// local variable allocation has failed, the output may be wrong!
int __usercall DecreaseHP_TRUE@<$v0>(int a1@<$a0>, float a2@<$f12>)
{
float v5; // $f20
Data *v6; // $f21
int v10; // $s0
float v13; // $f12
float v14; // $f0
float v15; // $f0
float v16; // $f20
int v18; // $v0
int v19; // $a0
Data *v29[2]; // [sp+10h] [-10h]
v29[1] = v6;
__asm
{
sd $s0, 0x20+var_20($sp)
sd $ra, 0x20+var_18($sp)
}
*(float *)v29 = v5;
_$F0 = a2;
if ( a2 <= 0.0 )
{
__asm { ld $s0, 0x20+var_20($sp) }
goto LABEL_13;
}
v10 = *(_DWORD *)(a1 + 32);
_$F20 = 20.0; // mtc1 zero, f20
// 0033DBA4 00 00 F4 C5
// 0033DBA4 00 A0 80 44
__asm { min.s $f20, $f0, $f20 }
sub_344EA4(v10, 0.0);
sub_168D5C();
v13 = -_$F20;
if ( (*(_DWORD *)(v10 + 1136) & 0x10) == 0 )
v13 = 0.0;
if ( v13 != 0.0 )
{
v14 = s_Data.player_life - (float)((float)(int)(float)(s_Data.player_life / 20.0) * 20.0);
if ( v14 > 0.0 )
v13 = -v14;
}
ChangeHP(v29[0], v13);
v16 = v15;
sub_29157C();
sub_2916D4(v16);
if ( v16 > 0.0 )
{
__asm { ld $s0, 0x20+var_20($sp) }
LABEL_13:
__asm { ld $ra, 0x20+var_18($sp) }
return ((int (*)(void))_$RA)();
}
v18 = sub_350988();
v19 = v10;
if ( v18 )
{
__asm
{
ld $ra, 0x20+var_18($sp)
ld $s0, 0x20+var_20($sp)
}
return sub_326128(v19);
}
else
{
__asm
{
ld $ra, 0x20+var_18($sp)
ld $s0, 0x20+var_20($sp)
}
return sub_33E158(v19);
}
}
于是可以在这里下断点,当玩家受到攻击时这里的断点会被触发,印证了我们上面的推测。
外挂编写
知道扣除血量代码的所在位置之后,我们可以对这里的代码进行修改,从而达到锁定血量的效果。
这里直接把伤害数值从 20.0
修改为 0.0
即可。
具体而言就是将 0x33DBA4
处的浮点数加载指令 lwc1 f20, (t7)
修改为 mtc1 zero, f20
。
PCSX2
模拟器提供了 pnach
作弊脚本,可以直接使用脚本修改内存中对应的字节码。
将下面的代码另存为 7571AAEE.pnach
即可,这里的 7571AAEE
对应游戏文件的 CRC
校验码:
gametitle=Ape Escape 3 (USA)
comment=Patches by Byaidu
// Disable HP Decrease
patch=1,EE,0033DBA4,word,4480A000
关卡逻辑
游戏中涉及到非常复杂的机关设计,这是很难完全使用 C++
进行实现的。
因此游戏采用了 Lua
脚本对场景中的对象进行管理。
在游戏启动时,程序会调用位于 0x21A93C
和 0x3854C8
的两个函数来注册 Lua
脚本中的自定义函数。
int SetupBindLauncher()
{
_DWORD *v0; // $s0
_DWORD *v1; // $s0
_DWORD *v2; // $s0
_DWORD *v3; // $s0
_DWORD *v4; // $s0
_DWORD *v5; // $s0
int v7; // $a0
v0 = (_DWORD *)malloc(0x30u);
CreateBind(v0, "BindTester");
*v0 = off_6ADF78;
sub_255DA4((int)v0);
v1 = (_DWORD *)malloc(0x30u);
CreateBind(v1, "createFeatureLauncher");
*v1 = s_SetSelectFortune;
sub_255DA4((int)v1);
v2 = (_DWORD *)malloc(0x30u);
CreateBind(v2, "createWarpGateLauncher");
*v2 = s_SetSelectStage;
sub_255DA4((int)v2);
v3 = (_DWORD *)malloc(0x30u);
CreateBind(v3, "createTutorialExit");
*v3 = s_SetSelectTutorial;
sub_255DA4((int)v3);
v4 = (_DWORD *)malloc(0x30u);
CreateBind(v4, "createMGSGenerator");
*v4 = off_6ADF00;
sub_255DA4((int)v4);
v5 = (_DWORD *)malloc(0x30u);
CreateBind(v5, "LocaleInfo_getID");
__asm { ld $ra, var_s8($sp) }
v7 = (int)v5;
*v5 = off_6ADF18;
__asm { ld $s0, var_s0($sp) }
return sub_255DA4(v7);
}
int SetupBindFlag()
{
_DWORD *v0; // $s0
_DWORD *v1; // $s0
_DWORD *v2; // $s0
_DWORD *v3; // $s0
_DWORD *v4; // $s0
_DWORD *v5; // $s0
_DWORD *v6; // $s0
_DWORD *v7; // $s0
_DWORD *v8; // $s0
_DWORD *v9; // $s0
_DWORD *v10; // $s0
int v12; // $a0
v0 = (_DWORD *)malloc(0x30u);
CreateBind(v0, "gw_set_flag");
*v0 = off_6B70F0;
sub_255DA4((int)v0);
v1 = (_DWORD *)malloc(0x30u);
CreateBind(v1, "gw_get_flag");
*v1 = off_6B70D8;
sub_255DA4((int)v1);
v2 = (_DWORD *)malloc(0x30u);
CreateBind(v2, "gw_get_string");
*v2 = off_6B70C0;
sub_255DA4((int)v2);
v3 = (_DWORD *)malloc(0x30u);
CreateBind(v3, "lw_set_flag");
*v3 = off_6B7090;
sub_255DA4((int)v3);
v4 = (_DWORD *)malloc(0x30u);
CreateBind(v4, "lw_get_flag");
*v4 = off_6B7078;
sub_255DA4((int)v4);
v5 = (_DWORD *)malloc(0x30u);
CreateBind(v5, "area_set_flag");
*v5 = off_6B7060;
sub_255DA4((int)v5);
v6 = (_DWORD *)malloc(0x30u);
CreateBind(v6, "area_get_flag");
*v6 = off_6B7048;
sub_255DA4((int)v6);
v7 = (_DWORD *)malloc(0x30u);
CreateBind(v7, "get_costume_possess");
*v7 = off_6B7018;
sub_255DA4((int)v7);
v8 = (_DWORD *)malloc(0x30u);
CreateBind(v8, "get_costume_possess_ninja");
*v8 = off_6B7000;
sub_255DA4((int)v8);
v9 = (_DWORD *)malloc(0x30u);
CreateBind(v9, "stage_var_get_int");
*v9 = off_6B70A8;
sub_255DA4((int)v9);
v10 = (_DWORD *)malloc(0x30u);
CreateBind(v10, "is_survival_mode");
__asm { ld $ra, var_s8($sp) }
v12 = (int)v10;
*v10 = off_6B7030;
__asm { ld $s0, var_s0($sp) }
return sub_255DA4(v12);
}
在每个关卡文件夹下都可以找到 area.luc
文件,用 file
看一下可以知道这是 Lua 5.0 Bytecode
文件,这里使用 unluac 对其进行反编译:
script_description = "area script 0.2"
function brk_wbox_l(a_name, se_name, num, a_spawn)
lua_param({namespace = a_name})
lua_param({len_add_sensor = 10000})
lua_param({len_add_col = 10000})
new_ent("Breakable", {
name = a_name,
model_name = "wbox_l",
tag_name = a_name,
effect_after_broken = "fx_stg_com_smoke_boxL",
model_name_particle_0 = "a_wes_d_box_piece1",
model_name_particle_1 = "a_wes_d_box_piece2",
model_name_particle_2 = "a_wes_d_box_piece3",
shake_force = 0.2,
particle_num = 6,
flg_save = false,
piece_desc_namespace = "piece_00"
})
new_ent("Controller", {name = se_name}, c_order(1, c_ctrl("trig_wait", {name = a_name, flag = true}), c_ctrl("se", {
name = "st_brk_woodbox",
target_name = a_name
}), c_order(num, c_ctrl("message_b", {
message = "spawn",
target_name = a_name,
param = a_spawn
}))))
end
lua_param({namespace = "piece_00"})
lua_param({
piece_type = "CUBIC",
piece_num = 9,
piece_model_0 = "wbox_l_p0",
piece_model_1 = "wbox_l_p1",
center_altitude = 20,
piece_size = 13,
piece_scale = 25 / 13
})
function setupArea(opts)
if true then
repeat
new_ent("Player", {
name = "player",
tag_name = opts.spawn_tag
})
new_ent("Monkey", {name = "ape_cty_01", tag_name = "ape_01"})
new_ent("Monkey", {name = "ape_cty_02", tag_name = "ape_02"})
new_ent("Monkey", {
name = "ape_cty_03a",
tag_name = "ape_03b"
})
new_ent("Monkey", {name = "ape_cty_19", tag_name = "ape_cty_19"})
new_ent("DemoLauncher", {
name = "intro_cty_a",
intro = true,
flg_save = true,
cam_1 = "op_cam3_cty_a_path",
cam_2 = "op_cam2_cty_a_path",
cam_3 = "op_cam1_cty_a_path",
time_enabled = 0,
time_disabled = 0
}, {
"ape_cty_02",
"ape_cty_03b",
"ape_cty_19",
"ape_cty_04"
})
lua_param({
namespace = "democam_superape_cty_a"
})
lua_param({
cam_1 = "democam_cut_cty_a_path",
cam_2 = "democam_cut_pan_cty_a_path",
cam_3 = "democam_super2_cty_a_path"
})
new_ent("DemoLauncher", {
name = "democam_superape_cty_a",
watch_trigger_name = "sw_kabe_cty_a",
cam_1 = "democam_cut_cty_a_path",
cam_2 = "democam_cut_pan_cty_a_path",
cam_3 = "democam_super1_cty_a_path",
time_enabled = 0.2,
flg_katinko = true,
gflg_save = true,
time_disabled = 1.6
}, {
"ape_cty_03a",
"ape_cty_03b",
"ape_cty_19",
"ape_cty_04"
})
if get_capture_flag(29) == false then
l_ratio_v_pl_commin = 0.8
lua_param({
namespace = "monkey_car_00"
})
lua_param({ratio_v_pl_comming = l_ratio_v_pl_commin})
new_ent("Car", {
name = "monkey_car_00",
model_name = "a_bay_b_sarucar_y",
path_name = "a_cty_a_sarucar_path",
monkeycar = true,
init_pos = 1,
v_runaway_ratio = 1,
a_runaway_ratio = 0.5,
hp = 3,
ape_name = "ape_cty_05"
})
end
if get_capture_flag(28) == false then
l_ratio_v_pl_commin = 0.9
lua_param({
namespace = "monkey_car_03"
})
lua_param({ratio_v_pl_comming = l_ratio_v_pl_commin})
new_ent("Car", {
name = "monkey_car_03",
model_name = "a_bay_b_sarucar_y",
path_name = "a_cty_a_sarucar_path",
monkeycar = true,
init_pos = 12,
v_runaway_ratio = 1,
a_runaway_ratio = 0.5,
hp = 3,
ape_name = "ape_cty_04"
})
end
...
end
其中 Breakable
描述可以打破的箱子,VehicleCar
描述可以乘坐的载具,ChangeArea
描述不同地点直接的切换,Collidable
描述具有碰撞体的踏板,Button
描述可以被触发的按钮。
控制电车运动的代码片段如下,两辆电车 train0
和 train1
先加速运行 1 s
,然后匀速运动 4.5 s
,再减速运行 0.75 s
,接下来改变方向循环往复:
new_ent("Controller", {
name = "se_train_cty_a"
}, c_order(1, c_ctrl("value", {
entity_name = "se_train_cty_a",
func_name = "reqLim",
argv0 = 0,
argv1 = 6.25
}), c_ctrl("value", {
entity_name = "se_train_cty_a",
func_name = "reqImm",
argv0 = 0
}), c_order(-1, c_ctrl("value", {
entity_name = "se_train_cty_a",
func_name = "reqVelocity",
argv0 = 1
}), c_ctrl("wait", {time = 1}), c_ctrl("se", {
name = "st_tra_towntrain_run",
target_name = "train0"
}), c_ctrl("se", {
name = "st_tra_towntrain_run",
target_name = "train1"
}), c_ctrl("wait", {time = 4.5}), c_ctrl("se", {name = "stopAll", target_name = "train0"}), c_ctrl("se", {name = "stopAll", target_name = "train1"}), c_ctrl("wait", {time = 0.75}), c_ctrl("value", {
entity_name = "se_train_cty_a",
func_name = "reqVelocity",
argv0 = -1
}), c_ctrl("wait", {time = 1}), c_ctrl("se", {
name = "st_tra_towntrain_run",
target_name = "train0"
}), c_ctrl("se", {
name = "st_tra_towntrain_run",
target_name = "train1"
}), c_ctrl("wait", {time = 4.5}), c_ctrl("se", {name = "stopAll", target_name = "train0"}), c_ctrl("se", {name = "stopAll", target_name = "train1"}), c_ctrl("wait", {time = 0.75}))))
作弊代码
在游戏标题界面按下 L1+L2+R1+R2
可以进入作弊界面,通过输入密码来触发特定的功能。
可以在程序中找到 passwd_chip_full
等相关字符串:
执行 grep -r passwd
可以找到相关的文件 debug/us/static/common_txt.bin
。
完整的作弊码列表如下:
Name | Password | Function |
---|---|---|
passwd_ape0 | grobyc | Unlock SAL-1000 |
passwd_ape1 | blackout | Unlock Dark Master |
passwd_ape2 | redmon | Unlock Pipotron Red |
passwd_ape3 | coolblue | Unlock Pipotron Blue |
passwd_ape4 | yellowy | Unlock Pipotron Yellow |
passwd_ape5 | 2nd man | Unlock Shimmy |
passwd_ape6 | krops | Unlock Spork |
passwd_ape7 | SAL3000 | Unlock SAL-3000 |
passwd_mgs | MESAL | Get Mesal Gear |
passwd_apethrow | MonkeyToss | Get Super Monkey Throw Stadium |
passwd_apefirst | KUNGFU | Get Ultim-ape Fighter |
passwd_mgs_theater | 2 snakes | Get Movie Tape And Movie File |
passwd_ratchet | AEAcademy | Get Special Movie Tape |
passwd_millimon | millimon | Get Mystery Movie Tape |
show_survival | survive | Get Survival Mode |
passwd_chip_full | RICH | Get 9999 Gotcha Coins |
passwd_mecha_full | Gadget | Get All Gadgets |
passwd_costume_full | Transforms | Get All Morphs |
password_type_a | ARAKURE | Unlock Wild West Town Stage |
password_type_b | NINNIN | Unlock Emperor's Castle Stage |
password_type_c | DANSU | Unlock Mirage Town Stage |
password_type_d | ATAMAYARAHASAKANA | Unlock All Three Morphs |