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,这个函数对解压的数据进行解析,随后将玩家信息存储到位于 0x649910Data 结构体中:

// 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_lifeGlobal 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 脚本对场景中的对象进行管理。

在游戏启动时,程序会调用位于 0x21A93C0x3854C8 的两个函数来注册 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 描述可以被触发的按钮。

控制电车运动的代码片段如下,两辆电车 train0train1 先加速运行 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

参考链接

https://apeescape.fandom.com/wiki/Ape_Escape_3

原文地址:https://www.cnblogs.com/algonote/p/15678057.html