MobileReversingEncodingMiscellaneous +150 points

4 months ago - 88 views

Pizza Pazzi

This was a mobile challenge where you had to reverse engineer an APK file. This was by far my favorite solution to a challenge, as I solved it in what I think is a completely unintended way. There were 4 parts to this challenge, and the last 3 were cheesed using the same trick. You could say I put the cheese on the pizza...

Getting the app running

We get an APK file called pizza-pazziv2.apk. Whenever I look at an APK file I like to open it in Android Studio first, using the emulator to run the app. But when I launched it this time it gave me the following error in the Run tab:

07/28 18:45:07: Launching 'pizza-pazziv2'.
Installation did not succeed.
The application could not be installed: INSTALL_PARSE_FAILED_NO_CERTIFICATES

List of apks:
[0] 'C:\Users\j0r1an\ApkProjects\pizza-pazziv2\pizza-pazziv2.apk'
APK signature verification failed.
Retry

INSTALL_PARSE_FAILED_NO_CERTIFICATES, it looks like the APK does not have a signing certificate. What you should normally do as a developer is compile your code, and then sign it with a certificate to allow it to be installed on a phone. This time, the last step was not done and the app was not signed. Luckily it's not too hard to sign it ourselves with apksigner. First, we need a keystore file which is essentially our certificate. Creating this keystore can be done with keytool:

Shell

$ keytool -genkey -keyalg RSA -keystore apk.keystore
Enter keystore password: password
Re-enter new password: password
What is your first and last name?
  [Unknown]:
What is the name of your organizational unit?
  [Unknown]:
What is the name of your organization?
  [Unknown]:
What is the name of your City or Locality?
  [Unknown]:
What is the name of your State or Province?
  [Unknown]:
What is the two-letter country code for this unit?
  [Unknown]:
Is CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown correct?
  [no]:  yes

Now that we have this keystore, we can sign the APK to install it on a phone. We just need to provide the keystore, and the password you set while creating it.

Shell

$ java -jar /usr/bin/apksigner sign -out pizza-pazziv2-signed.apk --ks apk.keystore --key-pass pass:password --ks-pass pass:password -v pizza-pazziv2.apk
Signed

Finally, we can verify that this worked ourselves using the verify feature of apksigned:

Shell

$ java -jar /usr/bin/apksigner verify -v pizza-pazziv2.apk
DOES NOT VERIFY
ERROR: Missing META-INF/MANIFEST.MF
$ java -jar /usr/bin/apksigner verify -v pizza-pazziv2-signed.apk
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Number of signers: 1

Now that we are sure the new APK is signed, we can try to install it again in Android Studio. When we do the same with this new pizza-pazziv2-signed.apk file it gets through the installing phase and opens on the phone. Now we have another problem though when we click the "Get Started" button, the app crashes!

Screenshot of the app crashing after clicking the start button

Luckily we have Android Studio which has a log of error messages in the Run tab. This can give us some clues as to why it crashed:

Java

...
W/FirebaseApp: Default FirebaseApp failed to initialize because no default options were found. This usually means that com.google.gms:google-services was not applied to your gradle project.
I/FirebaseInitProvider: FirebaseApp initialization unsuccessful
I/Info log: What i need to do
    Before the Network Service
D/NetworkService: Before the search
D/NetworkService: built search url: http://pizzapazzi.challenge.hackzon.org
D/NetworkSecurityConfig: No Network Security Config specified, using platform default
D/OkHttp: --> GET http://pizzapazzi.challenge.hackzon.org/ http/1.1
D/OkHttp: --> END GET
W/System.err: java.net.UnknownHostException: Unable to resolve host "pizzapazzi.challenge.hackzon.org": No address associated with hostname
W/System.err:     at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:141)
        at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:90)
W/System.err:     at java.net.InetAddress.getAllByName(InetAddress.java:787)
        at okhttp3.Dns$1.lookup(Dns.java:40)
        at okhttp3.internal.connection.RouteSelector.resetNextInetSocketAddress(RouteSelector.java:185)
        at okhttp3.internal.connection.RouteSelector.nextProxy(RouteSelector.java:149)
        at okhttp3.internal.connection.RouteSelector.next(RouteSelector.java:84)
        at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.java:214)
...

We see some errors relating to pizzapazzi.challenge.hackzon.org. It couldn't find the address of this domain name, and if we try ourselves in a browser for example it doesn't work either. But look very carefully here, the domain name says "hackzon" instead of "hackazon". The main page with all challenges is on portal.hackazon.org, so this is probably a typo. We can try to fix it and go to pizzapazzi.challenge.hackazon.org in the browser. This takes us to a valid page, saying "Welcome to nginx!", and it also shows the first flag in the text below.
CTF{St4RT_Y0uR_3NG1N3X}

Starting with Reverse Engineering

We can try to fix this error as well, but let's first try to understand the app a bit. I decompiled the app in the same way as my other writeup of Unlock Train Data. Check out that post to read in more detail about how it works. The gist of it is that I first use dex2jar on the APK to get the compiled java code in a .jar format. Then I unzip the JAR and view its contents using IntelliJ which decompiles the .class files automatically to readable Java source code.

I opened the folder in IntelliJ and started opening some random folders to find any code related to the application. Completely out of luck I opened the okio folder and saw the Base64 class on the top:

Screenshot of IntelliJ showing Base64 class in okio folder

Base64 is always interesting, especially because it's common in CTFs. Maybe the developer encoded some data in base64 to make it harder to find.

The Base64 Cheese

Here is where the probably unintended trick comes in. I had a wild idea, that I've always wanted to try. CTF flags in this event always were in the CTF{...} format, which means the flags always start with the CTF{ string. A common thing to do in challenges is to just grep for the flag format in hopes of finding the flag anywhere in plain text. This did not give any results in this challenge, but I had a different idea. Because it seems base64 is used somewhere in the challenge, there might be a chance that a flag is encoded in base64 somewhere.

Base64 works by taking the input string in binary, and then instead of using 8 bits per character, it splits the binary into 6 bits per character. Then all these 6-bit values are mapped to certain characters, known as the base64 alphabet. This means that changing a character at the end of the string does not change characters at the start. You can verify this yourself by typing a random string character by character into CyberChef, notice how only the very end of the output changes. So if we know the first few plaintext characters, we can convert them to base64 and search for those first few base64 characters.

Converting CTF{ to base64 results in Q1RGew==. The padding and the last character can change, but the first 5 characters must stay the same regardless of what comes after. So we can grep for Q1RGe to find any base64 string that starts with CTF{:

Shell

$ grep -r "Q1RGe"
pizza-pazziv2/smali_classes3/com/fooddeliveryapp/Model/Call.smali:    const-string v2, "Q1RGe1doMF80bV9JfQ=="
pizza-pazziv2/smali_classes3/com/fooddeliveryapp/Model/Call.smali:    const-string v3, "Q1RGe1doMF80bV9JfQ=="
pizza-pazziv2/smali_classes3/com/fooddeliveryapp/Model/Call.smali:    const-string v1, "Q1RGe1doNHRfMV80bV93MHJ0aH0="
pizza-pazziv2/smali_classes3/com/fooddeliveryapp/Model/Call.smali:    const-string v2, "Q1RGe1doNHRfMV80bV93MHJ0aH0="
pizza-pazziv2/smali_classes4/com/fooddeliveryapp/Activities/LoginActivity.smali:    const-string v1, "Q1RGe1doM3JlXzFzX3RoM19mMDBkfQ=="
pizza-pazziv2/smali_classes4/com/fooddeliveryapp/Activities/LoginActivity.smali:    const-string v2, "Q1RGe1doM3JlXzFzX3RoM19mMDBkfQ=="
Binary file pizza-pazziv2-dex2jar/com/fooddeliveryapp/Model/Call.class matches
Binary file pizza-pazziv2-dex2jar/com/fooddeliveryapp/Activities/LoginActivity.class matches

And this is where I almost fell off my chair. We see 3 different base64 strings that fit this format. Let's convert them from base64...

Shell

$ echo "Q1RGe1doMF80bV9JfQ==" | base64 -d
CTF{Wh0_4m_I}
$ echo "Q1RGe1doNHRfMV80bV93MHJ0aH0=" | base64 -d
CTF{Wh4t_1_4m_w0rth}
$ echo "Q1RGe1doM3JlXzFzX3RoM19mMDBkfQ==" | base64 -d
CTF{Wh3re_1s_th3_f00d}

Well, those were 3 quick flags... We can submit them to the event and yes, they are all correct.