Summary: Please, enjoy the game, put the effort to solve the riddles. It’s very rewarding to find the answers yourself. But, if you’re really really stuck, here are all the answers

La Neurona

For the last few weeks, the game that everyone has been talking about in Cuba is not Pokémon GO, but La Neurona.

La Neurona is a collaboration among UCI, ICAIC and, although not directly credited, ETECSA. I add ETECSA to the list because the game sports a business model that is common outside of Cuba, but cannot effectively be implemented in Cuba by any natural person. I’m talking of in-app purchases. The game implements them by giving you the option to send an SMS (which costs $0.16 or $1.00 CUC) to a four digit number for an in-game reward.

The gameplay is not new. It’s a trivia app where you can either pick the right answer from a set of 4 choices, or you type it down on a number of boxes using letters from a given set, much like Icon Pop Quiz, 94% et al.

The game content is the new thing. It’s based on a popular TV show called “La Neurona Intranquila”, hence the name, and questions range from international culture to geography to very Cuban sayings and traditions.

The geeky part

Now to the part I like. As soon as I got a copy of the game, I started inspecting the app to see how it was built. For almost all games I get, I like checking whether the developers used any popular engine like Unity, Corona or –my personal favorite– Cocos2D. My first action is to simply open up the apk file, which is nothing but a Zip file and look around. This also works for iOS’ ipa files, although, of course, the content is completely different.

The first inspection of La Neurona showed me that they seemed to have rolled the app natively on Android, using the standard Java SDKs. One interesting file I found inside was:


A database file! Without even asking file(1) to check it, I tried SQLite. Indeed, it was an SQLite database, and at first sight, it appeared to not even have been encrypted:

$ sqlite3 assets/database/NeuronaDB.db
sqlite> .schema
CREATE TABLE "android_metadata" ("locale" TEXT DEFAULT 'en_US');
CREATE TABLE "Modalities" (
"modality"  TEXT,
"levels"  INTEGER
CREATE TABLE "Curiosities" (
"Text"  TEXT
CREATE TABLE "Answers" (
"id_question"  INTEGER NOT NULL,
"answer"  TEXT,
"right"  INTEGER

But the first sight turned out to be wrong. When I tried to read the content of, say, the "Answers" table, I got this:

sqlite> .mode insert
sqlite> select * from Answers limit 10;
INSERT INTO table(_id,id_question,answer,right) VALUES(1,1,'5AGQfzur1a+w1sAz86AQIQ==',1);
INSERT INTO table(_id,id_question,answer,right) VALUES(2,2,'pHGSCRgFbZXLQnjC5+30mg==',1);
INSERT INTO table(_id,id_question,answer,right) VALUES(3,3,'E9lrBCyXcVd5q8uLdkr9RA==',1);
INSERT INTO table(_id,id_question,answer,right) VALUES(4,4,'72piviAP/DQvtTUOtwt0SQ==',1);
INSERT INTO table(_id,id_question,answer,right) VALUES(5,5,'NwZipMu06UeDWYhLtEwSaA==',1);
INSERT INTO table(_id,id_question,answer,right) VALUES(6,6,'Jb7Mj4mXTVRi2575SiIwTw==',1);
INSERT INTO table(_id,id_question,answer,right) VALUES(7,7,'pw3wZj0DXm3c1o7PGEOBOg==',1);
INSERT INTO table(_id,id_question,answer,right) VALUES(8,8,'1frERb7HnIT4S4mSTb56Nw==',1);
INSERT INTO table(_id,id_question,answer,right) VALUES(9,9,'PK6X+Kb0loFrEe3mPP81/Q==',1);
INSERT INTO table(_id,id_question,answer,right) VALUES(10,10,'P1cw9HVCZVb2WIRbLXOsKA==',1);

Although the DB schema was readable, the data seemed encrypted somehow.

Encrypted Database

Now, this is an app that needs to decrypt that data at some point. And unlike, for instance, a keychain app, the encryption key will not be provided by the user. So, I saw two possibilities here:

  1. The developers just wanted to obscure their data to make it harder for users to get all the answers; but all actual data is in the database, despite the encryption.
  2. The actual answers are not in the database; only a salted hash, as is done with passwords everywhere (sadly, almost everywhere).

If the developers went for option 1, and I had a hunch they had, then everything required to read the answers (algorithms & keys) was inside the app. If they went for option 2, then there was no cheap way to break it.

Time to Dig Deeper

So, let’s look at the code. To disassemble an Android app I use apktool, which is in Debian’s repos:

sudo apt install apktool

And then you just run it over the apk file:

apktool decode La\ Neurona.apk

My version of apktool was crashing when decoding resources so I ran it with -r. I wasn’t interested in resources at first. If the key turned out to be a string resource, well, I’d have to return to this point.

apktool produces a directory structure with a number of files. Compiled Dalvik classes get converted to .smali files. I’m not even sure what is SMALI, read more here, but it’s a relatively easy to read high-level assembly language. Browsing around for a while I finally found this file:


And here are the interesting bits of it. As a courtesy to the developers, I’m not showing the actual key & init vector. You can reproduce the entire procedure yourself if you want to see them.

.method static constructor <clinit>()V
    .locals 1

    .line 37
    const-string v0, "**init**vector**"

    sput-object v0, Lcu/vertex_icai/laneurona/Utils/Dbcrypto;->initVector:Ljava/lang/String;

    .line 38
    const-string v0, "******key*******"

    sput-object v0, Lcu/vertex_icai/laneurona/Utils/Dbcrypto;->key:Ljava/lang/String;

    .line 294
    .local v4, "skeySpec":Ljavax/crypto/spec/SecretKeySpec;
    const-string v5, "AES/CBC/PKCS5PADDING"

    invoke-static {v5}, Ljavax/crypto/Cipher;->getInstance(Ljava/lang/String;)Ljavax/crypto/Cipher;

I had the cipher (AES), the block mode (CBC), and the padding. And I had the 16 bytes Initialization Vector, and the key.

Let’s Try It

So, let’s run our findings through python-crypto:

$ python3
>>> from Crypto.Cipher import AES
>>> import base64
                     # These are not the actual key & init vector
>>> cipher ='******key*******', AES.MODE_CBC, '**init**vector**')
>>> answer_encrypted = base64.decodestring(b'5AGQfzur1a+w1sAz86AQIQ==')
>>> cipher.decrypt(answer_encrypted)

And Eureka! As you can see, the string ends with a padding of N bytes, each with the value N as well. That’s the PKCS5 padding. AES needs all input blocks to be a multiple of 16 bytes long. By looking at the last byte you know how much you need to trim.

Another encrypted answer looked like this:

>>> answer_encrypted = base64.decodestring(b'P1cw9HVCZVb2WIRbLXOsKA==')
>>> cipher.decrypt(answer_encrypted)

The padding bytes \t have value 9, and indeed, there are 9 of them. The 3rd & 4th bytes look funny, but it’s just the UTF-8 encoding of the letter Ñ. The last decoding step was:

>>> answer = cipher.decrypt(answer_encrypted)
>>> answer[:-answer[-1]].decode('utf8')

The rest was grunt work.


So, if you’re curious, or seriously stuck in some level, I’ve collected all the answers here.