Guide on reverse client-server apk on the example of the job NeoQUEST-2020


Today we have a rich program (there would be so many cybersecurity areas at a time!): Consider decompiling an Android application, intercepting traffic to get URLs, rebuilding apk without source code, working with cryptanalysts and much more :)

According to legend NeoQUEST-2020 , the hero found old robot parts that must be used to obtain the key. Let's get it started!

1. Reversim apk


So, before us is a little that we managed to extract from a semi-disassembled robot - an apk application that somehow should help us get the key. Let's do the most obvious: run apk and look at its functionality. The more than minimalistic application interface leaves no doubt - this is a custom FileDroid file client that allows you to download a file from a remote server. Okay, it looks easy. We connect the phone to the Internet, make a trial attempt to download (immediately key.txt - well, what if?) - unsuccessfully, the file is missing on the server.



We proceed to the next event in terms of complexity - we decompile apk using JADXand analyze the source code of the application, which, fortunately, is not at all obfuscated. Our current task is to understand what files the remote server offers for downloading, and select the one with the key from them.

We start with the com.ctf.filedroid.MainActivity class, which contains the most interesting onClick () method for us, in which the click on the “Download” button is processed. Inside this method, the ConnectionHandler class is called twice: first, the ConnectionHandler.getToken () method is called, and only then, ConnectionHandler.getEncryptedFile (), which passes the name of the file requested by the user, is called.



Yeah, that is, first we need a token! We will examine a little more with the process of obtaining it.
The ConnectionHandler.getToken () method takes two lines of input, and then sends a GET request, passing these lines as the “crc” and “sign” parameters. In response, the server sends the data in JSON format, from which our application extracts the access token and uses it to download the file. This is all, of course, good, but what are “crc” and “sign”?



To understand this, we move further towards the Checks class, kindly providing the badHash () and badSign () methods. The first one calculates the checksum from classes.dex and resources.arsc, concatenates these two values ​​and wraps it in Base64 (pay attention to the flag 10 = NO_WRAP | URL_SAFE, it will come in handy). And what about the second method? And he does the same with the SHA-256 fingerprint of the application signature. Eh, it looks like FileDroid is not really eager to be rebuilt :(



Ok, let's say that we received the token. What's next? We pass it to the input of the ConnectionHandler.getEncryptedFile () method, which appends the name of the requested file to the token and generates another GET request, this time with the “token” and “file” parameters. The server in response (judging by the name of the method) sends an encrypted file, which is stored on / sdcard /.

So, to summarize a little subtotal: we have two news, and ... both are bad. Firstly, FileDroid does not really support our zeal for modifying apk (checksum and signature are checked), and secondly, the file received from the server promises to be encrypted.

Okay, we will solve the problems as they become available, and now our main problem is that we still do not know which file we need to download. However, while studying the ConnectionHandler class, we couldn't help but notice that right between the getToken () and getEncryptedFile () methods, the FileDroid developers forgot another very seductive method called the getListing () name. So, the server supports such functionality ... It seems that this is what you need!



To get the listing, we will need the well-known “crc” and “sign” - not a problem, we already know where they come from. We read the values, send a GET request and ... So, stop. Where are we going to send the GET request? It would be nice to get the remote server URL first. Eh, we return to MainActivity.onClick () and see how the netPath arguments are generated to call the getToken () and getEncryptedFile () methods:

Method getSecureMethod = 
wat.class.getDeclaredMethod("getSecure", new Class[]{String.class});

// . . .

// netPath --> ConnectionHandler.getToken()
(String) getSecureMethod.invoke((Object) null, new Object[]{"fnks"})

// netPath --> ConnectionHandler. getEncryptedFile()
(String) getSecureMethod.invoke((Object) null, new Object[]{"qdkm"})

Strange letter combinations “fnks” and “qdmk” force us to turn to the result of decompilation of the wat.getSecure () method. Spoiler: JADX has this result so-so.



Upon closer inspection, it becomes clear that all this not very pleasant content of the method can be replaced with the usual switch-case of this kind:

// . . .
switch(CODE)
{
    case «qdkm»: 
        r.2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(2);
        break;
    case «tkog»: 
        r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(1);
        break;
    case «fnks»: 
        String r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(0);
	break;
}
java.lang.StringBuilder r1 = new java.lang.StringBuilder 
r1.<init>(r2) 
java.lang.String r0 = r1.toString() 
java.lang.String r1 = radon(r0)
return r1 

Since “fnks” and “qdmk” are already used to obtain a token and download a file, “tkog” should give the URL required to request a listing of available files on the server. It seems that there is a hope to get the required path cheaply ... First of all, let's see how URLs are stored in the application. We open the com.ctf.filedroid.x37AtsW8g.rlieh786d () function and see that each URL is saved as an encoded byte array, and the function itself forms a string from these bytes and returns it.



Good. But then the line is passed to the com.ctf.filedroid.wat.radon () function, the implementation of which is submitted to the native library libae3d8oe1.so. Reverse arm64? Nice try, FileDroid, but come on another time?

2. Get the server urls


Let's try to approach from the other side: to intercept traffic, get URLs in clear text (and as a bonus, also checksum and signature values!), Match them to byte arrays from com.ctf.filedroid.x37AtsW8g.rlieh786d () - it can Does encryption turn out to be the usual Caesar or XOR cipher? .. Then it will not be difficult to restore the third URL and perform the listing.

To redirect traffic, you can use any convenient proxy ( Charles , fiddler , BURPetc.). We configure forwarding on a mobile device, install the appropriate certificate, verify that the interception is successful, and launch FileFroid. We are trying to download an arbitrary file and ... see "NetworkError". This error was caused by the presence of certificate-pinning (see com.ctf.filedroid.ConnectionHandler.sendRequest method): the file client verifies that the certificate “wired” in the application corresponds to the server with which it interacts. Now it’s clear why the integrity of application resources is controlled!



However, in the intercepted traffic, we can see at least the domain name of the server accessed by the file client, which means that the hope of decrypting the URLs remains!



Let's return to the com.ctf.filedroid.x37AtsW8g.rlieh786d () function and note that the first few tens of bytes coincide in all arrays:

cArr[0] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'i', . . .};

cArr[1] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . .};

cArr[2] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . ., '='};

In addition, the last byte of the third array hints that it was not without base64. Let's try to decode and poke the resulting bytes with a known part of the URL:



It seems that no one has ever been so happy with ARMag3dd0n! The thing is small: sequentially decode base64 URLs and xorim with the key found. But ... and if it was not XOR, but a self-made permutation cipher, which you can’t pick up even with a hundred attempts?

3. Rebuild apk with Frida


As part of this write-up, we will consider a more painless (and, in our opinion, more beautiful) solution method - using the Frida framework , which will allow us to execute arbitrary apk application methods with the arguments we need in run-time. To do this, you need a phone with root rights or an emulator. We assume the following action plan:

  1. Installing Frida components on a PC and a test phone.
  2. Recover URLs matching token or listing requests and download the file (using Frida).
  3. Retrieving the checksum and signature values ​​of the original application.
  4. Getting a listing of files stored on the server, and identifying the desired file.
  5. Download and decrypt the file.

First, we will clarify the relationship between the rooted phone and apk. We install the application, run it, but the file client does not want to fully boot, it only blinks and closes. We check messages through logcat - yes, it is, FileDroid already feels that something is amiss and resists as it can.



We again turn to the MainActivity class and find that the doChecks () method is called in onCreate (), and it displays the following errors in the log:



In addition, onResume () also checks to see if the port typical for Frida is open:



Our file client is a little intolerant to debugging, root and Frida itself. Such opposition is absolutely not included in our plans, so we get the smali-code of the application using the apktool utility, open the MainActivity.smali file in any text editor, find the onCreate () method and turn the doChecks () call into a harmless comment:



Then we deprive the suicide () method of the opportunity to really shut down the application:



Next, let's build our slightly improved application again using apktool and sign him by executing the following commands (you may need Administrator rights):

cd "C:\Program Files\Java\jdk-14\bin"
.\keytool -genkey -v -keystore filedroid.keystore -alias filedroid_alias -keyalg RSA -keysize 2048 -validity 10000
.\jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore  filedroid.keystore filedroid_patched.apk filedroid_alias
.\jarsigner -verify -verbose -certs filedroid_patched.apk

We reinstall the application on the phone, run it - hurray, the download goes without incident, the log is clean!



We proceed to install the Frida framework on a PC and mobile device:

$ sudo pip3 install frida-tools
$ wget https://github.com/frida/frida/releases/download/$(frida --version)/frida-server-$(frida --version)-android-arm.xz
$ unxz frida-server-$(frida --version)-android-arm.xz
$ adb push frida-server-$(frida --version)-android-arm /data/local/tmp/frida-server

Launch the Frida framework server on a mobile device:

$ adb shell su - "chmod 755 /data/local/tmp/frida-server"
$ adb shell su - "/data/local/tmp/frida-server &"  

We are preparing a simple get-urls.js script that will call wat.getSecure () for all supported request servers:

Java.perform(function () 
{
     const wat = Java.use('com.ctf.filedroid.wat');
	console.log(wat.getSecure("fnks"));
	console.log(wat.getSecure("qdmk"));
	console.log(wat.getSecure("tkog"));
});

We launch FileDroid on the mobile device and “cling” with our script to the corresponding process:



4. Get the listing of files on the server


Finally, the remote server has become a little closer to us! Now we know that the server supports requests in the following ways:

  1. filedroid.neoquest.ru/api/verifyme?crc= {crc} & sign = {sign}
  2. filedroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc} & sign = {sign}
  3. filedroid.neoquest.ru/api/file?file= {file} & token = {token}

In order to get a listing of the available files, it remains to calculate the checksum and signature values ​​of the original application, and then encode them in base64.

Such a script in python3 will allow you to do this:

Spoiler
import hashlib
import binascii
import base64
from asn1crypto import cms, x509
from zipfile import ZipFile


def get_info(apk):
    with ZipFile(apk, 'r') as zipObj:
        classes = zipObj.read("classes.dex")
        resources = zipObj.read("resources.arsc")
        cert = zipObj.read("META-INF/CERT.RSA")
        crc = "%s%s" % (get_crc(classes), get_crc(resources))
        return get_full_crc(classes, resources).decode("utf-8"), get_sign(cert).decode("utf-8")


def get_crc(file):
    crc = binascii.crc32(file) & 0xffffffff
    return crc


def get_full_crc(classes, resources):
    crc = "%s%s" % (get_crc(classes), get_crc(resources))
    return base64.urlsafe_b64encode(bytes(crc, "utf-8"))


def get_sign(file):
    pkcs7 = cms.ContentInfo.load(file)
    data = pkcs7['content']['certificates'][0].chosen.dump()
    sha256 = hashlib.sha256()
    sha256.update(data)
    return base64.urlsafe_b64encode(sha256.digest())  

get_info('filedroid.apk')


You can also manually. We consider CRC32 from classes.dex and resources.arsc to be any convenient tool (for example, for Linux - the standard crc32 utility), we get the values ​​1276945813 and 2814166583 respectively, concatenate them (12769458132814166583 will come out) and encode in base64, for example, here :



In order to perform a similar procedure for signing the application, in the JADX window, go to the “APK Signature” section, copy the “SHA-256 Fingerprint” value and encode it in base64 as a byte array:



Important:in the original apk, base64 encoding is performed with the URL_SAFE flag, i.e. instead of the characters “+” and “/”, “-” and “_” are used, respectively. It is necessary to make sure that this will also be observed with independent coding. To do this, when coding online, you can replace the alphabet used with "ABCDEFGHIJKLMNOPQRSTUVWXYZabcde fghijklmnopqrstuvwxyz0123456789 + /" with "ABCDEFGHIJKLMNOPQRSTUVWXYZabc script64 using 64 script.jp64.jp64.jp64.jpcrqvqcdvcdcdbc

Finally, we have all the ingredients for successfully listing files:

  1. filedroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc} & sign = {sign}
  2. crc: MTI3Njk0NTgxMzI4MTQxNjY1ODM =
  3. sign: HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY =

We execute the GET request - and cheers, our listing! Moreover, the name of one of the files speaks for itself - "open-if-you-want-to-escape" - it seems that we need it.



Next, we request a one-time access token and download the file:

import requests

response = requests.get('https://filedroid.neoquest.ru/api/verifyme', 
    		params={    'crc': 'MTI3Njk0NTgxMzI4MTQxNjY1ODM=', 
'sign': HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY=},
verify=False)
    
token = response.json()['token']
print(token)
response = requests.get('https://filedroid.neoquest.ru/api/file', 
params={'token': token, 'file': '0p3n1fuw4nt2esk4p3.jpg'}, verify=False)

with open("0p3n1fuw4nt2esk4p3.jpg", 'wb') as fd:
        fd.write(response.content)

We open the downloaded file and recall one small circumstance that we left for later:



5. Add a pinch of cryptography ...


Eh, too early we put off FileDroid. Let's go back to JADX and see if the file client developers have left anything useful for us. Yes, this is the case when code cleanup is clearly not popular: the unused decryptFile () method is quietly waiting for our attention in the ConnectionHandler class. What we have?

Encryption AES mode the CBC , sinhroposylka occupies the first 16 bytes ... Laziness - the engine of progress, it is better again use Frida and decipher our 0p3n1fuw4nt2esk4p3.jpg effortlessly. But just pass as the encryption key? There are not many options, but given the presence of another “forgotten” savePlainFile (String file, String token) method, the choice is obvious.

Prepare the following decrypt.js script (as token indicate the actual value, for example, 'HoHknc572mVpZESSQN1Xa7S9zOidxX1PMbykdoM1EXI ='):

Java.perform(function () {
    const JavaString = Java.use('java.lang.String');
    const file_name = JavaString.$new('0p3n1fuw4nt2esk4p3.jpg');
    const ConnectionHandler = Java.use('com.ctf.filedroid.ConnectionHandler');
    const result = ConnectionHandler.savePlainFile(file_name, <token>);
    console.log(result);
});

We put the encrypted file 0p3n1fuw4nt2esk4p3.jpg on / sdcard /, run FileDroid and inject the decrypt.js script using Frida. After the script runs, the plainfile.jpg file will appear on / sdcard /. We open it and ... just solved!



This difficult task required participants to have knowledge and skills in several areas of information security at once, and we are glad that most competitors successfully coped with it!

We hope that those who did not have enough time or knowledge before receiving the key will now successfully pass similar tasks in any CTF :)

All Articles