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
root checking using well known library named rootbeer
debugging check using FLAG_DEBUGGABLE and getApplicationInfo
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


