Phoenix Wright: Ace Attorney - Dual Destinies is one of my favourite games on the 3DS. Recently I bought the Android version since I knew that it had debug symbols and I wanted to mod the game. But when I installed it I discovered that I needed to be online to launch it, or it would just show a “Transmission Error” dialog. Since I wanted to mod the game anyways, I thought it would be a fun project to try and remove the DRM.
After getting the APK and expansion data onto my computer for static analysis, I wanted to be able to perform dynamic analysis as well. Frida is a tool for this that I’ve used before, so that’s what I used. Because I don’t have a rooted device, I had to use Frida’s “gadget” library which requires modifying the target application to load it. But after rebuilding and installing the modified application, I got a different “Signature Error” dialog.
Now that I had Frida running, I could trace what the application is doing internally. By using frida-trace to trace the Java methods that got called, I noticed that there were some interesting calls to a method named getSignature, followed by a call to showSignatureConfirmDialog:
1171 ms MTFPActivity.getSignature()
1173 ms <= "3082035b30820243a00302..."
1173 ms MTFPActivity.getSignature()
1173 ms <= "3082035b30820243a00302..."
1175 ms MTFPActivity.getSystemFontDataNum()
1175 ms <= 0
/* TID 0x757a */
1195 ms MTFPActivity.onSensorChanged("<instance: android.hardware.SensorEvent>")
...
/* TID 0x75bb */
1204 ms MTFPActivity.showSignatureConfirmDialog()
1204 ms | MTFPActivity.setLVLState(0)
1206 ms | MTFPActivity.s("<instance: jp.co.capcom.android.mtfp.MTFPActivity$o>")
Curious to see where this was getting called from, I grepped for those methods and found one specific file of interest: libGS5_Android.so. This is the C++ library containing the majority of the game code.
After searching for references to getSignature in the library, I found this code in aScrnCheck::move:
bl native::android::getJavaActivity
mov x20, x0
mov x0, x21 {data_e5c9e7, "MTFPActivity"}
bl native::android::getJavaClass
mov x1, x0
mov x0, x20
mov x2, x22 {data_e5cea6, "getSignature"}
mov x3, x23 {data_e5ceb3, "()Ljava/lang/String;"}
bl native::android::callJavaMethod<_jobject*>
mov x20, x0
bl native::android::getJNIEnv
ldr x8, [x0]
mov x1, x20
mov x2, xzr {0x0}
ldr x8, [x8, #0x548 {JNINativeInterface_::GetStringUTFChars}]
blr x8
adrp x1, 0xe5c000
add x1, x1, #0xa01 {data_e5ca01, "308202333082019ca00302010…"}
bl strcmp
This calls getSignature through JNI and then compares the result against a hardcoded string. If the value doesn’t match, it will later call the showSignatureConfirmDialog of MTFPActivity which shows the error dialog. So to bypass this check, I can just replace getSignature with a method that returns the hardcoded string:
.method public getSignature()Ljava/lang/String;
.locals 1
const-string v0, "308202333082019ca0030201..."
return-object v0
.end method
After rebuilding the app with these changes and launching it again, it went back to showing the original “Transmission Error” dialog.
Conveniently, this dialog has a retry button which runs whatever checks the application is doing, so it was fairly easy to find the code responsible for the checks by using frida-trace again. After trying to wrap my head around the code that was called for a while, I stumbled on a repository for something called LVL by googling for constants in the code. It turns out that this is an open-source library for checking licenses using Google Play, which sounds like the exactly the kind of thing that I’d need to patch.
The way LVL works is that the application calls the checkAccess method of the LicenseChecker class, providing an implementation of the LicenseCheckerCallback interface. LVL will then call the allow, dontAllow, or applicationError method of the callback at some later point in time. If allow is called, then the application knows that the user has been verified, and can continue. The simplest way to bypass this is to replace the checkAccess method with one that always calls allow. Conveniently, checkAccess already has a piece of code that calls allow directly, so it was just a matter of deleting the surrounding code, leaving me with this:
.method public declared-synchronized f(Ljp/co/capcom/android/mtfp/c0;)V
.locals 1
# Policy.LICENSED
const/16 v0, 0x100
# allow(int reason)
invoke-interface {p1, v0}, Ljp/co/capcom/android/mtfp/c0;->a(I)V
return-void
.end method
After rebuilding the application with both patches, it booted into the game offline 🎉
