Decompiling Tenchu: Stealth Assassins part 4: juggling executables
Previously in this series of articles, we’ve set up an industrial-grade reverse-engineering setup, complete with servers and extensions. How about we finally start doing something with it?
Where to start?
As can be seen on Copetti’s write-up, the PlayStation’s CPU has access to 2 MiB of RAM as well as 1 KiB of scratchpad RAM. Even taking into account the extra ~1.5 MiB of RAM tucked away inside various subsystems, that’s two orders of magnitude less than a 650 MiB CD-ROM.
Like most CD-based consoles, games on the PlayStation need to juggle data between the RAM (fast but small) and the CD-ROM (large but slow). We’ve found several executables inside Rittai Ninja Katsugeki Tenchu: Shinobi Gaisen besides the one used to start the game, but we don’t know yet how the game uses them.
Let’s figure that out.
Address space recon
On the PlayStation, the RAM is mapped from 0x80000000
to 0x801fffff
, with the stack likely located at the end of that range.
We also know that the first 64 KiB from 0x80000000
to 0x8000ffff
are reserved for the kernel, according to the Run-Time Library Overview of the official PlayStation SDK.
In order to get a feel where stuff is located, let’s build a memory map table of every section for all executables for this game:
Name | .rdata |
.text |
.data |
.sdata |
.sbss |
.bss |
---|---|---|---|---|---|---|
SLPS_019.01 |
0x80011000 0x800132bb |
0x800132bc 0x80043ec3 |
0x80043ec4 0x8004ccbf |
0x8004ccc0 0x8004cda7 |
0x8004cda8 0x8004cdaf |
0x8004cdb0 0x8004cfff |
ENDING.EXE |
0x80011000 0x800138bf |
0x800138c0 0x80047897 |
0x80047898 0x800507d3 |
0x800507d4 0x8005091f |
0x80050920 0x80050927 |
0x80050928 0x80050fff |
MAIN.EXE |
0x80011000 0x80016133 |
0x80016134 0x80086763 |
0x80086764 0x80097697 |
0x80097698 0x80097f87 |
0x80097f88 0x80097f8f |
0x80097f90 0x80097fff |
MENU.EXE |
0x80011000 0x80013abf |
0x80013ac0 0x800503e3 |
0x800503e4 0x8005b48b |
0x8005b48c 0x8005b757 |
0x8005b758 0x8005b75f |
0x8005b760 0x8005b7ff |
TRIAL.EXE |
0x80011000 0x8001593f |
0x80015940 0x80087873 |
0x80087874 0x800970af |
0x800970b0 0x8009798f |
0x80097990 0x80097997 |
0x80097998 0x80097fff |
RUN.EXE |
0x80110000 0x80110a2b |
0x80110a2c 0x8011956b |
0x8011956c 0x8011af1f |
0x8011af20 0x8011af2f |
0x8011af30 0x8011af37 |
0x8011af38 0x8011afff |
The PlayStation does not have a MMU, which means software on it run on physical memory and have full control of the console. Therefore, this memory map is merely indicative ; once it is loaded an executable is not constrained by it.
We can see that every executable is located at the same base address 0x80110000
and overlap each other, except for RUN.EXE
which is at a higher base address 0x80110000
.
In other words, RUN.EXE
and only one of the other executables can be loaded at the same time in memory.
That tells us what can co-exist in RAM at the same time, but to find out how we need to pop the hood.
Executable switcharoo
Back in part 2, we’ve determined that Rittai Ninja Katsugeki Tenchu: Shinobi Gaisen starts by booting the executable file SLPS_019.01
, as specified by SYSTEM.CNF
.
Drilling down SLPS_019.01
with Ghidra, we can see that it does the following:
- Initializes a bunch of stuff, most of which isn’t interesting for now ;
- Plays
\TENCHU\MOVIE\SME.STR
(Sony Music Entertainment logo, the publisher) ; - Plays
\TENCHU\MOVIE\ACQUIRE.STR
(Acquire logo, the developer studio) ; - Calls a function at address
0x80016e10
with one parameter,0x10
.
The last point is interesting, as this function sets up a data structure at address 0x80100000
, like so:
struct RunExeData {
uint32_t signature; // always 0xdef0c0de
char path[80];
void* stack; // always 0x801ffff0
uint32_t data; // always 0x0
};
It then loads \TENCHU\RUN.EXE
and hands off execution to it.
That program checks the signature of that data structure, then proceeds to load the executable located at path
and jump into it.
What about that parameter with the value 0x10
?
It controls what executable is to be loaded:
Process ID | File |
---|---|
0x10 |
\TENCHU\MENU.EXE |
0x11 |
\TENCHU\MAIN.EXE |
0x12 |
\TENCHU\ENDING.EXE |
0x13 |
\TENCHU\TRIAL.EXE |
Long story short, whenever the game wants to switch to another executable, it launches first \TENCHU\RUN.EXE
out of the current executable’s way, which then launches the desired executable, overwriting the original one in the process:
Note that SLPS_019.01
isn’t part of the set of executables that can be switched to, which means it serves no purpose once the game has been bootstrapped.
Splitting the game into multiple executables reduces the amount of code and data that resides inside the RAM at any point. For example, the main menu doesn’t need to be loaded in memory at the same time as the current in-game level.
Run-time musical chairs
We’ve ascertained this mechanism through static analysis, but we can also observe it at work live.
Here, I hook up GDB to DuckStation, set up a breakpoint that triggers at the entrypoint of RUN.EXE
and display out the path at 0x80100004
, then play through the first level until I die and go back to the main menu:
$ gdb-multiarch
GNU gdb (Debian 10.1-1.7) 10.1.90.20210103-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) set architecture mips:3000
The target architecture is set to "mips:3000".
(gdb) target remote :1234
Remote debugging using :1234
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
0xbfc00000 in ?? ()
(gdb) display (char*)0x80100004
1: (char*)0x80100004 = 0x80100004 ""
(gdb) hbreak *0x801193dc
Hardware assisted breakpoint 1 at 0x801193dc
(gdb) continue
Continuing.
Program received signal SIGINT, Interrupt.
0x801193dc in ?? (warning: GDB can't find the start of the function at 0x801193dc.
GDB is unable to find the start of the function at 0x801193dc
and thus can't determine the size of that function's stack frame.
This means that GDB may be unable to access that stack frame, or
the frames below it.
This problem is most likely caused by an invalid program counter or
stack pointer.
However, if you think GDB should simply search farther back
from 0x801193dc for code which looks like the beginning of a
function, you can increase the range of the search using the `set
heuristic-fence-post' command.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MENU.EXE;1"
(gdb) continue
Continuing.
Program received signal SIGINT, Interrupt.
0x801193e0 in ?? (warning: GDB can't find the start of the function at 0x801193e0.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MENU.EXE;1"
(gdb) continue
Continuing.
Program received signal SIGINT, Interrupt.
0x801193dc in ?? (warning: GDB can't find the start of the function at 0x801193dc.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MAIN.EXE;1"
(gdb) continue
Continuing.
Program received signal SIGINT, Interrupt.
0x801193e0 in ?? (warning: GDB can't find the start of the function at 0x801193e0.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MAIN.EXE;1"
(gdb) continue
Continuing.
Program received signal SIGINT, Interrupt.
0x801193dc in ?? (warning: GDB can't find the start of the function at 0x801193dc.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MENU.EXE;1"
(gdb) continue
Continuing.
Program received signal SIGINT, Interrupt.
0x801193e0 in ?? (warning: GDB can't find the start of the function at 0x801193e0.
)
1: (char*)0x80100004 = 0x80100004 "\\TENCHU\\MENU.EXE;1"
(gdb) continue
Continuing.
It’s quite janky (GDB breaks twice on the breakpoint and spits out warnings), probably because DuckStation’s GDB stub hardly gets used and is therefore subject to regressions. Despite that, we can still observe the transitions:
- From
SLPS_019.01
toMENU.EXE
, after the two company logos are displayed ; - From
MENU.EXE
toMAIN.EXE
, after selecting the first level ; - From
MAIN.EXE
toMENU.EXE
, after hittingStart
on the game over screen.
Where’s my Grand Master rank?
We’ve uncovered the mechanism the game uses to switch from one executable to another, using a fixed buffer at a predetermined address to keep track of what to launch. However, this does not cover any data that the game probably wants to keep around across executables, like settings, level scores or a ninja’s inventory items.
There are ways to figure that out, but an easy hint comes from the GameShark codes that deal with the records screen and the items selection screen.
All of them target addresses just beyond 0x80010000
, which is right after the kernel reserved memory but before the game’s executables.
Now that we know where to look, we can see what’s up with these memory addresses inside the executables.
Looking at MAIN.EXE
, one of the very first things it does is call a function at address 0x8002328c
.
That function clears a buffer 3696 bytes long at 0x80010000
with memset()
and then initializes a whole bunch of values there.
Figuring out the contents of that global structure would require further analysis outside the scope of this part, but the GameShark codes descriptions makes it obvious that this is a memory block used for book-keeping important variables, safely outside the lifecycle of the executables. Additionally, there are similar patterns of memory references across the executables, further substantiating that theory.
Conclusion
We’ve shed some lights on how the address space of the game is structured at a very high level:
0x80000000-0x8000ffff
: the kernel reserved memory, off-limits to the game ;0x80001000-0x80010e70
: important data that the game wants to keep track of outside of the various lifecycles of the executables ;0x80011000-0x80097fff
: the current executable ;0x80100000-0x8010005c
: the inter-executable buffer for passing parameters toRUN.EXE
;0x80110000-0x8011afff
: theRUN.EXE
executable.
This is not an authoritative memory map, just an indicative one. In particular, it does not contain any information about memory range lifetimes.
For example, it’s possible that the in-game executable uses the memory range of RUN.EXE
to store level data, as it is unused until RUN.EXE
must be loaded in order to switch to another executable.
We’ve also found how the game juggles between its various executables and how it keeps important data across them.