Emulating Android Native Library using Qiling - Part 1

Study case ADDA CTF 2022 (wonder maze)

# Preface

During the competition my team got 2nd place on Quals and Final. It was my first time to do emulating using Qiling and it was very powerful since i didn't need to reconstruct the whole code like i did as usual.

Analyzing APK Statically

Given APK file, decompile using jadx-gui

package com.hexagonal.wondermaze;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.scottyab.rootbeer.RootBeer;
import java.util.Timer;
import java.util.TimerTask;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;

/* compiled from: MainActivity.kt */
@Metadata(d1 = {"\u00008\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0005\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005’\u0006\u0002\u0010\u0002J\u000e\u0010\u000b\u001a\u00020\f2\u0006\u0010\r\u001a\u00020\u000eJ\u0012\u0010\u000f\u001a\u00020\f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0011H\u0014J\u0010\u0010\u0012\u001a\u00020\f2\u0006\u0010\u0013\u001a\u00020\u0014H\u0002J\b\u0010\u0015\u001a\u00020\fH\u0002R\u000e\u0010\u0003\u001a\u00020\u0004X\u0082\u0004’\u0006\u0002\n\u0000R\u001c\u0010\u0005\u001a\u0004\u0018\u00010\u0006X\u0086\u000e’\u0006\u000e\n\u0000\u001a\u0004\b\u0007\u0010\b\"\u0004\b\t\u0010\n¨\u0006\u0016"}, d2 = {"Lcom/hexagonal/wondermaze/MainActivity;", "Landroidx/appcompat/app/AppCompatActivity;", "()V", "pow", "Lcom/hexagonal/wondermaze/ProofOfWork;", "timer", "Ljava/util/Timer;", "getTimer", "()Ljava/util/Timer;", "setTimer", "(Ljava/util/Timer;)V", "checkButton", "", "view", "Landroid/view/View;", "onCreate", "savedInstanceState", "Landroid/os/Bundle;", "toast", "message", "", "updateOtp", "app_release"}, k = 1, mv = {1, 6, 0}, xi = 48)
/* loaded from: classes.dex */
public final class MainActivity extends AppCompatActivity {
    private final ProofOfWork pow = new ProofOfWork();
    private Timer timer = new Timer();

    public final Timer getTimer() {
        return this.timer;
    }

    public final void setTimer(Timer timer) {
        this.timer = timer;
    }

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_main);
        if (new RootBeer(this).isRooted()) {
            finish();
            System.exit(0);
            throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
        } else if ((getApplicationContext().getApplicationInfo().flags & 2) != 0) {
            finish();
            System.exit(0);
            throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM.");
        } else {
            View findViewById = findViewById(R.id.text_otp);
            Intrinsics.checkNotNullExpressionValue(findViewById, "findViewById(R.id.text_otp)");
            ((TextView) findViewById).setText(String.valueOf(this.pow.getOtpCode()));
            Timer timer = this.timer;
            if (timer == null) {
                return;
            }
            timer.scheduleAtFixedRate(new TimerTask() { // from class: com.hexagonal.wondermaze.MainActivity$onCreate$$inlined$timerTask$1
                @Override // java.util.TimerTask, java.lang.Runnable
                public void run() {
                    final MainActivity mainActivity = MainActivity.this;
                    mainActivity.runOnUiThread(new Runnable() { // from class: com.hexagonal.wondermaze.MainActivity$onCreate$1$1
                        @Override // java.lang.Runnable
                        public final void run() {
                            MainActivity.this.updateOtp();
                        }
                    });
                }
            }, 0L, 30000L);
        }
    }

    public final void checkButton(View view) {
        Intrinsics.checkNotNullParameter(view, "view");
        if (this.pow.check(((EditText) findViewById(R.id.editText_otpInput)).getText().toString())) {
            toast("Proof of work is valid!");
            Intent intent = new Intent(this, NavigationActivity.class);
            Timer timer = this.timer;
            if (timer != null) {
                timer.cancel();
            }
            this.timer = null;
            startActivity(intent);
            return;
        }
        updateOtp();
    }

    /* JADX INFO: Access modifiers changed from: private */
    public final void updateOtp() {
        this.pow.regen();
        View findViewById = findViewById(R.id.text_otp);
        Intrinsics.checkNotNullExpressionValue(findViewById, "findViewById(R.id.text_otp)");
        ((TextView) findViewById).setText(String.valueOf(this.pow.getOtpCode()));
    }

    private final void toast(String str) {
        Toast.makeText(this, str, 0).show();
    }
}

From code above, we can see that there are security mechanism implemented by the APK

So if we run APK not in rooted device and without debug it, we will go to OTP section. if our OTP valid, we will go to the maze section (NavigationActivity.class) which is the main scene in this case.

Navigation class will create maze through this code

Now, take a look on Game class

We can see that there is no flag printed, but there are some suspicious function called from native library.

Analyzing Native Library Statically

Now, open native-lib file on lib/x86_64/libnative-lib.so.

Creating Emulation for Native Library (x86_64)

Actually we can reconstruct all of those code, but it should be easier to just "emulate" it. To emulate it we can utilize Qiling framework. Basically it was something like running assembly from start address until end address and define the value of register if needed. Lets try on checkDeviceHealth function first

  • checkDeviceHealth

    • There is no argument required

    • Start address located at 0x1190

    • Values that we need to dump stored on v4 or third argument (rdx) on _fprintf.

      • So end address could be 0x13d3 since it is the latest "values" processing before writing file initialization

      • Values stored on rsp, so at the end of the code we need to dump rsp values

Next, we try to emulate Java_com_hexagonal_wondermaze_Game_checkRoot.

  • Java_com_hexagonal_wondermaze_Game_checkRoot

    • There is no argument required

    • Start address located at 0x1870

    • Implement custom memcpy

      • copy array values to rsp

    • End address located at 0x19c6

      • Values stored on rsp

Last function we need to emulate is getSystemTime

  • getSystemTime

    • There is no argument required

    • Start address located at 0x1640

    • Since fopen and printf not used, we can skip those functions by setting rip value

      • hook at 0x176d

        • change rip to 0x1772

      • hook at 0x166d

        • change rip to 0x1672

    • End address located at 0x17f2

      • Values stored on rsp

Putting all function together and got the flag

Flag : ctf{Th3P4thKe3p5Ch4ng1n9}

Last updated