目前
由于Java字节码的抽象级别较高,因此Android的APK较容易被反编译。如果一款应用APK被破解,那么可能会被他人植入广告或者病毒以供他人盈利或窃取用户信息;如果一款游戏APK被破解,那么这款游戏可能会从收费版变成免费版,游戏的支付系统也形同虚设。不管是哪种情况,对于开发者来说,APK被破解绝对是一场噩梦,于是就有了apk保护策略。本文章从安全的角度介绍几种常用的保护策略及如何绕过这些,
一、APK保护策略-代码混淆
此类apk混淆方式主要是用短名称混淆其余的类、字段和方法。
1、分析APK中代码混淆保护策略
下载测试app最版本apk安装包
下载完成后,使用jadx进行反编译
可以看到很多abcd,点开里面还是abcd进行命名包名!
这就是最常见的java代码混淆,会把类名,方法名,字段等进行混淆。
写代码是不可能有这种命名方式,用androidKiller去查看下
很明显那么多方法多是后期被处理过的,他这种混淆或者重命名,系统函数是没有被处理的,混淆知识单纯的停留在表面,主要逻辑没变
那么在逆向时看着也是不舒服的,可以借助jadx操作
会对我们的代码进行再次混淆
可以看到再次混淆后,前面多了字符串,这时候对于逆向的我们来说方便寻找了,长字符串相比但字符串肯定方便很多,有那么一丢丢好处
2、常用的混淆开发工具
proguard是一个能够对Java 代码进行压缩,优化,混淆,预检的工具。 proguard会检测和移除封装应用中未使用的类、字段、方法和属性。ProGuard还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。
二、APK保护策略-资源文件混淆
资源混淆是可以解决apk瘦身,主要就是压缩了资源文件及修改了文件名字及映射关系.此类混淆方法是使用android Killer等软件反编译apk,回编译时报错。
1、分析APK中资源文件混淆保护策略
android Killer反编译apk
这时候反编译完成后,直接回编译
结果报错
代码也没修改,直接回编译,也是报错的,这是为什么呢,这时候如果解决呢?
这里用到MT管理器
安装到雷电模拟器上,MT管理器可以对文件进行单独操作,我们安装在模拟器上的apk都在/data/app目录下
找到我们需要的对用的apk目录下
我们看到了apk安装包,双击该包
此时的签名为V1,点击查看
assets目录
这里面一般放的是资源,这里面的资源通常是没有编译过的,可以直接用,有图片、js、html等。
lib目录
目录放的一般是so文件,也就是本地代码,好几个文件夹一般...MIPS、x86、x64等,CPU的一些平台。
META-INF 目录
我们每次打包APK后都需要做一个签名,在系统里面是需要做验证的,不管是代码验证还是在安卓系统里面都需要签名验证,假设如果把APK文件改了,在放回去的话本身的签名和修改后放回去签名是不对应的,这时候怎么解决呢?
需要破解器破解系统核心,就算不重签名也可以放上去;或者安装在雷电等模拟器上面进行操作,模拟器支持不重签名也可以操作。
res目录
放的资源,程序的图标、样式、布局、XML等,编译之后的文件,直接查看是乱码,需要反编译的!APKtool即可反编译查看。
AndroidManifest.xml
清单文件,直接查看大部分是乱码,清单:APK需要使用系统的权限、包名是什么、APK是否支持调试等等内容。
resources.arsc
编译之后的文件,语言包、程序内容等。
classes.dex
dex文件,运行在Dalivk虚拟机上的文件,是smali代码也就是源代码,需要反编译转换为smali代码,还可以把smali代码转换为java代码或者直接des转换为jar包也可以。
大型的apk里面会有好几个classes,本身是可以合成为一个的,为什么分开放,是因为文件大小是有限制的。
接下来双击classes.dex
使用Dex编辑器打开,进行反编译
随便打开一个类
然后进入后,随意修改文本信息,加入个空格测试,并退出
退出Dex编辑器,保存
勾选自动签名,保存更新文件
我们看到classes.dex变绿了
后退一步APK也变绿了
这时候点击apk,我们看到签名状态变了,点击安装
安装完成
三、APK保护策略-签名验证
1、分析APK中签名验证保护策略
开始详细讲解签名校验!先来看看签名验证的原理
我们首先分析一个网上的源码
LoginActivity.java
首先来看一看LoginActivity.java,我们看到LoginActivity继承了BaseActivity
继续看看,这里发现了几个变量和重写了onCreate方法
BaseActivity.java
看一下BaseActivity.java下面,BaseActivity继承了Activity主类
看一看getSignature()方法,此方法是获取签名信息,
往下还有个hashcode然后获取他的hash值,sig给他一个返回
会跟原来的sig值1774763026进行对比,如果不相等执行Collector.finishAll();如果等于提示程序正常运行。
PackageManager和getPackageManager是包管理器和获取包管理器的意思
getPackageInfo :获取包信息给pi
Collector.java
大框中都是在获取签名信息,当获取完签名信息后回进行判断,说这个签名信息是不是自己的,equals判断的是hashcode两个对象的hash
2、案例分析
使用Android Killer对apk重新签名
在雷电模拟器安装后,无法运行,因为存在签名验证!这里能分析到签名校验执行较早,在apk刚刚启动时就进行判断。
android killer 反编译
点击入口,进入MainActivity.smali文件,找到onCreate方法
然后点击咖啡杯,看java代码分析
首先MainActivity继承Activity这个界面,就时一些弹窗界面等信息,继续往下看有那些重点
找到onCreate、onClick,下面有个run方法,调用了一个bug方法,来查看下this.bug这个方法(点击进入)
目前找到这个bug就是签名AK反编译入口点smali里面的bug,他是对native修饰的方法,根据前面学习ndk开发的时候就知道,如果一个方法被native修饰就说明实现的逻辑在so里面
这儿就是最简单的java层分析到so层的一个逻辑思路!那么接下来用到IDA分析下apk中的so文件
查看so库文件第一步就是看有没有jni动态库,如果没有就查一下java_
双击进入后,抓住我们的目的,如果一个程序有签名校验,那么去弄清楚他的签名校验的逻辑所在处
这里我们看到getSigHashCode关键函数,就说明在这指令进行着hash校验,关键函数,使用TAB键切换为伪c代码
双击getSignHashCode方法
从上往下看
getPackageManager:获取包名
getPackageInfo:获取包信息
signatures:签名
还有一个getPackageName,他和第一个、第二个即使签名的jni方法,需要出现着三个名称,就是和签名有关
找到了地方接下来就要进行修改参数,先隐藏类型,Hide casts
如何修改参数了,按Y键
静态注册的第一个函数时JNIEnv *
这时候jni接口中的函数就被识别出来了
修改第二个参数时jobject obj
修改完参数之后,这些jni接口函数都被识别出来了
我们看最下边,这里有个result返回值,如果result不等于字符串(随机的),就执行exit(0)
那么这个字符串哪里获取的呢
前面的V21哈希,把V21和V19给传入V2中,V2会有一个结果,然后比较,如果不等于直接exit退出,不然就返回
result = ((*v2)->CallIntMethod)(v2, v19, v21);
分析到这里就知道整改的思路了,开始在killer反编译找到apk入口点进去,分析onCreate,分析java层代码找到native后掉调用了bug方法,然后知道逻辑在so层,并在so层分析逻辑代码,现在知道了逻辑后,我们就根据前几课的知识,直接将条件修改为nez条件就可
拓展知识:
如果在IDA修改了SO库文件后,如何保存呢
四、APK检测模拟器运行原理分析
例如有些apk应用有活动,邀请好友就送东西,这时候肯定是模拟器非常好用的,但是APK就会有模拟器检测,发现如果是迷你其就无法运行,怎么通过逆向找到逻辑点,过掉呢?
首先要知道APK如何检测是否是模拟器、蓝牙、wifi、电量、CPU、配置等,模拟器wifi是无的,蓝牙也是无,电量一直是百分百
1、检测通道
"/dev/socket/qemud","/dev/qemu_pipe"
首先回定义string[]字符串数组来检测两个通道,进行一个for循环,如果有就回返回true,就是不行
2、检测驱动文件内容
读取/proc/tty/drivers文件内容,然后检测已知的QEmu的驱动程序的列表
代码先new了一个file,它会判断这个文件(/proc/tty/drivers)存不存在,如果存在并且可以写(cnaRend)就执行下面代码:
上面这些是java中的IO流
3、检测模拟器上的特有的几个文件
会检测三个文件
1、/system/lib/libc_malloc_debug_qemu.so
2、/sys/qemu_trace
3、/system/bin/qemu-props
这三个文件时模拟器上特有的文件
4、检测模拟器默认的电话号码
会监测默认的电话号码,这里都是155开头的号码,当然在实际情况下,会通过正则之类的比较多。
5、监测设备IDS序列号
监测IDS是不是这串数值即可,如果是就被认为时模拟器
6、检测imsi号
检测imsi是不是这串数值,如果是就被认为时模拟器
7、检测手机上的一些硬件信息
检测蓝牙等硬件信息,如果没有就被认为时模拟器
8、检测手机运营商家
检测设备,是android就认为是模拟器
9、基于模拟器cpu信息检测
1、基于模拟器cpu信息的检测,cpu信息检测主要是在cpu信息看看是否包含intel、amd等字段,很多模拟器目前对于cpu信息还无法进行模拟。
2、模拟器的cpu和真机的cpu有所不同 一般而言模拟器读取的是电脑上的cpu,电脑是Inter或者AMD,那么模拟器的cpu也是Inter或者AMD,而手机一般是ARM。
3、要读取cpu信息,可以用java代码或者c代码,用c代码就要使用jni。
10、关键路径检测特定模拟器检测
对上面特定的目录或者文件进行检测,如果存在就是模拟器
五、实战详解-绕过无法在模拟器运行APK
拿一个APK在雷电模拟器上测试一下
会出现这几种情况,都是在模拟器上运行使用导致无法登录
还没进入程序的界面,在程序的入口点就进行检测的
使用androidkiller对APK进行反编译分析
反编译完成后,进来第一个入口点就是检测创建蓝牙连接,而模拟器是没有蓝牙的
可以删除相应的权限内容,这样是可以防止检测的,但是这样比较暴力,会导致程序崩溃现象
还有进行对一些隐私进行检测
工程搜索:应用在模拟器上运行
没有查到东西,转换成unicode
可以看到转换unicode后查询到了
我们来分析smali代码,他把字符串给了V1寄存器
那么V0是p0的上下文信息context,v0、v1、v5对应上下文,字符串、0x0。再往后看Toast指向makeText,后面是一个上下文、字符串、int(string.int),前面我用过Toast函数,这里的意识就是调用了Toast弹窗和停留时间。
观察下,都在一个代码smali里面,第一个是212行、第二个是321行
并且都在run方法中
在第二条分析查看到if-nez v0,:cond_6, v0是bluetoolthAddress(蓝牙地址)静态方法返回值,回去判断不等于就跳到cond_6,如果等于就说“应用在模拟器上运行”
跳到cond_6直接就return-void!这逻辑就很简单分析到了!继续分析入口界面
提示文件已丢失,丢失怎么办呢?去找到配置清单
入口点在com.vdog.VDogApplication
找到入口点后查看方法,发现有onCreate进入分析
public onCreate()V 其中的V,意识是viod,所以返回值为空
.locals 2表示有两个局部变量寄存器
.prologue表示从此开始执行程序
invoke-super表示调用了一个父类的方法onCreate
iget-object v0,p0 表示get是读,他会把p0的applicationName对象放到v0里面
if-eqz v0,:cond_0 表示对V0进行一个判断,如果等于1就跳过cond_0,,如果等于0就接着执行,什么时候会等于0?就是字段applicationName什么都没有获取就等于0。如果获取到了条件就不成立的。不成立就执行:
不成立就执行下面的逻辑
iget-object v0,p0 .... 表示获取一个对象,就是p0的mApplication的对象放在v0中
iget-object v1,p0 ... 表示获取一个对象,就是p0指向的mApplication的对象放在v0中
invoke-virtual {v1} ... 表示把v1作为一个对象调用getBaseContext方法,在java层的写法mApplication.getBaseContext(); mApplication就是V1
move-result-object v1 表示把结果放在v1中
invoke-static{v0,v1} 表示之前v1获取的getBaseContext方法作为参数通过实例方法(静态方法)传入v0,有两个方法Application和Context
刚好把这两个参数attach传入,javac层代码Application.getBaseContext()
attach包含进去后:attach(mApplication.Application.getBaseContext())第一个参数+第二个参数,把参数给到attach传进去
iget-obect v0,p0 表示同样的对象p0给到v0
invoke-virtual {v0} 表示把v0作为一个对象调用getBaseContext方法,上面是v1 getBaseContext,这里v0 getBaseContext
move-result-object v0 把结果放入v0里面,java层:v0=v0.getBaseContext();
iget-object v1,p0 : 表示获取一个对象,就是p0的mApplication方法的对象放入v1中
invoke-static {v0,v1} 表示把v0和v1作为一个参数给initInnerSdk,括号后面两个类型刚好是v0和v1,还原java层代码:initInnerSdk(v0,v1);
invoke-virtual {v0} ; v0是什么意思呢?
此时v0是一个对象调用了onCreate,java层代码:v0.onCreate();,v0又是getBaseContext,所以getBaseContext.onCreate();
分析完smlie,也重新复现了一边smlie代码后就,发现主要逻辑什么都没有,没有任何反调试逻辑的一个代码。回到前面搜索的第一条,点击咖啡杯,分析java层代码
首先InitAntiEmul$1实现了一个Runnable,接下来两个参数和run()方法,如果InitAntiEmul.access$2() > 8000执行InitAntiEmul.access$3(1);
if条件后检测一个模拟器,当检测到模拟器后,弹出会提示“应用在模拟器上运行”,执行Process.killProcess(Process.myPid());,进行一个程序杀死然后结束进程,那么有回到了改smali代码前面写出的知识,先找到哪里调用了run方法找到包名,可以注释修改源代码。
六、总结
本章重点详解解读了常见的APK保护策略(代码混淆、资源文件混淆、签名验证等)并通过一个案例来进行实战绕过问题。之前的篇幅中讲解了部分APK无法在模拟器上运行只能在手机上运行的原因及原理,并通过实战进行对绕过APK无法在模拟器进行测试,如果你想学习APK保护策略的知识可以进行参考,如有不对之处,欢迎大家指正,谢谢。