撰写了好长时间,终于写完这篇帖子了,这主要是为了解决当下APP中存在的一些常见调试检测策略,前面系列的文章介绍了很多Android APP中漏洞挖掘的手段和原理,但是面对当下防护手段如此复杂的APP,不掌握一些基本的逆向技巧,我们就更别谈进行APP漏洞挖掘了,本文将开始总结了当下APP的一些安全防护手段和技巧,帮助大家更加高效的进行漏洞挖掘。
本文通过收集了大量的资料,参考了看雪上众多大佬的帖子,肉丝大佬的知识星球等,本文的知识结构为:
本文第二节主要将检测防护手段的原理
本文第三节主要介绍当下APP中的动静态防护策略
本文第四节主要讲当下其他常见的反调试策略绕过方式
本文第五节将反调试技巧与案例结合,并列举了APP漏洞挖掘实例
模拟器是当时比较流行的工具,可以帮助工作人员更加便捷的进行调试工作。而随着APP安全防护技术的进一步发展,模拟器检测技术不断进行完善,使得很多APP不能在模拟器上运行,下面本文收集了当下模拟器检测技术的情况,并在后面拿案例进行讲解
模拟器检测可以参考:
看雪sossai大佬的文章:Android模拟器检测体系梳理
看雪大佬Vancir大佬的文章:检测Android虚拟机的方法和代码实现
后面我们将拿一个具体案例来看看android模拟器检测如何具体实现
Android上一般使用GDA+jadx-gui+AndroidKiller
对APP java层进行静态分析,一般使用ida
对so层进行静态分析
java层静态分析:
我们拿到一个APP,一般先使用GDA查看是够有加壳,如果有加壳,我们则需要对其进行脱壳
针对于加壳的类型一般分为dex加固和so层加固,我们大多时候只需要解决dex加壳问题,就可以满足我们的一般需求了
dex加壳一般分为三类:dex整体加壳、函数抽取、dex2c/VMP
dex脱壳解决方案:
我们脱壳后,就进入静态分析的流程
首先我们需要找到函数的入口点函数:
我们在AndroidManifest里面找打Main的Activity,或通过AndroidKiller直接找到
然后我们进入对应的入口函数,结合Activity的生命周期,执行流程一步步的分析源码
我们进行静态分析时,可以进行字符串定位来快速定位到我们需要定位的代码段
使用GDA+jadx-gui
可以很好的查看静态代码段,使用AndroidKiller
可以对代码进行修改,然后重新打包签名,当然后面需要面临的就是进一步的绕过签名机制
so层静态分析:
分析java层代码时,会可能碰到native函数,这样我们的分析就自然从java层过渡到了so层
首先我们确定jni函数对应的so文件
静态注册和动态注册的区别:
动态注册加载so文件的两种形式:
所以我们可以直接搜JNI_Onload
来判断是否为动态注册
动态注册:
静态注册:
然后我们可以进一步进行分析so层中的代码段,下面是一些ida的快捷指令:
Android中动态分析,java层我们一般使用Android Studio
和Jeb
两类工具进行动态调试,so层我们会使用IDA
和GDB
来完成动态调试,后面我们会拿一些实质的案例来进行一步步的操作
首先使用AndroidKiller对apk进行反编译,查看其反编译后的工程
把project文件导入Android stdio
问题:导入报错,显示没有setting.zip,可能是Android stdio版本的问题,这里使用的是4.0版本
安装插件smalidea:
配置Android stdio
1)给smail代码一个root权限
2)配置项目的jdk, 已经配置后可以不用
3)配置远程调试
添加远程调试remote
4)建立连接
查找到当前的进程PID
开始转发端口
或者将程序挂起:
点击调试:
5)开始调试
下断点
在虚拟机中运行APP,触发断点,既可以进行调试
GDB动态调试推荐使用可视化工具HyperPwn
,GDB的命令操作手册如下:
GDB命令操作手册
首先,将gdbserver进行启动
先使用objection来观察要调试的so文件
例如,我们要调试libcamera_client.so
我们此时查看目标进程的状态:
此时我们就可以使用gdb去调试端口
我们再打开hyper
然后我们设置
然后我们远程连接
ip是我们手机的ip地址,端口号是我们gdbserver转发的端口号,这里我们要注意我们的gdbserver不能关闭
这样就进入了我们的调试界面
root检测逐渐成为现在的APP防护的一种方式,而我们要进行hook等更多操作,必须要获得root权限
root检测目前一般分为下面的一些方法:
编译Android源码系统时,如果我们编译生成的是测试版,是自动拥有root权限的,这时我们将su修改名字就可以绕过root检测了
这返回结果“test-keys”,代表此系统是测试版
返回结果“release-keys”,代表此系统是正式版
设备被root,很有可能被安装busybox
执行busybox:
Android系统中私有目录必须要获取root权限才能进行访问,例如 /data、/system、/etc 等,可以通过读写测试来判断:
我们可以看出这是测试版,很大可能会具有root权限
一般市面上的模拟器都带有root权限,比如我们可以使用上文的模拟器检测来进一步检测root,比如夜神模拟器nox
等
无论Xposed还是frida都需要root,我们也可以通过检测hook框架来判断是否进行root
Xposed是一个动态插桩的hook框架,通过替换app_process原始进程,将java函数注册为native函数,从而获得更早的运行时机,Xposed检测详细可以参考我之前的文章Xposed定制
图中的特征修改点就是我们可以进行检测的地方
frida和Xposed原理差不多,同样是一个动态插桩工具,详细的源码分析可以参考文章:frida源码分析
frida检测的途径很多,这里只简单介绍一些
端口和frida_server检测:
最简单的一种检测方式,厂商通过检测端口是否为固定的27047,还可以检测运行的frida_server名称
so层系统API检测:
so层非系统API检测:
遍历连接手机所有端口发送D-bus消息,如果返回"REJECT"这个特征则认为存在frida-server
检测内存库来检测:
Frida 的各个模式都是用来注入的,我们可以利用的点就是 frida 运行时映射到内存的库。最直接的是挨个检查加载的库
inlinehook检测frida:
frida实现hook一定实现了inlinehook技术,所以我们还可以通过inlinehook库来检测
参考文章:从inlinehook角度检测frida
实验案例:client.apk,漏洞银行.apk
我们打开client.apk,发现使用360加壳
然后我们使用葫芦娃大佬Frida_Dexdump脱壳工具进行脱壳,项目代码:https://github.com/hluwa/frida-dexdump
我们脱壳完成后就进入正常的静态分析流程
首先,从入口点出发:
我们可以看见入口类最后一个函数是启动一个延时器,3秒后延时实例化SplachScreen$a()
类,然后我们进入此类
我们可以发现该类实现Runnable接口,启动一个线程,然后里面完成从当前的Activity跳转到MainActivity
类
进入MainAcitivity
类:
我们简单分析一下代码逻辑,可以发现该APP实现了一些常见的反调试手段:
案例:wifiKiller.apk
这里我们使用AndroidStudio进行动态调试,发现没有debuggable的值,就默认ro.debuggable = false防止反调:
Android studio在动态调试的时候出现错误:
java层过反调方法:
这里我们使用第一种方法
我们发现ddms可以查看进程说明已经过java层反调试了
样例:AliCrakme.apk
我们进行静态分析,分析到了native函数,就需要对so层进行分析了
我们继续上面的案例,首先运行android_server (这里这样是防止android_server反调试)
然后进行端口转发
然后附加程序
在module中查找对应的so文件
直接进入securityCheck函数
我们在函数头f2下一断点,开始动态调试
以上是可以直接将so文件,附加进来,但是有时IDA版本或者手机系统版本原因,我们不能在moudle模块中找到
解决办法:
首先,我们Ctrl+s找到so文件对应的基地址
其次我们相对地址就是我们静态调试时的函数地址
相对地址=11AB+B3AFA000=B3AF A1AB 我们可以发现这和我们附加进来的地址一致
这说明程序一定含有反调试的策略,我们接下来应该解决程序的反调试策略
我们可以查看TrancePId值来验证(我们需要重新执行以上步骤动态调试,不过不运行)
首先,我们查看程序进程状态
其次我们,根据找到的PID号,查看对应状态:
你发现这里的值不为0,你可以验证没有进行IDA调试时候的值,经过以上分析我们可以确定该APK采用了反调试策略
首先完成基础配置:
你要获得后面的程序,可以打开程序后,输入adb shell dumpsys activity top 则可以查看
我们发现ddms上的进程前面出现红色小虫子,说明程序被挂起,而手机端也会显示挂起的界面
然后进行附加进程,挂载三项
然后按F9运行,我们发现此时程序退出,这是因为我们此时是挂起调试,程序还没有初始化
我们运行:
IDA附加so文件,我们点击取消就可以加载进来(要是没显示就按照上文地址的计算方法)
我们此时进入libCramke.so文件,并进入JNI_OnLoad方法(我们进入这里主要是为了解决反调试)
我们按F9运行,并按F8进行单步步入
下面使用F8开始单步调试了,发现每次到达BLX R7这条指令执行完之后,JNI_OnLoad函数就退出了,这个地方存在问题,可能就是反调试的地方了。我们再次进入调试,看见BLX跳转的地方R7寄存器中是 pthread_create 函数
到了这里我们就找到了出问题的地方,接下来我们只需要修改对应的位置就可以了。
可以把 BLX R7 这条指令给nop掉,也就是把这条指令变成空指令(相当于删除这条指令)这样apk就不会新建线程去执行检测代码了。
我们在直接静态分析so的IDA中找到 BLX R7的位置
我们把这条指令给nop掉,我们打开其对应16进制的窗口,并把值改为0
我们保存修改后的so文件
我们保存,再AndroidKiller里面进行回编译,则得到绕过反调试的apk
我们上面静态分析了漏洞银行的案例,我们发现其采用了模拟器检测、root检测、frida检测,下面我们依次进行反调试绕过
首先我们定位到该处的代码段
经过分析我们可以分析关键变量:
即决定模拟器检测的变量是i3
我们从下至上逐一分析代码逻辑:
我们分析了模拟器检测的代码,但是这里我们发现这部分代码并没有直接导致程序崩溃的代码段
我们继续分析,发现
只有当我们进行root、frida等情况,才会将APP关闭,而此时我们还没有使用frida,这就是模拟器root的原因导致的
这里首先我们关闭模拟器的root设置,进行重启,按照判断,应该是可以正常安装
再次安装,发现奇怪还是一样安装失败
此时我们进一步分析,按理说就算检测到模拟器,检测root也应该是可以安装成功,只是打开时候结束,这里说明还有其他地方有防护
经过分析,我们在AndroidManifest.xml
中找到一个关键属性hardwareAccelerated
hardwareAccelerated=ture
意味着APP计划利用移动设备中GPU资源来使其运行,但是很显然我们的模拟器是没有GPU的,所以就会导致直接崩溃,安装不上去
这里我们简单将hardwareAccelerated
值改为false,然后重编译
然后我们再次安装
程序成功安装,并可以打开,这里我们启动root可以进一步验证一下刚才的分析:
果然程序就打开后,直接闪退,并报错检测到root,这和我们上面的分析一致,也证明这里我们解决了模拟器检测绕过
上面虽然我们关闭root可以解决模拟器检测绕过问题,但是很多时候没有root,我们就很受限了,这里我们尝试进一步绕过root检测
这里我们可以发现只需要将a.R()
的返回值修改就可以了,我们可以采用hook手段,如Xposed或frida
我们进一步分析,root检测实现:
绕过此处root方式很多,比如该程序没有签名校验机制,最简单直接修改代码,反编译,这里我们为了学习,通过hook技术,比如Xposed可以绕过,但是经过前面分析,该APP有frida检测,那我们就使用frida来进行测试
我们随便创建一个js脚本,然后启动frida
发现程序检测到Frida在运行,然后直接崩溃,我们进一步分析Frida检测的代码
可以发现主要是fridaCheck()这个函数
进一步分析,可以发现该方法是native方法,因此我们进一步进入so层分析,我们打开frida-check
so文件
没有JNI_Onload
程序是静态注册
代码逻辑很简单:
这里我们分析结束,很简单只需要不将frida_server运行在27042端口上即可
再次启动frida看是否生效
此时我们发现程序frida并未检测到,这说明我们上面的分析是正确的
接下来只需要编写hook代码,将root检测绕过即可
这里我们就发现成功的hook程序,进入了正常的界面
过Xposed检测详细可以参考文章:Xposed定制
案例:漏洞银行.apk
前面我们采用了一些方法进行了反调试绕过,这里我们拿漏洞银行的例子,去联合我们前面的漏洞挖掘技巧和反调试技巧,展现如何过反调试后挖掘Android里面的一些漏洞
经过我们上篇帖子,讲述了如何解决抓包中的安全防护绕过问题,参考Android APP漏洞之战(9)——验证码漏洞挖掘详解,当我们绕过这些常见的抓包防护后,最后就是hook和算法还原,我们前面也绕过了hook检测
我们配置好抓包环境:
我们使用burpsuit进行抓包:
我们发现这里是加密的字段,我们可以全文搜索enc_data
我们就可以定位到e
类中的两个函数段,这样我们只需要对这两处函数进行hook,将参数打印出来就是我们传输的字符串了
我们进行注册:
先看抓包情况:
再看脚本hook值:
这样我们就成功的将加密前的数据和加密后的数据打印出来了
我们通过动态调试验证一样
上面我们利用hook的方法成功的获得解密后的数据,一般在做逆向开发过程时,还可以通过编写解密函数来实现解密
首先我们要分析加密逻辑
我们可以发现这里就是先通过c函数进行加密,然后使用base64进行加密
编写解密函数:
这样也可以成功的解密
我们在前面的文章中也讲述了此类漏洞问题,主要是因为应用中采用了硬编码导致的
这里我们可以发现,可以获取部分的敏感信息
本文总结并一一分析了当下Android APP中常见的防护策略,并通过案例实操实现了反调试策略的过反调,学习并掌握本节可以更加进一步助力Android APP漏洞挖掘中的漏洞学习,最后我们将反调试技巧和漏洞挖掘实例进行结合,并实操了案例,本文后续会将实验案例逐一上传到知识星球中。
github首页:github
dex整体加固:这种方法往往通过动态加载的形式,交换Application的执行,一般我们可以通过hook方法,找到dex_file的起始地址或大小,进行脱取,也可以通过定制Room方法对关键的函数进行插桩,代表有fdex2、Frida_Dump
函数抽取:这种方法往往通过将函数代码抽取放入so文件中,执行时再从so文件读取还原,我们一般可以通过被动调用延时Dump的方法,或主动调用ArtMethod中invoke函数,触发每一个函数,然后进行回填,代表有youpk和fart
VMP:通过定制的指令集进行解释,这时往往需要手工分析,找到指令的映射表,然后进行一步步解释
dex整体加固:这种方法往往通过动态加载的形式,交换Application的执行,一般我们可以通过hook方法,找到dex_file的起始地址或大小,进行脱取,也可以通过定制Room方法对关键的函数进行插桩,代表有fdex2、Frida_Dump
函数抽取:这种方法往往通过将函数代码抽取放入so文件中,执行时再从so文件读取还原,我们一般可以通过被动调用延时Dump的方法,或主动调用ArtMethod中invoke函数,触发每一个函数,然后进行回填,代表有youpk和fart
VMP:通过定制的指令集进行解释,这时往往需要手工分析,找到指令的映射表,然后进行一步步解释
Java_完整包名_类名_方法名:静态注册
JNI_Onload: 动态注册
Java_完整包名_类名_方法名:静态注册
JNI_Onload: 动态注册
动态注册加载so文件两种方式:
(
1
)System.loadLibrary(
"native-lib"
);
(
2
)System.load(so文件绝对路径)
动态注册加载so文件两种方式:
(
1
)System.loadLibrary(
"native-lib"
);
(
2
)System.load(so文件绝对路径)
(
1
) 空格键:切换文本视图与图表视图
(
2
) ESC:返回上一个操作地址
(
3
) G:搜索地址和符号
(
4
) N:对符号进行重命名
(
5
) 冒号键:常规注释
(
6
) 分号键:可重复注释
(
7
) Alt
+
M:添加标签
(
8
) Ctrl
+
M:查看标签
(
9
) Ctrl
+
S:查看节的信息
(
10
) X:查看交叉应用
(
11
) F5:查看伪代码
(
12
) Alt
+
T:搜索文本
(
13
) Alt
+
B:搜索十六进制
(
14
) 代码数据切换
C
-
-
>代码
/
D
-
-
>数据
/
A
-
-
>ascii字符串
/
U
-
-
>解析成未定义的内容
(
15
) 拷贝伪C代码到反汇编窗口:右键>copy to
-
assembly
(
16
) IDA可以修改so的
hex
操作数来修改so文件,右键点击“edit”进行修改,
然后右键点击“edit
-
patchrogram”提交
(
1
) 空格键:切换文本视图与图表视图
(
2
) ESC:返回上一个操作地址
(
3
) G:搜索地址和符号
(
4
) N:对符号进行重命名
(
5
) 冒号键:常规注释
(
6
) 分号键:可重复注释
(
7
) Alt
+
M:添加标签
(
8
) Ctrl
+
M:查看标签
(
9
) Ctrl
+
S:查看节的信息
(
10
) X:查看交叉应用
(
11
) F5:查看伪代码
(
12
) Alt
+
T:搜索文本
(
13
) Alt
+
B:搜索十六进制
(
14
) 代码数据切换
C
-
-
>代码
/
D
-
-
>数据
/
A
-
-
>ascii字符串
/
U
-
-
>解析成未定义的内容
(
15
) 拷贝伪C代码到反汇编窗口:右键>copy to
-
assembly
(
16
) IDA可以修改so的
hex
操作数来修改so文件,右键点击“edit”进行修改,
然后右键点击“edit
-
patchrogram”提交
java层:dex加壳技术、混淆技术
so层:so加壳技术、ollvm高级混淆技术
java层:dex加壳技术、混淆技术
so层:so加壳技术、ollvm高级混淆技术
adb shell ps 显示当前的注册信息(adb shell ps | find)| grep
adb shell ps 显示当前的注册信息(adb shell ps | find)| grep
adb forward tcp:
8700
jdwp:
3924
adb forward tcp:
8700
jdwp:
3924
adb shell am start
-
D
-
n My.XuanAo.LiuYao
/
.main(包名加进程)
adb shell am start
-
D
-
n My.XuanAo.LiuYao
/
.main(包名加进程)
注意:Android stdio不能下断点,这是由于Android stdio 版本过高引起的
注意:Android stdio不能下断点,这是由于Android stdio 版本过高引起的
1.
创建模拟器(最好使用真机)
2.
在IDA里面找到android_server(dbgsrv目录)
3.
把android_server文件放到手机
/
data
/
local
/
tmp
adb push 文件名
/
data
/
local
/
tmp
4.
打开一个cmd窗口:运行android_server
1
)adb shell 连接手机
2
)给一个最高权限:su
3
)来到
/
data
/
local
/
tmp:cd
/
data
/
local
/
tmp
4
)给androi_server一个最高的权限:chmod
777
android_server
5
)查看android_server是否拥有权限:ls
-
l
6
)运行andorid_server: .
/
android_server(端口号默认是:
23946
)
补充:运行andorid_server并且修改端口号:.
/
android_server
-
p端口号
5.
端口转发:
adb forward tcp:端口号 tcp:端口号(之前转发的端口号是什么,这里就是什么)
6.
打开DDMS:观察程序的端口号
7.
挂起程序:
adb shell am start
-
D
-
n 包名
/
类名
例子:adb shell am start
-
D
-
n com.example.javandk1
/
.MainActivity
补充:此时观察DDMS,被调试的程序前面有一个红色的虫子;
8.IDA
里面勾选三项
1
)打开ida,选择debugger
-
第二项
-
Remote ARMlinux(第四项)
2
)添加hostname和portt:
hostname:主机号(默认
127.0
.
0.1
)
port:端口号(之前android_server运行时的端口号或者端口转发的端口号)
3
)出来进程列表:选择要调试的程序(可以ctrl
+
f,搜索包名)
4
)进来后,勾选三项:
Suspend on process entry point程序入口点 断下
Suspend on thread start
/
exit线程的退出或启动 断下
Suspend on library load
/
unload库的加载和卸载 断下
补充:可以直接在这里F9(左上角有一个三角形)运行程序,然后放手;
也可以,直接执行第九步,然后IDA运行程序
9.
挂载、释放(放手)
jdb
-
connect com.sun.jdi.SocketAttach:hostname
=
127.0
.
0.1
,port
=
端口号(是ddms里显示的端口
8600
)
补充:此时观察DDMS,被调试的程序前面有一个绿色的虫子;
1.
创建模拟器(最好使用真机)
2.
在IDA里面找到android_server(dbgsrv目录)
3.
把android_server文件放到手机
/
data
/
local
/
tmp
adb push 文件名
/
data
/
local
/
tmp
4.
打开一个cmd窗口:运行android_server
1
)adb shell 连接手机
2
)给一个最高权限:su
3
)来到
/
data
/
local
/
tmp:cd
/
data
/
local
/
tmp
4
)给androi_server一个最高的权限:chmod
777
android_server
5
)查看android_server是否拥有权限:ls
-
l
6
)运行andorid_server: .
/
android_server(端口号默认是:
23946
)
补充:运行andorid_server并且修改端口号:.
/
android_server
-
p端口号
5.
端口转发:
adb forward tcp:端口号 tcp:端口号(之前转发的端口号是什么,这里就是什么)
6.
打开DDMS:观察程序的端口号
7.
挂起程序:
adb shell am start
-
D
-
n 包名
/
类名
例子:adb shell am start
-
D
-
n com.example.javandk1
/
.MainActivity
补充:此时观察DDMS,被调试的程序前面有一个红色的虫子;
8.IDA
里面勾选三项
1
)打开ida,选择debugger
-
第二项
-
Remote ARMlinux(第四项)
2
)添加hostname和portt:
hostname:主机号(默认
127.0
.
0.1
)
port:端口号(之前android_server运行时的端口号或者端口转发的端口号)
3
)出来进程列表:选择要调试的程序(可以ctrl
+
f,搜索包名)
4
)进来后,勾选三项:
Suspend on process entry point程序入口点 断下
Suspend on thread start
/
exit线程的退出或启动 断下
Suspend on library load
/
unload库的加载和卸载 断下
补充:可以直接在这里F9(左上角有一个三角形)运行程序,然后放手;
也可以,直接执行第九步,然后IDA运行程序
9.
挂载、释放(放手)
jdb
-
connect com.sun.jdi.SocketAttach:hostname
=
127.0
.
0.1
,port
=
端口号(是ddms里显示的端口
8600
)
补充:此时观察DDMS,被调试的程序前面有一个绿色的虫子;
.
/
gdbserver
0.0
.
0.0
:
23946
-
-
attach
11021
.
/
gdbserver
0.0
.
0.0
:
23946
-
-
attach
11021
gdb
-
multiach
set
arch arm
set
arm fallback
-
mode thumb
set
arch arm
set
arm fallback
-
mode thumb
target remote
172.31
.
99.61
:
23946
target remote
172.31
.
99.61
:
23946
F8下一跳
F7步入
C 进入下一个断点
ctrl
+
shift
+
pageup 显示上一行状态
ctrl
+
shift
+
pagedown 显示下一行状态
如果没有调用,我们可以使用frida主动调用
F8下一跳
F7步入
C 进入下一个断点
ctrl
+
shift
+
pageup 显示上一行状态
ctrl
+
shift
+
pagedown 显示下一行状态
如果没有调用,我们可以使用frida主动调用
java层:
android.debuggable
=
false
so层:
通过检测ptrace值的变化
java层:
android.debuggable
=
false
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-4-27 15:27
被随风而行aa编辑
,原因: