Android逆向笔记:练手crackme之num1r0的几个apk
注意:本文涉及到的三个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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | 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,然后再来看校验的那部分:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 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这个方法,在这个方法中又调用了:
01 | new Data().getData() |
我们输入的内容要和这个方法的返回值一致,继续追进去看一下:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | 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的内容是:
01 02 03 | public String getFlag(String arg2) { return new Data().isPasswordOk(arg2) ? this .generate() : null ; } |
那么直接执行generate方法得到flag应该也是一样的,新建一个Java类运行一下就好了:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | 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_
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架