Reverse engineering Crypto_wallet FORTID CTF 2026
Solve for Crypto_wallet by aresinheaven
Let's do a quick recap of what I understood for this challenge.
So we were given an APk file Crypto_wallet.apk, Quick shoutout to https://github.com/MG1937 for making one of the best decompilers https://github.com/MG1937/ASC Out there; Okay so ASC helped me to understand the wallet recovery flow which basically splits a 12 word phrase into four what3words references.
And then the native code supplies four airport area coordinates which are published addresses for the nearby places which we can narrow the candidates to four BIP39 compatible triples,and then the resulting phrase derives the expected Ethereum address and authenticates the encrypted flag this was my completed understanding and flow of the challenge solve.
Lets Decompile the apk with ASC and trace out the recovery function
I used ASC through droidasc 0.1.1.post3.
(ctf-env) manavhowal@manavs-MacBook-Air downloads % droidasc getmanifest "$APK" -o "$OUT/AndroidManifest.xml"
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="2" android:versionName="1.1" android:compileSdkVersion="35" android:compileSdkVersionCodename="15" package="com.fortid.cryptowallet" platformBuildVersionCode="35" platformBuildVersionName="15">
<uses-sdk android:minSdkVersion="26" android:targetSdkVersion="35"/>
<permission android:name="com.fortid.cryptowallet.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:protectionLevel="0x00000002"/>
<uses-permission android:name="com.fortid.cryptowallet.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
<application android:theme="@7F0F0221" android:label="Cryptocurrency Wallet" android:icon="@7F070088" android:allowBackup="false" android:supportsRtl="true" android:extractNativeLibs="false" android:fullBackupContent="false" android:appComponentFactory="androidx.core.app.CoreComponentFactory">
<activity android:name="com.fortid.cryptowallet.MainActivity" android:exported="true" android:screenOrientation="1" android:windowSoftInputMode="0x00000010">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<provider android:name="androidx.startup.InitializationProvider" android:exported="false" android:authorities="com.fortid.cryptowallet.androidx-startup">
<meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
<meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/>
<meta-data android:name="androidx.profileinstaller.ProfileInstallerInitializer" android:value="androidx.startup"/>
</provider>
<receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" android:permission="android.permission.DUMP" android:enabled="true" android:exported="true" android:directBootAware="false">
<intent-filter>
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SKIP_FILE"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/>
</intent-filter>
</receiver>
</application>
</manifest>
(ctf-env) manavhowal@manavs-MacBook-Air downloads % droidasc listclass "$APK" --prefix com.fortid.cryptowallet
Lcom/fortid/cryptowallet/GeoPoint;
Lcom/fortid/cryptowallet/MainActivity;
Lcom/fortid/cryptowallet/NativeVerifier;
Lcom/fortid/cryptowallet/ObjectVicinityService;
Lcom/fortid/cryptowallet/PendingPartnershipVicinityService;
Lcom/fortid/cryptowallet/TrendChartView;
Lcom/fortid/cryptowallet/VicinityClient;
Lcom/fortid/cryptowallet/VicinityResult;
(ctf-env) manavhowal@manavs-MacBook-Air downloads % droidasc getclass "$APK" com.fortid.cryptowallet.MainActivity -o "$OUT/decompiled/MainActivity.java"
package com.fortid.cryptowallet;
public final class MainActivity extends e.h {
public static final synthetic int C;
public int A;
public int B;
public i0.a x;
public final java.util.ArrayList y;
public final java.util.concurrent.ExecutorService z;
public MainActivity()
{
((a0.e) this.e.c).e("androidx:appcompat", new a0.a(this));
this.h(new e.g(this));
this.y = new java.util.ArrayList();
this.z = java.util.concurrent.Executors.newSingleThreadExecutor();
return;
}
public final int t(int p2)
{
return ((int) (((float) p2) * this.getResources().getDisplayMetrics().density));
}
public final void onCreate(android.os.Bundle p21)
{
int v3_0 = super.onCreate(p21).getLayoutInflater().inflate(2131427356, 0, 0);
com.google.android.material.textfield.TextInputLayout v4_0 = 2131230768;
int v10_1 = ((android.widget.TextView) P.e.q(v3_0, 2131230768));
if (v10_1 != 0) {
v4_0 = 2131230769;
if (((com.google.android.material.card.MaterialCardView) P.e.q(v3_0, 2131230769)) != null) {
v4_0 = 2131230770;
int v11_4 = ((android.widget.TextView) P.e.q(v3_0, 2131230770));
if (v11_4 != 0) {
v4_0 = 2131230842;
int v12_2 = ((android.widget.TextView) P.e.q(v3_0, 2131230842));
if (v12_2 != 0) {
v4_0 = 2131230843;
int v13_2 = ((android.widget.LinearLayout) P.e.q(v3_0, 2131230843));
if (v13_2 != 0) {
v4_0 = 2131230882;
if (((com.fortid.cryptowallet.TrendChartView) P.e.q(v3_0, 2131230882)) != null) {
v4_0 = 2131230897;
android.widget.TextView v14_1 = ((android.widget.TextView) P.e.q(v3_0, 2131230897));
if (v14_1 != null) {
v4_0 = 2131230968;
if (((android.widget.TextView) P.e.q(v3_0, 2131230968)) != null) {
v4_0 = 2131231007;
if (((android.widget.TextView) P.e.q(v3_0, 2131231007)) != null) {
v4_0 = 2131231052;
android.widget.LinearLayout v15_1 = ((android.widget.LinearLayout) P.e.q(v3_0, 2131231052));
if (v15_1 != null) {
v4_0 = 2131231055;
com.google.android.material.button.MaterialButton v16_1 = ((com.google.android.material.button.MaterialButton) P.e.q(v3_0, 2131231055));
if (v16_1 != null) {
com.google.android.material.textfield.TextInputLayout v4_9 = ((androidx.core.widget.NestedScrollView) v3_0);
int v7_16 = 2131231069;
if (((android.widget.LinearLayout) P.e.q(v3_0, 2131231069)) != null) {
v7_16 = 2131231181;
android.widget.TextView v18_1 = ((android.widget.TextView) P.e.q(v3_0, 2131231181));
if (v18_1 != null) {
v7_16 = 2131231185;
android.widget.GridLayout v19_1 = ((android.widget.GridLayout) P.e.q(v3_0, 2131231185));
if (v19_1 != null) {
int v3_1 = new i0.a;
v3_1(v4_9, v10_1, v11_4, v12_2, v13_2, v14_1, v15_1, v16_1, v4_9, v18_1, v19_1);
this.x = v3_1;
this.setContentView(v4_9);
int v3_2 = this.x;
if (v3_2 == 0) {
T0.c.g();
throw 0;
} else {
this.A = v3_2.i.getPaddingBottom();
int v3_5 = this.x;
if (v3_5 == 0) {
T0.c.g();
throw 0;
} else {
K.B.l(v3_5.i, new I0.l(this));
int v3_7 = this.x;
if (v3_7 == 0) {
T0.c.g();
throw 0;
} else {
v3_7.b.setText("0xdc7d8b\u20264fb522");
int v3_9 = 0;
while (v3_9 < 12) {
int v11_1;
com.google.android.material.textfield.TextInputLayout v4_6 = new com.google.android.material.textfield.TextInputLayout(this, 0);
int v7_1 = (v3_9 + 1);
v4_6.setHint(String.valueOf(v7_1));
v4_6.setBoxBackgroundMode(2);
v4_6.setBoxBackgroundColor(v4_6.getContext().getColor(2131034884));
v4_6.setBoxStrokeColor(v4_6.getContext().getColor(2131034883));
v4_6.setHintTextColor(A.a.D(v4_6.getContext(), 2131034893));
com.google.android.material.textfield.TextInputEditText v8_9 = new android.widget.GridLayout$LayoutParams();
v8_9.width = 0;
v8_9.height = -2;
v8_9.columnSpec = android.widget.GridLayout.spec(-2147483648, 1065353216);
int v9_7 = 5;
int v10_3 = this.t(5);
if ((v3_9 % 2) != 0) {
v11_1 = 0;
} else {
v11_1 = 6;
}
v8_9.setMargins(0, v10_3, this.t(v11_1), this.t(5));
v4_6.setLayoutParams(v8_9);
com.google.android.material.textfield.TextInputEditText v8_11 = new com.google.android.material.textfield.TextInputEditText(v4_6.getContext(), 0);
v8_11.setInputType(524289);
if (v3_9 == 11) {
v9_7 = 6;
}
v8_11.setImeOptions(v9_7);
v8_11.setSingleLine(1);
v8_11.setTextSize(1097859072);
v8_11.setTextColor(v8_11.getContext().getColor(2131034892));
v8_11.setPadding(this.t(14), this.t(14), this.t(14), this.t(14));
v8_11.setOnFocusChangeListener(new h0.a(this, v4_6));
v8_11.setOnEditorActionListener(new h0.b(this, v3_9, v8_11));
v4_6.addView(v8_11, new android.widget.LinearLayout$LayoutParams(-1, this.t(54)));
int v3_13 = this.x;
if (v3_13 == 0) {
T0.c.g();
throw 0;
} else {
v3_13.k.addView(v4_6);
this.y.add(v8_11);
v3_9 = v7_1;
}
}
com.google.android.material.button.MaterialButton v1_2 = this.x;
if (v1_2 == null) {
T0.c.g();
throw 0;
} else {
v1_2.h.setOnClickListener(new I0.a(4, this));
return;
}
}
}
}
}
}
}
v4_0 = v7_16;
}
}
}
}
}
}
}
}
}
}
}
throw new NullPointerException("Missing required view with ID: ".concat(v3_0.getResources().getResourceName(v4_0)));
}
public final void onDestroy()
{
this.z.shutdownNow();
super.onDestroy();
return;
}
public final void u(android.view.View p8)
{
int v0_1 = new android.graphics.Rect();
p8.getDrawingRect(v0_1);
androidx.core.widget.NestedScrollView v1_4 = this.x;
if (v1_4 == null) {
T0.c.g();
throw 0;
} else {
v1_4.i.offsetDescendantRectToMyCoords(p8, v0_1);
int v8_1 = this.x;
if (v8_1 == 0) {
T0.c.g();
throw 0;
} else {
int v8_6 = ((int) (((float) v8_1.i.getHeight()) * 1054280253));
androidx.core.widget.NestedScrollView v1_1 = this.B;
if (v1_1 > null) {
v8_6 = v1_1;
}
androidx.core.widget.NestedScrollView v1_2 = this.x;
if (v1_2 == null) {
T0.c.g();
throw 0;
} else {
androidx.core.widget.NestedScrollView v1_5 = v1_2.i.getScrollY();
int v3_0 = this.x;
if (v3_0 == 0) {
T0.c.g();
throw 0;
} else {
int v3_5 = (((v3_0.i.getHeight() + v1_5) - v8_6) - this.t(36));
int v4_1 = this.x;
if (v4_1 == 0) {
T0.c.g();
throw 0;
} else {
int v8_8;
int v4_3 = v4_1.i.getScrollY();
int v6_0 = this.t(24);
int v4_4 = v0_1.bottom;
if (v4_4 <= v3_5) {
int v8_7 = v0_1.top;
if (v8_7 >= (v6_0 + v4_3)) {
return;
} else {
v8_8 = (v8_7 - this.t(24));
}
} else {
int v0_3 = this.x;
if (v0_3 == 0) {
T0.c.g();
throw 0;
} else {
v8_8 = (this.t(36) + ((v4_4 - v0_3.i.getHeight()) + v8_6));
}
}
if (v8_8 < 0) {
v8_8 = 0;
}
androidx.core.widget.NestedScrollView v1_7 = this.x;
if (v1_7 == null) {
T0.c.g();
throw 0;
} else {
androidx.core.widget.NestedScrollView v1_9 = v1_7.i;
v1_9.u((0 - v1_9.getScrollX()), (v8_8 - v1_9.getScrollY()), 0);
return;
}
}
}
}
}
}
}
public final void v()
{
Throwable v0_0 = this.x;
if (v0_0 == null) {
T0.c.g();
throw 0;
} else {
Throwable v0_3 = v0_0.a;
boolean v2_0 = v0_3.getResources().getText(2131623988);
int v3_3 = 0;
while (!(v0_3 instanceof androidx.coordinatorlayout.widget.CoordinatorLayout)) {
if ((v0_3 instanceof android.widget.FrameLayout)) {
if (v0_3.getId() != 16908290) {
v3_3 = ((android.view.ViewGroup) v0_3);
} else {
Throwable v0_4 = ((android.view.ViewGroup) v0_3);
if (v0_4 == null) {
throw new IllegalArgumentException("No suitable parent found from the given view. Please provide a valid view.");
} else {
H0.m v5_8;
int v3_6 = v0_4.getContext();
Object v4_4 = android.view.LayoutInflater.from(v3_6);
H0.m v5_7 = v3_6.obtainStyledAttributes(H0.k.A);
int v6 = 0;
int v8 = v5_7.getResourceId(0, -1);
int v10 = v5_7.getResourceId(1, -1);
v5_7.recycle();
if ((v8 == -1) || (v10 == -1)) {
v5_8 = 2131427361;
} else {
v5_8 = 2131427412;
}
boolean v2_13;
H0.m v5_10 = new H0.k(v3_6, v0_4, ((com.google.android.material.snackbar.SnackbarContentLayout) v4_4.inflate(v5_8, v0_4, 0)), ((com.google.android.material.snackbar.SnackbarContentLayout) v4_4.inflate(v5_8, v0_4, 0)));
((com.google.android.material.snackbar.SnackbarContentLayout) v5_10.i.getChildAt(0)).getMessageView().setText(v2_0);
v5_10.i.setBackgroundTintList(android.content.res.ColorStateList.valueOf(this.getColor(2131034885)));
((com.google.android.material.snackbar.SnackbarContentLayout) v5_10.i.getChildAt(0)).getMessageView().setTextColor(this.getColor(2131034892));
Throwable v0_16 = H0.n.j();
if (android.os.Build$VERSION.SDK_INT < 29) {
v2_13 = 0;
} else {
v2_13 = C.b.c(v5_10.z);
}
int v3_9 = v5_10.s;
if (!v0_16.k(v3_9)) {
H0.m v5_1 = ((H0.m) v0_16.d);
if ((v5_1 != null) && (v5_1.a.get() == v3_9)) {
v6 = 1;
}
if (v6 == 0) {
v0_16.d = new H0.m(v2_13, v3_9);
} else {
((H0.m) v0_16.d).b = v2_13;
}
boolean v2_2 = ((H0.m) v0_16.c);
if ((!v2_2) || (!v0_16.b(v2_2, 4))) {
v0_16.c = 0;
v0_16.t();
} else {
}
} else {
H0.m v1_2 = ((H0.m) v0_16.c);
v1_2.b = v2_13;
((android.os.Handler) v0_16.b).removeCallbacksAndMessages(v1_2);
v0_16.s(((H0.m) v0_16.c));
}
return;
}
}
}
Throwable v0_2 = v0_3.getParent();
if (!(v0_2 instanceof android.view.View)) {
v0_3 = 0;
} else {
v0_3 = ((android.view.View) v0_2);
}
if (v0_3 == null) {
v0_4 = v3_3;
}
}
v0_4 = ((android.view.ViewGroup) v0_3);
}
}
public final void w()
{
int v0_1 = this.getSystemService("input_method");
T0.c.c(v0_1, "null cannot be cast to non-null type android.view.inputmethod.InputMethodManager");
android.os.IBinder v1_3 = this.x;
if (v1_3 == null) {
T0.c.g();
throw 0;
} else {
((android.view.inputmethod.InputMethodManager) v0_1).hideSoftInputFromWindow(v1_3.a.getWindowToken(), 0);
return;
}
}
public final void x(android.view.View p6)
{
i0.a v0_0 = this.x;
if (v0_0 == null) {
T0.c.g();
throw 0;
} else {
v0_0.i.post(new h0.c(this, p6, 0));
i0.a v0_1 = this.x;
if (v0_1 == null) {
T0.c.g();
throw 0;
} else {
v0_1.i.postDelayed(new h0.c(this, p6, 1), 120);
i0.a v0_3 = this.x;
if (v0_3 == null) {
T0.c.g();
throw 0;
} else {
v0_3.i.postDelayed(new h0.c(this, p6, 2), 320);
return;
}
}
}
}
public final void y(boolean p4)
{
com.google.android.material.button.MaterialButton v0_0 = this.x;
if (v0_0 == null) {
T0.c.g();
throw 0;
} else {
v0_0.h.setEnabled((p4 ^ 1));
com.google.android.material.button.MaterialButton v0_3 = this.x;
if (v0_3 == null) {
T0.c.g();
throw 0;
} else {
String v4_2;
if (p4 == null) {
v4_2 = this.getString(2131624113);
T0.c.d(v4_2, "getString(...)");
} else {
v4_2 = "Verifying\u2026";
}
v0_3.h.setText(v4_2);
return;
}
}
}
public final String z(java.util.ArrayList p25)
{
byte[] v1_21;
byte[] v4_1 = 0;
javax.crypto.spec.SecretKeySpec v6_3 = 1;
int v7_42 = com.fortid.cryptowallet.NativeVerifier.INSTANCE.targetCoordinates();
if (v7_42.length == 8) {
String v8_5 = new java.util.ArrayList(4);
long[] v10_0 = 0;
while (v10_0 < 4) {
long v12_10 = (v10_0 * 2);
v8_5.add(new com.fortid.cryptowallet.GeoPoint(v7_42[v12_10], v7_42[(v12_10 + 1)]));
v10_0++;
}
byte[] v1_11 = new java.util.ArrayList(4);
String v2_1 = 0;
while (v2_1 < 4) {
int v7_28 = (v2_1 * 3);
v1_11.add("///".concat(M0.c.B0(p25.subList(v7_28, (v7_28 + 3)), ".")));
v2_1++;
}
String v2_2 = new boolean[16];
int v7_18 = v1_11.size();
long[] v10_2 = 0;
while (v10_2 < v7_18) {
int v11_6 = v8_5.size();
long v12_2 = 0;
while (v12_2 < v11_6) {
v2_2[((v10_2 * 4) + v12_2)] = com.fortid.cryptowallet.VicinityClient.INSTANCE.checkObjectVicinity(((String) v1_11.get(v10_2)), ((com.fortid.cryptowallet.GeoPoint) v8_5.get(v12_2)), 900).getInVicinity();
v12_2++;
javax.crypto.Cipher v5 = 8;
}
v10_2++;
v5 = 8;
}
v1_21 = com.fortid.cryptowallet.NativeVerifier.INSTANCE.hasPerfectMatching(v2_2);
} else {
v1_21 = 0;
}
if (v1_21 != null) {
if (p25.size() == 12) {
String v3_4 = j0.a.d;
if (v3_4 == null) {
v3_4 = j0.a.d;
if (v3_4 == null) {
String v3_23 = this.getAssets().open("bip39_english.txt");
T0.c.d(v3_23, "open(...)");
String v3_0 = new java.io.BufferedReader(new java.io.InputStreamReader(v3_23, Y0.a.a), 8192);
try {
int v7_0 = P.e.P(v3_0);
} catch (java.nio.charset.Charset v0_1) {
String v2_0 = v0_1;
try {
throw v2_0;
} catch (java.nio.charset.Charset v0_2) {
byte[] v4_0 = v0_2;
P.e.j(v3_0, v2_0);
throw v4_0;
}
}
v3_0.close();
String v3_2 = new M0.p(new M0.j(0, v7_0));
int v7_2 = M0.r.A0(M0.e.A0(v3_2));
if (v7_2 < 16) {
v7_2 = 16;
}
String v8_4 = new java.util.LinkedHashMap(v7_2);
String v3_3 = v3_2.iterator();
while (((java.util.Iterator) ((M0.q) v3_3).c).hasNext()) {
int v7_39 = ((M0.o) ((M0.q) v3_3).next());
v8_4.put(v7_39.b, Integer.valueOf(v7_39.a));
}
j0.a.d = v8_4;
v3_4 = v8_4;
}
}
byte[] v1_1 = new java.util.ArrayList(M0.e.A0(p25));
int v7_9 = p25.iterator();
while (v7_9.hasNext()) {
String v8_38 = ((Integer) v3_4.get(((String) v7_9.next())));
if (v8_38 != null) {
v1_1.add(v8_38);
}
}
String v3_5 = new byte[16];
byte[] v1_2 = v1_1.iterator();
int v7_10 = 0;
String v8_7 = 0;
while (v1_2.hasNext()) {
String v9_19 = ((Number) v1_2.next()).intValue();
int v11_11 = 10;
long v12_11 = -1;
while (v12_11 < v11_11) {
long v12_13 = ((v9_19 >> v11_11) & 1);
if (v8_7 >= 128) {
v7_10 = ((v7_10 << 1) | v12_13);
} else {
if (v12_13 != 0) {
long v13_18 = (v8_7 / 8);
v3_5[v13_18] = ((byte) ((1 << (7 - (v8_7 % 8))) | v3_5[v13_18]));
}
}
v8_7++;
v12_11 = -1;
v11_11 += -1;
}
}
if (v7_10 == ((java.security.MessageDigest.getInstance("SHA-256").digest(v3_5)[0] & 255) >> 4)) {
if (p25.size() != 12) {
throw new IllegalArgumentException("Failed requirement.");
} else {
if (!p25.isEmpty()) {
byte[] v1_13 = p25.iterator();
while (v1_13.hasNext()) {
int v7_13 = ((String) v1_13.next());
String v8_9 = java.util.regex.Pattern.compile("[a-z]+");
T0.c.d(v8_9, "compile(...)");
T0.c.e(v7_13, "input");
if (!v8_9.matcher(v7_13).matches()) {
throw new IllegalArgumentException("Failed requirement.");
}
}
}
byte[] v1_15 = java.text.Normalizer$Form.NFKD;
java.nio.charset.Charset v0_8 = java.text.Normalizer.normalize(M0.c.B0(p25, " "), v1_15);
byte[] v1_16 = java.text.Normalizer.normalize("mnemonic", v1_15);
T0.c.b(v0_8);
java.nio.charset.Charset v0_9 = v0_8.toCharArray();
T0.c.d(v0_9, "toCharArray(...)");
T0.c.b(v1_16);
String v8_11 = java.nio.charset.StandardCharsets.UTF_8;
T0.c.d(v8_11, "UTF_8");
byte[] v1_17 = v1_16.getBytes(v8_11);
T0.c.d(v1_17, "getBytes(...)");
java.nio.charset.Charset v0_13 = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512").generateSecret(new javax.crypto.spec.PBEKeySpec(v0_9, v1_17, 2048, 512)).getEncoded();
T0.c.b(v0_13);
int v7_21 = java.nio.charset.StandardCharsets.US_ASCII;
T0.c.d(v7_21, "US_ASCII");
int v7_22 = "Bitcoin seed".getBytes(v7_21);
T0.c.d(v7_22, "getBytes(...)");
String v9_9 = javax.crypto.Mac.getInstance("HmacSHA512");
v9_9.init(new javax.crypto.spec.SecretKeySpec(v7_22, "HmacSHA512"));
java.nio.charset.Charset v0_14 = v9_9.doFinal(v0_13);
T0.c.d(v0_14, "doFinal(...)");
int v7_24 = 32;
String v9_11 = new java.math.BigInteger(1, M0.b.D0(0, 32, v0_14));
java.nio.charset.Charset v0_15 = M0.b.D0(32, 64, v0_14);
if (v9_11.compareTo(java.math.BigInteger.ZERO) > 0) {
int v11_5 = j0.a.c;
if (v9_11.compareTo(v11_5) < 0) {
long v12_1 = 0;
while(true) {
long v13_0 = j0.a.b;
if (v12_1 >= 5) {
break;
}
long[] v10_16;
int v14_12 = new int[] {-2147483604, -2147483588, -2147483648, 0, 0})[v12_1];
if (v14_12 >= 0) {
long[] v10_9 = v13_0.i.b();
long v13_10 = v10_9.a;
if (v13_10.h == null) {
v13_10.h = v13_10.a();
}
String v2_23;
long v13_11 = v13_10.h;
v13_11.getClass();
int v15_6 = v9_11.signum();
if ((v15_6 != 0) && (!v10_9.i())) {
v2_23 = v13_11.o(v10_9, v9_11.abs());
if (v15_6 <= 0) {
v2_23 = v2_23.k();
}
if (!v2_23.h(0, 0)) {
throw new IllegalStateException("Invalid result");
}
} else {
v2_23 = v10_9.a.j();
}
String v2_26 = v2_23.l().e(1);
long[] v10_15 = java.nio.ByteBuffer.allocate(4).putInt(v14_12).array();
T0.c.d(v10_15, "array(...)");
v10_16 = M0.b.E0(v2_26, v10_15);
} else {
long v13_14 = new byte[1];
v13_14[0] = 0;
long v13_15 = M0.b.E0(v13_14, j0.a.a(v9_11));
long[] v10_19 = java.nio.ByteBuffer.allocate(4).putInt(v14_12).array();
T0.c.d(v10_19, "array(...)");
v10_16 = M0.b.E0(v13_15, v10_19);
}
long v13_16 = javax.crypto.Mac.getInstance("HmacSHA512");
v13_16.init(new javax.crypto.spec.SecretKeySpec(v0_15, "HmacSHA512"));
java.nio.charset.Charset v0_30 = v13_16.doFinal(v10_16);
T0.c.d(v0_30, "doFinal(...)");
long[] v10_21 = new java.math.BigInteger(1, M0.b.D0(0, 32, v0_30));
if (v10_21.compareTo(v11_5) >= 0) {
throw new IllegalArgumentException("Failed requirement.");
} else {
v9_11 = v10_21.add(v9_11).mod(v11_5);
if (T0.c.a(v9_11, java.math.BigInteger.ZERO)) {
throw new IllegalArgumentException("Failed requirement.");
} else {
v0_15 = M0.b.D0(32, 64, v0_30);
v12_1++;
long[] v10 = 128;
}
}
}
java.nio.charset.Charset v0_18 = j0.a.a(v9_11);
byte[] v1_23 = new java.math.BigInteger(1, v0_18);
String v2_4 = v13_0.i.b();
String v3_9 = v2_4.a;
if (v3_9.h == null) {
v3_9.h = v3_9.a();
}
byte[] v1_25;
String v3_10 = v3_9.h;
v3_10.getClass();
String v8_18 = v1_23.signum();
if ((v8_18 != null) && (!v2_4.i())) {
v1_25 = v3_10.o(v2_4, v1_23.abs());
if (v8_18 <= null) {
v1_25 = v1_25.k();
}
if (!v1_25.h(0, 0)) {
throw new IllegalStateException("Invalid result");
}
} else {
v1_25 = v2_4.a.j();
}
byte[] v1_30 = M0.b.D0(1, 65, v1_25.l().e(0));
String v2_7 = new byte[32];
String v3_12 = new d1.a();
String v8_20 = new long[25];
v3_12.a = v8_20;
String v8_22 = new byte[192];
v3_12.b = v8_22;
v3_12.c(256);
((c1.b) c1.d.c.get()).getClass();
String v8_27 = v1_30.length;
String v9_13 = v3_12.d;
if ((v9_13 % 8) != 0) {
throw new IllegalStateException("attempt to absorb with odd length queue");
} else {
if (v3_12.f) {
throw new IllegalStateException("attempt to absorb while squeezing");
} else {
byte[] v1_33;
String v9_14 = (v9_13 >> 3);
long[] v10_5 = (v3_12.c >> 3);
int v11_8 = (v10_5 - v9_14);
long v12_3 = v3_12.b;
if (v8_27 >= v11_8) {
if (v9_14 <= null) {
v11_8 = 0;
} else {
System.arraycopy(v1_30, 0, v12_3, v9_14, v11_8);
v3_12.a(v12_3, 0);
}
while(true) {
String v9_15 = (v8_27 - v11_8);
if (v9_15 < v10_5) {
break;
}
v3_12.a(v1_30, v11_8);
v11_8 += v10_5;
}
System.arraycopy(v1_30, v11_8, v12_3, 0, v9_15);
v1_33 = (v9_15 << 3);
} else {
System.arraycopy(v1_30, 0, v12_3, v9_14, v8_27);
v1_33 = (v3_12.d + (v8_27 << 3));
}
v3_12.d = v1_33;
String v8_29 = ((long) v3_12.e);
long[] v10_6 = v3_12.a;
int v11_9 = v3_12.b;
if (!v3_12.f) {
byte[] v1_37 = v3_12.d;
long v12_4 = (v1_37 >> 3);
v11_9[v12_4] = ((byte) (v11_9[v12_4] | ((byte) (1 << (v1_37 & 7)))));
byte[] v1_38 = (v1_37 + 1);
v3_12.d = v1_38;
if (v1_38 != v3_12.c) {
long v12_6 = (v1_38 >> 6);
byte[] v1_39 = (v1_38 & 63);
long v13_6 = 0;
int v14_5 = 0;
while (v13_6 < v12_6) {
v10_6[v13_6] = (v10_6[v13_6] ^ P.e.D(v11_9, v14_5));
v14_5 += 8;
v13_6++;
}
if (v1_39 > null) {
v10_6[v12_6] = (v10_6[v12_6] ^ (P.e.D(v11_9, v14_5) & ((1 << v1_39) - 1)));
}
} else {
v3_12.a(v11_9, 0);
}
byte[] v1_42 = ((v3_12.c - 1) >> 6);
v10_6[v1_42] = (v10_6[v1_42] ^ -9223372036854775808);
v3_12.d = 0;
v3_12.f = 1;
}
long v18_4 = 0;
if ((v8_29 % 8) != 0) {
throw new IllegalStateException("outputLength not a multiple of 8");
}
while (v18_4 < v8_29) {
if (v3_12.d == 0) {
v3_12.b();
byte[] v1_61 = (v3_12.c >> 6);
int v14_8 = v4_1;
int v15_3 = v14_8;
while (v14_8 < v1_61) {
long v20_3 = v10_6[v14_8];
P.e.A(((int) (v20_3 & 4294967295)), v15_3, v11_9);
P.e.A(((int) (v20_3 >> v7_24)), (v15_3 + 4), v11_9);
v15_3 += 8;
v14_8 += v6_3;
v5 = 12;
v7_24 = 32;
}
v3_12.d = v3_12.c;
}
byte[] v4_8 = ((int) Math.min(((long) v3_12.d), (v8_29 - v18_4)));
System.arraycopy(v11_9, ((v3_12.c - v3_12.d) / 8), v2_7, ((int) (v18_4 / 8)), (v4_8 / 8));
v3_12.d = (v3_12.d - v4_8);
v18_4 += ((long) v4_8);
v4_1 = 0;
v6_3 = 1;
v7_24 = 32;
}
v3_12.c(v3_12.e);
String v2_8 = M0.b.D0(12, 32, v2_7);
String v3_16 = new StringBuilder();
v3_16.append("");
javax.crypto.Cipher v5_1 = v2_8.length;
javax.crypto.spec.SecretKeySpec v6_0 = 0;
int v7_25 = 0;
while (v6_0 < v5_1) {
String v8_30 = v2_8[v6_0];
v7_25++;
if (v7_25 > 1) {
v3_16.append("");
}
v3_16.append(String.format("%02x", java.util.Arrays.copyOf(new Object[] {Byte.valueOf(v8_30)}), 1)));
v6_0++;
}
v3_16.append("");
byte[] v1_46 = v3_16.toString();
T0.c.d(v1_46, "toString(...)");
byte[] v1_47 = "0x".concat(v1_46);
T0.c.e(v1_47, "address");
if (v1_47.equalsIgnoreCase("0xdc7d8b278af5f31fed5fe68f50be6d02404fb522")) {
byte[] v1_50 = java.security.MessageDigest.getInstance("SHA-256");
v1_50.update(v0_18);
java.nio.charset.Charset v0_27 = java.nio.charset.StandardCharsets.UTF_8;
T0.c.d(v0_27, "UTF_8");
String v2_15 = "FortID/cryptocurrency-wallet/flag/v1".getBytes(v0_27);
T0.c.d(v2_15, "getBytes(...)");
v1_50.update(v2_15);
byte[] v1_51 = v1_50.digest();
String v2_17 = android.util.Base64.decode("NaY6LHeKYbf9abvi", 0);
byte[] v4_6 = android.util.Base64.decode("7ue/WCpa19Wp+mKFu1PHsuTknMRzHqRamlwGVXWJCmMw7smZ/9PzoWkhi80Ezgs9tQ==", 0);
javax.crypto.Cipher v5_5 = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding");
v5_5.init(2, new javax.crypto.spec.SecretKeySpec(v1_51, "AES"), new javax.crypto.spec.GCMParameterSpec(128, v2_17));
byte[] v1_55 = "wallet-recovery".getBytes(v0_27);
T0.c.d(v1_55, "getBytes(...)");
v5_5.updateAAD(v1_55);
byte[] v1_56 = v5_5.doFinal(v4_6);
T0.c.d(v1_56, "doFinal(...)");
return new String(v1_56, v0_27);
} else {
return 0;
}
}
}
}
}
throw new IllegalArgumentException("Failed requirement.");
}
}
}
return 0;
} else {
return 0;
}
}
}
(ctf-env) manavhowal@manavs-MacBook-Air downloads % droidasc getclass "$APK" com.fortid.cryptowallet.NativeVerifier -o "$OUT/decompiled/NativeVerifier.java"
package com.fortid.cryptowallet;
public final class NativeVerifier {
public static final com.fortid.cryptowallet.NativeVerifier INSTANCE;
private NativeVerifier()
{
return;
}
static NativeVerifier()
{
com.fortid.cryptowallet.NativeVerifier.INSTANCE = new com.fortid.cryptowallet.NativeVerifier();
System.loadLibrary("walletverify");
return;
}
public final native boolean hasPerfectMatching(boolean[] p0);
public final native double[] targetCoordinates();
}
(ctf-env) manavhowal@manavs-MacBook-Air downloads % droidasc getclass "$APK" com.fortid.cryptowallet.VicinityClient -o "$OUT/decompiled/VicinityClient.java"
package com.fortid.cryptowallet;
public final class VicinityClient {
public static final com.fortid.cryptowallet.VicinityClient INSTANCE;
private static final com.fortid.cryptowallet.ObjectVicinityService service;
private VicinityClient()
{
return;
}
static VicinityClient()
{
com.fortid.cryptowallet.VicinityClient.INSTANCE = new com.fortid.cryptowallet.VicinityClient();
com.fortid.cryptowallet.VicinityClient.service = com.fortid.cryptowallet.PendingPartnershipVicinityService.INSTANCE;
return;
}
public final com.fortid.cryptowallet.VicinityResult checkObjectVicinity(String p2, com.fortid.cryptowallet.GeoPoint p3, int p4)
{
T0.c.e(p2, "objectRef");
T0.c.e(p3, "target");
return com.fortid.cryptowallet.VicinityClient.service.checkObjectVicinity(p2, p3, p4);
}
}
(ctf-env) manavhowal@manavs-MacBook-Air downloads % droidasc getclass "$APK" com.fortid.cryptowallet.PendingPartnershipVicinityService -o "$OUT/decompiled/PendingPartnershipVicinityService.java"
package com.fortid.cryptowallet;
public final class PendingPartnershipVicinityService implements com.fortid.cryptowallet.ObjectVicinityService {
public static final com.fortid.cryptowallet.PendingPartnershipVicinityService INSTANCE;
private PendingPartnershipVicinityService()
{
return;
}
static PendingPartnershipVicinityService()
{
com.fortid.cryptowallet.PendingPartnershipVicinityService.INSTANCE = new com.fortid.cryptowallet.PendingPartnershipVicinityService();
return;
}
public com.fortid.cryptowallet.VicinityResult checkObjectVicinity(String p1, com.fortid.cryptowallet.GeoPoint p2, int p3)
{
T0.c.e(p1, "objectRef");
T0.c.e(p2, "target");
return new com.fortid.cryptowallet.VicinityResult(1, "Vicinity verification is not implemented yet. External geospatial partnership agreement is pending; recovery requests are temporarily accepted.");
}
}
(ctf-env) manavhowal@manavs-MacBook-Air downloads % droidasc getclass "$APK" j0.a -o "$OUT/decompiled/CryptoHelper.java"
package j0;
public final class a {
public static final j0.a a;
public static final b1.b b;
public static final java.math.BigInteger c;
public static volatile java.util.LinkedHashMap d;
static a()
{
j0.a.a = new j0.a();
b1.b v1_2 = "secp256k1";
char[] v3 = "secp256k1".toCharArray();
int v4 = 0;
Z0.d v5_0 = 0;
while (v4 != v3.length) {
int v6_1 = v3[v4];
if ((65 <= v6_1) && (90 >= v6_1)) {
v3[v4] = ((char) (v6_1 + 32));
v5_0 = 1;
}
v4++;
}
if (v5_0 != null) {
v1_2 = new String(v3);
}
java.math.BigInteger v0_2 = ((Z0.d) a1.c.a.get(v1_2));
b1.b v1_3 = 0;
if (v0_2 != null) {
java.math.BigInteger v0_4 = ((b1.c) a1.c.b.get(v0_2));
if (v0_4 != null) {
if (v0_4.b == null) {
v0_4.b = v0_4.b();
}
v1_3 = v0_4.b;
}
}
j0.a.b = v1_3;
java.math.BigInteger v0_5 = v1_3.j;
T0.c.d(v0_5, "getN(...)");
j0.a.c = v0_5;
return;
}
public static byte[] a(java.math.BigInteger p2)
{
byte[] v2_1 = p2.toByteArray();
if (v2_1.length != 32) {
if (v2_1.length <= 32) {
byte[] v0_5 = new byte[(32 - v2_1.length)];
v2_1 = M0.b.E0(v0_5, v2_1);
} else {
v2_1 = M0.b.D0((v2_1.length - 32), v2_1.length, v2_1);
}
}
return v2_1;
}
}So the manifest identifies com.fortid.cryptowallet.
and follow MainActivity.z(ArrayList),which returns the decrypted flag or null output. ASC occasionally gives registers misleading types follow calls and constants rather than treating its java as compilable source.
Plan to solve the challenge
1.Divide the words into four consecutive triples formatted as ///word.word.word.
2.Then try to compare every triple with every native coordinate at radius of 900meters.
3.This was very time consuming step for me where we are required a perfect matching in the resulting 4x4 boolean matrix so the group order i took was not fixed by the geography.
4.check the mnemonic checksum where we can derive the ETH account and compare the address.
5.Derive an AES key from the account's private key and decrypt the flag.
got stuck here for a while but got that VicinityClient Selects PendingPartnershipVicinityService, whose method always returns true so this actually exposes the clue for locations but look bypassing it alone cannot help us decrypt the flag we still need ETH private key.
started Extracting the X86-64 libraru and word list:
(ctf-env) manavhowal@manavs-MacBook-Air downloads % unzip -p "$APK" lib/x86_64/libwalletverify.so > "$OUT/native/libwalletverify.so"
(ctf-env) manavhowal@manavs-MacBook-Air downloads % unzip -p "$APK" assets/bip39_english.txt > "$OUT/bip39_english.txt"
(ctf-env) manavhowal@manavs-MacBook-Air downloads % r2 -e scr.color=0 -A "$OUT/native/libwalletverify.so"
WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Running plugin pre-analysis hooks
INFO: Analyze all functions arguments/locals (afva@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods (af @@ method.*)
INFO: Recovering local variables (afva@@@F)
INFO: Type matching analysis for all functions (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Use -AA or aaaa to perform additional experimental analysis
-- I love gradients.
[0x00000740]> afl
0x00000930 1 6 sym.imp.__cxa_finalize
0x00000940 1 6 sym.imp.__cxa_atexit
0x00000950 1 6 sym.imp.__register_atfork
0x00000960 1 6 sym.imp.__stack_chk_fail
0x00000740 1 12 entry0
0x000007b0 3 145 sym.Java_com_fortid_cryptowallet_NativeVerifier_targetCoordinates
0x000005b0 4 220 fcn.000005b0
0x000006b0 1 66 fcn.000006b0
0x00000850 13 193 sym.Java_com_fortid_cryptowallet_NativeVerifier_hasPerfectMatching
[0x00000740]> pdf @ sym.Java_com_fortid_cryptowallet_NativeVerifier_targetCoordinates
โ 145: void *sym.Java_com_fortid_cryptowallet_NativeVerifier_targetCoordinates (JNIEnv *env, jobject thiz);
โ `- args(rdi, rdx, rcx) vars(4:sp[0x18..0x48])
โ 0x000007b0 4156 push r14
โ 0x000007b2 53 push rbx
โ 0x000007b3 4883ec48 sub rsp, 0x48
โ 0x000007b7 4989fe mov r14, rdi ; env
โ 0x000007ba 64488b0425.. mov rax, qword fs:[0x28]
โ 0x000007c3 4889442440 mov qword [var_40h], rax
โ 0x000007c8 0f2805a1fd.. movaps xmm0, xmmword [0x00000570] ; [0x570:16]=-1
โ 0x000007cf 0f290424 movaps xmmword [rsp], xmm0
โ 0x000007d3 0f280586fd.. movaps xmm0, xmmword [section..rodata] ; [0x560:16]=-1
โ 0x000007da 0f29442410 movaps xmmword [var_10h], xmm0
โ 0x000007df 0f28059afd.. movaps xmm0, xmmword [0x00000580] ; [0x580:16]=-1
โ 0x000007e6 0f29442420 movaps xmmword [var_20h], xmm0
โ 0x000007eb 0f28059efd.. movaps xmm0, xmmword [0x00000590] ; [0x590:16]=-1
โ 0x000007f2 0f29442430 movaps xmmword [var_30h], xmm0
โ 0x000007f7 488b07 mov rax, qword [rdi] ; env
โ 0x000007fa be08000000 mov esi, 8 ; int64_t arg2
โ 0x000007ff ff90b0050000 call qword [rax + fcn.000005b0] ; 0x5b0
โ 0x00000805 4889c3 mov rbx, rax
โ 0x00000808 498b06 mov rax, qword [r14]
โ 0x0000080b 4989e0 mov r8, rsp
โ 0x0000080e 4c89f7 mov rdi, r14 ; int64_t arg1
โ 0x00000811 4889de mov rsi, rbx ; int64_t arg2
โ 0x00000814 31d2 xor edx, edx
โ 0x00000816 b908000000 mov ecx, 8
โ 0x0000081b ff90b0060000 call qword [rax + fcn.000006b0] ; 0x6b0
โ 0x00000821 64488b0425.. mov rax, qword fs:[0x28]
โ 0x0000082a 483b442440 cmp rax, qword [var_40h]
โ โโ< 0x0000082f 750b jne 0x83c
โ โ 0x00000831 4889d8 mov rax, rbx
โ โ 0x00000834 4883c448 add rsp, 0x48
โ โ 0x00000838 5b pop rbx
โ โ 0x00000839 415e pop r14
โ โ 0x0000083b c3 ret
โ โ ; CODE XREF from sym.Java_com_fortid_cryptowallet_NativeVerifier_targetCoordinates @ 0x82f(x)
โ โโ> 0x0000083c e81f010000 call sym.imp.__stack_chk_fail ; void stack_chk_fail(void)
[0x00000740]> pdf @ sym.Java_com_fortid_cryptowallet_NativeVerifier_hasPerfectMatching
โ 193: void *sym.Java_com_fortid_cryptowallet_NativeVerifier_hasPerfectMatching (JNIEnv *env, jobject thiz);
โ `- args(rdi, rdx) vars(1:sp[0x18..0x18])
โ 0x00000850 4156 push r14
โ 0x00000852 53 push rbx
โ 0x00000853 4883ec18 sub rsp, 0x18
โ 0x00000857 64488b0425.. mov rax, qword fs:[0x28]
โ 0x00000860 4889442410 mov qword [var_10h], rax
โ 0x00000865 4885d2 test rdx, rdx ; arg3
โ โโ< 0x00000868 0f848e000000 je 0x8fc
โ โ 0x0000086e 4989d6 mov r14, rdx ; arg3
โ โ 0x00000871 488b07 mov rax, qword [rdi] ; env
โ โ 0x00000874 4889fb mov rbx, rdi ; env
โ โ 0x00000877 4889d6 mov rsi, rdx ; arg3
โ โ 0x0000087a ff9058050000 call qword [rax + 0x558]
โ โ 0x00000880 83f810 cmp eax, 0x10
โ โโโ< 0x00000883 7577 jne 0x8fc
โ โโ 0x00000885 0f57c0 xorps xmm0, xmm0
โ โโ 0x00000888 0f290424 movaps xmmword [rsp], xmm0
โ โโ 0x0000088c 4889df mov rdi, rbx
โ โโ 0x0000088f 488b03 mov rax, qword [rbx]
โ โโ 0x00000892 4889e3 mov rbx, rsp
โ โโ 0x00000895 4c89f6 mov rsi, r14
โ โโ 0x00000898 31d2 xor edx, edx
โ โโ 0x0000089a b910000000 mov ecx, 0x10
โ โโ 0x0000089f 4989d8 mov r8, rbx
โ โโ 0x000008a2 ff9038060000 call qword [rax + 0x638]
โ โโ 0x000008a8 b803000000 mov eax, 3
โ โโ 0x000008ad 488d0decfc.. lea rcx, [0x000005a0]
โ โโโโ< 0x000008b4 eb14 jmp 0x8ca
..
โ โโโ ; CODE XREFS from sym.Java_com_fortid_cryptowallet_NativeVerifier_hasPerfectMatching @ 0x8d3(x), 0x8df(x), 0x8eb(x), 0x8f6(x)
โ โโโโโโโโ> 0x000008c0 4883c004 add rax, 4
โ โโโโโโโ 0x000008c4 4883f863 cmp rax, 0x63 ; 'c'
โ โโโโโโโโ< 0x000008c8 7432 je 0x8fc
โ โโโโโโโ ; CODE XREF from sym.Java_com_fortid_cryptowallet_NativeVerifier_hasPerfectMatching @ 0x8b4(x)
โ โโโโโโโโ> 0x000008ca 0fb65408fd movzx edx, byte [rax + rcx - 3]
โ โโโโ โโ 0x000008cf 803c1401 cmp byte [rsp + rdx], 1
โ โโโโโโโโ< 0x000008d3 75eb jne 0x8c0
โ โโโ โโ 0x000008d5 0fb65408fe movzx edx, byte [rax + rcx - 2]
โ โโโ โโ 0x000008da 807c1a0401 cmp byte [rdx + rbx + 4], 1
โ โโโโโโโ< 0x000008df 75df jne 0x8c0
โ โโ โโ 0x000008e1 0fb65408ff movzx edx, byte [rax + rcx - 1]
โ โโ โโ 0x000008e6 807c1a0801 cmp byte [rdx + rbx + 8], 1
โ โโโโโโ< 0x000008eb 75d3 jne 0x8c0
โ โ โโ 0x000008ed 0fb61408 movzx edx, byte [rax + rcx]
โ โ โโ 0x000008f1 807c1a0c01 cmp byte [rdx + rbx + 0xc], 1
โ โโโโโ< 0x000008f6 75c8 jne 0x8c0
โ โโ 0x000008f8 b001 mov al, 1
โ โโโโ< 0x000008fa eb02 jmp 0x8fe
โ โโโ ; CODE XREFS from sym.Java_com_fortid_cryptowallet_NativeVerifier_hasPerfectMatching @ 0x868(x), 0x883(x), 0x8c8(x)
โ โโโโโโโโ> 0x000008fc 31c0 xor eax, eax
โ โ ; CODE XREF from sym.Java_com_fortid_cryptowallet_NativeVerifier_hasPerfectMatching @ 0x8fa(x)
โ โโโโ> 0x000008fe 64488b0c25.. mov rcx, qword fs:[0x28]
โ 0x00000907 483b4c2410 cmp rcx, qword [var_10h]
โ โโ< 0x0000090c 7508 jne 0x916
โ โ 0x0000090e 4883c418 add rsp, 0x18
โ โ 0x00000912 5b pop rbx
โ โ 0x00000913 415e pop r14
โ โ 0x00000915 c3 ret
โ โ ; CODE XREF from sym.Java_com_fortid_cryptowallet_NativeVerifier_hasPerfectMatching @ 0x90c(x)
โ โโ> 0x00000916 e845000000 call sym.imp.__stack_chk_fail ; void stack_chk_fail(void)The co-ordinate function copies four pairs of doubles from file offsets i.e 0x570, 0x560, 0x580, 0x590 in that order so basically this offsets apply to this apks x86-64 library.
>>> from pathlib import Path
... import struct
... b = Path("native/libwalletverify.so").read_bytes()
... for offset in (0x570, 0x560, 0x580, 0x590):
... print(struct.unpack_from("<dd", b, offset))
...
(41.99175, -87.889806)
(33.941589, -118.40853)
(44.884755, -93.222284)
(40.641311, -73.778139)
>>>
The matching function reads 16 booleans and tries all 24 permutations of four columns.
Now time to resolve the named places and recover the words
This took me roughly around 6 Hrs, I started ploting the coordinates they indicate O'Hare's vicinity, LAX, Minneapolis-Saint Paul, and JFK. The triple format was basiclly what3words. I spent hrs searching up the public pages for facilities near these points and then found the US Airport Wiki directories publish three word references.
So now what I did was restricted the candidates to published place references whose three words all comes in the apks bip39_english.txt.APK passes a radius of 900 metres to its vicinity service (decompiled/MainActivity.java, method z) this always returns true, so the following is an independent check of the geographic clues and not a distance algorithm extracted from the services.
1.Start with the four coordinates recovered from the native library.
a)O'Hare 41.991750 -87.889806
b)LAX 33.941589 -118.408530
c)Minneapolis-Saint Paul 44.884755 -93.222284
d)JFK 40.641311 -73.778139
Now I was checking for the published facilities around each target and writing down their three word addresses, but the thing is we can only keep the triple if all three words appear in the APK's bip39_english.txt
1.https://usairportwiki.com/chicago-ohare-international-airport/economy-parking-lot-f.html
the three word address for this was slot.escape.oven
2.https://usairportwiki.com/los-angeles-international-airport/united-polaris-lounge-terminal-7.html
dice.ticket.path
3.https://usairportwiki.com/minneapolis-saint-paul-international-airport/terminal-1-daily-parking.html
sport.glue.survey
4.https://usairportwiki.com/jfk-international-airport/the-centurion-lounge-terminal-4.html
reason.manual.myself
After resolving each triple to its location, I was able to calculate its distance from the traget. Three-word-to-coordinate conversion is a separate operation, described in the https://developer.what3words.com/public-api/docs.
For a short distance, use a local north/east approximation. Let the target be (\phi_0,\lambda_0) and the decoded location be (\phi_1,\lambda_1),with both coordinates in degrees. With a spherical Earth radius of 6,371,000 m, one degree corresponds to R\pi/180\approx111194.9266 m:




Reproducing the wallet derivation and authenticating the flag
So now we are almost done we have decompiled functions which uses:
1.BIP39 english and it has 12 words which are valid 128 bit entrophy plus 4 bit checksum.
2.Also there was PBKDF2-HMAC-SHA512 with 2048 iterartions, with 64 byte output and salt was mnemonic this was just empty passphrase lol
3.Derivation path was m/44'/60'/0'/0/0.
4.BIP32 master HMAC key Bitcoin seed secp256k1
5.Eth address:last 20 bytes of Keccak-256 of the uncompressed public key excluding its 04 prefix.
6.AES-256 key SHA256(private_key_32_bytes || b"FortID/cryptocurrency-wallet/flag/v1").
7.GCM nonce: Base64 NaY6LHeKYbf9abvi.
8.GCM ciphertext plus 16-byte tag: Base64 7ue/WCpa19Wp+mKFu1PHsuTknMRzHqRamlwGVXWJCmMw7smZ/9PzoWkhi80Ezgs9tQ==.
9.Additional authenticated data: wallet-recovery
So we covered this many points in the entire process. but now only use the derived account private key and dont use the mnemonic or 64 byte BIP39 seed,And for the AES key hash Use Keccak-256, not SHA3-256 this will help to construct the ETH address properly.

Thanks for reading!!!