注意:本文涉及到的三个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_