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

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

2019-4-23 12:02
17941

一、静态完整性校验

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值保存在资源文件或其他自定义文件中相关代码:
    1
    2
    3
    4
    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文件加载 运行的位置(本人暂时还没碰到)
    相关代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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以下)

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

相关代码:

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

相关代码:

1
2
3
4
5
6
7
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路径,也可以绕过校验。

1
2
3
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以下):
    1
    2
    3
    android.content.pm.PackageInfo.getPacketInfo(ClassName,flags).signatures
    这里需要注意 当flags为64的时候,该函数会获取签名信息
    所以需要在getpackageinfo下断点,当flags为64的时候,就是获取签名信息
  2. V2签名(Android 7-9):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 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签名的系统验证函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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)检测关键字

1
2
3
4
de.robv.android.xposed.XposedHelpers类的
静态fieldCache字段        保存被hook的字段信息
静态methodCache字段       保存被hook的方法信息
静态constructorCache字段  保存被hook的类信息

(2)检测内存

1
2
3
检测内存映射列表中是否包含如下文件:
XposedBridge.so
XposedBridge.jar

(3)检测方法的调用栈

1
2
3
4
5
6
7
8
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)检测标记位

1
2
3
de.robv.android.xposed.XposedBridge类
静态disableHooks   成员变量
这个字段表示是否对当前应用进行hook操作

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2020-6-17 11:43 被陌殇编辑 ,原因:
收藏
免费 4
支持
分享
赞赏记录
参与人
雪币
留言
时间
PLEBFE
为你点赞~
2023-1-26 04:57
wx_凡若尘曦
为你点赞~
2020-12-28 17:39
Victorgg
为你点赞~
2019-4-26 14:34
世人谓我
为你点赞~
2019-4-24 10:12
最新回复 (15)
雪    币: 324
活跃值: (419)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
2
2019-4-23 12:03
0
雪    币: 1867
活跃值: (4113)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
3
DexFile dexFile = Class.getDex(); 这个接口获取的dex数据,是原始dex的数据么?还是被优化过之后的,能否用作完整性校验?
2019-4-23 13:24
0
雪    币: 15407
活跃值: (6673)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
java层都被hook了,怎么检测也白搭。
看利用so库检测是不是好些?
2019-4-23 15:43
0
雪    币: 1867
活跃值: (4113)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
5
在来两个
多开检测:

1
2
3
4
5
6
7
8
9
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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检测
1
2
3
4
5
6
7
8
9
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
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;
        }
    }
}



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

由于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编辑 ,原因:
2020-7-16 17:21
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册