Reversing a Playstation X Application?

Table of Contents

WGMY 2024 – RmRf

“What happened to my system? It has been working perfectly for more than 20 years.”

We are given a zip that contains a bin and cue file. If we run strings, we see that it points to a playstation/PSX program.

I extracted the ISO file by running binwalk --extract rmrf.bin. From the ISO file, we can extract the playstation EXE by using unar.

rmrf > binwalk --extract rmrf.bin
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
4888          0x1318          ISO 9660 Primary Volume,


rmrf > unar _rmrf.bin.extracted/1318.iso
_rmrf.bin.extracted/1318.iso: ISO 9660
  PROGRAM.EXE  (45056 B)... OK.
  SYSTEM.CNF  (59 B)... OK.
Successfully extracted to "1318".

rmrf > ls
1318  _rmrf.bin.extracted  rmrf.bin  rmrf.cue

rmrf > ls 1318/
PROGRAM.EXE  SYSTEM.CNF

rmrf > strings 1318/PROGRAM.EXE
# PS-X EXE
# Not Licensed or Endorsed by Sony Computer Entertainment Inc.
# Built using GCC and PSn00bSDK libraries
# [ERROR] Multiple buttons pressed at once!
# Welcome to PSX OS v0.0.1
# Initializing.......
# Loading modules....OK
# Starting services..OK
# Starting shell.....OK
# jonny
# 909321251121f77557c8c8fad239de4f
# Welcome to PSX OS v0.0.1
# PSX login:
# password:
# su root
# [jonny@psx]$
# Password:
# Invalid access to the system detected!
# Attack discovered!!
# Self destructing...!
# rm -rf /
# [root@psx]$
# The whole system was deleted...
# except for...
# wgmy{
# abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{};':",./<>?\|`~
# $Id: sys.c,v 1.140 1998/01/12 07:52:27 noda Exp yos $

We can throw this PROGRAM.EXE into IDA/Ghidra to investigate and reverse engineer the executable.

Instead of starting from the entry point which seems tedious to reverse, we can find cross references to relevant strings such as “wgmy{” which would bring us to the flag generation portion of the code.

        sub_80011158(dword_8001A590, "The whole system was deleted...\nexcept for... \n");
        memset(v13, 0, sizeof(v13));
        v14 = 0;
        ((void (__fastcall *)(_DWORD, int *, int *, int))sub_8001124C)(0, dword_80019000, v13, 32);
        sub_80011158(dword_8001A590, "wgmy{");
        sub_80011158(dword_8001A590, v13);
        sub_80011158(dword_8001A590, "}!\n");

As we can see, sub_8001124c seems to generate the flag.

int __fastcall sub_8001124C(char *ct, char *a2, char *pt, int a4)
{
  int v7; // $v0
  int i; // $v1
  char *v10; // $t1
  signed int v11; // $v1
  int v12; // $t0
  int v13; // $a2
  bool v14; // dc
  char *v15; // $s3
  int v16; // $a0
  int v17; // $v1
  char v18; // $v0
  char v19; // $a3
  char sbox[260]; // [sp+10h] [-104h] BYREF

  v7 = strlen(ct);
  for ( i = 0; i != 256; ++i )                  // init sbox
    sbox[i] = i;
  v10 = sbox;
  v11 = 0;
  v12 = 0;
  do
  {
    if ( !v7 )
      _break(7u, 0);
    v13 = (unsigned __int8)*v10;
    v11 = (ct[v12 % v7] + v13 + v11) & (unsigned int)&unk_800000FF;
    ++v12;
    if ( v11 < 0 )
      v11 = ((v11 - 1) | 0xFFFFFF00) + 1;
    *v10++ = sbox[v11];
    sbox[v11] = v13;
  }
  while ( v12 != 256 );
  v14 = a4 == 0;
  v15 = &a2[a4];
  if ( !v14 )
  {
    LOBYTE(v16) = 0;
    LOBYTE(v17) = 0;
    do
    {
      v17 = (unsigned __int8)(v17 + 1);
      v18 = sbox[v17];
      v16 = (unsigned __int8)(v18 + v16);
      v19 = *a2;
      sbox[v17] = sbox[v16];
      sbox[v16] = v18;
      ++pt;
      ++a2;
      *(pt - 1) = sbox[(unsigned __int8)(v18 + sbox[v17])] ^ v19;
    }
    while ( v15 != a2 );
  }
  return 0;
}

If you have seen implementation for RC4 encryption/decryption before, this will look familiar to you. Essentiually, it seems like the flag is encrypted with dword_80019000 being the encrypted flag and … 0 as the pointer to the key…?

Let’s look more closely at the assembly when the parameters are passed into the rc4_decrypt function.

lw      $a0, dword_8001A520
li      $a1, dword_80019000
addiu   $a2, $sp, 0x3C+var_2C
li      $a3, 0x20 
jal     sub_8001124C

Although the decompilation showed that the first parameter to the rc4_decrypt function is 0, the assembly says otherwise. Let’s rename the stuff we have identified so far to key_ptr, enc_flag and rc4_decrypt.

We are most keen in finding out what the correct key_ptr value is required to decrypt the flag.

In one of the cross references, we can find the key_ptr pointer being initialized.

sub_800110D0(dword_8001A52C, 100, &key);
int __fastcall sub_800110D0(int a1, int a2, _DWORD *a3)
{
  int result; // $v0

  result = memset(a1, 0, a2);
  *a3 = a1;
  a3[1] = a2;
  a3[2] = 0;
  return result;
}

As we can see, it seems like it initializes the key_ptr with some sort of a struct. We can define a struct as such

struct psx_str {
    char* buf;
    int max_size;
    int cur_size;
}

If we look at other cross references,

  if ( ((v0 - 1) & v0) != 0 )
  {
    sub_80016580("[ERROR] Multiple buttons pressed at once!\n");
    return 0;
  }
  result = 1;
  if ( (v2 & 0x800) == 0 )
  {
    if ( (v2 & 0x10) != 0 )
      append(&key, 0x54);
    if ( (v2 & 0x40) != 0 )
      append(&key, 0x58);
    if ( (v2 & 0x80) != 0 )
      append(&key, 0x53);
    if ( (v2 & 0x20) != 0 )
      append(&key, 0x43);
    if ( (v2 & 0x1000) != 0 )
      append(&key, 0x55);
    if ( (v2 & 0x4000) != 0 )
      append(&key, 0x44);
    if ( (v2 & 0x8000) != 0 )
      append(&key, 0x4C);
    if ( (v2 & 0x2000) != 0 )
    {
      append(&key, 82);
      *(_DWORD *)(v4 - 32688) = v2;
      return 0;
    }

It seems like its parsing console inputs and appending it to the key stream. As we can see, there are 8 possible values here, [0x54, 0x58, 0x53, 0x43, 0x55, 0x44, 0x4c].

Finally at the last interesting cross reference, we find some sort of validation for the key. This is where we start figuring out the correct key.

int __fastcall sub_800107CC(_BYTE *a1)
{
  unsigned __int16 v3; // [sp+10h] [-20h] BYREF
  __int16 v4; // [sp+12h] [-1Eh]
  __int16 v5; // [sp+14h] [-1Ch]
  __int16 v6; // [sp+16h] [-1Ah]
  __int16 v7; // [sp+18h] [-18h]
  __int16 v8; // [sp+1Ah] [-16h]
  __int16 v9; // [sp+1Ch] [-14h]
  __int16 v10; // [sp+1Eh] [-12h]
  int v11; // [sp+20h] [-10h]
  int v12; // [sp+24h] [-Ch]
  int v13; // [sp+28h] [-8h]
  int v14; // [sp+2Ch] [-4h]

  v12 = 0;
  v13 = 0;
  v14 = 0;
  v3 = 32 * MEMORY[0];
  v4 = 32 * MEMORY[1];
  v5 = 32 * MEMORY[2];
  v6 = 32 * MEMORY[3];
  v7 = 32 * MEMORY[4];
  v8 = 32 * MEMORY[5];
  v10 = 32 * MEMORY[7];
  v9 = 32 * MEMORY[6];
  v11 = (unsigned __int16)(32 * MEMORY[8]);
  MulMatrix2(dword_80019020, &v3);
  v3 *= 4;
  v4 *= 4;
  v5 *= 4;
  v6 *= 4;
  v7 *= 4;
  v8 *= 4;
  v9 *= 4;
  v10 *= 4;
  LOWORD(v11) = 4 * v11;
  if ( sub_80011D98(&v3) )
    *a1 = 1;
  else
    sub_80011158(dword_8001A590, "Invalid access to the system detected!\nAttack discovered!!\nSelf destructing...!\n");
  return 1;
}

Although the decompilation fails, we recognize that the MEMORY correspond with our key, and that there is some matrix multiplication going on with another value before it is validated.

I spent significant time trying to solve this, but without much success. Finally, knowing that the validation function checks for 9 characters, and that there are only 8 possible characters, I decided to write a brute force script to brute force the 8**9 number of permutations.

from Crypto.Cipher import ARC4
from tqdm import tqdm
from itertools import product

ct = bytes.fromhex("85BB8F174AC63EA0958916A79ED692F27E8AF6F4C2235B7B042C65E364D9945D")
possible = list("TXSCUDLR")

for a in tqdm(product(possible, repeat=9)):
    rc4 = ARC4.new("".join(a).encode())
    flag = rc4.decrypt(ct)
    try:
        print(f"\n{flag.decode()}\n"))
    except:
        continue

# output: 22954993it [02:44, 140627.19it/s]
# output: a92a70e1f4935d0a7dbb729368829848

comments powered by Disqus