前面学习了安卓Java层的逆向以及Hook框架的使用。这里记录自己通过一个安卓crack me程序练手的实验过程。 首先在夜深模拟器中运行cm1,
这里需要我们获得程序中设置的flag信息。
使用jeb分析apk程序,查看manifest.xml,程序的入口点为f8left.cm1.MainActivity,
从Bytecode目录结果可以看出,核心代码所在包名为f8left.cm1,主要包括App、BuildConfig、Check、MainActivity以及R类。只要对安卓开发有初步的了解就知道App类在程序正式执行MainActivity中的代码之前执行,App类代码如下所示:
这里进行了反调试检测,如果处于调试状态下则调用killProcess方法终止程序进程执行。
看一下MainActivity代码:
程序运行逻辑很简单,获取用户输入的flag(必须是16位),然后调用Check类中的check方法对flag进行校验,那么这里的Check类就是我们要重点分析的对象了,为了便于观察我们将Jeb反编译后的Java代码贴上来:
package f8left.cm1;
import android.content.Context;
import android.content.pm.PackageManager$NameNotFoundException;
import android.content.pm.Signature;
public class Check {
private static String cFlag;
static {
Check.cFlag = "\\F8LEFT/";
}
public Check() {
super();
}
public static String byteArrayToHexString(byte[] arg2) {
return Check.byteArrayToHexString(arg2, 0, arg2.length);
}
public static String byteArrayToHexString(byte[] arg5, int arg6, int arg7) {
if(arg5 != null && arg6 >= 0 && arg7 <= arg5.length && arg6 <= arg7) {
StringBuilder v0 = new StringBuilder();
while(arg6 < arg7) {
v0.append(String.format("%02X", Byte.valueOf(arg5[arg6])));
++arg6;
}
return v0.toString();
}
throw new IllegalArgumentException();
}
public static String byteArrayToHexString(byte[] arg1, int arg2) {
return Check.byteArrayToHexString(arg1, arg2, arg1.length);
}
boolean check(Context arg6, String arg7) {
boolean v2 = false;
byte[] v0 = Check.hexStringToByteArray(arg7);
if(v0.length == 8) {
Check.r4(this.genKey(arg6), v0, 0, v0.length);
if(new String(v0).equals(Check.cFlag)) {
v2 = true;
}
}
return v2;
}
public byte[] genKey(Context arg8) {
byte[] v2 = null;
try {
Signature[] v3 = arg8.getPackageManager().getPackageInfo(arg8.getPackageName(), 64).signatures;
if(v3.length <= 0) {
return v2;
}
v2 = v3[0].toByteArray();
}
catch(PackageManager$NameNotFoundException v0) {
v0.printStackTrace();
}
return v2;
}
public static byte[] hexStringToByteArray(String arg2) {
return Check.hexStringToByteArray(arg2, 0, arg2.length());
}
public static byte[] hexStringToByteArray(String arg9, int arg10, int arg11) {
byte[] v0 = null;
if(arg10 >= 0 && arg10 <= arg11 && (arg11 - arg10) % 2 == 0) {
int v5 = (arg11 - arg10) / 2;
byte[] v2 = new byte[v5];
int v4 = 0;
while(true) {
if(v4 < v5) {
int v6 = arg10 + 2;
try {
v2[v4] = ((byte)Integer.parseInt(arg9.substring(arg10, v6), 16));
arg10 += 2;
++v4;
continue;
}
catch(NumberFormatException v3) {
byte[] v1 = v0;
return v1;
}
}
else {
break;
}
return v0;
}
v0 = v2;
}
return v0;
}
public static void r4(byte[] arg10, byte[] arg11, int arg12, int arg13) {
int v7 = 256;
int v0 = 0;
int v4 = arg10.length;
byte[] v5 = new byte[v7];
int v1;
for(v1 = 0; v1 < v7; ++v1) {
v5[v1] = ((byte)v1);
}
v1 = 0;
int v2;
for(v2 = 0; v2 < v7; ++v2) {
v1 = (v5[v2] + v1 + arg10[v2 % v4]) % 256 & 255;
byte v6 = v5[v2];
v5[v2] = v5[v1];
v5[v1] = v6;
}
v1 = 0;
while(arg12 < arg13) {
v1 = (v1 + 1) % 256 & 255;
v0 = (v5[v1] + v0) % 256 & 255;
byte v3 = v5[v1];
v5[v1] = v5[v0];
v5[v0] = v3;
arg11[arg12] = ((byte)(v5[(v5[v1] + v5[v0]) % 256 & 255] ^ arg11[arg12]));
++arg12;
}
}
}
在Check方法中首先调用hexStringToByteArray(String)
方法将十六进制的flag串转化成字节数组,方法很简单,即将类似于”0123456789ABCDEF”形式的flag每两个字符作为一个十六进制形式数(如0x01)并调用Integer类中的parseInt()
方法将其转换为十进制数存到byte[]数组中返回。
返回后的结果存放在v0数组中,Check方法中接下来调用Check.r4(this.genKey(arg6), v0, 0, v0.length);
这里传入的四个参数分别是:
genKey()方法返回的byte[]数组;
处理后的flag输入;
v0的开始位置以及长度。
嗯,我们再来看一看genKey(Context arg)
方法的实现,这里如果熟悉安卓开发的就会知道(我不是很熟悉。。)这里获取的是程序的签名信息。也就是说,如果我们对程序进行了修改,那么就需要对apk重新进行签名,此时的签名值就发生了改变,进而重新计算出来的flag也就变了,那这个flag自然不是原来的正确的flag了。
所以我们在这里就对apk程序进行静态分析,嗯,先看一看r4方法吧,这里对通过获取到的签名值以及输入的flag生成字符串,如果最终与”\F8LEFT/”一致则程序验证正确。由于该算法很简单,并且与Input相关的只有最后一句的异或操作,所以其逆算法只要再异或回来就好了。这里逆算法和r4完全一致,在密码学中可以称为对称算法了。。
接下来看一看如何获得签名值,这里初步有三种方法可以选择:
1)使用Xposed或者Cydia Substrate框架对获取签名的方法进行hook,并在日志中打印签名值;
2)编写代码通过包名获取签名值,参考链接;
3)直接通过CERT.RSA文件获取。
这里我们通过第二种方法获取应用的签名值,这里就不做过多的解释了,自己也是参照上面的链接中给出的代码实现的,
这里就手动将要获取签名值的包名写在程序中,当程序一加载就将签名信息打印在日志中。这里需要将包名改写为“f8left.cm1”,执行程序,日志中打印结果如下:
这里我们就成功获取了apk的签名值。
这里我们通过微信提供的获取签名的工具也可以得到相同的结果:
最后就是编写程序获取最终的flag值:
这里的代码是基于上面链接中给出的获取apk签名信息的代码改的,由于生成字符串的正向算法与逆向算法是一致的,因此直接将r4的代码部分拷过来就可以了,这里有一点还是比较坑的,就是确定签名的输入形式,上面给出代码中或者说微信工具中获取到的签名信息是经过MD5处理后的,而在apk的校验代码中,并没有经过MD5处理,所以如果直接将sigInfo设置为上面的MD5值的话得到的就不是正确的flag了。在这一点上还是需要仔细一点。
嗯,这样我们就得到了最终的flag了,
总结:在这个简单的安卓逆向中,并没有涉及到Native层代码,我完全是通过静态分析的方式来获取flag值的。静态分析时涉及到的关键点包括对反调试代码的分析、编写代码获取apk的签名信息、逆向代码的实现。
其实,在获取apk的签名信息中我们可以通过Xposed框架或者Cydia Substrate框架来实现,或者直接通过解析CERT.RSA文件来获得签名信息。
关于通过Xposed框架来获取apk的签名信息,可以参考这篇文章,Hook神器Xposed框架 。
同样,我们通过动态调试来解决问题,步骤上应该与之前自己参照这篇博客实践Android逆向之旅—动态方式破解apk前奏篇(Eclipse动态调试smail源码) 的步骤是差不多的,有时间的话也可以试一试。但是由于程序中加入了反调试,所以我们如果直接修改smali代码的话会导致签名信息发生改变,所以要在不修改smali代码的前提下对抗反调试,这里又可以用到Xposed框架来进行了。