Terraria on the HTC Desire Z: a DRM loop and a dead edge
Contents
My thumb is on the row of hearts at the top of the screen, and nothing happens. Not a slow response, not a wrong item selected: the game behaves as if the thumb is somewhere else entirely. Tapping again, slower and with more conviction, is a debugging technique with a perfect record of not working, and it kept that record. By the end of the afternoon I had patched the game's license check three times, signed it with two different keys and swapped it for a newer build, all to arrive at a screen I could not touch.
The phone is an HTC Desire Z, the 2010 Android slider with a keyboard that slides out from under the screen, running CyanogenMod 7, the community build of Android 2.3, with no Google services on it at all. The game is Terraria, an old Android build from archive.org that needs Google Play to tell it whether I paid. This post is the afternoon of getting it to run: a license check that called itself until the app died, an install path that a signing key quietly broke, and a strip along one edge of the glass that no amount of software could reach.
Not the GPU: a license check that calls itself#
My own bug report blamed 3D. I had just flashed a custom kernel (the battery-first build later in this series), Terraria no longer launched, and PAC-MAN drew its menu text as garbage. Two broken games after a kernel change reads like a broken graphics driver. The kernel's GPU config turned out to be identical to the stock one, and the logcat for the Terraria crash had nothing to do with graphics:
E/AndroidRuntime( 2001): at com.and.games505.Terraria.License.dontAllow(License.java:98)
E/AndroidRuntime( 2001): at com.and.games505.Terraria.License.checkLicense(License.java:65)
E/AndroidRuntime( 2001): at com.and.games505.Terraria.License.dontAllow(License.java:98)
E/AndroidRuntime( 2001): at com.and.games505.Terraria.License.checkLicense(License.java:65)
The same two frames, over and over, until the activity manager force-finished the game. Terraria uses Google's License Verification Library: checkLicense() asks the Play store app whether this copy was bought, and the answer comes back through allow() or dontAllow(). On a phone with no Play store, the library cannot even bind to the licensing service, so the answer is always no. Decompiled with jadx, the game's own dontAllow() shows what it does with a no:
public void dontAllow(int reason) {
if (!this.activity.isFinishing()) {
OctarineBridge.nativePrint(this.activity.getString(R.string.license_dont_allow));
checkLicense();
if (reason == 291) {
OctarineBridge.nativePrint("Terraria Free");
allow(reason);
} else {
It asks again, before it looks at the reason. The question cannot succeed, so the asking never stops. Persistence is a virtue in people. In DRM it is a stack trace.
One smali edit, then a signing wall#
Java recompiled from a decompiler rarely builds, so the patch went in one level down. baksmali 3.0.9 turned classes.dex into smali, the assembler-like text form of Dalvik bytecode, and I changed dontAllow() so that a no stopped asking the question again and was treated as a yes. smali reassembled it, the new classes.dex went back into the APK, and the old signature came out. Signing hit the second wall of the afternoon: Android 2.3 wants SHA1 signatures, and the modern JDK's jarsigner refuses to make them:
The SHA1 algorithm specified for the -digestalg option is considered a security risk and is disabled.
The SHA1withRSA algorithm specified for the -sigalg option is considered a security risk and is disabled.
The Zulu Java 6 jarsigner from my app toolchain still makes them without comment. I signed with the same AOSP platform key as my own apps (the key Google publishes with the source), pushed the APK into /system/app/ and launched it.
A system app with no native library#
E/AndroidRuntime( 1653): Caused by: java.lang.UnsatisfiedLinkError: Couldn't load Terraria: findLibrary returned null
The game engine lives in libTerraria.so, a 4,735,504-byte native library inside the APK. A normal install unpacks it; my APK sitting in /system/app/ had nothing unpacked. I extracted it by hand into the app's lib/ directory, and Terraria started. The license library logged Could not bind to service, my patched dontAllow() said yes anyway, and the game was on screen.
It was also drawn larger than the screen, with only part of the world visible, and the activity log complained that the game had leaked a ServiceConnection from checkLicense(), which still tried to bind every time. So checkLicense() got the same treatment as dontAllow() and stopped trying to bind.
The scaling problem pointed at the install path, so I tried to install the game properly with adb install:
Failure [INSTALL_FAILED_INVALID_INSTALL_LOCATION]
Then with -f for internal storage. Then with -s for the SD card. Then with -r -t. Four attempts, four ways of asking, one answer. I kept my voice level, the way you do with someone who has said no four times and is clearly starting to enjoy it. Somewhere in replacing the system copy and rebooting, my hand-extracted library also disappeared from the app's lib/ directory, which settled how long the manual route would last.
The one thing I had not changed was the key. I generated a throwaway RSA key with the Java 6 keytool, signed the same patched APK with it, and adb install answered Success. The game unpacked its own library and came up full screen at the right scale; a later build installed the same way kept its library and its saves across a reboot. I never found out why the platform-signed copy could not be installed the normal way; all I know is that the key was the last thing I changed before it could.
The top row: a status bar that was not the problem#
Then the actual playing started, and the top row of the screen ignored me. Inventory slots, hearts, everything along that edge in landscape: tap, nothing.
The first theory was the status bar. The game's theme is Theme.NoTitleBar, which hides the title bar and keeps the status bar, so maybe the invisible bar was eating the touches. I patched FLAG_FULLSCREEN into onCreate(), rebuilt, and the status bar vanished from the screenshots. The dead row did not notice.
The second theory was the game. I asked, with less patience than this sentence suggests, whether we were even running the latest version. archive.org has a whole shelf of Terraria builds. I checked three: 1.2.12785 needs API 15, and Android 2.3 is API 10, while 1.06 and 1.1.92 still declare sdkVersion:'9'. I took 1.1.92, the newer of the two, without checking the three builds that sit between it and 1.2.12785. Its code is obfuscated, but the license check was easy to find and got the same kind of one-method patch. It installed with the throwaway key, launched, and the top row was still dead.
A 78-line touch test#
If neither the status bar nor the game was responsible, the touches were not arriving at all. To see where they stop, I wrote a 78-line touch test app: black screen, a grey grid every 80 pixels with the coordinates written on it, and a green dot wherever a finger lands. The dots stay, so a session leaves a map. This is all the input handling it has:
public boolean onTouchEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
if (e.getAction() == MotionEvent.ACTION_DOWN || e.getAction() == MotionEvent.ACTION_MOVE) {
dots.add(new float[]{x, y});
lastCoord = (int)x + "," + (int)y;
}
invalidate();
return true;
}
The APK came out at 4,863 bytes. In landscape, no dot landed in a band about 90 pixels deep under the top edge: one row of dots sat pinned to y = 0 itself, and the next began near y = 100. In portrait the top and left edges reached zero, but the right edge had the same shape: nothing between about x = 385 and x = 470 on a 480-pixel-wide screen, however much I swiped there, and a column of dots pinned to the edge. Rotate the phone and the right edge in portrait is the top edge in landscape. One physical edge of the glass, seen twice.
The clip registers: four zeros, no change#
The touch controller is an Atmel maXTouch (qt602240), and HTC's kernel can talk to it from a shell. The driver passes raw x and y straight through to the input layer without clamping them, and the controller itself has edge-clip settings in its T9 touchscreen object: XLOCLIP, XHICLIP, YLOCLIP and YHICLIP, offsets 22 to 25 in the kernel's atmel_qt602240.h. The board file board-vision.c carries several T9 configurations for different panel variants. Which one the phone actually loaded is visible in /sys/android_touch/regdump: the T9 object starts at register 279, so the clips sit at 301 to 304:
addr[301]: 0
addr[302]: 0
addr[303]: 0
addr[304]: 30
That matches the config in board-vision.c with YHICLIP at 30 and ORIENT at 5 (register 288 read back 5). The driver's register file takes writes as w:ADDR:VAL, three decimal digits each. I zeroed register 304 and tested: nothing changed. With ORIENT at 5 the controller swaps its axes, so the "Y" clip might not be the edge I cared about, and I zeroed all four. The regdump read back four zeros. The touch test drew exactly the same map. The registers took the zeros politely and changed nothing, which is the most Android 2.3 thing a register can do.
That leaves whatever sits below the controller's clipping: the sensor itself. I never opened the phone, so "the digitizer does not resolve that strip" is the last suspect standing rather than a measured fact. Everything I could reach from a shell had been ruled out.
Reverse landscape: the dead zone moves, the problem stays#
One idea remained, and it was a good one. Terraria 1.1.92 sets landscape in its manifest (screenOrientation 0). Flip the game to reverse landscape, orientation 8, and the dead edge should land at the bottom of the game's screen instead of under the inventory. I added setRequestedOrientation(8) to onCreate() in smali, rebuilt, uninstalled the old copy and installed the new one.
The reinstall started from a clean slate, so the game wanted a new character first. I could not create one. My guess is that a control that screen needed now sat on the dead strip, but I never checked which one: I had stopped wanting to know. No character, no game, and that is where I stopped.
What the glass would not give#
Two of the three problems were software, and software gave in. One smali edit ended the license loop, and a throwaway key got the game installed the normal way. The third problem sits along one edge of the touchscreen. Firmware clipping was ruled out by register writes, the driver by its source, the game by a test app that knows nothing about Terraria. What's left is the panel.
I gave up the way a sensible person does, and replaced the Reboot tile on my launcher with an autorotate toggle, so that at least I decide which way the phone faces. That is the kind of revenge available to a man with root and a phone that has been right about its own limits all afternoon.
Filed under: android, htc-desire-z, cyanogenmod, reverse-engineering, gingerbread
Read next#
- The skeleton key Google publishes: why my apps are signed with the AOSP platform key, the same key that kept Terraria from installing normally
- The battery-first kernel: the custom kernel I blamed first for the broken games
- Building a camera app for Android 2.3: the same toolchain on an app I wrote from scratch, and the phone's other hardware buttons
- Browsers on Gingerbread: the next part, and the other thing Android 2.3 cannot do on its own
Sources#
- google/smali: baksmali/smali, the Dalvik disassembler and assembler (3.0.9 was used, from the baksmali/smali release assets)
- Android License Verification Library: how
checkAccess(),allow()anddontAllow()fit together - atmel.c in the Kali- htc-kernel-msm7x30 tree: the
android_touchsysfs files (register,regdump) and the raw x/y report - atmel_qt602240.h: T9 offsets,
T9_CFG_XLOCLIP(22) toT9_CFG_YHICLIP(25) - board-vision.c, Desire Z touchscreen configs: the T9 array with
ORIENT5 andYHICLIP30 that the phone's regdump matched - ActivityInfo constants:
SCREEN_ORIENTATION_LANDSCAPE(0) andSCREEN_ORIENTATION_REVERSE_LANDSCAPE(8)