简单Android CrackMe分析2
这是在网上找到的一个Android CrackMe,属于比较简单的类型,只是使用了ProGuard进行处理。
一、switch结构
在分析这个CrackMe之前,先说一下JD-GUI对switch结构的支持问题,知道这个BUG的存在就好了。JD-GUI反编译出来的switch语句可读性很差,所以最好结合一下smali代码看一下分支的走向。我们先来自己写一段switch代码,体会一下这个BUG。编译下面一段代码:
Button btnTest = (Button)findViewById(R.id.btnTest); final EditText editInput = (EditText)findViewById(R.id.editText); btnTest.setOnClickListener(new OnClickListener() { public void onClick(View v) { int n = Integer.parseInt(editInput.getText().toString()); String strText; switch (n) { case 0: strText = "AAAA"; break; case 1: strText = "BBBB"; break; case 2: strText = "CCCC"; break; default: strText = "DEFAULT"; break; } Toast.makeText(getApplicationContext(), strText, Toast.LENGTH_LONG).show(); } });
把classes.dex转成jar包,然后用JD-GUI查看,已然不一样了:
public void onClick(View paramView) { String str; switch (Integer.parseInt(this.val$editInput.getText().toString())) { default: str = "DEFAULT"; case 0: case 1: case 2: } while (true) { Toast.makeText(this.this$0.getApplicationContext(), str, 1).show(); return; str = "AAAA"; continue; str = "BBBB"; continue; str = "CCCC"; } }
对于刚接触的人来说,确实不怎么好看。不过凭经验可以理解为case 0对应AAAA,case 1对应BBBB,case 2对应CCCC,default对应DEFAULT,然后执行Toast的代码后返回。
看看smali代码的结构:
packed-switch v0, :pswitch_data_0 # v0为switch参数 .line 47 const-string v1, "DEFAULT" # default值 # 省略N多代码 :pswitch_data_0 .packed-switch 0x0 :pswitch_0 :pswitch_1 :pswitch_2 .end packed-switch
上面的代码可以这样解释:执行完第一句代码之后来到.packed-switch处检查v0的值,因为我们原始的检查范围是0~2,所以这里指定初值为0×0,有三个分支,分别对应0、1、2,如果检查到相等,则跳转到相应的分支,如果没有那么再跳回去,也就是default分支了。
当然,如果我们在switch中检查的值不是连续的,那么.packed-switch就有一点点变化了,比如:
sparse-switch v0, :sswitch_data_0 # v0为switch参数 .line 36 const-string v1, "DEFAULT" # default值 :sswitch_data_0 .sparse-switch 0x4d2 -> :sswitch_0 # 分支1 0x929 -> :sswitch_1 # 分支2 0xd80 -> :sswitch_2 # 分支3 .end sparse-switch
发现关键字从packed-switch变成了sparse-switch了,分支结构是具体的值对应一个分支。关于switch将介绍到这里了,主要是让大家知道JD-GUI怎么看switch的代码。
其实不只是switch,有时候JD-GUI的代码并不是很好看,这时候就得结合smali代码一起分析。
二、CrackMe分析
下面开始解剖这个CrackMe,先使用ApkTool GUI反编译apk文件,查看AndroidManifest.xml可以知道MainActivity类为Main。接着用解压缩软件从APK包中提取出classes.dex并将其转换为jar包,就可以使用JD-GUI查看Java代码了,看到里面很多a、b、c之类的方法名和类名,就应该知道这个被ProGuard处理过了,不过不要紧,代码还是能看的。
2.1 类b代码分析
可以先从Main类的代码开始看,看到里面使用到了a、b、c,这里我们先看类b的代码。类b提供了一个公共的构造函数b,一个私有的成员函数b以及一个公有成员函数a。私有方法b通过TelephonyManager获取设备相关的一些信息,以及通过PackageManager获取自身的签名(com.lohan.crackme1),然后把这些字符串串接起来。
类b的方法a为调用方法b获取字符串,然后通过SharedPreferences.Editor将这个字符串值存储到键machine_id,也就是所谓的机器码了。
经过上面的分析,类b对外提供方法a,功能就是生成机器码并存储到系统中,对应的键为machine_id。
2.2 类c代码分析
类c提供的方法比较多,下面一个一个的分析他们的作用。
1. public c(Context paramContext)
构造函数,同时定义两个字符串:
b = “f0d412b5530e1f9841aab434d989cc77″;
c = “4ec407446b872351e613111339daae9″;
2. public static boolean b()
通过getPackageManager获取自身的签名,如果签名与构造函数中的两个字符串b或者c任意一个相等,那么返回false,否则返回true。
3. private static String b(String paramString)
通过MessageDigest计算paramString的MD5值。
4. public static int a(String paramString)
jd-gui的代码有点乱,结合smali代码看。还原的代码如下:
可以看出这段代码的功能为计算机器码的MD5,如果与传入的参数一致,那么通过SharedPreferences存入到serial字段中。当然还有调用b方法进行一些判断,自身的签名不能是已知的两个。
public static int a(String paramString) { if (b() == false) { SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(a); String mId = localSharedPreferences.getString("machine_id", ""); String idMd5 = b(mId); if (idMd5.equals(paramString) == false) { return 0; } SharedPreferences.Editor editor1 = localSharedPreferences.edit(); editor1.putString("serial", paramString); editor1.commit(); return 1; } return 0; }
5. public static boolean a()
这个其实就是上面的int a(String paramString)的包装函数,通过SharedPreferences获取serial字段,并传给这个方法,返回相应的返回值。
2.3 类a代码分析
倒计时6秒钟,然后调用类c的a方法(boolean那个),如果返回false的话,就设置TextView内容提示注册。
2.4 类Main代码分析
在OnCreate方法中,先调用b.a()存储机器码,然后调用c.a(),也就是判断是否已经存储了serial,并判断是否能通过算法校验:
invoke-static {}, Lcom/lohan/crackme1/c;->a()Z move-result v0 if-eqz v0, :cond_0 :try_start_0 invoke-direct {p0}, Lcom/lohan/crackme1/Main;->a()V :try_end_0 .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 :cond_0 :goto_0 return-void
如果不能通过,则什么都不做,如果能通过,则调用自身的方法a()。而该方法中又调用了c.b()方法,如果c.b()返回false,那么就把Button和EditText设置为隐藏(setVisibility(4)),并设置TextView的文本为PRO VERSION!(id=”0x7f040003″),并启用倒计时类a,这样看来,这里就有了两次校验了。
OnClick方法中,将输入的注册码传给c.a(String)方法检查,如果通过则提示Thanks for purchasing!,否则提示Invalid serial!。
经过上面的分析,如果APK自身签名是f0d412b5530e1f9841aab434d989cc77或者4ec407446b872351e613111339daae9,那么即使序列号通过验证,也只是开始的6秒钟显示PRO VERSION!,之后就提示要注册了。不过APK的签名是很长的一串啊,所以这里应该是没有什么影响了。
三、编写Keygen
可以参考类b的方法b,先获取机器码,然后计算MD5值。核心代码如下:
btnKeygen.setOnClickListener(new OnClickListener() { public void onClick(View v) { TelephonyManager tm = (TelephonyManager)getSystemService("phone"); String str1 = tm.getDeviceId(); String str2 = tm.getLine1Number(); String str3 = tm.getDeviceSoftwareVersion(); String str4 = tm.getSimSerialNumber(); String str5 = tm.getSubscriberId(); String machineId; PackageManager pm = getPackageManager(); try { PackageInfo pkgInfo = pm.getPackageInfo("com.lohan.crackme1", PackageManager.GET_SIGNATURES); String sig = pkgInfo.signatures[0].toCharsString(); machineId = str1 + str2 + str3 + str4 + str5 + sig; // 机器码 editMachineId.setText(machineId); // 签名 editSig.setText(sig); // 注册码 MessageDigest md = MessageDigest.getInstance("MD5"); int len = machineId.length(); md.update(machineId.getBytes(), 0, len); BigInteger bigInt = new BigInteger(1, md.digest()); String serial = bigInt.toString(16); editSerial.setText(serial); } catch (Exception e) { editMachineId.setText("没有发现安装CrackMe"); } } });
KeyGen运行截图如下:
把注册码输入到CrackMe进行注册,提示成功:
四、相关资源
Android的CrackMe比较难找,crackmes.de上也只能找到几个而已。本文的CrackMe来源于网上,作者的博客是http://androidcracking.blogspot.com/,上面有一些关于Android逆向相关的文章,有兴趣的朋友可以看一下,自备*。
CrackMe / Keygen下载:
http://pan.baidu.com/share/link?shareid=2857217394&uk=369321854
Copyed From 程序人生
Home Page:http://www.programlife.net
推荐阅读
-
简单Android CrackMe分析2
-
Android Framework------之Keyguard 简单分析
-
Android自定义View2--onMeasure,onLayout源码分析和自定义流式布局
-
Kotlin入门实战:2、 Android 创建一个简单的 Kotlin 应用程序
-
Android系统中Thread,Looper,MessageQueue,Message,Handler相互关系的简单分析
-
Android 8.1 开机流程分析(2)
-
Android 8.1 开机流程分析(2)
-
简单Android CrackMe分析
-
100天搞定机器学习|Day2简单线性回归分析
-
Wifi模块—源码分析Wifi热点扫描2(Android P)