Hi! As the first article of the current blog, today we’ll try to get unlimited moves on King’s Candy Crush Soda Saga. Although my main language is Spanish, I’ll try to publish every article in both languages, Spanish and English; I’lltry my best, sorry for my bad English!
Spanish version / Versión en español: http://reversingyourcode.blogspot.com.es/2015/01/movimientos-ilimitados-en-candy-crush.html
Spanish version / Versión en español: http://reversingyourcode.blogspot.com.es/2015/01/movimientos-ilimitados-en-candy-crush.html
Let’s get started! We first inspect the APK with Java Decompiler (dex2jar -> Java Decompiler)
There are many interesting classes:
//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 paramBundle, int paramInt)
{
this.mUncaughtExceptionWriter = new UncaughtExceptionWriter(getApplicationContext());
super.onCreate(paramBundle);
this.mDisplay = getWindow().getWindowManager().getDefaultDisplay();
GameLib.mContext = this;
WebViewHelper.mActivity = this;
this.mView = new GameView(this, this.mForceQuitWhenDone, this.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(this, this.mDisplay);
this.mNativeApplication = new NativeApplication();
this.mNativeApplication.create(paramInt, this, getApplicationContext());
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();
…
We see many references to native methods, and an interesting LoadLibrary(“stritz”); besides this, there isn’t any other interesting thing, nor any game logic controller, so we assume that the game logic must be outside ofthe dex file, maybe in the referenced native library. Next we unpack the APK with apktool, and we see that in the directory “lib/armeabi-v7a” there’s a cool native library named “libstritz.so”. We get IDA Pro to startexamining it.
At first glance, we see a enormous number of functions, as many statically linked libraries; no obfuscation and every method are with their symbol in the export table!!!
We see some interesting classes: Switcher, Switcher::GameCommunicator, SugarCrushView, Switcher::GameMode (abstract), CStritzGameModeFactory, and various xxxxxxGameMode.
In an initial analysis we can see a complex and hierarchycal structure of classes and interfaces, and many design patterns (Factory, observer…). We also see a event driven design, with many interesting functions in the formof “OnXXXX”.
In a more detailed analysis, we see that one of the main controllers is the Switch class, where the different GameMode are managed. At first glance, it looks that many of the game logic is implemented in the GameModeclasses.
We search for interesting methods of GameMode classes:
IsCompleted(void)
IsFailed(void)
GetFailReason(void)
GetWinReason(void)
IsSugarCrushAllowed(void)
IsSugarCrushCompleted(void)
OnSuccessfulSwitch(Switcher::SwapInfo *)
OnUnsuccessfulSwitch(Switcher::Item *,Switcher::Item *)
We also search for references to “MovesLeft”, “DecreaseMovesLeft” in the function names:
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
We see logic game states defined:
Switcher::LogicState::INIT 007606CC
Switcher::LogicState::SUGAR_CRUSH 007606D0
Switcher::LogicState::SHUFFLE 007606D8
Switcher::LogicState::COMPLETE 007606DC
Switcher::LogicState::FAIL 007606E0
We starting with an interesting function with the name “DecreaseNumberOfMovesLeft”. We disassemble the function 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
We see that this function is very simple: it loads in R2 the value at (this+0x58) (R0 = this), next add the value of R1 (int parameter of the function) and next, stores the result at the same place (this+0x58). Before finishing, itcalls this->ShowMoves(). It looks interesting, so we try to replace the subtract instruction (RSB R1,R1,R2) by one equivalent to NOP. We run the game, and see that the counter is not decreasing, so it looks that weaccomplished our target. But, when we make the allowed moves, we lose, so we assume that there must be other internal counter besides the GUI counter we just killed. As this counter is not the ours, we undo the changes.
In the next stage, we focus on the GameMode objects, that looks that contain the game logic. There aren’t many methods into them, so we focus in the following:
IsSugarCrushAllowed(void)
IsSugarCrushCompleted(void)
OnSuccessfulSwitch(Switcher::SwapInfo *)
OnUnsuccessfulSwitch(Switcher::Item *,Switcher::Item *)
IsSugarCrushAllowed must have any check to determine if we can move or not, so we disassemble 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)
We see that this function only checks that the value at (this+8) is greater than 0, where it returns 1; else it returns 0. This is probably the counter that we are looking for, so we’ll find any reference in the methods abovementioned.
We found a very interesting one at 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 *)
It loads the value, compares it with 0, and if it’s greater than 0, subtracts 1 to it and stores it in the same place. We look for the offset of the SUBGT instruction, that’s 0x323FAC, and we replace that 1 by a 0 so it subtracts 0:
We save the binary, repack the APK, sign it, install it and run it. Effectively, now we have infinite moves! As the GUI counter and the internal counter are independent, we see the counter decreasing, but when it reaches 0, itcomes to -1 and so on xD
We continue with every other game modes, patching every xxxxxGameMode::OnSuccessfulSwitch function.
We go with 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 *)
The same as before. This time the offset is 0x326AAC, and we need to replace the 1 byte by a 0.
GiantBearsGameMode is identical to the previous, so I skip the details; search the SUBGT offset, replace 1 by 0 and so on.
We repeat the same procedure with HoneyGameMode, CFloatingNutsMode and CChocolateNemesisGameMode;
And we finished!! Enjoy Candy Crush Soda with infinite moves!!