首页
社区
课程
招聘
[原创]Android应用完整性保护总结
2019-4-23 12:02 16107

[原创]Android应用完整性保护总结

2019-4-23 12:02
16107

一、静态完整性校验

1. 对Dex文件进行完整性校验
  1. 由于Android 稍高版本在安装apk的过程中会把classes.dex 抠出来转化为odex或者vdex等其他格式的优化文件,原apk中无classes.dex,但是会存在除classes.dex外的其他多dex文件(比如classes2.dex,classes3.dex等),故无法校验classes.dex,也无法校验整个APK文件哈希值
  2. 通过读取apk包中的除classes.dex外的其他dex文件,并计算其CRC的值,将得到的CRC与原始CRC进行比较,判断是否被修改过,原始CRC值保存在资源文件或其他自定义文件中相关代码:
    String apkPath=context.getPackageCodePath();//获取Apk的路径
    ZipFile zipFile=new ZipFile(apkPath);
    ZipEntry dexEntry=ZipFile.getEntry("classes.dex");//读取zip包中的classes.dex文件
    String dexCRC=dexEntry.getCrc().toString();//计算dex的CRC文件值
    
  3. 对Dex文件的局部(可以是头部,代码段等等)求MD5值,并将MD5写入dex文 件的末尾或其他不影响dex文件加载 运行的位置(本人暂时还没碰到)
    相关代码:
    MessageDigest digest=MessageDigest.getInstance("MD5");
    byte[] bytes=new byte[1024];
    FileInputStream fileinputStream=new FileInputStream(new File(dexPath));
    int byteCount;  
    while(fileinputStream.read(bytes)!=-1){
     digest.update(bytes,0,byteCount);
    }
    BigInteger bigInteger=new BigInteger(1,digest.digest());//计算dex的哈希
    String MD5=bigInteger.toString();//dex的哈希值
    

2、对Apk文件进行完整性校验(Android 5.0以下)

通过APK包的MD5摘要进行判断文件是否被修改过,这里需要计算APK的MD5值,然后将MD5上传到服务器进行判断,或者等待服务器下发原始文件的MD5值,进行比较判断。

相关代码:

String apkPath=context.getPackageCodePath();//获取Apk的路径
MessageDigest digest=MessageDigest.getInstance("MD5");
byte[] bytes=new byte[1024];
FileInputStream fileinputStream=new FileInputStream(new File(apkPath));//读取apk文件
int byteCount;
while(fileinputStream.read(bytes)!=-1){
    digest.update(bytes,0,byteCount);
}
BigInteger bigInteger=new BigInteger(1,digest.digest());//计算apk的哈希
String MD5=bigInteger.toString();//apk的哈希值

3、签名信息校验。

<1>.通过android.content.pm.PackageInfo.getPacketInfo()函数获取包信息,然后拿到签名信息
<2>.该签名信息为一个byte数组,可以转换成 X.509格式的证书信息,或者直接对该签名信息求哈希
<3>.现阶段为了增加分析的难度,通常都会将部分代码放到.so文件中。

相关代码:

PackageInfo  packageInfo=content.getPackageManager()
.getPackageInfo(content.getPackageName()
PackageManager.GET_SIGNATURES);//获取包信息
        Signature[] signature=packageInfo.signatures;
        Signature sign=signature[0];
MessageDigest digest=MessageDigest.getInstance("MD5");
digest.update(sign);

二、静态完整性校验apk的一些逆向思路

1.获取apk签名需要获取apk的路径,我们可以在获取apk路径的地方下断点。这样可以定位到相关校验代码。通过修改参数,传入一个官方apk路径,也可以绕过校验。

Context.getPackageCodePath()  用来获得当前应用程序对应的 apk 文件的路径:/data/app/包名/xxx.apk
Context.getPackageResourcePath()    获取该程序的安装包路径 : /data/app/包名/xxx.apk
packageInfo.applicationInfo.sourceDir  这里面也可以获取apk路径

2.大部分应用在获取签名信息时都会调用系统api,我们可以在相关的api下断:

  1. V1签名(Android 7.0以下):
    android.content.pm.PackageInfo.getPacketInfo(ClassName,flags).signatures
    这里需要注意 当flags为64的时候,该函数会获取签名信息
    所以需要在getpackageinfo下断点,当flags为64的时候,就是获取签名信息
    
  2. V2签名(Android 7-9):
    // 1.反射实例化PackageParser对象
    Object packageParser = getPackageParser(path);
    // 2.反射获取parsePackage方法
    Object packageObject = getPackageInfo(path,packageParser);
    // 3.调用collectCertificates方法
    Method collectCertificatesMethod = packageParser.getClass().       getDeclaredMethod("collectCertificates",packageObject.getClass(),int.class);
    collectCertificatesMethod.invoke(packageParser,packageObject,0);
    // 4.获取mSignatures属性
    Field signaturesField =             packageObject.getClass().getDeclaredField("mSignatures");
    signaturesField.setAccessible(true); 
    Signature[] mSignatures = (Signature[])  signaturesField.get(packageObject);
    
  3. V3签名(Android 9及以上):
    这几个是v3签名的系统验证函数
    PackageManagerService.InstallPackageLI()
    PackageParser.collectCertificates()
    ApkSignatureVerifier.verify()
    ApkSignatureSchemeV3Verifier.verify()
    ApkSigningBlockUtils.findSignature()
    ApkSigningBlockUtils.findApkSigningBlock()
    ApkSigningBlockUtils.findApkSignatureSchemeBlock()
    SignatureInfo.SignatureInfo()
    ApkSignatureSchemeV3Verifier.verify()
    ApkSignatureSchemeV3Verifier.verifySigner()
    ApkSignatureSchemeV3Verifier.verifyAdditionalAttributes()
    ApkSignatureSchemeV3Verifier.verifyProofOfRotationStruct()
    ApkSignatureSchemeV3Verifier.VerifiedProofOfRotation()
    

3.可以在获取哈希相关的api处下断

4.可以通过弹出窗口或者toast提示进行代码回溯,定位到签名校验的附近

三、动态完整性

1.Xpose Hook 检测

(1)检测关键字
de.robv.android.xposed.XposedHelpers类的
静态fieldCache字段        保存被hook的字段信息
静态methodCache字段       保存被hook的方法信息
静态constructorCache字段  保存被hook的类信息
(2)检测内存
检测内存映射列表中是否包含如下文件:
XposedBridge.so
XposedBridge.jar
(3)检测方法的调用栈
handleHookMethod
invokeOriginalMethodNative
注:
1>.在dalvik.system.NativeStart.main方法后出现
  de.robv.android.xposed.XposedBridge.main的方法调用
2>如果Xposed hook了调用栈里的一个方法,
  还会有de.robv.android.xposed.XposedBridge.handleHookedMethod
  和de.robv.android.xposed.XposedBridge.invokeOriginalMethodNative调用
(4)检测标记位
de.robv.android.xposed.XposedBridge类
静态disableHooks   成员变量 
这个字段表示是否对当前应用进行hook操作

2.Frida Hook 检测

(1) 检测进程

FridaServer通过TCP与PC上的Frida进行通信,所以可以检测进程中是否存在 FridaServer进程

(2) 检测端口
默认端口:27047
非默认端口:
1>.用nmap -sV来找到开放端口
2>对每个开放端口发送D-Bus认证协议
3>哪个端口回复了哪个就是FridaServer进程的端口
(3) 检测内存
搜索内存是否存在以下两个文件
frida-gadget.so
frida-agent.so`
(4) 暴力扫描

在映射的so文件中扫描Frida的库特征,例如:“gadgets”,“LIBFRIDA”等等,这 两个在Frida的所有版本中都有存在

3.Cydia Substrate Hook检测

(1)检测包名
检测设备安装目录是否存在com.saurik.substrate
(2)检测调用栈
com.android.internal.os.ZygoteInit
com.saurik.substrate.MS$2
注:
1>.在dalvik.system.NativeStart.main调用后会出现2次
com.android.internal.os.ZygoteInit.main,而不是一次。
2>.如果Substrate hook了调用栈里的一个方法,还会出现com.saurik.substrate.MS$2.invoked、
com.saurik.substrate.MS$MethodPointer.invoke和跟Substrate扩展相关的方法

(3)检测内存

检测内存映射中是否存在com.saurik.substrate文件

4.动态完整性校验的一些逆向思路

1.搜索字符串
例如:搜索  de.robv.android.xposed.XposedBridge 或者搜索上述字符串的base64编码后的值

2.获取堆栈的函数下断

大部分应用都是主动触发异常,然后获取堆栈信息,判断里面是否有相关hook的关键字
可以对 getStackTrace() 下断,这个是获取堆栈的api

3.对文件读取函数下断

如果获取内存,需要打开当前进程的 /proc文件,可以对文件读取相关的api下断
例如: new file 、new FildeReader

4.对相关的获取包名下断

函数  getPackageName() 获取相关的包名信息,判断参数里是否包含hook工具的包名

---



[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2020-6-17 11:43 被陌殇编辑 ,原因:
收藏
点赞4
打赏
分享
最新回复 (15)
雪    币: 319
活跃值: (239)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
Lucaks 1 2019-4-23 12:03
2
0
雪    币: 1867
活跃值: (3703)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
virjar 1 2019-4-23 13:24
3
0
DexFile dexFile = Class.getDex(); 这个接口获取的dex数据,是原始dex的数据么?还是被优化过之后的,能否用作完整性校验?
雪    币: 13465
活跃值: (4793)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tDasm 2019-4-23 15:43
4
0
java层都被hook了,怎么检测也白搭。
看利用so库检测是不是好些?
雪    币: 1867
活跃值: (3703)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
virjar 1 2019-4-23 17:03
5
1
在来两个
多开检测:

package com.tencent.StubShell;

public class CheckVirtual {
    private static final java.lang.String TAG = "CheckVirtual";

    private static java.lang.String exec(java.lang.String str) {
        java.io.BufferedOutputStream bufferedOutputStream;
        java.lang.Object obj;
        java.lang.Object obj2;
        java.io.BufferedInputStream obj3;
        java.lang.Throwable th;
        java.lang.Throwable th2;
        java.lang.String str2 = null;
        java.lang.Process exec;
        try {
            exec = java.lang.Runtime.getRuntime().exec("sh");
            try {
                bufferedOutputStream = new java.io.BufferedOutputStream(exec.getOutputStream());
            } catch (java.lang.Exception e) {
                obj3 = str2;
                obj2 = str2;
                if (bufferedOutputStream != null) {
                }
                if (obj3 != null) {
                }
                if (exec != null) {
                }
                return str2;
            } catch (java.lang.Throwable th3) {
                obj2 = str2;
                java.lang.String str3 = str2;
                th = th3;
                obj3 = str3;
                if (bufferedOutputStream != null) {
                }
                if (obj3 != null) {
                }
                if (exec != null) {
                }
                throw th;
            }
            try {
                obj3 = new java.io.BufferedInputStream(exec.getInputStream());
                try {
                    bufferedOutputStream.write(str.getBytes());
                    bufferedOutputStream.write(10);
                    bufferedOutputStream.flush();
                    bufferedOutputStream.close();
                    exec.waitFor();
                    str2 = getStrFromBufferInputSteam(obj3);
                    if (bufferedOutputStream != null) {
                        try {
                            bufferedOutputStream.close();
                        } catch (java.io.IOException e2) {
                            e2.printStackTrace();
                        }
                    }
                    if (obj3 != null) {
                        try {
                            obj3.close();
                        } catch (java.io.IOException e3) {
                            e3.printStackTrace();
                        }
                    }
                    if (exec != null) {
                        exec.destroy();
                    }
                } catch (java.lang.Exception e4) {
                } catch (Throwable th4) {
                    th = th4;
                    if (bufferedOutputStream != null) {
                    }
                    if (obj3 != null) {
                    }
                    if (exec != null) {
                    }
                    throw th;
                }
            } catch (java.lang.Exception e5) {
                obj3 = str2;
                if (bufferedOutputStream != null) {
                    try {
                        bufferedOutputStream.close();
                    } catch (java.io.IOException e22) {
                        e22.printStackTrace();
                    }
                }
                if (obj3 != null) {
                    try {
                        obj3.close();
                    } catch (java.io.IOException e32) {
                        e32.printStackTrace();
                    }
                }
                if (exec != null) {
                    exec.destroy();
                }
                return str2;
            } catch (java.lang.Throwable th32) {
                th2 = th32;
                obj3 = str2;
                th = th2;
                if (bufferedOutputStream != null) {
                    try {
                        bufferedOutputStream.close();
                    } catch (java.io.IOException e222) {
                        e222.printStackTrace();
                    }
                }
                if (obj3 != null) {
                    try {
                        obj3.close();
                    } catch (java.io.IOException e322) {
                        e322.printStackTrace();
                    }
                }
                if (exec != null) {
                    exec.destroy();
                }
                throw th;
            }
        } catch (java.lang.Exception e6) {
            exec = str2;
            obj3 = str2;
            bufferedOutputStream = str2;
        } catch (java.lang.Throwable th5) {
            obj3 = str2;
            bufferedOutputStream = str2;
            th2 = th5;
            exec = str2;
            th = th2;
            if (bufferedOutputStream != null) {
            }
            if (obj3 != null) {
            }
            if (exec != null) {
            }
            throw th;
        }
        return str2;
    }

    private static java.lang.String getStrFromBufferInputSteam(java.io.BufferedInputStream bufferedInputStream) {
        if (bufferedInputStream == null) {
            return "";
        }
        byte[] bArr = new byte[512];
        java.lang.StringBuilder stringBuilder = new java.lang.StringBuilder();
        int read;
        do {
            try {
                read = bufferedInputStream.read(bArr);
                if (read > 0) {
                    stringBuilder.append(new java.lang.String(bArr, 0, read));
                    continue;
                }
            } catch (java.lang.Exception e) {
                e.printStackTrace();
            }
        } while (read >= 512);
        return stringBuilder.toString();
    }

    public static java.lang.String getUidStrFormat() {
        java.lang.String exec = exec("cat /proc/self/cgroup");
        if (exec == null || exec.length() == 0) {
            return null;
        }
        int lastIndexOf = exec.lastIndexOf("uid");
        int lastIndexOf2 = exec.lastIndexOf("/pid");
        if (lastIndexOf < 0) {
            return null;
        }
        if (lastIndexOf2 <= 0) {
            lastIndexOf2 = exec.length();
        }
        try {
            if (!isNumber(exec.substring(lastIndexOf + 4, lastIndexOf2).replaceAll("\n", ""))) {
                return null;
            }
            return java.lang.String.format("u0_a%d", new java.lang.Object[]{java.lang.Integer.valueOf(java.lang.Integer.valueOf(exec.substring(lastIndexOf + 4, lastIndexOf2).replaceAll("\n", "")).intValue() - 10000)});
        } catch (java.lang.Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static boolean isNumber(java.lang.String str) {
        if (str == null || str.length() == 0) {
            return false;
        }
        for (int i = 0; i < str.length(); i++) {
            if (!java.lang.Character.isDigit(str.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    public static boolean isRunInVirtual() {
        java.lang.CharSequence uidStrFormat = getUidStrFormat();
        java.lang.String exec = exec("ps");
        if (exec == null || exec == "") {
            return false;
        }
        java.lang.String[] psLine = exec.split("\n");
        if (psLine == null || psLine.length <= 0) {
            return false;
        }
        int i = 0;
        for (int i2 = 0; i2 < psLine.length; i2++) {
            if (psLine[i2].contains(uidStrFormat)) {
                int lastIndexOf = psLine[i2].lastIndexOf(" ");
                java.lang.String substring = psLine[i2].substring(lastIndexOf <= 0 ? 0 : lastIndexOf + 1, psLine[i2].length());
                if (new java.io.File(java.lang.String.format("/data/data/%s", new java.lang.Object[]{substring, java.util.Locale.CHINA})).exists()) {
                    i++;
                }
            }
        }
        return i > 1;
    }
}

xposed检测
package com.tencent.StubShell;

public class XposedCheck {
    private static final java.lang.String XPOSED_BRIDGE = "de.robv.android.xposed.XposedBridge";
    private static final java.lang.String XPOSED_HELPERS = "de.robv.android.xposed.XposedHelpers";

    private static boolean isXposedExistByThrow() {
        try {
            throw new java.lang.Exception("exe xp");
        } catch (java.lang.Exception e) {
            for (java.lang.StackTraceElement className : e.getStackTrace()) {
                if (className.getClassName().contains(XPOSED_BRIDGE)) {
                    return true;
                }
            }
            return false;
        }
    }

    private static boolean isXposedExists() {
        try {
            java.lang.ClassLoader.getSystemClassLoader().loadClass(XPOSED_HELPERS).newInstance();
            try {
                java.lang.ClassLoader.getSystemClassLoader().loadClass(XPOSED_BRIDGE).newInstance();
                return true;
            } catch (java.lang.InstantiationException e) {
                e.printStackTrace();
                return true;
            } catch (java.lang.IllegalAccessException e2) {
                e2.printStackTrace();
                return true;
            } catch (java.lang.ClassNotFoundException e3) {
                e3.printStackTrace();
                return false;
            }
        } catch (java.lang.InstantiationException e4) {
            e4.printStackTrace();
            return true;
        } catch (java.lang.IllegalAccessException e22) {
            e22.printStackTrace();
            return true;
        } catch (java.lang.ClassNotFoundException e32) {
            e32.printStackTrace();
            return false;
        }
    }

    public static boolean tryShutdownXposed() {
        if (!isXposedExistByThrow()) {
            return true;
        }
        try {
            java.lang.reflect.Field declaredField = java.lang.ClassLoader.getSystemClassLoader().loadClass(XPOSED_BRIDGE).getDeclaredField("disableHooks");
            declaredField.setAccessible(true);
            declaredField.set(null, java.lang.Boolean.valueOf(true));
            return true;
        } catch (java.lang.NoSuchFieldException e) {
            e.printStackTrace();
            return false;
        } catch (java.lang.ClassNotFoundException e2) {
            e2.printStackTrace();
            return false;
        } catch (java.lang.IllegalAccessException e3) {
            e3.printStackTrace();
            return false;
        }
    }
}



雪    币: 1112
活跃值: (2649)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
Amun 2019-4-24 17:49
6
1
方向是对的,但方法还需要考量一下,至少不要在 JVM 中操作,涉及到的 I/O 操作也最好从系统调用走。
雪    币: 73
活跃值: (893)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hixhi 2019-4-25 02:25
7
0
hook来hook去也没啥意思
雪    币: 355
活跃值: (10)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
Victorgg 2019-4-26 14:34
8
0
辛苦楼主,赞一个!
雪    币: 38
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
cjkillyes 2019-6-14 00:50
9
0
辛苦楼主,总结的很好
雪    币: 574
活跃值: (257)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
miyuecao 2019-6-14 14:51
10
0
赞一个,长知识了
雪    币: 122
活跃值: (1405)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
koflfy 1 2019-6-14 18:05
11
0
mark
雪    币: 109
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_pslillqf 2019-6-19 15:32
12
0
需要加固可以私信哟,专业的虚机技术,基于LLVM编译器进行改造的IR指令。
雪    币: 13659
活跃值: (1200)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
严启真 2019-6-25 11:33
13
0
写得好,收藏一下好好学习
雪    币: 15
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
上官瑞杰 2019-12-10 18:59
14
0
make下,好好学习
雪    币: 1909
活跃值: (1653)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
LivedForward 2020-2-7 09:34
15
0
Amun 方向是对的,但方法还需要考量一下,至少不要在 JVM 中操作,涉及到的 I/O 操作也最好从系统调用走。
自己写.S吗?有没有什么具体思路可以分享一下的?
雪    币: 376
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
peparam 2020-7-16 17:21
16
0

由于Android 稍高版本在安装apk的过程中会把classes.dex 抠出来转化为odex或者vdex等其他格式的优化文件,原apk中无classes.dex,

...,故无法校验classes.dex,也无法校验整个APK文件哈希值


楼主,你说原apk中无classes.dex是什么意思啊?

为什么你第3点又说对dex文件做MD5校验呢?

Android从什么版本开始这样做的?


---------------------------

找到一个资料,主要是和ART 的编译选项有关

LOCAL_DEX_PREOPT 支持分别使用值“true”和“false”来启用和停用预先优化功能。此外,如果不想在预先优化过程中将 classes.dex 文件从 APK 或 JAR 文件中剥离出来,则可以指定“nostripping”。通常情况下,此文件会被剥离出来,因为在进行预先优化之后将不再需要该文件;但若要使第三方 APK 签名仍保持有效,则必须使用最后这个选项。



最后于 2020-7-17 18:58 被peparam编辑 ,原因:
游客
登录 | 注册 方可回帖
返回