Featured image of post Zombie Cafe Revival

Zombie Cafe Revival

Reverse engineering and reviving an (un)dead mobile game

Zombie Cafe is an old mobile game from 2011, that I used to play on my little ipod touch. Unfortunately, at some point it was removed from app stores, and the servers have been offline for a long time.

There are a few available APK downloads for this game, but none of them are without issues. There is a download for the 1.1.2.0a version, which crashes upon trying to open the game. And there is a download for the 1.7.0.0 version, which is mostly functional, but is also in Japanese.

Compounding these issues, the servers that this game is programmed to connect to are now either offline or redirecting elsewhere, which the game doesn’t know how to handle, and causes some crashing problems.

The goal of this project is to fix major crashes in the 1.1.2.0a version of the game, reimplement the game servers, and backport content from the Japanese version of the game back to this english version. I would also like to remove the reliance on microtransactions from the game, since you can no longer purchase these and they lock away a significant amount of the game.

Admittedly I was somewhat nerdsniped by this project…

Getting started

The general workflow I’ll be using for this project is to extract the contents of the APK using apktool, then making changes to the game data and smali code and packing it back to an apk also using apktool.

Fixing major crashes

Crash during startup

The first and most obvious crash happens right when loading the game. Running the game in an android VM and reading the log exposed the following message: Permission Denial: not allowed to send broadcast android.intent.action.MEDIA_MOUNTED

This appears to be due to a permission change in the Android API, which must have happened after this version was released. I was able to find the smali code which was calling this API pretty quickly and remove it. After doing this, the game actually opens!

First shared screenshot of the 1.1.2.0a version of the game running on modern android

However, there is a clear graphical issue, a lot of objects appear to be rendering a very bright cyan color… We’ll get back to this in a bit

Random crashing during cafe load

The next most frequent crash sometimes happens shortly after opening the game. There wasn’t any super clear cause, but the android logs indicated that the crash was happening when the game made an HTTP request to fetch gifts. Zombie Cafe had a strong social aspect which integrated with Facebook, and you could send gifts to your friends who also played the game.

Following the HTTP request, the request gets redirected a few times, and returns some website, which clearly is not what the game is expecting, and thus crashes when attempting to parse the data.

As a quick workaround for this fix, I found a function in the smali for checking if there is an active internet connection. I just patched this function to always return false. Though I will need to fix this properly later if I want to restore online functionality.

Crashing during animation/texture unload

This one was particularly hard to track down. The logs indicated that the crash was occuring on the graphics thread, and often occured after the log prints a string about deleting images.

1
2
3
4
5
6
06-20 23:46:09.758  6151  6182 E ZombieCafe::saveGameState: Ended Saving
06-20 23:46:09.759  6151  6182 E GameStateCafe: AboutToDestroyImages!
06-20 23:46:09.760  6151  6182 I scudo   : Scudo ERROR: corrupted chunk header at address 0xbcb35c50
--------- beginning of crash
06-20 23:46:09.869  6151  6182 F libc    : Fatal signal 6 (SIGABRT), code -6 (SI_TKILL) in tid 6182 (GLThread 371), pid 6151 (mbiecafeandroid)
06-20 23:46:09.902  6264  6264 I crash_dump32: obtaining output fd from tombstoned, type: kDebuggerdTombstone

The rest of the log was somewhat cryptic, but seemed to be pointing towards an issue with the game’s memory management regarding textures

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
backtrace:
#00 pc 00000b99  [vdso] (__kernel_vsyscall+9)
#01 pc 0005ad68  /apex/com.android.runtime/lib/bionic/libc.so (syscall+40) (BuildId: 6e3a0180fa6637b68c0d181c343e6806)
#02 pc 00076511  /apex/com.android.runtime/lib/bionic/libc.so (abort+209) (BuildId: 6e3a0180fa6637b68c0d181c343e6806)
#03 pc 0004aeea  /apex/com.android.runtime/lib/bionic/libc.so (scudo::die()+26) (BuildId: 6e3a0180fa6637b68c0d181c343e6806)
#04 pc 0004b5c2  /apex/com.android.runtime/lib/bionic/libc.so (scudo::ScopedErrorReport::~ScopedErrorReport()+50) (BuildId: 6e3a0180fa6637b68c0d181c343e6806)
#05 pc 0004b6bc  /apex/com.android.runtime/lib/bionic/libc.so (scudo::reportHeaderCorruption(void*)+76) (BuildId: 6e3a0180fa6637b68c0d181c343e6806)
#06 pc 0004cd38  /apex/com.android.runtime/lib/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::deallocate(void*, scudo::Chunk::Origin, unsigned long, unsigned long)+392) (BuildId: 6e3a0180fa6637b68c0d181c343e6806)
#07 pc 0004cb9d  /apex/com.android.runtime/lib/bionic/libc.so (scudo_free+45) (BuildId: 6e3a0180fa6637b68c0d181c343e6806)
#08 pc 000430ba  /apex/com.android.runtime/lib/bionic/libc.so (free+42) (BuildId: 6e3a0180fa6637b68c0d181c343e6806)
#09 pc 00004570  /system/lib/libndk_translation_proxy_libc.so (ndk_translation::TrampolineFuncGenerator<false, void (void*)>::Func(void const*, ndk_translation::ProcessState*)+32) (BuildId: dcc623ca607d771168a7a5840b7718a0)
#10 pc 00032383  <anonymous:c6ee9000>

I was stuck here for a while, not really sure how to progress. I couldn’t find any way to attach a debugger to this and the backtrace wasn’t showing a lot of information after calls to ndk_translation. So all I could tell was there was a issue with freeing some memory somewhere.

I decided to research a little more in to what ndk_translation was, and luckily for me it appeared to be something that only the Android VM uses for translating native calls. So I loaded the apk up on my phone and low and behold, the crash log contained a lot more information when running on an actual device.

 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
Abort message: 'Scudo ERROR: corrupted chunk header at address 0xd550f8b0'
06-21 21:43:14.103 26622 26622 F DEBUG   :     r0  00000000  r1  000067a0  r2  00000006  r3  d5be21a8
06-21 21:43:14.103 26622 26622 F DEBUG   :     r4  d5be21b8  r5  d5be21a0  r6  0000677c  r7  0000016b
06-21 21:43:14.103 26622 26622 F DEBUG   :     r8  00000000  r9  ffffffff  r10 d5be21a8  r11 00000000
06-21 21:43:14.103 26622 26622 F DEBUG   :     ip  000067a0  sp  d5be2188  lr  e85bb4d3  pc  e85bb4e6
06-21 21:43:14.103 26622 26622 F DEBUG   : backtrace:
#00 pc 0003a4e6  /apex/com.android.runtime/lib/bionic/libc.so (abort+138) (BuildId: 9e5101d790f828ae8b754029c778f7e2)
#01 pc 0002db2b  /apex/com.android.runtime/lib/bionic/libc.so (scudo::die()+2) (BuildId: 9e5101d790f828ae8b754029c778f7e2)
#02 pc 0002ded1  /apex/com.android.runtime/lib/bionic/libc.so (scudo::ScopedErrorReport::~ScopedErrorReport()+16) (BuildId: 9e5101d790f828ae8b754029c778f7e2)
#03 pc 0002df55  /apex/com.android.runtime/lib/bionic/libc.so (scudo::reportHeaderCorruption(void*)+36) (BuildId: 9e5101d790f828ae8b754029c778f7e2)
#04 pc 0002ed6d  /apex/com.android.runtime/lib/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::deallocate(void*, scudo::Chunk::Origin, unsigned int, unsigned int)+184) (BuildId: 9e5101d790f828ae8b754029c778f7e2)
#05 pc 0002eca7  /apex/com.android.runtime/lib/bionic/libc.so (scudo_free+18) (BuildId: 9e5101d790f828ae8b754029c778f7e2)
#06 pc 00138e8f  /data/app/.../libZombieCafeAndroid.so (operator delete(void*)+6)
#07 pc 0013d9ef  /data/app/.../libZombieCafeAndroid.so (CImage::~CImage()+10)
#08 pc 0013f8cd  /data/app/.../libZombieCafeAndroid.so (PackedImage::destroyImages(CImage**) const+28)
#09 pc 0014a789  /data/app/.../libZombieCafeAndroid.so (ToonAnimation::destroy()+8)
#10 pc 0006c60b  /data/app/.../libZombieCafeAndroid.so (CharacterInstance::setCurrentAnimation(unsigned char, unsigned char, bool)+210)
#11 pc 000e0c6d  /data/app/.../libZombieCafeAndroid.so (MoveTask::tick(long)+436)
#12 pc 0006d013  /data/app/.../libZombieCafeAndroid.so (CharacterInstance::tick(long)+226)
#13 pc 000aa5e7  /data/app/.../libZombieCafeAndroid.so (GameStateCafe::tickCustomers(long)+58)
#14 pc 000aeb49  /data/app/.../libZombieCafeAndroid.so (GameStateCafe::tick(long)+3408)
#15 pc 00139a45  /data/app/.../libZombieCafeAndroid.so (CapcomGame::tick(long)+188)
#16 pc 0012709d  /data/app/.../libZombieCafeAndroid.so (ZombieCafe::tick(long)+196)
#17 pc 00139405  /data/app/.../libZombieCafeAndroid.so (CapcomApp::tick(long)+52)
#18 pc 0013945f  /data/app/.../libZombieCafeAndroid.so (CapcomApp::tickApp(CapcomApp*, int)+46)
#19 pc 0005e023  /data/app/.../libZombieCafeAndroid.so (Java_com_capcom_zombiecafeandroid_CapcomRenderer_render+10)
#20 pc 0008bcab  /data/app/~~eb2KTjtEPW_gyH43a25cgQ==/com.capcom.zombiecafeandroid-7aS6BojUdnPJ7O3-O6nIMw==/oat/arm/base.odex (art_jni_trampoline+74)
#21 pc 0008fe37  /data/app/~~eb2KTjtEPW_gyH43a25cgQ==/com.capcom.zombiecafeandroid-7aS6BojUdnPJ7O3-O6nIMw==/oat/arm/base.odex (com.capcom.zombiecafeandroid.CapcomRenderer.onDrawFrame+126)

Now I was easily able to see that this was occurring during calls to the destructor of CImage. Great! I know where the crash is happening, but actually fixing this one was a bit harder. With other crashes, I could fix them easily by editing the smali, only touching the Java side of things. But this crash was occuring inside the native library.

I suspected from the start I would eventually need to find a good way to patch the native binary, but wasn’t too sure how to approach it. While I have done my fair share of patching x86/x64 binaries on PC, I had no idea how to best approach patching an ARM binary on mobile. I tried a few methods, from just manually patching the file with a hex editor, to using radare2 to try apply patches in command line. But ultimately I decided I should just compile my own library, and apply patches at runtime with memory manipulation.

libZombieCafeExtension

libZombieCafeExtension is the library I wrote to apply these patches. Initially, I wanted to create a ‘wrapper’ around the game’s original libZombieCafeAndroid, where I just expose all the original exports of that library in my own library, and bridge the calls across accordingly. While I technically got this working, it was resulting in some interesting issues where the game would render and behave in slightly strange ways. It still doesnt make a lot of sense to me as to why, but I decided making the library in this way was changing an unnecessarily large surface area, and I was writing more code trying to fix these new issues I created, instead of just fixing the actual issues in the game.

Ultimately I ended up just writing a library that doesnt mess with any exports, and just patched the smali code to load my library directly after it loads the original libZombieCafeAndroid library. This way I can make all my patches before anything else in the game has a chance to run.

Then I was able to “fix” the texture unloading crash. Ideally I would have written this to hook the games original texture allocating code and handle it myself, for now I have simply opted to NOP out the call to the texture destructor. Yes I know, this causes a memory leak. But textures dont get unloaded very frequently, and this is only deallocating a reference to a texture, its only 0x18 bytes. I think our modern phones can handle it… Maybe I will come back and fix this properly if I get bored, but for now a memory leak is better than a crash!

Fixing Cyan Rendering

This problem just about made me give up. There was seemingly no reason this should have been happening, and without fixing this the whole project was relatively useless, as I cant exactly claim to be ‘preserving’ a game if I have made it render so poorly.

The only thing with any indication of causing an error was a shader compile error during the game’s startup.

Screencap of shader compile error

I got to work investigating this, replacing the shader code in the game’s binary but nothing I did seemed to have any effect at all. I tried changing the shader to always return White, but still things rendered as cyan. I replaced the shader with nonsense code that couldnt possibly compile. Still cyan. I replaced the shader with a different shader from other parts of the game. Cyan.

Now, I was solving this issue somewhat in parallel with trying to figure out where that texture unload crash was coming from. Right when I loaded the APK up on my actual phone, I noticed. THE CYAN WAS GONE. Without any shader tricks at all, the game rendered perfectly fine on my phone, and was cyan only in the virtual machine. So problem “solved”?

Anyways, with the most major issues in the game fixed, I could get started on the actual fun parts. Reversing the games proprietary file formats, and netcode. What I found will shock you! ;)

File Formats

For the most part, the file formats were relatively easy to figure out. They are mostly standard binary files, just containing raw data. With a bit of disassembly of the game code, just looking in the hex editor, and some trial and error with changing bytes and seeing what effect it has, I was able to figure out enough of these files to be able to make substantial changes to the game. I wont go in to a lot of detail for these files, I think if you’ve seen one C style struct you’ve seen em all. But there was one file format that stood out from the others…

.cct File

.cct.mid aka CCTX. The cct files contain the majority of the games texture assets. but looking at the data didnt clearly show particulary much of anything. It started with file magic of 4 bytes making up the string CCTX and then just seemingly random data. I couldn’t understand much of it, and decided to have a look around online.

This lead me to to a few sources saying that .cct is a CAST file created in Adobe Director. A now discontinued software which wikipedia says:

Adobe Shockwave (formerly Macromedia Shockwave and MacroMind Shockwave) is a discontinued multimedia platform for building interactive multimedia applications and video games.

It certainly sounds like it could be our culprit.

With this information, I found a few projects people had already made for unpacking this file format, mainly ProjectorRays. I downloaded this project and had a try using it to decompile my files, but it wasn’t working.

I joined the ProjectorRays Discord server to ask if my file looked at all similar to Shockwave’s cast file format, where I was promptly told that no, unfortunately it didnt look similar at all.

Screencap of conversation

Defeated, I went back to the hex editor and stared for a while. When suddenly, out of the ashes of my defeat a message comes in from the ProjectorRays server.

ZLib Compression

My Hero

OH! 78 DA of course this is zlib data! Even though my project turned out to be completely unrelated, the ProjectorRays / Adobe Shockwave arc turned out to be extremely beneficial. Funny how things work out.

With a quick hop skip and a jump I got to work decompressing this data, and immediately upon looking at the decompressed data in a hex editor I could see it was raw image data. The way the ascii representation made interesting patterns made it clear. So I took this data in to GIMP and tried to interpret it as raw data. After tweaking the settings a little, the following emerged

GIMP interpretation of data

I mean yea, its not perfect but we are clearly moving in the right direction. With this new found momentum I was also able to figure out that some of the data in the .cct.mid file before the compressed blob contained information about the image resolution and compressed data size. (Note that this data was stored in little endian, regardless of the fact that big endian is used in EVERY OTHER FILE FORMAT IN THIS GAME)

Regardless, with this extra information I was able to get the image to display somewhat better

Correct-er display of image

But the colors are clearly wrong, and the image has been seemingly duplicated but with minor differences once to the left. I was stuck here for a while. I thought maybe the duplication could be some form of mip mapping? maybe this is some weird compression thing? Compression could make sense, since I would expect a 2048 x 2048 RGBA uncompressed image to be a bit over 16MB, and this is only 8MB.

Eventually I was able to trace the loading of this texture back to the following code:

Disassembly of texture loading code

glCompressedTexImage2D? Maybe it is compressed after all? The number 0x8c01 lead me to the following formats:

OpenGL compression formats

I downloaded some tools that can display textures compressed with this file format but nothing seemed to be working. All the ‘decompressed’ versions were coming back even more broken than what I already was getting in gimp. I decided this must not be compressed, it wouldn’t make sense for a compressed file to even display close to what I was getting in gimp. There must be something else going on here. That’s when I realised I had made a bold assumption that a color must be represented with 4 bytes, 1 byte per channel. While maybe this is a fair assumption in the current year, this is a much older game, maybe they were using 16 bit color? a 2048 x 2048 image with 2 bytes per color would actually come out to about 8MB so it makes sense. In the end, this was exactly the case.

The data is an uncompressed image where every two bytes makes up one color. 4 bits per color RGBA. With this, I was finally able to read the data in all its 16 bit glory

Cropped texture

You may think this is the end of the line for CCT files, but you would be wrong. The developers of this game had another trick up their sleeve, ready to stop me in my tracks.

Hard coded scaling factors per file and image offsets

As you can see in the previous image, a single CCT file contains an image with other textures packed in to it. “This is no problem” I thought to myself, as I already understood the file format the game uses to store information about texture offsets in this file. I was able to quickly extract individual textures from this file using the offsets data.

Recipe Images

Furniture Images

Huh? Thats weird, why was recipeImages working perfectly while furnitureImages wasn’t? Why are some of the coordinates in furnitureImagesOffsets claiming to have an X coordinate of "X": 2327 when the image is only 2048x2048 in resolution?

Well as it turns out, the developers, in their infinite wisdom, decided to hard code scaling factors for the coordinates of these offsets on a per file basis. So there is no information in the files themselves about what scale factor it uses, the developer just needs to remember the correct one I guess? I think maybe its meant to accomodate for switching between different resolutions based on the user’s device?

1
2
3
4
5
6
7
8
0x00654cc  if (SmDev() != 4)
0x00654d2      r2_20 = 0x3faaaaab  // 1.3333
0x00654d6      r3_5 = 0x3f400000  // 0.75
0x0065808  else  // 0.375
0x0065808      r3_5 = 0x3ec00000
0x006580a      r2_20 = 0x402aaaab  // 2.6666
0x00654d8  *(r5_2 + 0x18) = r2_20
0x00654da  *(r5_2 + 0x1c) = r3_5

But with this brain blast I was able to get textures unpacking properly.

Then with the help of some rectangle packing algorithms I was able to reverse the whole process and can now write my own CCT file, complete with custom offsets! Combine this with the characterData file I have already reversed, I have all the information necessary for adding entirely new characters from the game, so I can start on backporting some characters from the Japanese version.

Though, the japanese version has 5 different character sheets wheras the english version is only built to handle 2. So I am limited by how many characters I can fit in to the sheet, unless I hook the game’s code and make the necessary modifications to load more data.

Networking

If you thought the implementation of the cct file format was questionable, well strap in because the networking is no better.

Thanks to libZombieCafeExtension, I am eaily able to patch the URLs that the game makes requests to, and spin up a simple http server to listen for requests.

The first thing I found is that the game will periodically upload the user’s current save game, thats fine I can store that and use the data to restore functionality to allow people to visit other player’s cafes again.

An interesting thing I found is the Gifting “protocol” if you can call it that. A Gift is sent using a string like so:

1
2
FBA5543
1:160,1:0:0:user_id:user_name:f209a648a8630f1af5916b2e651643f7

They’re just parsing it based on line endings and a good old string.Split(":"). I guess thats one way to do it, but come on guys, I know you already have a functioning JSON parser in the code because I’ve seen your requests to the facebook api. Why are we doing this?

Also interestingly, when the game fetches gifts from the server, it stores this in a file where gifts are seperated only by a new line. But I think maybe there is a bug in the code, unless maybe I’m not understanding it properly. But it appears that the game always skips the first line when parsing this file, and then continues to read to the next line, So I need to always prepend my responses with a new line for it to be consistent? It’s strange. I think it isnt exactly a great idea to split two parts of a single message only with newline, as if two messages are both written to the file and the newline isnt exactly where it should be, the game cant figure out which line is which part of data?

I decided to host this service using cloudflare workers, and a simple KV storage to store user data. While it’s not ideal, its an easy solution and I was able to make use of my existing data management code thanks to syumai/workers

Microtransactions Replacement

A major issue with this game is its reliance on microtransactions, which are no longer purchaseable as the game is no longer on any stores. In order to make this game more accessible, I needed to add a way to get items which would otherwise be locked behind this paywall.

In the game, it is possible to buy regular game currency with the premium currency. I decided patching this to make it so you can buy premium currency with regular currency would be the easiest way to make this more accessible. I was able to achieve this by editing the games resource files to change the prices and amounts of currency purchased via this method, and then patched the game binary so it would increase the premium currency instead of the regular currency.

Writing this patch was quite interesting, and I was able to discover what I assume is some kind of micro optimization in ARM compiler.

I was able to track this purchase down to this code here

1
2
3
4
5
6
0x00ab014      if (r3_14 != 10)
0x00ab014          return sub_ab028(r0_25, r1_8, r2_11, r3_14) __tailcall
0x00ab016      void* r1_10 = *(r4_2 + 0xc)
0x00ab01c      int32_t r0_27 = *(r2_11 + 0x54)
0x00ab020      int32_t r2_14 = r0_27 + *(r1_10 + 0x1ac)
0x00ab022      *(r1_10 + 0x1ac) = r2_14

With a bit of effort, we can see that (r1_10 + 0x1ac) is where the game’s current money is stored. at 0x00ab020 we can see a number being added to this amount, and then being set back to the games money amount at 0x00ab022. So if I can change this offset from 0x1ac to point to the premium currency instead, we can make the game increase our premium currency.

When it came to patching this assembly though, things got interesting

1
2
3
4
5
6
7
8
9
ldr     r1, [r4, 0xc]
movs    r3, 0xd6
lsls    r3, r3, 1
ldr     r0, [r2, 0x54]
ldr     r2, [r1, r3]
adds    r2, r0, r2
str     r2, [r1, r3]
ldr     r3, [r4, 0x64]
ldrb    r3, [r3, r5]  {sub_ab028}

There is a significant lack of the number 0x1ac in the assembly that executes this code. But lines 2 and 3 are particularly interesting, as it loads the number 0xd6 in to r3 and then bitshifts once to the left. This results in 0x1ac being stored in the register. Im not sure exactly why the compiler chose to use this method as opposed to loading 0x1ac in to the register directly. I assume its either slightly faster or results in a smaller binary size?

My patch just changes the code to load 0xb8 in tor3, which I figured out was the offset to the premium currency, and then I just NOP the bitshift so r3 keeps the value of 0xb8 thus updating the game’s premium currency amount!

Conclusion

Anyways, I’ve managed to research enough to be able to implement all the changes I would like to. If I have the time I would like to research the game’s animation file format, but its not particularly useful for anything at this time beyond purely academic purposes. Though, the academic value of researching Zombie Cafe Animation File Format is questionable.

Check out the project here