ZenHAX

Free Game Research Forum | Official QuickBMS support | twitter @zenhax | SSL HTTPS://zenhax.com
It is currently Thu Oct 29, 2020 7:37 am

All times are UTC




Post new topic  Reply to topic  [ 9 posts ] 
Author Message
PostPosted: Sat Apr 11, 2020 8:47 am 

Joined: Sat Apr 11, 2020 7:48 am
Posts: 4
Is there someone can help me do reverse engineering on the savegames of Thimbleweed Park?
Here is some information about it: https://blog.thimbleweedpark.com/savegame

I'm the author of engge https://github.com/scemino/engge, this is an open source game engine which is able run Thimbleweed Park, an awesome adventure game by Ron Gilbert.
Now the big limitations are the savegames.
It would be very nice to be able to load and save the original savegames. With your help, maybe it's possible.
Thank you


Top
   
PostPosted: Sat Apr 11, 2020 12:11 pm 

Joined: Sat Apr 11, 2020 7:48 am
Posts: 4
Good news: I have made some progress, I bypassed the function which encrypts the data and I get the same structure as in a ggpack file (the one starting with the signature 0x04030201
Here is an example of 1 savegame converted to json, I had to remove some parts due to the size of the content
Code:
{
  "actors": {
  "bankmanager": {
      "_costume": "BankMgrAnimation",
      "_dir": 2,
      "_lockFacing": 0,
      "_pos": "{541,61}",
      "_roomKey": "Bank",
      "defaultVerb": 3,
      "detective": 0,
      "dialog": null,
      "enterWalk": 10,
      "flags": 3158024,
      "gender": 3145728,
      "last_selected": 0,
      "name": "@30103",
      "rambleTID": 0,
      "sawRaysBadge": 0,
      "sawReyesBadge": 0,
      "selectable": 0
    },
    // other actors,
  },
  "callbacks": {
    "callbacks": [],
    "nextGuid": 8000000
  },
  "currentRoom": "Bridge",
  "dialog": {},
  "easy_mode": 1,
  "gameGUID": "",
  "gameScene":
  "actorsSelectable": 0,
    "actorsTempUnselectable": 0,
    "forceTalkieText": 0,
    "selectableActors": [
      {
        "_actorKey": "ray",
        "selectable": 0
      },
      // etc.
      ]
  },
  "gameTime": 1003.47,
  "globals": {
  {
    "abducted_agent": null,
    "abducted_agent_seen": 0,
    "act1": 1,
    "act2": 0,
    "act2_delores_intro": 0,
    "act2_franklin_intro": 0,
    "act2_ransome_intro": 0,
    "act3": 0,
    "act4": 0,
    "act4_ransome_done": 0,
    "act4_ray_done": 0,
    "act4_reyes_done": 0,
    "activeRatHole": {
      "_objectKey": "bigtopHole4"
    },
    "actorGreetingTID": 10000375,
    "agent_kidnapped": 0,
    "agent_needs_dime": 0,
    // etc.
    },
  "inputState": 101,
  "inventory": {
    "slots":
    [
      {
        "objects": [
          "raysBadge",
          "raysNotebook",
          "cellPhone"
        ],
        "scroll": 0
      },
      // etc.
      ]
  },
  "objects":
  "aStreetArcadeDoorWF": {
      "flags": 1073742912,
      "name": "@29000"
    },
    // etc.
    },
  "rooms": {
    "AStreet": {
      "background": "AStreet",
      "speck_of_dust": 1,
      "speck_of_dust_collected": 0
    },
    // etc.
    },
  "savebuild": 958,
  "savetime": 1586606075,
  "selectedActor": "boris",
  "version": 2
}


Top
   
PostPosted: Sun Apr 12, 2020 6:24 am 
User avatar

Joined: Sat Dec 27, 2014 8:49 pm
Posts: 188
We would need some saved game files and generally the game exe / dll's (if any are used) to be able to assist.

_________________
My personal site: http://atom0s.com
Donations can be made via Paypal: Click Here


Top
   
PostPosted: Sun Apr 12, 2020 7:25 am 

Joined: Sat Apr 11, 2020 7:48 am
Posts: 4
atom0s wrote:
We would need some saved game files and generally the game exe / dll's (if any are used) to be able to assist.


You're totally right you can find some savegames here http://www.mediafire.com/file/pck64rym4 ... rk.7z/file

The game is in Gog or steam https://www.gog.com/game/thimbleweed_pa ... gJr7fD_BwE and https://store.steampowered.com/app/5698 ... weed_Park/


Top
   
PostPosted: Sun Apr 12, 2020 8:04 am 
User avatar

Joined: Sat Dec 27, 2014 8:49 pm
Posts: 188
The game is using TEA encryption for the saved files with some extra checking afterward.

Decryption is done via:
Code:
void __cdecl sub_4D9710(_DWORD *a1, signed int a2, int a3)
{
  int v3; // ecx
  unsigned int *v4; // edx
  unsigned int v5; // eax
  int v6; // esi
  unsigned int v7; // edi
  unsigned int v8; // ebx
  unsigned int v9; // edx
  int v10; // esi
  int v11; // eax
  unsigned int v12; // edx
  int v13; // esi
  int v14; // eax
  bool v15; // zf
  _DWORD *v16; // edx
  int v17; // edi
  int v18; // ebx
  unsigned int v19; // eax
  unsigned int v20; // ecx
  int v21; // ebx
  int v22; // esi
  int v23; // edi
  unsigned int *v24; // [esp+Ch] [ebp-10h]
  _DWORD *v25; // [esp+Ch] [ebp-10h]
  unsigned int v26; // [esp+10h] [ebp-Ch]
  int i; // [esp+10h] [ebp-Ch]
  int v28; // [esp+14h] [ebp-8h]
  int v29; // [esp+14h] [ebp-8h]
  int v30; // [esp+18h] [ebp-4h]
  unsigned int v31; // [esp+28h] [ebp+Ch]
  int v32; // [esp+28h] [ebp+Ch]

  if ( a2 <= 1 )
  {
    if ( a2 < -1 )
    {
      v16 = a1;
      v17 = -a2 - 1;
      v29 = -a2 - 1;
      v18 = 0x9E3779B9 * (52 / -a2 + 6);
      v19 = *a1;
      v25 = &a1[-a2 - 1];
      v32 = 0x9E3779B9 * (52 / -a2 + 6);
      do
      {
        v20 = v18;
        v21 = v17;
        for ( i = (v20 >> 2) & 3; v21; --v21 )
        {
          v22 = v16[v21 - 1];
          v23 = (16 * v22 ^ (v19 >> 3)) + ((v16[v21 - 1] >> 5) ^ 4 * v19);
          v16 = a1;
          v16[v21] -= ((v32 ^ v19) + (v22 ^ *(_DWORD *)(a3 + 4 * (i ^ v21 & 3)))) ^ v23;
          v19 = a1[v21];
        }
        v16 = a1;
        *v16 -= ((v32 ^ v19) + (*v25 ^ *(_DWORD *)(a3 + 4 * (i ^ v21 & 3)))) ^ ((16 * *v25 ^ (v19 >> 3))
                                                                              + ((*v25 >> 5) ^ 4 * v19));
        v15 = v32 == 0x9E3779B9;
        v18 = v32 + 0x61C88647;
        v19 = *a1;
        v17 = v29;
        v32 += 0x61C88647;
      }
      while ( !v15 );
    }
  }
  else
  {
    v3 = 0;
    v4 = a1;
    v28 = 52 / a2 + 6;
    v5 = a1[a2 - 1];
    v6 = a2 - 1;
    v24 = &a1[a2 - 1];
    v31 = a1[a2 - 1];
    v26 = v6;
    do
    {
      v7 = 0;
      v30 = v3 - 0x61C88647;
      v8 = ((unsigned int)(v3 - 0x61C88647) >> 2) & 3;
      if ( v6 )
      {
        do
        {
          v9 = v4[v7 + 1];
          v10 = (16 * v31 ^ (v9 >> 3)) + ((v5 >> 5) ^ 4 * v9);
          v11 = (v30 ^ v9) + (v31 ^ *(_DWORD *)(a3 + 4 * (v8 ^ v7 & 3)));
          v4 = a1;
          v4[v7] += v11 ^ v10;
          v5 = a1[v7++];
          v31 = v5;
        }
        while ( v7 < v26 );
      }
      v12 = *v4;
      v13 = (16 * v31 ^ (v12 >> 3)) + ((v5 >> 5) ^ 4 * v12);
      v3 -= 0x61C88647;
      v14 = (v30 ^ v12) + (v31 ^ *(_DWORD *)(a3 + 4 * (v8 ^ v7 & 3)));
      v4 = a1;
      *v24 += v14 ^ v13;
      v15 = v28-- == 1;
      v5 = *v24;
      v6 = v26;
      v31 = *v24;
    }
    while ( !v15 );
  }
}

char __cdecl sub_4D95E0(void *Src, size_t Size, int a3, int a4, int a5)
{
  char result; // al
  _DWORD *v6; // eax
  _DWORD *v7; // esi
  unsigned int v8; // eax
  signed int v9; // edi
  int v10; // edx
  int v11; // ebx
  int v12; // ecx
  int v13; // edi
  int v14; // eax
  char *Srca; // [esp+10h] [ebp+8h]
  size_t Sizea; // [esp+14h] [ebp+Ch]

  if ( !Src || !a5 )
    return 0;
  if ( (signed int)Size % 8 )
    return 0;
  v6 = malloc(Size);
  v7 = v6;
  if ( v6 )
    memcpy(v6, Src, Size);
  sub_4D9710(v7, (signed int)Size / -4, a5);
  v8 = *((unsigned __int8 *)v7 + Size - 1);
  v9 = Size - v8 - 9;
  Sizea = Size - v8 - 9;
  if ( v8 > 8 || v9 <= 0 )
    goto LABEL_23;
  v10 = 0;
  Srca = (char *)0x6583463;
  v11 = 0;
  v12 = 0;
  if ( v9 >= 2 )
  {
    v13 = v9 - 1;
    do
    {
      v10 += *((unsigned __int8 *)v7 + v12);
      v14 = *((unsigned __int8 *)v7 + v12 + 1);
      v12 += 2;
      v11 += v14;
    }
    while ( v12 < v13 );
    v9 = Sizea;
  }
  if ( v12 < v9 )
    Srca = (char *)(*((unsigned __int8 *)v7 + v12) + 106443875);
  if ( &Srca[v11 + v10] != (char *)(*((unsigned __int8 *)v7 + v9) | ((*((unsigned __int8 *)v7 + v9 + 1) | (*(unsigned __int16 *)((char *)v7 + v9 + 2) << 8)) << 8)) )
  {
LABEL_23:
    if ( v7 )
      free(v7);
    result = 0;
  }
  else
  {
    *(_DWORD *)a3 = v7;
    *(_DWORD *)a4 = v9;
    result = 1;
  }
  return result;
}


The TEA key is:
Code:
const uint8_t key[] = { 0xF3, 0xED, 0xA4, 0xAE, 0x2A, 0x33, 0xF8, 0xAF, 0xB4, 0xDB, 0xA2, 0xB5, 0x22, 0xA0, 0x4B, 0x9B };


This will decrypt the files back to a GGData object.

_________________
My personal site: http://atom0s.com
Donations can be made via Paypal: Click Here


Top
   
PostPosted: Sun Apr 12, 2020 8:09 am 
User avatar

Joined: Sat Dec 27, 2014 8:49 pm
Posts: 188
From there, the game then checks for the file marker:
Code:
01 02 03 04

Code:
int __cdecl sub_4C1EB0(int a1, int a2)
{
  _BYTE *v3; // ecx
  _DWORD *v4; // eax
  int v5; // [esp+0h] [ebp-Ch]
  int v6; // [esp+4h] [ebp-8h]
  int v7; // [esp+8h] [ebp-4h]

  if ( !a1 )
    return 0;
  if ( *(_DWORD *)(a1 + 20) > 4 )
  {
    v3 = *(_BYTE **)(a1 + 16);
    if ( *v3 == 1 && v3[1] == 2 && v3[2] == 3 && v3[3] == 4 )
      return sub_4C1F30((_DWORD *)a1, a2);
  }
  v4 = sub_456A10(a1);
  v5 = 0;
  v6 = 0;
  v7 = 0;
  if ( !v4 )
    return 0;
  return sub_4C1BC0((int)&v5, (int)v4, a2, 0);
}


If that matches it looks like it parses the data as a GGArray<GGString* >.
Code:
int __cdecl sub_4C1F30(_DWORD *a1, int a2)
{
  _DWORD *v2; // esi
  int v3; // eax
  int v4; // eax
  _DWORD *v5; // esi
  signed int v6; // ecx
  char *v7; // eax
  signed int v9; // eax
  signed int v10; // eax
  signed int v11; // eax
  signed int v12; // eax
  int v13; // eax
  int v14; // ebx
  char v15; // cl
  int i; // eax
  _DWORD *v17; // eax
  _DWORD *v18; // edi
  int v19; // ecx
  int v20; // ST10_4
  int v21; // esi
  void **v22; // [esp+10h] [ebp-28h]
  int v23; // [esp+14h] [ebp-24h]
  int v24; // [esp+18h] [ebp-20h]
  int v25; // [esp+1Ch] [ebp-1Ch]
  int v26; // [esp+20h] [ebp-18h]
  int v27; // [esp+24h] [ebp-14h]
  int v28; // [esp+28h] [ebp-10h]
  int v29; // [esp+34h] [ebp-4h]

  v2 = (_DWORD *)dword_6D7440;
  if ( dword_6D7440 )
  {
    v3 = *(_DWORD *)(dword_6D7440 + 8);
    if ( v3 != -1000 )
      *(_DWORD *)(dword_6D7440 + 8) = v3 - 1;
    (*(void (__thiscall **)(_DWORD *))(*v2 + 8))(v2);
    v4 = v2[2];
    if ( v4 != -1000 && v4 <= 0 )
    {
      ++dword_6E7754;
      (*(void (__thiscall **)(_DWORD *, signed int))*v2)(v2, 1);
      dword_6E7754 -= 2;
    }
  }
  v5 = a1;
  dword_6D7440 = 0;
  if ( !a1 )
    return 0;
  v6 = a1[5];
  if ( v6 > 4 )
  {
    v7 = (char *)a1[4];
    if ( *v7 != 1 || v7[1] != 2 || v7[2] != 3 || v7[3] != 4 )
    {
      sub_4C2120("bad marker: %d,%d,%d,%d", *v7, *v7 + 1, *v7 + 2, *v7 + 3);
      return 0;
    }
  }
  v24 = 1;
  v25 = 0;
  v22 = &GGArray<GGString *>::`vftable';
  v26 = 0;
  v27 = 0;
  v28 = 0;
  v23 = 2;
  v9 = a1[6];
  v29 = 0;
  if ( v9 < v6 )
    a1[6] = v9 + 1;
  v10 = v5[6];
  if ( v10 < v6 )
    v5[6] = v10 + 1;
  v11 = v5[6];
  if ( v11 < v6 )
    v5[6] = v11 + 1;
  v12 = v5[6];
  if ( v12 < v6 )
    v5[6] = v12 + 1;
  sub_4C2870(v5);
  v13 = sub_4C2870(v5);
  v14 = v5[6];
  v5[6] = v13;
  if ( v13 >= v5[5] || (v15 = *(_BYTE *)(v13 + v5[4]), v5[6] = v13 + 1, v15 != 7) )
  {
    v21 = 0;
  }
  else
  {
    for ( i = sub_4C2870(v5); i != -1; i = sub_4C2870(v5) )
    {
      v17 = (_DWORD *)sub_4D00F0((void *)(v5[4] + i));
      v18 = v17;
      if ( v17 )
      {
        v19 = v17[2];
        if ( v19 != -1000 )
          v17[2] = v19 + 1;
        (*(void (__thiscall **)(_DWORD *))(*v17 + 4))(v17);
        a1 = v18;
        sub_443D50(&v26, (unsigned int *)&a1);
      }
    }
    v20 = a2;
    v5[6] = v14;
    v21 = sub_4C2770(v5, &v22, v20);
  }
  sub_417F50();
  return v21;
}


This last part looks like it potentially ties into Squirrel scripting though while parsing the string data back from the file and loading directly into the script engine.

_________________
My personal site: http://atom0s.com
Donations can be made via Paypal: Click Here


Top
   
PostPosted: Sun Apr 12, 2020 8:17 am 

Joined: Sat Apr 11, 2020 7:48 am
Posts: 4
:o Wow I'm impressed by the quick answer.
I will have a look to the TEA encryption, thank you for your help.


Top
   
PostPosted: Sun Apr 12, 2020 8:28 am 
User avatar

Joined: Sat Dec 27, 2014 8:49 pm
Posts: 188
From the look of it, they 'serialize' the file in a manner that turns it into parts.
- A header with some basic information.
- A string index table holding all the lookups to the actual key/values.
- A string table holding the real string information.

_________________
My personal site: http://atom0s.com
Donations can be made via Paypal: Click Here


Top
   
PostPosted: Sun Apr 12, 2020 9:14 am 
User avatar

Joined: Sat Dec 27, 2014 8:49 pm
Posts: 188
Looks like you guys have the rest of what is needed done in your repo here:
https://github.com/scemino/engge/blob/m ... GGPack.hpp

For me to get this working with the save file I had to adjust a few things:
- The GGPack::readPack function needs to just directly assume the file is already decrypted after the above steps/info.
- The sig is immediately valid as the first 4 bytes due to the above.
- readHash needs to be adjusted for n_pairs == 0, this seems to be a valid case in save files so ignore throwing an exception there.
- readPack then needs to be adjusted to basically just return everything instead of just file entries.

Example of it working for me:
Image

_________________
My personal site: http://atom0s.com
Donations can be made via Paypal: Click Here


Top
   
Display posts from previous:  Sort by  
Post new topic  Reply to topic  [ 9 posts ] 

All times are UTC


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Powered by phpBB® Forum Software © phpBB Limited