OWASP MSTG
Mobile Security Testing Guide
The Mobile Security Testing Guide (MSTG) is a comprehensive manual for mobile app security development, testing and reverse engineering.
blabla. It’s a book with a nice checklist and all the techniques you need for pentesting. There is also a complementary book for writing secure software (MASVS).
We sometimes do app pentests so I thought I’d look into the OWASP MSTG and do the examples to get a hang of the tools (and a excuse to finally get a android vm up and running).
Link here and the CrackMes are here.
Tools, Setup and Stuff
Emulator
As a emulator I’m using Genymotion with an Android 9.0 device (because DIVA didn’t run on 7.0, but any version above 4.4 should work).
To install Genymotion, first install virtualbox with
sudo apt install virtualbox
then
- Download the .bin from here.
- Execute that file (it’s a bash script; execute as root if you want it in /opt/).
- Start Genymotion and create a device (any version should suffice)
- You should now be able to boot into a android device
Decompiler
To de/recompile I’m using apktool. You could use apt to install that, but I had problems with the dirty version.
Head over to the official website and follow the install instructions (download wrapper & jar, chmod, run).
You also need a java version. I’m using openjdk-11, but it’s recommended to use jdk-8.
MobSF
Another useful tool is mobsf, a static/dynamic analysis framework for all kinds of mobile apps (iOS, Windows, Android).
I’m running it in a docker container, because it’s way easier to use.
sudo apt install docker.io
sudo service docker start
sudo docker pull opensecurity/mobile-security-framework-mobsf
sudo docker run -p 8000:8000 opensecurity/mobile-security-framework-mobsf
Also a docker-compose file, cause I’m too lazy to save the long ass docker name
version: "2.0"
services:
mobfs:
ports:
- 8000:8000
image: opensecurity/mobile-security-framework-mobsf
It’s then reachable on port 8000.
Ghidra
The ol’ trusty decompiler. Needed later…
Uncrackable Level 1
This app holds a secret inside. Can you find it?
Drag-and-drop the apk into our emulator and it should start directly.
We immediately get prompted with this message:
And it exits. Uhh Genymotion automatically roots the device. Can we like disable that? No.
Let’s decompile the app and change the code.
apktool d UnCrackable-Level1.apk
We now have the code in smali/sg/vantagepoint
but it’s only dex files. Either use some program like dex2jar on it or use
MobSF
Upload the .apk in the web interface and it should something like this
Scroll down till you see “View Java” to look at the java code.
In MainActivity.java:
/* access modifiers changed from: protected */
public void onCreate(Bundle bundle) {
if (c.a() || c.b() || c.c()) {
a("Root detected!");
}
if (b.a(getApplicationContext())) {
a("App is debuggable!");
}
super.onCreate(bundle);
setContentView(R.layout.activity_main);
}
In /smali/sg/vantagepoint/uncrackable1/MainActivity.smali:
.method protected onCreate(Landroid/os/Bundle;)V
.locals 1
invoke-static {}, Lsg/vantagepoint/a/c;->a()Z
move-result v0
if-nez v0, :cond_0
invoke-static {}, Lsg/vantagepoint/a/c;->b()Z
...
if-eqz v0, :cond_2
const-string v0, "App is debuggable!"
invoke-direct {p0, v0}, Lsg/vantagepoint/uncrackable1/MainActivity;->a(Ljava/lang/String;)V
:cond_2
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
Let’s remove that in the decompiled source (Everything between .locals 1
and :cond2
) and recompile with
apktool b UnCrackable-Level1
If you now drop the apk from /dist/ into the emulator it will show an error. Why? Because it’s not signed.
Now we will:
- Install apksigner
- Generate a key in ~/.android/debug.keystore (if there isn’t already one => android studio creates one too)
- Sign the new apk with our key
sudo apt install apksigner
keytool -genkey -v -keystore ~/.android/debug.keystore -alias signkey -keyalg RSA -keysize 2048 -validity 20000
apksigner sign --ks ~/.android/debug.keystore --ks-key-alias signkey UnCrackable-Level1.apk
Now we can install the apk on the device and start it without problems (hopefully).
Now onto the secret string…
We have this verifiy function in MainActivity:
public void verify(View view) {
String str;
String obj = ((EditText) findViewById(R.id.edit_text)).getText().toString();
AlertDialog create = new AlertDialog.Builder(this).create();
if (a.a(obj)) {
create.setTitle("Success!");
str = "This is the correct secret.";
} else {
create.setTitle("Nope...");
str = "That's not it. Try again.";
}
create.setMessage(str);
create.setButton(-3, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
});
create.show();
}
Looks good. Let’s look at a.a() in uncrackable1/a.java:
public class a {
public static boolean a(String str) {
byte[] bArr;
byte[] bArr2 = new byte[0];
try {
bArr = sg.vantagepoint.a.a.a(b("8d127684cbc37c17616d806cf50473cc"), Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0));
} catch (Exception e) {
Log.d("CodeCheck", "AES error:" + e.getMessage());
bArr = bArr2;
}
return str.equals(new String(bArr));
}
public static byte[] b(String str) {
int length = str.length();
byte[] bArr = new byte[(length / 2)];
for (int i = 0; i < length; i += 2) {
bArr[i / 2] = (byte) ((Character.digit(str.charAt(i), 16) << 4) + Character.digit(str.charAt(i + 1), 16));
}
return bArr;
}
}
What does sg.vantagepoint.a.a.a()?
public class a {
public static byte[] a(byte[] bArr, byte[] bArr2) {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES/ECB/PKCS7Padding");
Cipher instance = Cipher.getInstance("AES");
instance.init(2, secretKeySpec);
return instance.doFinal(bArr2);
}
}
Okay so it:
- Does some funky stuff with
8d127684cbc37c17616d806cf50473cc
and base64 decodes5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=
- Gives that into an AES Cipher (and decrypts it thereby)
- Compares the decrypted string with the input
There are several ways we can now get the secret. Here’s my way:
I just copy the java and run it in some online compiler. ayyy.
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class HelloWorld{
public static void main(String []args){
a("test");
}
public static byte[] decrypt(byte[] bArr, byte[] bArr2) {
try
{
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES");
Cipher instance = Cipher.getInstance("AES");
instance.init(2, secretKeySpec);
return instance.doFinal(bArr2);
}
catch (Exception e)
{
System.out.println(e);
System.out.println("fck");
}
return null;
}
public static boolean a(String str) {
byte[] bArr;
byte[] bArr2 = new byte[0];
try {
bArr = decrypt(b("8d127684cbc37c17616d806cf50473cc"), Base64.getDecoder().decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc="));
} catch (Exception e) {
bArr = bArr2;
}
System.out.println(new String(bArr));
return str.equals(new String(bArr));
}
public static byte[] b(String str) {
int length = str.length();
byte[] bArr = new byte[(length / 2)];
for (int i = 0; i < length; i += 2) {
bArr[i / 2] = (byte) ((Character.digit(str.charAt(i), 16) << 4) + Character.digit(str.charAt(i + 1), 16));
}
return bArr;
}
}
Notice the Class Name
And it outputs the secret I want to believe
.
Put that into the app and we get a prompt:
ADB
We can also use adb (Android Debug Bridge) to get a shell (shell), install apps (install) or read the logs (logcat) of our physicial/virtual device. For that use:
adb connect IP_ADDRESS
and then your desired adb command (ex. adb shell).
Notice: the IP Address is not the one in the Genymotion window name. Get it through Settings > System > About Phone or Settings > Network > Wi-Fi > The Wifi > Advanced > IP address.
Uncrackable Level 2
This app holds a secret inside. May include traces of native code.
Same layout, can’t start in genymotion (because of superuser) Again Root & Debugable Detection + Debugger Check in MainActivity.onCreate => remove, recompile with
apktool b UnCrackable-Level2
But it gave this error:
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
I: Using Apktool 2.4.1
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
W: /home/exp/Downloads/UnCrackable-Level2/AndroidManifest.xml:1: error: No resource identifier found for attribute 'compileSdkVersion' in package 'android'
W:
W: /home/exp/Downloads/UnCrackable-Level2/AndroidManifest.xml:1: error: No resource identifier found for attribute 'compileSdkVersionCodename' in package 'android'
W:
brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [/tmp/brut_util_Jar_15841733277938222659.tmp, p, --forced-package-id, 127, --min-sdk-version, 19, --target-sdk-version, 28, --version-code, 1, --version-name, 1.0, --no-version-vectors, -F, /tmp/APKTOOL6222115682080151551.tmp, -e, /tmp/APKTOOL16926908881491950083.tmp, -0, arsc, -I, /home/exp/.local/share/apktool/framework/1.apk, -S, /home/exp/Downloads/UnCrackable-Level2/res, -M, /home/exp/Downloads/UnCrackable-Level2/AndroidManifest.xml]
Here the solution:
apktool empty-framework-dir --force
Now we can recompile it again and push onto the device.
Let’s look at the code again:
...
public void onCreate(Bundle bundle) {
...
this.m = new CodeCheck();
...
}
public void verify(View view) {
...
if (this.m.a(obj)) {
create.setTitle("Success!");
str = "This is the correct secret.";
} else {
...
}
Okay this time we give our input into CodeCheck.a():
public class CodeCheck {
private native boolean bar(byte[] bArr);
public boolean a(String str) {
return bar(str.getBytes());
}
}
So we check it in the native library…
Short Digression: Native Libraries
I’ll just yoink the text out the MSTG
Some ambiguity exists when discussing native apps for Android as the platform provides two development kits - the Android SDK and the Android NDK. The SDK, which is based on the Java and Kotlin programming language, is the default for developing apps. The NDK (or Native Development Kit) is a C/C++ development kit used for developing binary libraries that can directly access lower level APIs (such as OpenGL). These libraries can be included in regular apps built with the SDK. Therefore, we say that Android native apps (i.e. built with the SDK) may have native codebuilt with the NDK.
TL;DR: Native Libraries are just in C/C++ compiled libs, so no easy reversing from java bytecode.
Back again
The library is found under /lib/ARCH/libfoo.so
file libfoo.so
libfoo.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=1adb8eb0bf49daddce60e3e1ed000158e424bc9d, stripped
Ah fuck, I can’t believe you’ve done this it’s stripped.
After I read the section of reversing native code in the MSTG, I know that it still has symbols (which makes sense, cause it’s a library). You can read those with readelf(linux)/greadelf (mac).
readelf -W -s libfoo.so
Symbol table '.dynsym' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND waitpid@LIBC (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2)
3: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC (2)
4: 00000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@LIBC (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND fork@LIBC (2)
6: 00000000 0 FUNC GLOBAL DEFAULT UND getppid@LIBC (2)
7: 00000f60 199 FUNC GLOBAL DEFAULT 12 Java_sg_vantagepoint_uncrackable2_CodeCheck_bar
8: 00000000 0 FUNC GLOBAL DEFAULT UND _exit@LIBC (2)
9: 00000f30 40 FUNC GLOBAL DEFAULT 12 Java_sg_vantagepoint_uncrackable2_MainActivity_init
10: 00000000 0 FUNC GLOBAL DEFAULT UND ptrace@LIBC (2)
11: 00000000 0 FUNC GLOBAL DEFAULT UND strncmp@LIBC (2)
12: 00000000 0 FUNC GLOBAL DEFAULT UND pthread_create@LIBC (2)
13: 00000000 0 FUNC GLOBAL DEFAULT UND pthread_exit@LIBC (2)
14: 00004004 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
15: 00004009 0 NOTYPE GLOBAL DEFAULT ABS _end
16: 00004004 0 NOTYPE GLOBAL DEFAULT ABS _edata
So let’s put it in Ghidra and go to the interesting functions (CodeCheck_bar & MainActivity_init). I’m using the x86 lib here (but it shouldn’t make any difference if your tool supports that arch).
CodeCheck_bar:
Java_sg_vantagepoint_uncrackable2_CodeCheck_bar(int *param_1,undefined4 param_2,undefined4 param_3)
{
...
if (DAT_00014008 == '\x01') {
local_30 = 0x6e616854;
local_2c = 0x6620736b;
local_28 = 0x6120726f;
local_24 = 0x74206c6c;
local_20 = 0x6568;
local_1e = 0x73696620;
local_1a = 0x68;
__s1 = (char *)(**(code **)(*param_1 + 0x2e0))(param_1,param_3,0);
iVar1 = (**(code **)(*param_1 + 0x2ac))(param_1,param_3);
if (iVar1 == 0x17) {
iVar1 = strncmp(__s1,(char *)&local_30,0x17);
if (iVar1 == 0) {
uVar2 = 1;
...
}
Three parameters? The java code only puts in a byte array.
Seems like it strncmp some hex values with _sl
. Is that our input? Does it get transformed?
Let’s first convert those hex values into a string (Ghidra: In asm window > right click on hex > Convert > Char Sequence) and have a look if those make any sense.
So long and “Thanks for all the fish”?
Was that already the secret? Let’s put it in the app:
Okay… That worked? No encryption or encoding? Interesting…
Uncrackable Level 3
The crackme from hell!
First look into the code in MobSF:
- a xorKey “pizzapizzapizzapizzapizz”
- Native Function again from libfoo.so: baz, init(byte[]), bar(byte[])
- It uses an crc checksum of libs & classes.dex in verifyLibs() to check for tampering
Order of Functions:
onCreate():
- verifyLibs()
- init(xorkey)
- Debugger Detection
- Root Detection
- Creates CodeCheck object
Verify():
- Calls check.check_code(input)
check_code(str):
- bar(str.getBytes()) => native func
What to patch:
- verifyLibs call
- Debugger Detection
- Root Detection
To decompile
apktool d UnCrackable-Level3.apk
Patching out: In onCreate I found this .line X
marker. A quick google showed that those numbers are used for debugging. When something goes wrong it can show the java line code.
So we’re safe to delete those.
I patched out in onCreate:
.method protected onCreate(Landroid/os/Bundle;)V
.registers 6
.line 104
invoke-direct {p0}, Lsg/vantagepoint/uncrackable3/MainActivity;->verifyLibs()V
const-string v0, "pizzapizzapizzapizz"
.line 106
invoke-virtual {v0}, Ljava/lang/String;->getBytes()[B
move-result-object v0
invoke-direct {p0, v0}, Lsg/vantagepoint/uncrackable3/MainActivity;->init([B)V
.line 109
new-instance v0, Lsg/vantagepoint/uncrackable3/MainActivity$2;
...
.line 130
:cond_46
new-instance v0, Lsg/vantagepoint/uncrackable3/CodeCheck;
...
The verifyLibs call, leave the const and the init call there, and basically everything from there till the CodeCheck call.
The ol’ razzle dazzle:
apktool b UnCrackable-Level3
cd UnCrackable-Level3/dist/
apksigner sign --ks ~/.android/debug.keystore --ks-key-alias signkey UnCrackable-Level3.apk
If we’ve done it right, it should now be able to start and accept any string on our emulator.
What to check in native lib:
- MainActivity::init => what does it do with the xorkey? set it and in bar xors?
- MainActvity::baz => not really necessary; should return classes.dex checksum; maybe sets though
- CodeCheck::bar => get key; maybe xor it
MainActivity_baz:
undefined8 Java_sg_vantagepoint_uncrackable3_MainActivity_baz(void)
{
return 0x18110e3;
}
- Only returns checksum. Nice.
MainActivity_init:
void Java_sg_vantagepoint_uncrackable3_MainActivity_init
(int *param_1,undefined4 param_2,undefined4 param_3)
{
char *__src;
FUN_00013250();
__src = (char *)(**(code **)(*param_1 + 0x2e0))(param_1,param_3,0);
strncpy((char *)&DAT_0001601c,__src,0x18);
(**(code **)(*param_1 + 0x300))(param_1,param_3,__src,2);
DAT_00016038 = DAT_00016038 + 1;
return;
}
- FUN_00013250: checks for debugger via ptrace
- Copies probably the xorkey (also a length of 0x18/24) into 0x1601c
- 0x16038: checks == 2 in bar => probably a init check
CodeCheck_bar:
undefined4
Java_sg_vantagepoint_uncrackable3_CodeCheck_bar(int *param_1,undefined4 param_2,undefined4 param_3)
{
...
if (isInited == 2) {
FUN_00010fa0(&local_40);
input = (**(code **)(*param_1 + 0x2e0))(param_1,param_3,0);
inputLength = (**(code **)(*param_1 + 0x2ac))(param_1,param_3);
if (inputLength == 0x18) {
uVar1 = 0;
puVar3 = &xorkey;
do {
if (*(byte *)(input + uVar1) != (*(byte *)puVar3 ^ *(byte *)((int)&local_40 + uVar1)))
goto LAB_00013456;
uVar1 = uVar1 + 1;
puVar3 = (undefined4 *)((int)puVar3 + 1);
} while (uVar1 < 0x18);
uVar2 = CONCAT31((int3)(uVar1 >> 8),1);
if (uVar1 == 0x18) goto LAB_00013458;
}
}
...
}
- Gets a string from 0x10fa0
- Xors that with the key, byte-for-byte, and compares with the input
Some things
- Input has the same length as the xorkey and the xored bytes (0x18)
FUN_00010fa0:
void FUN_00010fa0(undefined4 *param_1)
{
uVar4 = DAT_00016004 * 0x41c64e6d + 0x3039;
DAT_00016004 = uVar4;
puVar2 = (uint *)malloc(8);
if (puVar2 != (uint *)0x0) {
*puVar2 = uVar4 & 0x7fffffff;
ppuVar3 = (uint **)&_1_sub_doit__opaque_list1_1;
if (_1_sub_doit__opaque_list1_1 == (uint *)0x0) {
*(uint **)(puVar2 + 1) = puVar2;
}
else {
ppuVar3 = (uint **)(_1_sub_doit__opaque_list1_1 + 1);
puVar2[1] = _1_sub_doit__opaque_list1_1[1];
}
*ppuVar3 = puVar2;
}
...
That’s confusing? It’s a really long function too (1500 loc)…
If we take a look at the end we see this though:
if (_1_sub_doit__opaque_list1_1 != (uint *)0x0) {
param_1[1] = 0;
*param_1 = 0;
param_1[3] = 0;
param_1[2] = 0;
*(undefined *)(param_1 + 6) = 0;
*param_1 = 0x1311081d;
param_1[1] = 0x1549170f;
param_1[2] = 0x1903000d;
param_1[3] = 0x15131d5a;
param_1[4] = 0x5a0e08;
param_1[5] = 0x14130817;
}
That looks more interesting… Don’t forget the 0x00.
If we put that into CyberChef we get: cxrgt9~ucbpdoi|*3trucamz
Not a nice flag. Input that into the emulator and it fails…
I searched a bit around, looked in the other libs, weirdly enough for x86_64 the last bytes are reversed…
Parameters found
I also found this in main:
undefined8 main(undefined4 param_1,undefined8 param_2,undefined8 param_3)
{
_global_argc = param_1;
_global_envp = param_3;
_global_argv = param_2;
return 0;
}
Maybe all lib functions have this scheme?
The Solution
Okay I’m dumb?
Okay if we look at this:
*param_1 = 0x1311081d;
std::cout << std::hex << param_1[i] << std::endl;
We get like we would expect 0x1311081d
.
But because we’re little endian, in memory the address will look like this: 0x1d081113.
And if we ask c++ to transform that into a byte it will give us 0x1d (because it prints the first byte).
But if we just copy the hex string out (which is reversed) and xor it with a string (which is in the right order) it of course won’t work.
But the question is, how did not notice that until now? I mean I often use the Ghidra Convert function, but I also copied xored bytes out with no problem? Maybe because the key was also inverted? Fuckin hell, I have no clue. At least now I know it.
Okay so the solution: Take those bytes, reverse them, then concat them and xor. The key: make owasp great again
Debugging
While I haven’t used it it’s pretty useful.
adb connect <IP>
adb shell
ps -A (the -A shows all processes; only needed for >=Android 8) | grep <APP>
gdbserver :<PORT> --attach <PID>
gdb
target remote <IP>:<PORT>
And it should load all the libs and such. You could also port forward to your system with
adb forward tcp:<LOCAL PORT> tcp:<REMOTE PORT>
Android License Validator
A brand new Android app sparks your interest. Of course, you are planning to purchase a license for the app eventually, but you’d still appreciate a test run before shelling out $1. Unfortunately no keygen is available!
Run file
over it:
file validate
validate: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, stripped
Mhh it’s a ARM binary, stripped.
Okay let’s open it up in Ghidra.
We have a few symbols like free, entry, exit and __libc_init
.
Through the init function, we can find the main (the 3rd parameter) at 0x00011874
.
undefined4 main(int argc,int argv)
{
size_t length;
undefined4 len;
undefined decoded [16];
if (argc != 2) {
PrintUsage();
}
length = strlen(*(char **)(argv + 4));
if (length != 0x10) {
ExitWrongFormat();
}
puts("Entering base32_decode");
base32_decode(0,*(undefined4 *)(argv + 4),0x10,decoded,&len);
printf("Outlen = %d\n",len);
puts("Entering check_license");
check_license(decoded);
return 0;
}
Okay so it:
- Takes the first parameter as the key
- Checks for length 0x10/16
- Base32 decodes it
- Then checks the decoded key
If we have a 16 char long encoded string, the decoded key is 6-10 chars long (with padding).
The check_license
:
void check_license(byte *key)
{
uint uVar1;
byte local_20 [8];
int local_18;
byte *local_14;
local_18 = 0;
local_14 = key;
while (local_18 < 5) {
local_20[local_18] = *local_14 ^ local_14[1];
local_14 = local_14 + 2;
local_18 = local_18 + 1;
}
uVar1 = 0x4C();
if (((((uint)local_20[0] == uVar1) && (uVar1 = 0x4F(), (uint)local_20[1] == uVar1)) &&
(uVar1 = 0x4C(), (uint)local_20[2] == uVar1)) &&
((uVar1 = 0x5a(), (uint)local_20[3] == uVar1 && (uVar1 = 0x21(), (uint)local_20[4] == uVar1))))
{
puts("Product activation passed. Congratulations!");
}
else {
puts("Incorrect serial.");
}
return;
}
- Key gets xored in pairs (so char 0 ^ 1, char 2 ^ 3, …)
- Then the xored chars get compared to set values
Here is my python (pseudo?) code:
def check(key):
xored = []
for i in range(0, 5):
j = i*2
xored[i] = key[j] ^ key[j + 1]
if xored[0] == 0x4c and
xored[1] == 0x4f and
xored[2] == 0x4c and
xored[3] == 0x5a and
xored[4] == 0x21:
print("ye")
else:
print("neh")
The key gets reduced in the end to five set chars, which ends up in the string LOLZ!
.
To now write a keygen, we need to find 5 pairs of chars, which get xor’ed to the respective char in the “final key”. We could do only ‘a’, but I decided to make it random:
import random, string, base64
def genChar():
return random.choice(string.ascii_letters)
def keyGen():
xored = "LOLZ!"
newKey = ""
for c in xored:
char = genChar()
newKey += char
newKey += chr(ord(char) ^ ord(c))
print(f"Decoded: {newKey}")
key = base64.b32encode(newKey.encode())
print(f"Key: {key}")
print(f"Key Length: {len(key)}")
keyGen()
To test it (you may need Genymotion ARM translation, if you’re using Genymotion):
> .\adb connect <IP>:5555
> .\adb push ..\validate /data/local/tmp
..\validate: 1 file pushed, 0 skipped. 19.6 MB/s (9364 bytes in 0.000s)
> .\adb shell chmod 755 /data/local/tmp/validate
> .\adb shell /data/local/tmp/validate
Usage: ./validate <serial>
> .\adb shell /data/local/tmp/validate MIXGGLCQDR3CYRLE
Entering base32_decode
Outlen = 10
Entering check_license
Product activation passed. Congratulations!