Preface

Today, I want to talk about best practices related to the usage of securing your confidential data for your mobile applications. I will reverse engineer an Android application to show my point of view.

Example

I am going to use this application for demonstration purposes only. This app is a parcel tracking application. It creates JWT with a key on the device. However, the key is obfuscated and embedded inside the application. (PS: Using JWT like this is a terrible idea. Anybody can create a JWT with any date/data and can use it indefinitely. )

Let’s try to find this JWT key. When we open this app on our emulator, we are greeted with a message that our app doesn’t run on rooted devices. Great, we are dealing with such a secure app. We have to use some static analysis first to find an attack point. We open our app with Jadx and search for the error message.

    if (new sb.b(this).b()) {
        O("perangkat anda terdeteksi sebagai rooted device, untuk alasan keamanan data & transaksi anda, tiki apps saat ini tidak dapat di jalankan di device anda");
    } else {
        P();
    }

    public boolean a(String str) {
        String[] a10;
        boolean z10 = false;
        for (String str2 : a.a()) {
            String str3 = str2 + str;
            if (new File(str2, str).exists()) {
                tb.a.c(str3 + " binary detected!");
                z10 = true;
            }
        }
        return z10;
    }

    public boolean b() {
        return a("su");
    }

It basically checks the presence of the su binary. We can easily bypass this by using any anti-root detection scripts. Following Frida script will be enough to pass this check. Save the below file as fix.js

Java.perform(function() {
    var NativeFile = Java.use('java.io.File');
    NativeFile.exists.implementation = function() {
        var name = NativeFile.getName.call(this);
        if (name == 'su') {
            return false;
        } else {
            return this.exists.call(this);
        }
    };
})

If we run our app with

frida --no-pause -U -l fix.js -f id.tiki.tikiapp

The app will start and we will see the home screen. After some digging, we will find the JWT key generation code below

    public String a() {
        this.f4973b.a();
        SecretKey hmacShaKeyFor = Keys.hmacShaKeyFor(("xMhdbrzkML2Scqu4K5CiUf7" + this.f4972a.getString(R.string.tiki_jwt_parts_v4) + "T1GMj6Dlyb19qhbMDZSX0" + this.f4975d + "QGhcDtFTE0jk1").getBytes(StandardCharsets.UTF_8));
        Long valueOf = Long.valueOf(new Date().getTime());
        Date date = new Date();
        date.setTime(valueOf.longValue());
        Date date2 = new Date();
        date2.setTime(valueOf.longValue() + 30000);
        return Jwts.builder().setIssuedAt(date).setExpiration(date2).setSubject("tiki").signWith(hmacShaKeyFor, SignatureAlgorithm.HS256).compact();
    }

As you can see, they’re creating the JWT key by concatenating some values. This is called security through obscurity. It can only stop novice attackers. I can get the f4972a and f4975d variables and concatenate them myself. However, this is not necessary at all. At the end of the concatenation, I will have the full key. Instead of calculating this key, I can capture the key by using Frida by hooking the right function. This app uses javax.crypto.spec.SecretKeySpec to create the key so hooking that function will be enough to capture the key.

Modify our above script and make it like below

Java.perform(function() {
    var NativeFile = Java.use('java.io.File');
    NativeFile.exists.implementation = function() {
        var name = NativeFile.getName.call(this);
        if (name == 'su') {
            return false;
        } else {
            return this.exists.call(this);
        }
    };

    var String = Java.use("java.lang.String")
    var SecretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec');
    SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function(key, type) {
        var s = String.$new(key, "UTF-8")
        console.log("[+] Key (Decimal): " + key)
        console.log("[+] Key (String): " + s)
        console.log("[+] Type: " + type)
        return this.$init(key,type)
    }

})

Run the app again

frida --no-pause -U -l fix.js -f id.tiki.tikiapp

Just track a package and you will see the following log on the console.

[+] Key (Decimal): 120,77,104,100,...
[+] Key (String): xMhdbrzkML2Scqu4K5CiUf7xwx...
[+] Type: HmacSHA512

Since I have the JWT key, I can create any JWT I want. This was a little bit complicated but most of the time, developers don’t even protect their keys. They just embed it in their application. Therefore capturing network traffic is enough to find the API keys. It is also very easy to sniff SSL traffic or SSL pinning. Please check this blog post to learn more

Best practices

  • Do not embed your API keys if you’re paying for the usage. Instead, use a small server and just proxy your request through that server. Having a small server will help you to see who is using that API key. You can restrict the usage of the key based on users. Rotating the key will be much easier. Remember, all the requests you make via your app can be captured, hooked, and analyzed. Your API keys should not appear in network traffic.
  • If you use a server, you can also cache the result of your requests and prevent over-usage. Use Redis, memcached, or similar technologies to cache the result.
  • You also need to have some kind of authentication/authorization between your mobile app and your server. If you don’t have any authorization, I can simply call your server instead of using your app. You can check the device for Jailbreak and act accordingly. However, never alienate your users who jailbreak their devices. Just give them some score and act accordingly. You may use Apple’s App Attest Service or Google’s Play Integrity API
I am available for new work
Interested? Feel free to reach