注意:本文涉及到的三个apk都是非常非常简单的级别,适合逆向新手阅读。
num1r0是一个人(其实我也不知道是谁,就是搜Android练手apk时搜到的),他做了几个android crack me,只是感觉蛮有意思的所以研究一下。
https://persianov.net/crackme-challenges-for-android
我很喜欢这种风格,于是把整个网站截图放在这里:



不过目前来看只提供了三个,作者是把它们放到了GitHub上了:
https://github.com/num1r0/android_crackmes
好接下来就挨个玩耍一下。
crackme_0x01
https://github.com/num1r0/android_crackmes/tree/master/crackme_0x01
或:
下载下来安装到夜神模拟器,如果安装不了可能是自己的模拟器版本过期,自行创建更高版本的模拟器:

让我们输入密码,那就在密码框随便输入点内容提交:

提示我们错误的密码,OK,现在把apk文件拖到jeb打开看一下:

看上去项目结构十分简单,于是就挨个看一下,首先是MainActivity:
package com.entebra.crackme0x01;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AlertDialog.Builder;
import android.support.v7.app.AppCompatActivity;
import android.view.View.OnClickListener;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
private final String description;
private final String name;
public MainActivity() {
this.name = "CrackMe 0x01";
this.description = "Level: Beginner";
}
public void follow_clicked(View arg3) {
try {
this.startActivity(new Intent("android.intent.action.VIEW", Uri.parse("twitter://user?screen_name=num1r0")));
}
catch(Exception unused_ex) {
this.startActivity(new Intent("android.intent.action.VIEW", Uri.parse("https://twitter.com/#!/num1r0")));
}
}
@Override // android.support.v7.app.AppCompatActivity
protected void onCreate(Bundle arg3) {
this.getSupportActionBar().hide();
super.onCreate(arg3);
this.setContentView(0x7F09001B); // layout:activity_main
((Button)this.findViewById(0x7F070078)).setOnClickListener(new View.OnClickListener() { // id:submit
@Override // android.view.View$OnClickListener
public void onClick(View arg4) {
String v4 = new FlagGuard().getFlag(((EditText)this.findViewById(0x7F070055)).getText().toString()); // id:password
if(v4 != null) {
Builder v0 = new Builder(MainActivity.this);
v0.setTitle("Congratulations!");
v0.setMessage("The flag is: " + v4);
v0.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface arg1, int arg2) {
arg1.dismiss();
}
});
v0.create().show();
return;
}
Builder v4_1 = new Builder(MainActivity.this);
v4_1.setTitle("Nope!");
v4_1.setMessage("Wrong password -> No flag :))");
v4_1.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface arg1, int arg2) {
arg1.dismiss();
}
});
v4_1.create().show();
}
});
}
}
比较重要的是onCreate中为submit按钮绑定了一个事件,当按钮被单击时获取id为password的文本输入框的内容进行校验,如果校验结果不为null则认为是ok,然后再来看校验的那部分:
package com.entebra.crackme0x01;
import android.util.Log;
public class FlagGuard {
private String flag;
private final String pad;
private final String scr_flag;
public FlagGuard() {
this.pad = "abcdefghijklmnopqrstuvwxyz";
this.scr_flag = "qw4r_q0c_nc4nvx3_0i01_srq82q8mx";
this.flag = "";
}
public String getFlag(String arg2) {
return arg2.equals(new Data().getData()) ? this.unscramble() : null;
}
private String unscramble() {
String v5_1;
StringBuilder v0 = new StringBuilder();
int v2 = 0;
char[] v3 = "qw4r_q0c_nc4nvx3_0i01_srq82q8mx".toCharArray();
while(v2 < v3.length) {
char v5 = v3[v2];
Log.e("Char: ", String.valueOf(v5));
int v6 = "abcdefghijklmnopqrstuvwxyz".indexOf(v5);
Log.e("indexOf: ", String.valueOf(v6));
if(v6 < 0) {
v5_1 = String.valueOf(v5);
}
else {
int v5_2 = "abcdefghijklmnopqrstuvwxyz".length();
int v6_1 = (v6 - ((int)Integer.valueOf(String.valueOf(1337).split("\.")[0]))) % v5_2;
v5_1 = v6_1 >= 0 ? String.valueOf("abcdefghijklmnopqrstuvwxyz".toCharArray()[v6_1]) : String.valueOf("abcdefghijklmnopqrstuvwxyz".toCharArray()[v6_1 + "abcdefghijklmnopqrstuvwxyz".length()]);
}
Log.e("letter ", v5_1);
v0.append(v5_1);
++v2;
}
Log.e("FLAG: ", v0.toString());
return v0.toString();
}
}
其中比较重要的是getFlag这个方法,在这个方法中又调用了:
new Data().getData()
我们输入的内容要和这个方法的返回值一致,继续追进去看一下:
package com.entebra.crackme0x01;
public class Data {
private final String secret;
public Data() {
this.secret = "s3cr37_p4ssw0rd_1337";
}
public String getData() {
this.getClass();
return "s3cr37_p4ssw0rd_1337";
}
}
OK,这个方法只是简单的返回了一个字符串s3cr37_p4ssw0rd_1337,而这个字符串就是我们要输入的字符串,再切换到app界面,输入试一下:

crackme_0x02
apk下载地址为:
https://github.com/num1r0/android_crackmes/tree/master/crackme_0x02
或:
拖到夜神模拟器安装看一下:

随便输入点东西,然后submit:

好了,拖到jeb看下代码,结构比较简单:

打开MainActivity,它的onCreate方法中标记的这一行比较关键,这是获取我们输入的字符串,然后调用另一个方法校验,如果返回的值不为空就认为是通过了:

然后看它调用的这样代码,这里面又用到了一个Data,我们的输入要和Data.getData()相等:

然后Data这个类比较简单,就是读取一个资源id的值,这里jeb已经自动识别出来这个资源id是一个strings.xml中的名为secret值为s0m3_0th3r_s3cr3t_passw0rd:

这个s0m3_0th3r_s3cr3t_passw0rd就是我们要输入的值,来试一下:

OK,破解成功。
crackme_0x03
apk下载地址:
https://github.com/num1r0/android_crackmes/tree/master/crackme_0x03
或:
把apk文件下载下来拖到jeb看下项目结构:

这个家伙的套路怎么都一模一样,要不是看源码不同我都怀疑我搞错apk文件了...
然后看下MainActivity:
package net.persianov.crackme0x03;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AlertDialog.Builder;
import android.support.v7.app.AppCompatActivity;
import android.view.View.OnClickListener;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
public void followClicked(View arg3) {
try {
this.startActivity(new Intent("android.intent.action.VIEW", Uri.parse("twitter://user?screen_name=num1r0")));
}
catch(Exception unused_ex) {
this.startActivity(new Intent("android.intent.action.VIEW", Uri.parse("https://twitter.com/#!/num1r0")));
}
}
@Override // android.support.v7.app.AppCompatActivity
protected void onCreate(Bundle arg3) {
this.getSupportActionBar().hide();
super.onCreate(arg3);
this.setContentView(0x7F09001B); // layout:activity_main
((Button)this.findViewById(0x7F070078)).setOnClickListener(new View.OnClickListener() { // id:submit
@Override // android.view.View$OnClickListener
public void onClick(View arg4) {
String v4 = new FlagGuard().getFlag(((EditText)this.findViewById(0x7F070055)).getText().toString()); // id:password
if(v4 != null) {
Builder v0 = new Builder(MainActivity.this);
v0.setTitle("Congratulations!");
v0.setMessage("The flag is: " + v4);
v0.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface arg1, int arg2) {
arg1.dismiss();
}
});
v0.create().show();
return;
}
new Data();
Builder v0_1 = new Builder(MainActivity.this);
v0_1.setTitle("Nope!");
v0_1.setMessage("Unknown error...");
v0_1.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface arg1, int arg2) {
arg1.dismiss();
}
});
v0_1.create().show();
}
});
}
}
次奥几乎一毛一样...
然后进入熟悉的FlagGuard:
package net.persianov.crackme0x03;
import android.os.Build.VERSION;
public class FlagGuard {
private char[] flag;
public FlagGuard() {
this.flag = new char[20];
}
private String generate() {
int[] v1 = new int[]{13, 1, 19, 14, 5, 8, 18, 9, 2, 11, 17, 3, 10, 6, 15, 7, 0, 16, 12, 4};
StringBuilder v2 = new StringBuilder();
int[] v3 = new int[20];
int v4 = 0;
int v5;
for(v5 = 0; v5 < v3.length; ++v5) {
v3[v5] = 27;
}
if(Build.VERSION.CODENAME.length() > 0) {
int v5_1;
for(v5_1 = 0; v5_1 < 5; ++v5_1) {
v3[v5_1] = 65;
switch(v5_1) {
case 1: {
int v6 = v5_1 - 1;
v3[v5_1] = v3[v6] + 4;
++v5_1;
v3[v5_1] = v3[v6] + 30;
break;
}
case 3: {
v3[v5_1] <<= 1;
v3[v5_1] += -23;
break;
}
case 4: {
v3[v5_1] = v3[v5_1 - 1] + 14;
}
}
}
int v5_2;
for(v5_2 = 5; v5_2 < 10; ++v5_2) {
switch(v5_2) {
case 5: {
v3[v5_2] = v3[v5_2 - 3];
break;
}
case 6: {
int v8 = v5_2 - 1;
v3[v5_2] = v3[v8] - 11;
v3[v5_2 + 2] = v3[v5_2] - 2;
v3[v5_2 + 1] = 103;
v3[v5_2 + 3] = v3[v8];
}
}
}
v3[10] = 73;
v3[13] = 0x4F;
v3[12] = 0x4F;
v3[11] = v3[7] - 2;
int v5_3;
for(v5_3 = 15; v5_3 < 20; ++v5_3) {
switch(v5_3) {
case 15: {
v3[v5_3 - 1] = v3[v5_3 - v5_3 + 1];
v3[v5_3] = 0x75;
break;
}
case 16: {
v3[v5_3] = 104;
v3[v5_3 + 2] = v3[v5_3 - 1] - 1;
break;
}
case 17: {
v3[v5_3] = v3[5];
v3[v5_3 + 2] = v3[v5_3];
}
}
}
}
else {
v2.replace(0, v2.length(), "");
}
int v5_4 = 0;
while(v4 < v1.length) {
this.flag[v1[v4]] = (char)v3[v5_4];
++v5_4;
++v4;
}
v2.append(this.flag);
return v2.toString();
}
public String getFlag(String arg2) {
return new Data().isPasswordOk(arg2) ? this.generate() : null;
}
}
看他的getFlag仍然是调用的Data的一个方法,再看Data:
package net.persianov.crackme0x03;
import android.util.Log;
import java.security.MessageDigest;
public class Data {
private static String lastError = "Unknown error...";
private final String long_password_message;
private final String password_hash;
private int password_length;
private final String short_password_message;
private final String wrong_password_message;
static {
}
public Data() {
this.short_password_message = "Password too SHORT";
this.long_password_message = "Password too LONG";
this.wrong_password_message = "WRONG password entered";
this.password_length = 6;
this.password_hash = "ac43bb53262e4edd82c0e82a93c84755";
}
private boolean MD5Compare(String passwd, String passwdHash) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(passwd.getBytes());
byte[] passwdMd5Bytes = md5.digest();
md5.reset();
StringBuilder md5String = new StringBuilder();
int i;
for(i = 0; i < passwdMd5Bytes.length; ++i) {
String j; // 这个补前缀0的方法真是看得我要炸了
for(j = Integer.toHexString(passwdMd5Bytes[i] & 0xFF); j.length() < 2; j = "0" + j) { // 这个补前缀0的方法真是看得我要炸了
}
md5String.append(j);
}
return md5String.toString().contentEquals(passwdHash) ? 1 : 0;
}
catch(Exception v8) {
Log.e("Exception MD5 compare", v8.getMessage());
return 0;
}
}
public String getData() {
this.getClass();
return "ac43bb53262e4edd82c0e82a93c84755";
}
public String getLastError() {
return Data.lastError;
}
public boolean isPasswordOk(String passwd) {
if(passwd.length() < this.password_length) {
Data.lastError = "Password too SHORT";
return 0;
}
if(passwd.length() > this.password_length) {
Data.lastError = "Password too LONG";
return 0;
}
if(passwd.length() == this.password_length) {
this.getClass();
if(!this.MD5Compare(passwd, "ac43bb53262e4edd82c0e82a93c84755")) {
Data.lastError = "WRONG password entered";
return 0;
}
this.getClass();
return this.MD5Compare(passwd, "ac43bb53262e4edd82c0e82a93c84755");
}
return 0;
}
}
是需要我们输入的文本的MD5是ac43bb53262e4edd82c0e82a93c84755,找了几个免费的网站解密了一下都没出来,看来可能不是一个常见的字符串,那咋办,回头看注意到FlagGuard方法的getFlag的内容是:
public String getFlag(String arg2) {
return new Data().isPasswordOk(arg2) ? this.generate() : null;
}
那么直接执行generate方法得到flag应该也是一样的,新建一个Java类运行一下就好了:
package cc11001100.android.crack_me_kotlin;
class A {
private static String generate(boolean versionGtZero) {
char[] flag = new char[20];
int[] v1 = new int[]{13, 1, 19, 14, 5, 8, 18, 9, 2, 11, 17, 3, 10, 6, 15, 7, 0, 16, 12, 4};
StringBuilder v2 = new StringBuilder();
int[] v3 = new int[20];
int v4 = 0;
int v5;
for (v5 = 0; v5 < v3.length; ++v5) {
v3[v5] = 27;
}
// if(Build.VERSION.CODENAME.length() > 0) {
if (versionGtZero) {
int v5_1;
for (v5_1 = 0; v5_1 < 5; ++v5_1) {
v3[v5_1] = 65;
switch (v5_1) {
case 1: {
int v6 = v5_1 - 1;
v3[v5_1] = v3[v6] + 4;
++v5_1;
v3[v5_1] = v3[v6] + 30;
break;
}
case 3: {
v3[v5_1] <<= 1;
v3[v5_1] += -23;
break;
}
case 4: {
v3[v5_1] = v3[v5_1 - 1] + 14;
}
}
}
int v5_2;
for (v5_2 = 5; v5_2 < 10; ++v5_2) {
switch (v5_2) {
case 5: {
v3[v5_2] = v3[v5_2 - 3];
break;
}
case 6: {
int v8 = v5_2 - 1;
v3[v5_2] = v3[v8] - 11;
v3[v5_2 + 2] = v3[v5_2] - 2;
v3[v5_2 + 1] = 103;
v3[v5_2 + 3] = v3[v8];
}
}
}
v3[10] = 73;
v3[13] = 0x4F;
v3[12] = 0x4F;
v3[11] = v3[7] - 2;
int v5_3;
for (v5_3 = 15; v5_3 < 20; ++v5_3) {
switch (v5_3) {
case 15: {
v3[v5_3 - 1] = v3[v5_3 - v5_3 + 1];
v3[v5_3] = 0x75;
break;
}
case 16: {
v3[v5_3] = 104;
v3[v5_3 + 2] = v3[v5_3 - 1] - 1;
break;
}
case 17: {
v3[v5_3] = v3[5];
v3[v5_3 + 2] = v3[v5_3];
}
}
}
} else {
v2.replace(0, v2.length(), "");
}
int v5_4 = 0;
while (v4 < v1.length) {
flag[v1[v4]] = (char) v3[v5_4];
++v5_4;
++v4;
}
v2.append(flag);
return v2.toString();
}
public static void main(String[] args) {
System.out.println(generate(true)); // hERe_yOu_gO_tAkE_IT_
System.out.println(generate(false)); // 没有输出
}
}
最后的输出应该是hERe_yOu_gO_tAkE_IT_