Webs amigas

jueves, 8 de enero de 2015

Movimientos ilimitados en Candy Crush Soda Saga

Versión en inglés / English version: http://reversingyourcode-eng.blogspot.com.es/2015/01/unlimited-moves-in-candy-crush-soda-saga.html

¡Buenas! Como primer artículo del presente blog, hoy vamos a tratar de conseguir movimientos ilimitados en Candy Crush Soda Saga, de King. Para ello iniciamos inspeccionando el APK con Java Decompiler, tras pasarlo por dex2jar.
Inspeccionamos las clases, y de las mismas encontramos varias que son interesantes:
//com.king.candycrushsodasaga.StritzActivity
public void onCreate(Bundle paramBundle)
  {
    System.loadLibrary("stritz");
    super.setHasSplashScreen(true);
    this.mSplashView = new SplashView(this);
    super.setMinimizeInsteadOfForceQuit(true);
    super.onCreate(paramBundle, PlatformProxy.createNativeInstance(this));
    this.mFrameLayout = new FrameLayout(this);
    this.mFrameLayout.addView(this.mSplashView, new FrameLayout.LayoutParams(-1, -1));
    this.mFrameLayout.addView(getGameView(), 0, new FrameLayout.LayoutParams(-1, -1));
    setContentView(this.mFrameLayout);
    addListener(new GameActivityFacebookListener(getGameView()));
    handleIntent(getIntent());
  }

//com.king.candycrushsodasaga.PlatformProxy
public static native int createNativeInstance(StritzActivity paramStritzActivity);

//com.king.core.GameActivity.onCreate
  protected void onCreate(Bundle paramBundleint paramInt)
  {
    this.mUncaughtExceptionWriter = new UncaughtExceptionWriter(getApplicationContext());
    super.onCreate(paramBundle);
    this.mDisplay = getWindow().getWindowManager().getDefaultDisplay();
    GameLib.mContext = this;
    WebViewHelper.mActivity = this;
    this.mView = new GameView(thisthis.mForceQuitWhenDonethis.mUseSleepInLoop);
    this.mView.setFocusable(true);
    this.mView.setFocusableInTouchMode(true);
    if (!this.mHasSplashScreen)
      setContentView(this.mView);
    addListener(new GameActivityDeepLinkListener(this.mView));
    addCustomGameListeners();
    this.mSensorManager = ((SensorManager)getSystemService("sensor"));
    this.mAccelerometer = this.mSensorManager.getDefaultSensor(1);
    getWindow().addFlags(1152);
    if (Build.VERSION.SDK_INT >= 9);
    this.mRotationCompensator = new RotationCompensatedListener(thisthis.mDisplay);
    this.mNativeApplication = new NativeApplication();
    this.mNativeApplication.create(paramIntthisgetApplicationContext());
    handleIntent(getIntent());
  }

//com.king.core.NativeApplication
public class NativeApplication
{
  public native void create(int paramInt, Activity paramActivity, Context paramContext);
 
  public native void destroy();
 
  public native void init(int paramInt1, int paramInt2, int paramInt3, int paramInt4);
 
  public native void onAccelerometer(float paramFloat1, float paramFloat2, float paramFloat3);
 
  public native void onBackKeyDown();
 
//...
 
Vemos que hay muchas referencias a métodos nativos, y un LoadLibrary(stritz”); aparte de eso, no encontramos mucho más que sea de interés para nuestro cometido, ni ningún control de la lógica de juego, por lo que deducimos que el mismo ha de realizarse en alguna librería nativa. Desempaquetamos el APK con apktool, y observamos que efectivamente, en el directorio lib/armeabi-v7a, se encuentra una bonita librería nativa de 7,36MB llamada libstritz.so. Abrimos IDA Pro, y vamos a por ella.
A primera vista vemos que es enorme y tiene una ingente cantidad de funciones, así como varias librerías vinculadas estáticamente; la ofuscación brilla por su ausencia, así mismo todos los métodos se encuentran en la tabla de exportaciones con sus respectivos símbolos por lo que esto es una gran ventaja:

A primera vista, vemos algunas clases interesantes: SwitcherSwitcher::GameCommunicatorSugarCrushViewSwitcher::GameMode (abstract), CStritzGameModeFactory, y varias xxxxxxGameMode.
En un análisis inicial, podemos observar una estructura jerarquizada y compleja de clases e interfaces (OOP), varios patrones de diseño (Factory, Observeretc). Asimismo, se observa que prevalece la programación orientada a eventos, con varias funciones interesantes OnXXXXX.
En un análisis más detallado, vemos que uno de los controladores principales se trata de la clase Switcher, donde se gestionan los diferentes GameMode. A primera vista, parece que en los GameMode se define la implementación de cada modalidad de juego.
Buscamos funciones interesantes en los GameMode:
IsCompleted(void)
IsFailed(void)
GetFailReason(void)
GetWinReason(void) 
IsSugarCrushAllowed(void)
IsSugarCrushCompleted(void)
OnSuccessfulSwitch(Switcher::SwapInfo *)
OnUnsuccessfulSwitch(Switcher::Item *,Switcher::Item *)

Tambien buscamos referencias a “MovesLeft”, “DecreaseMovesLeft”:
CSpecialCandiesCreationState::GetNumExpectedMovesLeft(int,int,int) 00293C94 
Switcher::GameCommunicator::OnSugarCrushDecreaseMovesLeft(int) 00465E84 
CStritzGameModeHudPresenter::DecreaseNumberOfMovesLeft(int) 002FD374
CStritzGameModeHudPresenter::IncreaseNumberOfMovesLeft(int,float) 002FD38C
CStritzGameModeHudView::OnSugarCrushDecreaseMovesLeft(int) 002FE654
CStritzGameModeHudView::IncreaseNumberOfMovesLeft(int,float) 002FE690 
CStritzGameModeHudView::DecreaseNumberOfMovesLeft(int) 002FE910 

Vemos que se definen estados de juego:
Switcher::LogicState::INIT 007606CC 
Switcher::LogicState::SUGAR_CRUSH 007606D0 
Switcher::LogicState::SHUFFLE 007606D8 
Switcher::LogicState::COMPLETE 007606DC 
Switcher::LogicState::FAIL 007606E0

A primera vista existen varias funciones interesantes con el nombre “DecreaseNumberOfMovesLeft”. Desensamblamos la función CStritzGameModeHudView::DecreaseNumberOfMovesLeft(int):
LDR             R2, [R0,#0x58]
RSB             R1, R1, R2
STR             R1, [R0,#0x58]
B               _ZN22CStritzGameModeHudView9ShowMovesEv ; CStritzGameModeHudView::ShowMoves(void)
; End of function CStritzGameModeHudView::DecreaseNumberOfMovesLeft(int
 
Vemos que esta función simplemente carga en R2 el valor que se encuentra en this+0x58 (R0 = this), le suma el parámetro int que tiene la presente función (R1) y acto seguido almacena el resultado en this+0x58. Seguidamente antes de finalizar, realiza una llamada a this->ShowMoves(). Suena interesante, así que probamos a anular la resta reemplazando la instrucción RSB R1,R1,R2 por una equivalente a NOP. Ejecutamos el juego y vemos que el contador no avanza, por lo que en principio hemos conseguido nuestro objetivo. Pero probándolo en detalle, vemos que realmente sí que nos cuenta los movimientos, lo único que hemos anulado es la capa de presentación, por lo que deducimos que por detrás debe haber otro contador que es el que es tenido en cuenta por la lógica de juego. Como este no es el contador que buscamos, deshacemos la modificación y dejamos el ejecutable en su estado inicial.
En la siguiente etapa nos centramos en los objetos GameMode, que parecen ser los que contienen las diferentes lógicas de juego para cada uno de los modos de juego. No son muchos los métodos que contienen, así que nos centraremos en los siguientes:
IsSugarCrushAllowed(void)
IsSugarCrushCompleted(void)
OnSuccessfulSwitch(Switcher::SwapInfo *)
OnUnsuccessfulSwitch(Switcher::Item *,Switcher::Item *)

IsSugarCrushAllowed debe tener algún tipo de comprobación para determinar si podemos realizar movimiento o no podemos, así que desensamblamos SodaToTheBrimGameMode::IsSugarCrushAllowed:
; SodaToTheBrimGameMode::IsSugarCrushAllowed(void)const
EXPORT _ZNK21SodaToTheBrimGameMode19IsSugarCrushAllowedEv
_ZNK21SodaToTheBrimGameMode19IsSugarCrushAllowedEv
LDR             R0, [R0,#8]
CMP             R0, #0
MOVLE           R0, #0
MOVGT           R0, #1
BX              LR
; End of function SodaToTheBrimGameMode::IsSugarCrushAllowed(void)
 
Observamos que esta función solamente comprueba que el valor almacenado en this+8 sea mayor que 0, en cuyo caso devuelve 1, y en caso contrario devuelve 0. Vemos que es muy probable que this+8 sea el contador que buscamos, por lo que buscamos alguna referencia en los otros métodos anteriormente mencionados.
Observamos una muy interesante en OnSuccessfulSwitch:
; SodaToTheBrimGameMode::OnSuccessfulSwitch(Switcher::SwapInfo *)
EXPORT _ZN21SodaToTheBrimGameMode18OnSuccessfulSwitchEPN8Switcher8SwapInfoE
_ZN21SodaToTheBrimGameMode18OnSuccessfulSwitchEPN8Switcher8SwapInfoE
LDR             R3, [R0,#8]
CMP             R3, #0
SUBGT           R3, R3, #1
STRGT           R3, [R0,#8]
LDR             R0, [R0,#0x28]
CMP             R0, #0
BXEQ            LR
B               _ZN16CLemonadeSeaTask18OnSuccessfulSwitchEv ; CLemonadeSeaTask::OnSuccessfulSwitch(void)
; End of function SodaToTheBrimGameMode::OnSuccessfulSwitch(Switcher::SwapInfo *)
 
Vemos que lo primero que hace es cargar el mencionado valor, compararlo con 0, y en caso de ser mayor que 0, restarle 1 y almacenarlo. Buscamos el offset de la instrucción SUBGT en el binario, que resulta ser 0x323FAC, y reemplazamos ese 1 de “SUBGT R3,R3,#1” por un 0:

Simple y efectivo. Guardamos, reempaquetamos, firmamos, instalamos y ejecutamos el APK. Efectivamente, ¡ahora tenemos movimientos ilimitados! Como el contador de la capa de presentación y el contador interno son independientes, vemos que eventualmente se nos muestra en la interfaz que tenemos movimientos negativos xD
Proseguimos con el objetivo de conseguir lo mismo para cada modo de juego, parcheando el resto de funciones OnSuccessfulSwitch.
Vamos con BubbleGumGameMode:
; BubbleGumGameMode::OnSuccessfulSwitch(Switcher::SwapInfo *)
EXPORT _ZN17BubbleGumGameMode18OnSuccessfulSwitchEPN8Switcher8SwapInfoE
_ZN17BubbleGumGameMode18OnSuccessfulSwitchEPN8Switcher8SwapInfoE
LDR             R3, [R0,#8]
CMP             R3, #0
SUBGT           R3, R3, #1
STRGT           R3, [R0,#8]
BX              LR
; End of function BubbleGumGameMode::OnSuccessfulSwitch(Switcher::SwapInfo *)
 
Más de lo mismo. Esta vez el offset es 0x326AAC, reemplazamos el 1 por un 0.
Toca el turno a GiantBearsGameMode, que resulta una vez más ser un código idéntico, por lo que me salto los detalles; Buscamos el offset de la instrucción SUBGT y reemplazamos el 1 por un 0.
Repetimos exactamente el mismo procedimiento con HoneyGameModeCFloatingNutsMode y CChocolateNemesisGameMode; exactamente iguales que los anteriores.
Y con esto hemos finalizado, a disfrutar de Candy Crush Soda con movimientos ilimitados!!

2 comentarios:

  1. con los candy crush no vale la pena porque es mas facil usando el cheat engine, pero hay otros juegos en los que el cheat engine no funciona y es necesario aplicar ingenieria inversa.

    ResponderEliminar
    Respuestas
    1. Buenas!
      No puedes usar cheat engine en Android, salvo que lo ejecutes virtualizado en PC. Sin embargo existen otras alternativas, pero todas ellas requieren de haber rooteado previamente el Android, por lo que en este caso el parcheo del ejecutable es útil para dispositivos sin root.
      Saludos!

      Eliminar