Anchored
6 minutes to read
We are given an APK file (Anchored.apk
) and a README.txt
file:
1. Install this application in an API Level 29 or earlier (i.e. Android 10.0 (Google Play)).
1. Install this application in a non-rooted device (i.e. In Android Studio AVD Manager select an image that includes (Google Play)).
Reverse engineering
The first thing we can do is to decompile de APK file and read Java source code.
APK decompilation
If we load the APK in www.javadecompilers.com and select “APK decompiler”, we will see this code for the MainActivity.java
(sources/com/example/anchored/MainActivity.java
), where I have commented some parts that are not relevant:
package com.example.anchored;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import d.h;
import java.util.HashMap;
import java.util.Map;
import v0.p;
import v0.r;
import w0.j;
import w0.l;
public class MainActivity extends h {
/* renamed from: o reason: collision with root package name */
public Button f1944o;
/* renamed from: p reason: collision with root package name */
public TextView f1945p;
public class a implements View.OnClickListener {
/* renamed from: com.example.anchored.MainActivity$a$a reason: collision with other inner class name */
public class C0012a implements p.b<String> {
public C0012a() {
}
public void a(Object obj) {
try {
MainActivity.this.f1945p.setText("Thank you for requesting early access.");
} catch (Exception e2) {
e2.printStackTrace();
MainActivity.this.f1945p.setText("Thank you for requesting early access.");
}
}
}
public class b implements p.a {
public b() {
}
public void a(r rVar) {
MainActivity.this.f1945p.setText("Thank you for requesting early access.");
}
}
public class c extends j {
public c(int i2, String str, p.b bVar, p.a aVar) {
super(i2, str, bVar, aVar);
}
public Map<String, String> g() {
HashMap hashMap = new HashMap();
hashMap.put("Content-Type", "application/x-www-form-urlencoded");
return hashMap;
}
public Map<String, String> h() {
HashMap hashMap = new HashMap();
hashMap.put(MainActivity.this.prp(), MainActivity.this.s());
hashMap.put(MainActivity.this.mrm(), MainActivity.this.frf());
return hashMap;
}
}
public a() {
}
public void onClick(View view) {
MainActivity.this.f1945p.setText("Thank you for requesting early access.");
l.a(MainActivity.this).a(new c(i2:1, str:"https://anchored.com:4443/test.php", new C0012a(), new b()));
}
}
public class b implements View.OnClickListener { /* ... */ }
public class c implements View.OnClickListener { /* ... */ }
public class d implements View.OnClickListener { /* ... */ }
static {
System.loadLibrary("anchored");
}
public native int c8();
public native String frf();
public native String mrm();
/* JADX WARNING: Removed duplicated region for block: B:106:0x025b */
/* ... */
/* Code decompiled incorrectly, please refer to instructions dump. */
public void onCreate(android.os.Bundle r20) { /* ... */ }
public native String prp();
public String s() {
return ((EditText) findViewById(R.id.editTextTextEmailAddress)).getText().toString();
}
}
Notice that there are some native
functions (prp
, frf
, mrm
, c8
), which come from a library at libs/x86_64/libanchored.so
. These functions are used to create a HashMap
:
public Map<String, String> g() {
HashMap hashMap = new HashMap();
hashMap.put("Content-Type", "application/x-www-form-urlencoded");
return hashMap;
}
public Map<String, String> h() {
HashMap hashMap = new HashMap();
hashMap.put(MainActivity.this.prp(), MainActivity.this.s());
hashMap.put(MainActivity.this.mrm(), MainActivity.this.frf());
return hashMap;
}
}
The s
method simply takes an email address from the initial page. Notice that there is a string with a URL: https://anchored.com:4443/test.php
, and another HashMap
with an HTTP header:
public Map<String, String> g() {
HashMap hashMap = new HashMap();
hashMap.put("Content-Type", "application/x-www-form-urlencoded");
return hashMap;
}
So, we can guess that the result of one of prp
, mrm
or frf
will have the flag. Probably, it is frf
, because prp
might give something like email
, and mrm
something like flag
as they are used as the HashMap
keys, not values (actually, if we analyze them, prp
returns "mail"
and mrm
returns "msg"
).
Library decompilation
So, if we load libanchored.so
in Ghidra, we will see this function for frf
(Java_com_example_anchored_MainActivity_frf
):
undefined8 Java_com_example_anchored_MainActivity_frf(long *param_1) {
byte bVar1;
byte bVar2;
undefined *puVar3;
undefined8 uVar4;
byte *pbVar5;
ulong uVar6;
byte *pbVar7;
ulong uVar8;
ulong uVar9;
long lVar10;
long in_FS_OFFSET;
undefined local_1b8[8];
ulong uStack_1b0;
undefined *local_1a8;
byte *local_198[14];
undefined4 local_128;
undefined4 uStack_124;
undefined4 uStack_120;
undefined4 uStack_11c;
undefined *local_118;
undefined1 *puStack_110;
undefined *local_108;
undefined1 *puStack_100;
undefined4 local_f8;
undefined4 uStack_f4;
undefined4 uStack_f0;
undefined4 uStack_ec;
byte *local_e8 [22];
long local_38;
local_38 = *(long *) (in_FS_OFFSET + 0x28);
_Z14encryptDecryptv();
_local_1b8 = ZEXT816(0);
local_1a8 = NULL;
pbVar5 = &DAT_0012d0c9;
local_e8[0] = &DAT_0012d0c9;
local_e8[1] = &DAT_0012d0b5;
local_e8[2] = &DAT_0012d0cb;
local_e8[3] = &DAT_0012d0cd;
local_e8[4] = &DAT_0012d0c9;
local_e8[5] = &DAT_0012d0cf;
local_e8[6] = &DAT_0012d0d1;
local_e8[7] = &DAT_0012d0d3;
local_e8[8] = &DAT_0012d0d5;
local_e8[9] = &DAT_0012d097;
local_e8[10] = &DAT_0012d0d7;
local_e8[11] = &DAT_0012d0b7;
local_e8[12] = &DAT_0012d0d9;
local_e8[13] = &DAT_0012d0cb;
local_e8[14] = &DAT_0012d0db;
local_e8[15] = &DAT_0012d0dd;
local_e8[16] = &DAT_0012d0df;
local_e8[17] = &DAT_0012d0d7;
local_e8[18] = &DAT_0012d0c9;
local_e8[19] = &DAT_0012d0cb;
local_e8[20] = &DAT_0012d0b7;
local_e8[21] = &DAT_0012d0cf;
pbVar7 = &DAT_0012d0e1;
local_198[0] = &DAT_0012d0e1;
local_198[1] = &DAT_0012d091;
local_198[2] = &DAT_0012d0e1;
local_198[3] = &DAT_0012d091;
local_198[4] = &DAT_0012d0e1;
local_198[5] = &DAT_0012d091;
local_198[6] = &DAT_0012d091;
local_198[7] = &DAT_0012d097;
local_198[8] = &DAT_0012d091;
local_198[9] = &DAT_0012d0e1;
local_198[10] = &DAT_0012d0e1;
local_198[11] = &DAT_0012d091;
local_198[12] = &DAT_0012d095;
local_198[13] = &DAT_0012d0e1;
local_128 = 0x12d091;
uStack_124 = 0;
uStack_120 = 0x12d091;
uStack_11c = 0;
local_118 = &DAT_0012d095;
puStack_110 = &DAT_0012d0e1;
local_108 = &DAT_0012d0e3;
puStack_100 = &DAT_0012d0e1;
local_f8 = 0x12d091;
uStack_f4 = 0;
uStack_f0 = 0x12d091;
uStack_ec = 0;
lVar10 = 1;
uVar9 = 0;
do {
bVar2 = *pbVar7;
bVar1 = *pbVar5;
if ((uVar9 & 1) == 0) {
uVar8 = uVar9 >> 1;
uVar6 = 0x16;
if (uVar8 == 0x16) goto LAB_0010f581;
LAB_0010f552:
if ((uVar9 & 1) != 0) goto LAB_0010f558;
LAB_0010f5a6:
local_1b8[0] = (char) uVar8 * '\x02' + '\x02';
puVar3 = local_1b8 + 1;
} else {
uVar6 = ((ulong)local_1b8 & 0xfffffffffffffffe) - 1;
uVar8 = uStack_1b0;
if (uStack_1b0 != uVar6) goto LAB_0010f552;
LAB_0010f581:
// try { // try from 0010f581 to 0010f59e has its CatchHandler @ 0010f63a
_ZNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE9__grow_byEmmmmmm(local_1b8, uVar6, 1, uVar6, uVar6, 0, 0);
if ((_local_1b8 & (undefined[16]) 0x1) == (undefined[16]) 0x0) goto LAB_0010f5a6;
LAB_0010f558:
uStack_1b0 = uVar8 + 1;
puVar3 = local_1a8;
}
puVar3[uVar8] = bVar2 ^ bVar1;
puVar3[uVar8 + 1] = 0;
if (lVar10 == 0x16) {
puVar3 = local_1b8 + 1;
if ((_local_1b8 & (undefined[16]) 0x1) != (undefined[16]) 0x0) {
puVar3 = local_1a8;
}
// try { // try from 0010f5eb to 0010f5f6 has its CatchHandler @ 0010f638
uVar4 = (**(code **) (*param_1 + 0x538))(param_1, puVar3);
if ((_local_1b8 & (undefined [16])0x1) != (undefined [16])0x0) {
_ZdlPv(local_1a8);
}
if (*(long *) (in_FS_OFFSET + 0x28) == local_38) {
return uVar4;
}
// WARNING: Subroutine does not return
__stack_chk_fail();
}
pbVar5 = local_e8[lVar10];
pbVar7 = local_198[lVar10];
uVar9 = (ulong) local_1b8[0];
lVar10 = lVar10 + 1;
} while (true);
}
The code is very ugly, but the encryption mechanism is just XOR, we can notice it here:
// ...
lVar10 = 1;
uVar9 = 0;
do {
bVar2 = *pbVar7;
bVar1 = *pbVar5;
if ((uVar9 & 1) == 0) {
uVar8 = uVar9 >> 1;
uVar6 = 0x16;
// ...
puVar3[uVar8] = bVar2 ^ bVar1;
puVar3[uVar8 + 1] = 0;
// ...
pbVar5 = local_e8[lVar10];
pbVar7 = local_198[lVar10];
uVar9 = (ulong) local_1b8[0];
lVar10 = lVar10 + 1;
} while (true);
}
We only must take the values of local_e8
and local_198
and use XOR. These values are constants that can be seen in Ghidra, some of them are:
DAT_0012d0c9 XREF[3]: Java_com_example_anchored_MainAc
Java_com_example_anchored_MainAc
Java_com_example_anchored_MainAc
0012d0c9 74 undefined1 74h t
0012d0ca 00 ?? 00h
DAT_0012d0cb XREF[2]: Java_com_example_anchored_MainAc
FUN_0011af50:0011b171(*)
0012d0cb 75 ?? 75h u
0012d0cc 00 ?? 00h
DAT_0012d0cd XREF[1]: Java_com_example_anchored_MainAc
0012d0cd 39 ?? 39h 9
0012d0ce 00 ?? 00h
DAT_0012d0cf XREF[1]: Java_com_example_anchored_MainAc
0012d0cf 38 ?? 38h 8
0012d0d0 00 ?? 00h
DAT_0012d0d1 XREF[1]: Java_com_example_anchored_MainAc
0012d0d1 3f ?? 3Fh ?
0012d0d2 00 ?? 00h
Flag
We can use Python to decrypt everything and find the flag:
$ python3 -q
>>> from pwn import xor
>>>
>>> ct = b't%u9t8?M/~bx&uz-ebtux8'
>>> key = b'!K!K!KK~K!!KT!KKT!@!KK'
>>> xor(ct, key)
b'UnTrUst3d_C3rT1f1C4T3s'
So, the flag is:
HTB{UnTrUst3d_C3rT1f1C4T3s}