android应用加固1

非虫的文章:http://www.mottoin.com/89035.html

Android Java混淆(ProGuard)

该开源项目用来混淆java字节码,现已集成在sdk中:SDK\tools\proguard。
使用:build.gradle中配置:

1
2
3
4
5
buildTypes {
release {
minifyEnabled true //开启混淆
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'//指定混淆配置文件为 proguard-rules.pro
}

该混淆配置文件位于build同目录下。

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
#下面是常见的proguard.cfg配置项
#指定代码的压缩级别
-optimizationpasses 5
#包名不混合大小写
-dontusemixedcaseclassnames
#不去忽略非公共的库类
-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共的库的类的成员
-dontskipnonpubliclibraryclassmembers
#优化 不优化输入的类文件
-dontoptimize
#预校验
-dontpreverify
#混淆时是否记录日志
-verbose
# 混淆时所采用的算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
#保护注解
-keepattributes *Annotation*
#忽略警告
-ignorewarning
##记录生成的日志数据,gradle build时在本项目根目录输出##
#apk 包内所有 class 的内部结构
-dump class_files.txt
#未混淆的类和成员-printseeds seeds.txt
#列出从 apk 中删除的代码
-printusage unused.txt
#混淆前后的映射-printmapping mapping.txt
########记录生成的日志数据,gradle build时 在本项目根目录输出-end#####
#需要保留的东西
# 保持哪些类不被混淆
-keep public class * extends android.app.Fragment
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.support.v4.**
-keep public class com.android.vending.licensing.ILicensingService
#如果有引用v4包可以添加下面这行
-keep public class * extends android.support.v4.app.Fragment
##########JS接口类不混淆,否则执行不了
-dontwarn com.android.JsInterface.**
-keep class com.android.JsInterface.** {*; }
#极光推送和百度lbs android sdk一起使用proguard 混淆的问题#http的类被混淆后,导致apk定位失败,保持apache 的http类不被混淆就好了
-dontwarn org.apache.**
-keep class org.apache.**{ *; }
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
public void set*(...);
}
#保持 native 方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#保持自定义控件类不被混淆
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
#保持自定义控件类不被混淆
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
#保持 Parcelable 不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#保持 Serializable 不被混淆
-keepnames class * implements java.io.Serializable
#保持 Serializable 不被混淆并且enum 类也不被混淆
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
#保持枚举 enum 类不被混淆 如果混淆报错,建议直接使用上面的 -keepclassmembers class * implements java.io.Serializable即可
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * {
public void *ButtonClicked(android.view.View);
}
#不混淆资源类
-keepclassmembers class **.R$* {
public static <fields>;
}
#避免混淆泛型 如果混淆报错建议关掉
#–keepattributes Signature
######混淆保护自己项目的部分代码以及引用的第三方jar包library########
#如果引用了v4或者v7包
-dontwarn android.support.**
#如果用到Gson解析包的,直接添加下面这几行就能成功混淆,不然会报错
#gson
#-libraryjars libs/gson-2.2.2.jar
-keepattributes Signature
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { *; }
#客户端代码中的JavaBean(实体类)的类名与其字段名称全部变成了a、b、c、d等等字符串,这与服务端返回的json字符串中的不一致,导致解析失败。所以,解决的办法是:在进行混淆编译进行打包apk的时候,过滤掉存放所有JavaBean(实体类)的包不进行混淆编译
-keep class com.android.model.** {*;}
####混淆保护自己项目的部分代码以及引用的第三方jar包library-end####

签名验证

在java&native代码中加入验证签名方式。检测是否是自己的签名。

动态加载dex

如何动态加载dex?
native层的代码拥有更高权限,可以看根目录等。

Android 系统提供了 DexClassLoader 来支持在程序运行过程中动态加载包含 classes.dex的.jar 或者.apk 文件。DexClassLoader 实例可以继续加载dex中的类,后可通过反射调用,调用的代码中需要的类会自动加载执行?

检测frida

https://bbs.pediy.com/thread-217482.htm?source=1
frida开源!必看相关hook模块。

反调试

  1. IDA调试端口检测:读取/proc/net/tcp,查找IDA远程调试所用的23946端口,若发现说明进程正在被IDA调试。
  2. 调试器进程名检测,遍历进程,查找固定的进程名,找到说明调试器在运行。
  3. 父进程名检测 有的时候不使用apk附加调试的方法进行逆向,而是写一个.out可执行文件直接加载so进行 调试,这样程序的父进程名和正常启动apk的父进程名是不一样的。
    测试发现:
    (1)正常启动的apk程序:父进程是zygote
    (2)调试启动的apk程序:在AS中用LLDB调试发现父进程还是zygote
    (3)附加调试的apk程序:父进程是zygote
    (4)vs远程调试 用可执行文件加载so:父进程名为gdbserver
    结论:父进程名非zygote的,判定为调试状态
  4. 自身进程名检测 :也是解决上面问题的。
  5. apk线程检测 :同样.out加载so来脱壳的场景正常apk进程一般会有十几个线程在运行(比如会有jdwp线程),自己写可执行文件加载so一般只有一个线程,可以根据这个差异来进行调试环境检测。
  6. apk进程fd文件检测:根据/proc/pid/fd/路径下文件的个数差异,判断进程状态。 (apk启动的进程和非apk启动的进程fd数量不一样) (apk的debug启动和正常启动,进程fd数量也不一样)
  7. 安卓系统自带调试检测函数:
    java层:android.os.Debug.isDebuggerConnected();
    so层: libdvm.so中的dvmDbgIsDebuggerConnected()函数,调用他就能得知程序是否被调试。dlopen(/system/lib/libdvm.so) dlsym(_Z25dvmDbgIsDebuggerConnectedv)这个函数可能不让用了。
    art:art模式下,结果存放在libart.so中的全局变量gDebuggerActive中, 符号名为_ZN3art3Dbg15gDebuggerActiveE
  8. ptrace检测:每个进程同时刻只能被1个调试进程ptrace,再次p自己会失败。1 主动ptrace自己,根据返回值判断自己是否被调试了。2 或者多进程ptrace。
  9. 函数hash值检测:so文件中函数的指令是固定,但是如果被下了软件断点,指令就会发生改变(断点地址被改 写为bkpt断点指令),可以计算内存中一段指令的hash值进行校验,检测函数是否被修改或 被下断点。
  10. 断点指令检测:上面说了,如果函数被下软件断点,则断点地址会被改写为bkpt指令,可以在函数体中搜索bkpt指令来检测软件断点。
  11. 系统源码修改检测:安卓native下流行的反调试方案是读取进程的status或stat来检测tracepid,原理是调试状态下的进程tracepid不为0。对于这种调试检测手段,彻底的绕过方式是修改系统源码后重新编译,让tracepid永远为 0。对抗这种bypass手段,我们可以创建一个子进程,让子进程主动ptrace自身设为调试状态, 此时正常情况下,子进程的tracepid应该不为0。此时我们检测子进程的tracepid是否为0, 如果为0说明源码被修改了。
  12. 单步调试陷阱:
    原理:调试器从下断点到执行断点的过程分析:
    1 保存:保存目标处指令
    2 替换:目标处指令替换为断点指令
    3 命中断点:命中断点指令(引发中断 或者说发出信号)
    4 收到信号:调试器收到信号后,执行调试器注册的信号处理函数。
    5 恢复:调试器处理函数恢复保存的指令
    6 回退:回退PC寄存器
    7 控制权回归程序.
    主动设置断点指令/注册信号处理函数的反调试方案:
    1 在函数中写入断点指令
    2 在代码中注册断点信号处理函数
    3 程序执行到断点指令,发出信号 分两种情况:
    (1)非调试状态 进入自己注册的函数,NOP指令替换断点指令,回退PC后正常指令。 (执行断点发出信号—进入处理信号函数—NOP替换断点—退回PC)
    (2)调试状态 进入调试器的断点处理流程,他会恢复目标处指令失败,然后回退PC,进入死循环。
  13. 利用IDA先截获信号特性的检测
    原理: IDA会首先截获信号,导致进程无法接收到信号,导致不会执行信号处理函数。将关键流程 放在信号处理函数中,如果没有执行,就是被调试状态。
  14. 利用IDA解析缺陷反调试
    原理: IDA采用递归下降算法来反汇编指令,而该算法大的缺点在于它无法处理间接代码路径, 无法识别动态算出来的跳转。而arm架构下由于存在arm和thumb指令集,就涉及到指令集 切换,IDA在某些情况下无法智能识别arm和thumb指令,进一步导致无法进行伪代码还原。
    在IDA动态调试时,仍然存在该问题,若在指令识别错误的地点写入断点,有可能使得调试 器崩溃。( 可能写断点 ,不知道写ARM还是THUMB ,造成的崩溃)
  15. 五种代码执行时间检测
    原理: 一段代码,在a处获取一下时间,运行一段后,再在b处获取下时间, 然后通过(b时间­a时间)求时间差,正常情况下这个时间差会非常小, 如果这个时间差比较大,说明正在被单步调试。
    做法: 五个能获取时间的api:
    time()函数 time_t结构体
    clock()函数 clock_t结构体
    gettimeofday()函数 timeval结构 timezone结构
    clock_gettime()函数 timespec结构
    getrusage()函数 rusage结构
  16. 三种进程信息结构检测
    原理: 一些进程文件中存储了进程信息,可以读取这些信息得知是否为调试状态。
    做法:
    第一种: /proc/pid/status /proc/pid/task/pid/status TracerPid非0 statue字段中写入t(tracing stop)
    第二种: /proc/pid/stat /proc/pid/task/pid/stat 第二个字段是t(T)
    第三种: /proc/pid/wchan /proc/pid/task/pid/wchan ptrace_stop
  17. Inotify事件监控dump
    原理: 通常壳会在程序运行前完成对text的解密,所以脱壳可以通过dd与gdb_gcore来dump /proc/pid/mem或/proc/pid/pagemap,获取到解密后的代码内容。
    可以通过Inotify系列api来监控mem或pagemap的打开或访问事件, 一旦发生时间就结束进程来阻止dump。

elf

  1. ELF常见HOOK方案及应用
    a) Inline hook
    b) GOT hook
    c) PRELOAD hook
    d) Linker重定位hook

  2. ELF若干种保护方案
    a) UPX壳及分析
    b) Shellcode保护方案
    c) 链接器及加载器
    d) VMP

  3. ELF混淆方案
    a) 花指令
    b) 指令乱序,使用B衔接
    c) 指令替换,替换为B指令
    d) 指令索引并且乱序,使用索引表来跳转
    e) LLVM方式混淆

  4. ELF反调试
    a) 捕捉信号
    b) 检测tracerPID
    c) 检测调试进程
    d) 处理ELF格式,阻止IDA加载
    e) 多进程守护
    f) CRC校验
    g) 调试中断指令检测(类似x86 0xCC)

  5. ELF函数加密
    a) ELF入口加解密
    b) 函数动态加解密,指令级加解密
    c) 基于加载器的函数加解密

https://bbs.pediy.com/thread-183116.htm中的:

Dex 中方法的隐藏

一种:
class段中将 DexMethod 的 methodIdx 值设为 0x0,相当于将原先的方法指向了前一个方法,因为这个是依据上一个方法架构体中Idx的偏移。
访问标志符 accessFlags 一般不需要修改,在Dex文件格式里,directMethods和 virtualMethods 是分开的。
将codeOffset 设置为前一个方法的代码偏移地址。
更新需隐藏方法的下一个方法的methodIdx,可以使用公式next_method_idx=original_next_method_idx+original_method_idx
重新计算 Dex 的 SHA1哈希值和Adler校验值,并用以更新 DexHeader,可以使用DexFixer修复 classes.dex文件
重新打包生成 APK 文件:

二种:
class段中,将相应的 directMethodsSize 或virtualMethodsSize 减 1,同时将表示该需要隐藏方法的 DexMethod 结构体中的数据全部修改为 0,这样就可以将该方法隐藏起来

读取隐藏方法:
取数据流,用反射调用 android.content.res.AssetManager.openNonAsset 方法打开当前应用程序的classes.dex文件;或者Context.getPackageCodePath()来获得当前应用程序对应的apk文件的路径,利用此路径构造ZipFile对 象 ,进而获取classes.dex的ZipEntry ,利用ZipFile的getInputStream(ZipEntry)方法获取classes.dex 的数据流。
修复 Dex 文件,将之前隐藏方法的 DexMethod 结构体恢复
将修复后的 Dex 数据使用类加载器重新加载;
搜索调用。

Dex 完整性校验

首先在代码中完成校验值比对的逻辑,此部分代码后续不能再改变,否则 CRC 值
会发生变化;
从生成的 APK 文件中提取出 classes.dex 文件,计算其 CRC 值,其他 hash 值类似;
将计算出的值放入 strings.xml 文件中。

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
1. String apkPath = this.getPackageCodePath();
2. Long dexCrc = Long.parseLong(this.getString(R.string.dex_crc));
3. try {
4. ZipFile zipfile = new ZipFile(apkPath);
5. ZipEntry dexentry = zipfile.getEntry("classes.dex");
6. if(dexentry.getCrc() != dexCrc){
7. System.out.println("Dex has been *modified!");
8. }else{
9. System.out.println("Dex hasn't been modified!");
10. }
11. } catch (IOException e) {
12. // TODO Auto-generated catch block
13. e.printStackTrace();
14. }

APK 完整性校验类似,不过数据放在服务器,app只是请求和处理判断。

Java 反射

Java 反射机制允许运行中的 Java 程序对自身进行检查,并能直接操作程序的内部属性或方法,可动态生成类实例、变更属性内容以及调用方法。
增加静态分析难度

字串混淆

dex中的,string中的。

代码乱序

通过goto切割代码。

APK 伪加密

根据操作系统安装apk时与普通zip文件的不同来修改apk文件。比如General purpose bit flags 第 0 位置 1

Manifest Cheating

这个方法挺好的。360中也遇见了
生成的 R.java文件中包含了相应的资源类型、名称以及对应的 id 值。资源 id 是 32bit 的整型值,格式为:0xPPTTNNNN。其中 PP 表示使用该资源的包,TT 代表该资源的类型,而 NNNN 是该类型中资源的名称。对于应用程序资源,PP 值固定为 7f,而对于被引用的系统资源包,其 PP值为 01。

Manifest Cheating 的基本原理是,在 AndroidManifest 的节点中插入一个未知id(如 0x0),名称为 name 的属性,其值可以是一个从未定义实现的 Java 类文件名。而对AndroidManifest 的修改需要在二进制格式下进行,这样才能不会破坏之前 aapt 对资源文件的处理。由于是未知的资源 id,在应用程序运行过程中,Android 会忽略此属性。但是在使用apktool 进行重打包时,首先会将 AndroidManifest.xml 转换为明文,进而会包含名称为 name的属性,而相应的 id 信息会丢失,apktool 重打包会重新进行资源打包处理,由于该 name属性值是一个未实现的 Java 类,重打包后的应用程序在运行过程中,由于 application 节点中定义的类是先于所有其他组件运行的,若系统找不到对应的类,会出现运行时错误,Dalvik虚拟机会直接关闭。

黄皮书中的第8章

有具体的代码

  1. 代码混淆
  2. 资源混淆
  3. 签名保护
  4. 手动注册native
  5. 反调试检测