发新帖

安卓逆向实践——cm1逆向

[复制链接]
17753 0

前面学习了安卓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框架来进行了。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

举报 使用道具

回复
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表