RE: Stardew Valley

Stardew Valley es el simulador de granjas más popular que existe hasta la fecha. Durante un tiempo jugué la versión móvil, tenía un viejo APK guardado así que se me ocurrió intentar abrirlo y ver algunas cosas.

Índice

Introducción

En este artículo cubriré el proceso de ingeniería inversa de un juego de Unity para Android. La versión utilizada fue la 1.4.5. Se puede descargar aquí.

Herramientas utilizadas

  • apktool: Una herramienta para revertir apks. Sirve para obtener código Smali, un AndroidManifest.xml legible, etc.
  • adb: Herramienta para comunicarse con un dispositivo Android para depuración. Instalable por medio de un packet manager como apt.
  • Xamarin_XALZ_decompress.py: Script de Python para descomprimir archivos XALZ.
  • dnSpy: Desensamblador, decompilador y depurador de .NET para Windows.
  • IlSpy: Desemsamblador y decompilador .NET para Linux.
  • xnb-js: Herramienta para empacar o desempacar archivos con formato XNB.
  • keytool: Una utilidad para manejo de certificados y claves. Es parte del JDK. Instalable por medio de un packet manager como apt.
  • jarsigner: Firma y verifica archivos JAR (ZIP, APK, etc..). Es parte del JDK. Instalable por medio de un packet manager como apt.
  • makeDebuggable.py: Script de python para marcar una APK como depurable.
  • medit

Decompilación

Normalmente los juegos de Unity dependen de Mono para correr ejecutables .NET. Contienen una dll con todo el código del juego, llamada Assembly-CSharp.dll, la cual podemos decompilar y modificar directamente. Pero esto al parecer usa algo llamado ‘xamarin’:

1
2
3
4
$ ls lib/x86_64
libmono-btls-shared.so libmonosgen-2.0.so libxamarin-app.so
libmonodroid.so libopenal32.so
libmono-native.so libxa-internal-api.so

Leyendo este artículo sobre Pentesting en apps Xamarin encontré que en unknown/assemblies están las DLL .NET con el código del juego:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ file unknown/assemblies/Stardew*
unknown/assemblies/StardewValley.dll: Sony PlayStation Audio
unknown/assemblies/StardewValley.GameData.dll: Sony PlayStation Audio
$ xxd unknown/assemblies/StardewValley.dll| head
00000000: 5841 4c5a 1200 0000 00cc 5600 f203 4d5a XALZ......V...MZ
00000010: 9000 0300 0000 0400 0000 ffff 0000 b800 ................
00000020: 0100 2f40 0001 000f f32e 8000 0000 0e1f ../@............
00000030: ba0e 00b4 09cd 21b8 014c cd21 5468 6973 ......!..L.!This
00000040: 2070 726f 6772 616d 2063 616e 6e6f 7420 program cannot
00000050: 6265 2072 756e 2069 6e20 444f 5320 6d6f be run in DOS mo
00000060: 6465 2e0d 0d0a 2444 00c4 5045 0000 4c01 de....$D..PE..L.
00000070: 0300 409a fd5f 5800 e2e0 0022 200b 0130 ..@.._X...." ..0
00000080: 0000 c456 0000 0614 0060 cecc 5600 0020 ...V.....`..V..
00000090: 0a00 1057 0500 1110 0c00 1202 b800 1300 ...W............

Xamarin es una plataforma de código abierto diseñada para que los desarrolladores creen aplicaciones para iOS, Android y Windows utilizando los frameworks .NET y C#.

XALZ es un formato para almacena programas comprimidos usado por Xamarin. Se puede extraer usando esta herramienta.

1
2
3
4
5
6
7
$ ./Xamarin_XALZ_decompress.py stardew_valley/unknown/assemblies/StardewValley.dll StardewValley.dll
header index: b'\x12\x00\x00\x00'
compressed payload size: 1908108 bytes
uncompressed length according to header: 5688320 bytes
result written to file
$ file StardewValley.dll
StardewValley.dll: PE32 executable (DLL) (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections

Ya con el binario .NET lo abrimos con un decompilador como dnSpy y podemos empezar a modificar el código:

Modificación de assets

Los assets están en formato XNB, se puede usar esta herramienta para desempacarlos, modificarlos y volverlos a empacar.

Por ejemplo, vamos a modificar los diálogos del minijuego del abuelo que se muestran al iniciar la partida. La ruta es assets/Content/Strings/StringsFromCSFiles.es-ES.xnb. Usando la herramienta web obtenemos StringsFromCSFiles.es-ES.json, donde se encuentran estos mensajes:

1
2
3
4
5
6
7
8
9
10
11
12
"GrandpaStory.cs.12026": "...Y para mi querido nieto:",
"GrandpaStory.cs.12028": "...Y para mi querida nieta:",
"GrandpaStory.cs.12029": "Quiero que tengas este sobre sellado.",
"GrandpaStory.cs.12030": "No, no, no lo abras aún... Ten paciencia.",
"GrandpaStory.cs.12031": "Presta mucha atención...",
"GrandpaStory.cs.12032": "Algún día, la pesadumbre y la desesperación te asaltarán...",
"GrandpaStory.cs.12033": "...Y tu espíritu luchará por mantenerse despierto.",
"GrandpaStory.cs.12034": "Llegará un día en el que el peso de la vida moderna se convertirá en una carga...",
"GrandpaStory.cs.12035": "...Y tu espíritu alegre se desvanecerá frente a un vacío cada vez mayor.",
"GrandpaStory.cs.12036": "Cuando eso ocurra, querido, habrá llegado el momento de aceptar mi regalo.",
"GrandpaStory.cs.12038": "Cuando eso ocurra, querida, habrá llegado el momento de aceptar mi regalo.",
"GrandpaStory.cs.12040": "Ahora déjame descansar...",

Voy a cambiar el contenido de GrandpaStory.cs.12029 y empaquetarlo con la misma herramienta.

Para modificar el apk no utilicé apktool, no estoy claro por qué me falló. En su lugar usé este script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/bin/bash

# Crear directorio de trabajo
mkdir stardew_mod
cd stardew_mod

# Extraer el APK original
unzip ../stardew_valley_original.apk -d .

# ELIMINAR la carpeta META-INF (esto "des-firma" el APK)
rm -rf META-INF

# Editar asset
cp ~/Descargas/StringsFromCSFiles.es-ES.xnb assets/Content/Strings/StringsFromCSFiles.es-ES.xnb

# Empaquetar sin META-INF
zip -r ../stardew_unsign.apk * -0

# Generar un keystore si no tienes uno
keytool -genkey -v -keystore ~/.android/debug.keystore -alias debug -keyalg RSA -keysize 2048 -validity 10000

# Firmar el APK con jarsigner
# Esto crea META-INF con:
# |- MANIFEST.MF conteniendo los hashes SHA-1 de cada archivo
# |- DEBUG.SF conteniendo el hash SHA-1 de MANIFEST.MF. Firmado con la clave privada
# |- DEBUG.RSA conteniendo el certificado con la clave pública
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/.android/debug.keystore ../stardew_unsign.apk debug

# Alinear el APK firmado
zipalign -v -p 4 ../stardew_unsign.apk ../stardew_final.apk

cd ..

# Desinstalar el actual y reinstalarlo con adb
adb uninstall com.chucklefish.stardewvalley
adb install stardew_final.apk

rm stardew_unsign.apk
rm stardew_final.apk

Modificación de código

Para modificar el código podemos usar un desensamblador / decompilador .NET como dnSpy. Dado que los assemblies están empaquetados con XALZ, primero los descomprimimos:

1
2
3
4
5
6
7
8
9
#!/bin/bash

if [ ! -d "new_assemblies" ]; then
mkdir new_assemblies
fi

for file in "$1"/*.dll; do
./Xamarin_XALZ_decompress.py "$file" new_assemblies/$(basename "$file")
done

Para editar tuve que pasar a Windows por un momento. Cargamos todas las librerías en dnSpy y modificamos un método cualquiera para probar, por ejemplo, ShopMenu.ChargePlayer:

  • Hacemos click en la lupa y buscamos “Charge Player”.
  • Hacemos doble click en el resultado abajo.
  • Posicionamos el cursor sobre “ChargePlayer” en el decompilado y hacemos click derecho -> “Editar método (C#) …”
  • Eliminamos todo el cuerpo del método y le damos al botón de “Compilar” en la esquina inferior derecha. Con este cambio en cualquier compra no se nos descontará nada de la moneda usada.
  • Vamos a Archivo -> “Guardar Module…”

Modifiqué el script anterior para ahora copiar la dll modificada. No es necesario comprimirla a XALZ antes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash

# Crear directorio de trabajo
mkdir stardew_mod
cd stardew_mod

# Extraer el APK original
unzip ../stardew_valley_original.apk -d .

# ELIMINAR la carpeta META-INF (esto "desfirma" el APK)
rm -rf META-INF

# Copiar cambios
cp ../StardewValley_modified.dll assemblies/StardewValley.dll

# Empaquetar sin META-INF
zip -r ../stardew_unsign.apk * -0

# Generar un keystore si no tienes uno
keytool -genkey -v -keystore ~/.android/debug.keystore -alias debug -keyalg RSA -keysize 2048 -validity 10000

# Firmar el APK con jarsigner
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/.android/debug.keystore ../stardew_unsign.apk debug

# Alinear el APK firmado
if [ -f ../stardew_final.apk ]; then
rm ../stardew_final.apk
fi
zipalign -v -p 4 ../stardew_unsign.apk ../stardew_final.apk

cd ..
# Desinstalar el actual y reinstalarlo con adb
adb uninstall com.chucklefish.stardewvalley
adb install stardew_final.apk

Esto en la mayoría de los juegos de Unity sería suficiente. Pero en este caso provoca un crash. Viendo el resultado con adb logcat | grep stardew:

Protecciones contra modificación

El juego tiene alguna clase de protección anti-tampering, una forma de protegerse ante cambios no autorizados. En los logs se puede observar la lista de llamadas:

El error ocurre en la llamada a CheckUsingServerManagedPolicy en OnCreatePartTwo.

No revisé a fondo su funcionamiento pero se puede asumir que hace algún tipo de verificación de integridad contra los assemblies. Si eliminamos esta llamada nuestros cambios se aplicarán sin problemas al juego. En esta ocasión utilicé “Editar instrucciones IL…” en lugar de “Editar método (C#) …”, este método es más preciso si tienes dominio de CIL. Solo reemplacé ldarg.0 y call por instrucciones nop(No OPeration):



Depuración de juegos en Android sin root

En Windows y Linux tenemos CheatEngine, posiblemente el mejor escáner de memoria / depurador para modificar juegos jamás creado. En Android los equivalentes a CheatEngine están limitados por la ausencia de permisos de root en estos dispositivos. Una alternativa que encontré es medit, una herramienta de terminal que nos permite hacer cambios sencillos que en la mayoría de los casos suelen bastar.

La herramienta se ejecuta bajo los mismos privilegios que el proceso objetivo. Se debe ejecutar run-as <package> para obtener los UID/GID de la aplicación especificada y así medit tenga acceso a lectura/escritura a los archivos privados de la app. El comando run-as necesita que la aplicación tenga el atributo android:debuggable="true" en su AndroidManifest.xml. Esto se puede hacer con apkutil pero ya que estamos encontré este script que hace eso mismo.

Probemos su funcionamiento cambiando la cantidad de dinero del jugador. Agregamos primero la línea para hacerlo depurable al script y comentamos la que agrega el parche anterior:

1
2
3
4
# Reemplazar codigo
#cp ../StardewValley_modified.dll assemblies/StardewValley.dll
# Hacer depurable
../makeDebuggable.py xml AndroidManifest.xml AndroidManifest.xml

Luego seguimos las instrucciones del repositorio y vemos los cambios.

Cheats

Tuve curiosidad por los exploits que tiene el juego, como el Item Duplication Cheat. Cada objeto del juego tiene un código numérico único, y al usar hasta tres de estos códigos entre corchetes como nombre de tu personaje, harás que esos objetos aparezcan en tu inventario cada vez que se mencione tu nombre en el juego. Una lista completa de los códigos puede encontrarse tanto aquí como en el artículo anterior.

Los diálogos vulnerables más explotados son los de Gus, en base/assets/Content/Characters/Dialogue/Gus.es-ES.xnb:

1
2
3
4
5
6
{
"Saloon8": "Hola, @. ¡Me alegro de verte! Aquí siempre te recibimos encantados.",
"Saloon_Tue": "¡Hola, @! Por favor, relájate y disfruta.",
"Fri": "Hola, @. Si alguna vez tienes sed, el Salón es el lugar adecuado.",
"Sat": "¿Te gusta cocinar, @?#$e#Con una cocina y recetas puedes cocinar algunos platos muy prácticos.#$e#Y un plato hecho en casa siempre es un buen regalo.",
}

Es evidente que “@” es reemplazado con el nombre del jugador. La función que hace el parsing en este caso parece ser Dialoge.getCurrentDialoge():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public string getCurrentDialogue()
{
//IL_0230: Unknown result type (might be due to invalid IL or missing references)
if (currentDialogueIndex >= dialogues.Count || finishedLastDialogue)
{
return "";
}
showPortrait = true;
if (speaker.Name.Equals("Dwarf") && !farmer.canUnderstandDwarves)
{
return convertToDwarvish(dialogues[currentDialogueIndex]);
}
if (temporaryDialogue != null)
{
return temporaryDialogue;
}
if (dialogues.Count > 0 && dialogues[currentDialogueIndex].Contains("}"))
{
farmer.mailReceived.Add(dialogues[currentDialogueIndex].Split('}')[0]);
dialogues[currentDialogueIndex] = dialogues[currentDialogueIndex].Substring(dialogues[currentDialogueIndex].IndexOf("}") + 1);
dialogues[currentDialogueIndex] = dialogues[currentDialogueIndex].Replace("$k", "");
}
// 1. Busca cualquier diálogo que contenga un '[' y un ']'
if (dialogues.Count > 0 && dialogues[currentDialogueIndex].Contains('[') && dialogues[currentDialogueIndex].IndexOf(']') > 0)
{
// 2. Encuentra la posición del primer número entre los corchetes
int startIndex = dialogues[currentDialogueIndex].IndexOf('[') + 1;
int length = dialogues[currentDialogueIndex].IndexOf(']') - dialogues[currentDialogueIndex].IndexOf('[') - 1;
try
{
// 3. Extrae el texto que está dentro de los corchetes (ej: "74 75 76")
string text = dialogues[currentDialogueIndex].Substring(startIndex, length);
// 4. Separa los números por espacios
string[] array = text.Split(' ');
int result = -1;
// 5. Elige uno de los números al azar
if (int.TryParse(array[Game1.random.Next(array.Length)], out result))
{
// 6. Verifica si ese número es un ID de objeto válido
if (Game1.objectInformation.ContainsKey(result))
{
// 7. Si es válido, añade el objeto al inventario del jugador
farmer.addItemToInventoryBool(new Object(Vector2.get_Zero(), result, null, canBeSetDown: false, canBeGrabbed: true, isHoedirt: false, isSpawnedObject: false), makeActiveObject: true);
// 8. Actualiza la animación del personaje para mostrar que lleva un objeto nuevo
farmer.showCarrying();
}
// 9. Elimina el código del diálogo para que no se muestre al jugador
dialogues[currentDialogueIndex] = dialogues[currentDialogueIndex].Replace("[" + text + "]", "");
}
}
// ...

Conclusiones

La comunidad de Stardew Valley es enorme y hay información redundante sobre las mecánicas del juego y su modificación ¡Incluso tienen un framework para facilitar el hacer mods!

Esta tarea me vino bien porque me enfrenté a una protección de software, analicé un exploit y aprendí mucho del ecosistema Android, tema en el que sigo siendo bastante ignorante. En algún próximo artículo me gustaría cubrir el parcheo de código por medio de scripts, de tal forma que se puedan aplicar y retirar en tiempo real.

Eso es todo.

Enlaces

Stardew Valley 1.4.5, https://www.apksum.com/download/com.chucklefish.stardewvalley_1.4.5.139_free
apktool, https://github.com/ibotpeaches/apktool
Xamarin_XALZ_Decompress.py, https://github.com/x41sec/tools/blob/master/Mobile/Xamarin/Xamarin_XALZ_decompress.py
dnSpy, https://github.com/dnSpy/dnSpy/
ILSpy, https://github.com/icsharpcode/AvaloniaILSpy/releases
xnb-js, https://github.com/lybell-art/xnb-js
makeDebuggable.py, https://github.com/julKali/makeDebuggable/blob/master/makeDebuggable.py
medit, https://github.com/sterrasec/apk-medit
Xamarin Apps | Hacktricks, https://angelica.gitbook.io/hacktricks/mobile-pentesting/xamarin-apps
Mono (software), https://en.wikipedia.org/wiki/Mono_(software)
Tamper Check, https://support.preemptive.com/hc/en-us/articles/31819918306577-Tamper-Check
Common Intermediate Language (CIL), https://en.wikipedia.org/wiki/Common_Intermediate_Language
¿Alguien me puede sugerir algún equivalente de Cheat Engine para Android?, https://www.reddit.com/r/AndroidGaming/comments/1ajpbuc/can_somebody_suggest_me_any_cheat_engine/
Stardew Valley cheats | GameRadar+, https://www.gamesradar.com/stardew-valley-cheats/
Modding:Objects/Object sprites, https://stardewvalleywiki.com/Modding:Objects/Object_sprites