type
status
date
slug
summary
tags
category
icon
password
 

0x00 引言

在前期对App进行漏洞挖掘的过程中发现,以纯人工的方式去逆向某些App并且发现其中的漏洞已经不太现实,主要有以下几点原因:
  • Android系统体系十分庞大,App代码量巨大,存在超大型App(抖音目前已经有150万个函数),人工分析极度不现实
  • 对Android静态分析精力耗费巨大,自动化静态分析完全可以简化大量重复的工作。
 
Appshark 是一个针对安卓的静态分析工具,它的设计目标是针对超大型App的分析(抖音目前已经有150万个函数). Appshark支持众多特性:
  • 基于json的自定义扫描规则,发现自己关心的安全漏洞以及隐私合规问题
  • 灵活配置,可以在准确率以及扫描时间空间之间寻求平衡
  • 支持自定义扩展规则,根据自己的业务需要,进行定制分析
 

0x01 AppShark说明

1. 概述

notion image
 
apk文件预处理
主要是提取app中的基本信息,比如导出组件,manifest解析,以及发现一些manifest中常见的漏洞. 这里面还有一个最重要的工作就是会使用jadx对apk进行反编译,其生成的java源码会在最后的漏洞详情中展示.
代码预处理
代码预处理最主要有三个功能:
  1. 生成SSA
  1. 生成基本的call graph
  1. 根据配置进行各种指令的patch,比如callback注入.
用户自定义规则解析
该部分主要的功能就是将模糊的用户自定义规则翻译为准确的source以及sink,然后根据用户的规则配置,查找相关的分析入口,生成TaintAnalyzer. 所谓的TaintAnalyzer 就是source,sink,entry的一个综合体.
指针以及数据流分析
该模块的输入主要是一个所谓的入口函数,当然也包含了一系列用户自定义的或者系统预置的分析规则. 通过较长时间的指针分析,生成AnalyzeContextAnalyzeContext 里面包含了从指定的入口分析以后,得到的指针指向关系以及数据流流向关系. 该模块的主要思想主要是参考了论文: P/Taint: unified points-to and taint analysis
漏洞查找
该模块的输入主要用三部分:
  1. TaintAnalyzer,查找其中的source到sink的路径
  1. AnalyzeContext, 包含了数据流图
  1. 关联规则中的Sanitizer,用于过滤掉不符合要求的路径.
该模块会依据AnalyzeContext提供的数据流图,查找从source到sink的路径,如果找到,并且该路径没有被Sanitizer过滤掉,那么就会在最终结果中添加一个漏洞.
Sanitizer
该模块的功能就是根据用户自定的sanitizer,过滤掉不符合要求的路径.
报告生成模块
每个漏洞会以用户可以阅读的方式进行展示. 同时会给一个result.json,这里面包含了所有的漏洞信息.
 

2. 使用

  • 首先clone项目,并且下载发行的jar或者自己编译,建议自己编译,因为使用v0.1.2发行版和教程有些部分不符,比如文档中的规则写法是sanitizer,并且在源码中也为sanitizer,但是并没有进行编译和发行,所以只能使用旧的规则写法sanitize
  • 修改config文件
      1. 将apkPath修改为你想要扫描的apk绝对路径.
      1. 指明你要使用的规则,以逗号分隔.并且这些规则应该都放在config/rules目录下. 因为appshark是通过这个路径来查找这些规则的.
      1. 指定输出结果保存的目录,默认是当前目录下的out文件,你可以指定一个其他目录.
  • 启动appshark java -jar AppShark-0.1.2-all.jar config/config.json5
 

3. 规则撰写

 
#分析入口
使用Appshark进行数据流分析,最重要的就是明确告诉Appshark你关心的分析入口,source,sink以及sanitizer. 根据source的特殊性,将分析入口模式分类为:
  • ConstStringMode 支持常量字符串作为source
  • ConstNumberMode 支持常量整数作为source
  • SliceMode和DirectMode 其他类型的source
 
#Appshark分析的对象是经过SSA处理的jimple指令,因此在指定source/sink的时候,引用的函数以及field签名必须符合jimple格式.
jimple函数签名
<android.content.Intent: android.content.Intent parseUri(java.lang.String,int)> 这是一个通用的Java函数签名,包含了类名,函数名,函数返回类型,参数类型列表. 在指定source,sink的过程中,每个部分都可以用*来模糊匹配. 比如<*: android.content.Intent parseUri(java.lang.String,int)> 匹配所有类中,函数名字为parseUri ,返回类型是android.content.Intent以及参数列表为java.lang.String,int的函数.
jimple field签名
<com.security.TestClass: android.content.Intent fieldName> 这是一个通用的Java 对象的field签名,这个field的类名是com.security.TestClass,类型是android.content.Intent,field的名字是fieldNamefield签名不支持模糊匹配指定,必须准确给出
💡
需要格外注意在签名中的类名后必须紧接着:,否则将会无法处理函数签名导致分析失败,即不能是<android.content.Intent : android.content.Intent parseUri(java.lang.String,int)> 只能<android.content.Intent: android.content.Intent parseUri(java.lang.String,int)>
💡
除此之外还需要注意:new一个实例对象的函数名为<init>例如new一个File实例的函数签名应当为 <java.io.File: * <init>(*)> ,其他类的实例创建也一样。
一般规则包含四个部分,分别是:1. 分析的入口 2. source 3. sink 4. sanitizer.
#分析入口的指定
分析的入口一般是一个函数. 比如
entry只有在DirectMode下需要明确指定,其他三个模式下,都无需明确指明分析入口. 如果你不知道分析入口是什么,说明你不应该使用DirectMode.
 
#一般source的指定
需要说明的是,appshark内部真正的source点会是具体的变量,因此无论哪种写法,都会转换成一个具体的变量. source可以有很多种类型,分别是:
  • 常量字符串,注意这与ConstStringMode是没关系的
  • 函数返回值
  • 函数的某个参数
  • 对象的某个field
  • 某个对象的创建
下面分别举例介绍这五种情况.
常量字符串
那么:
s将成为source. 函数f的参数1将成为source.
函数的返回值
这种一种最常见的source形式,比如:
也就是getName的返回值将会是source. 那么:
name将成为source点.
某对象的field
比如:
那么:
uri将会成为source点. 注意不区分该field是静态field还是非静态field
某个函数的参数
函数参数作为source一般在重写系统类的情况, 比如:
首先注意,p0是第一个参数,p1是第二个参数,这里类型为WebResourceRequest才是source.
某个对象的创建
这个规则非常特殊,一般不会用到. 比如:
那么:
这时候变量i将成为source点.
 
#一般sink的指定
目前sink点只能是函数的周边,可以是:
  • this指针 @this
  • 函数的某个参数 p0,p1,p2
  • 函数的所有参数 p*
  • 函数的返回值 return
sink
需要强调的是,所有的sink都会在内部转换成具体的变量. sink的指定相对于source的指定要简单许多,种类也比较单一. 例如:
那么:
这里面的ffileOutputStream都会是sink点. appshark会检查能否找到从source到这些变量的一个污点传播路径.
sink还有一个可配置的选项就是LibraryOnly,默认值为false,如果设置为true,那么就要求匹配到的函数签名必须是EngineConfig.json5中指定的Library. 以上面例子为例,如果在EngineConfig.json5中指定com.security为Library,那么path就是sink点. 否则如果没有在EngineConfig.json5中指定com.security为Library,那么path就不是sink点.
#sanitizer的指定
sanitizer目的是消除误报. 虽然发现了一条从source到sink的完整传播路径,但是因为已经对source做了严格的校验,所以这并不是一条有效的路径. 下面以unzipSlip规则为例来介绍一下sanitizer的原理.
zip slip漏洞的原理可以参考Directory traversal attack. 主要是在解压zip文件的时候,没有检查文件名中是否包含"../",导致如果zip文件外部可控的话,可能会导致任意文件覆盖问题.
source和sink就不展开说了,上面刚刚介绍过.
重点说一下sanitizer,因为它的设计不是那么容易理解.
顶层规则是或的关系
sanitizer分别包含了三个子key:
  • getCanonicalPath
  • containsDotdot
  • indexDotdot
这三个规则是或的关系. 根据规则,我们会找到N个 source,M个sink. 那么理论上就会存在N*M条路径.对于其中的任意一条路径,如果它满足了这三条规则中的任意一条,就会被sanitize掉.
二层规则之间是与的关系
由于这个例子中,二层规则都只有单独一条,所以这里造一个规则来演示.
如果某条路径同时满足对<java.lang.String: boolean contains(java.lang.CharSequence)><java.io.File: * init(java.lang.String)>这两个函数的限制,那么这条路径就会被sanitize掉.
具体规则的含义
再次强调,appshark分析的是污点在变量之间的传递关系,所以无论是source,还是sink,还是sanitizer描述的具体粒度都是变量. 以containsDotdot为例:
它的限制有两个:
  1. TaintCheck 从source出发,传播到的所有变量中,是否污染到了<java.lang.String: boolean contains(java.lang.CharSequence)>这个函数的this指针. 比如:
那么这里的path就是contains的this指针,
  1. 参数取值的限制 "p0":["..*"]的含义是: 常量字符串..*要能污染到contains的参数0.
  1. NotTaint 这个和TaintCheck格式一样,意思相反,要求函数的这些地方不能被source污染到.
这两个条件之间也是与的关系,因此:
满足我们的sanitizer,从source(path)到sink(file)的这条传播路径就会被sanitize掉.
反之: 下面的例子中,就不能被sanitize掉.
这个例子中s.contains("../")满足了对p0的检验,但是没有满足TaintCheck的检验. 而path.contains("root")满足了对TaintCheck的检验,但是没有满足对p0的检验. 所以这条传播路径是有效的.
sanitizer总结
sanitizer针对的是,已经找到了一条从source到sink的路径,再根据source污染到的所有变量进行过滤,如果满足条件就删掉这条路径,否则保留.
 

4. 四种mode的特殊性

#DirectMode的特殊性
它需要明确指明分析的入口,比如:
那么这条规则的分析入口就是UnZipFolder这个函数.
这里还可以是一些虚拟入口,但是目的都是一样的,比如:
这里的意思是,每个安卓的导出组件都是分析入口. 比如Activity的onCreate,onDestroy等等都是分析的入口. 这些入口有appshark根据manifest文件的解析得到,而不是写死,相对灵活一点. 当然你也可以针对具体的app,自行分析manifest文件,然后把每个导出的组件中的函数写到规则中,这样效果是一样的.
这里还有一个关键特性就是traceDepth,这里指的是从分析入口函数开始,分析多少层函数调用为止. 如果调用层级超过这个深度,会被忽略.
#SliceMode的特殊性
SliceMode和DirectMode的区别是它的分析入口不是固定的,而是根据具体的source,sink计算得到的. 这个mode的提出针对的是,在某些场景下,就没有固定的分析入口. 或者从指定的入口开始到我们想要分析的那部分代码之间距离太远,导致不能在有效的时间内取得分析结果.
怎么根据source和sink计算分析入口,主要有两种情况:
  • source为某个函数的参数
  • 其他形式的source
source为某个函数参数
以下面的例子来说明:
首先shouldInterceptRequest针对是WebViewClient的子类而言的,因为android.webkit.WebViewClient是安卓的framework,我们并不会直接去分析framework的代码. 这里的source是shouldInterceptRequest的p1,也就是WebResourceRequest这个参数. 如果我们override了shouldInterceptRequest这个函数,那么将会从这个函数出发,找出它所有的显式或者隐式的被调函数,看看里面有没有包含sink点的函数. 如果有就将这个override的shouldInterceptRequest函数作为分析入口.
这里有一个规则的traceDepth,指的是从shouldInterceptRequest出发,查找的函数层数.
 
#其他形式的source
比如:
意思是从source点往下搜索traceDepth层,从sink点往上搜索搜索traceDepth层,找到它们最近交汇的函数作为分析入口.
 
#ConstStringMode
它之所以特殊,因为app中的常量字符串太多可能非常多,所以其分析的入口不受traceDepth的约束,它分析的入口就是指定的常量字符串所在的函数. 比如:
如果s是满足我们条件的常量字符串,那么f就是分析入口. 限制常量字符串的条件有:
  • constLen 长度必须是这个长度的倍数
  • minLen 长度不能小于这个长度
  • targetStringArr 形式上满足这个数组中的任意一个,比如 "targetStringArr": ["AES","DES","*ECB*"]
ConstNumberMode
它与ConstStringMode类似,其分析入口也是这个常量数值所在的函数.
对于常量数值的限制,只有targetNumberArr,它表示只关心这个数组里面的数值. 比如:
 

5. 结果解读

SecurityInfo
这里的安全漏洞会根据你在规则中desc指定的categoryname进行分类. 方便程序处理,也方便人工阅读. 其中vulners字段是这种类型漏洞的列表. 其中每个漏洞都有一个hash字段,该字段可以认为是漏洞的唯一标识. details字段包含了漏洞的大量信息:
  • Source 规则中source字段匹配到的变量.
  • Sink 规则中sink字段匹配到的变量
  • position source对应变量所在的函数
  • entryMethod 分析的入口
  • target 污点在变量之间传播的过程.
  • url 以html格式展示的污点在变量之间传播的过程.
ComplianceInfo
ComplianceInfo专门针对隐私合规问题. 如果category是ComplianceInfo,那么appshark将会到其特殊处理.比如:
其分类将是:
  • 第一级是ComplianceInfo
  • 第二级是ComplianceCategory指定的PersonalDeviceInformation_NetworkTransfer
  • 第三级是name指定的GAID_NetworkTransfer_body.
比如:
至于vulners中的字段和SecurityInfo中的含义是一样的.
漏洞详情网页介绍
漏洞详情网页设计的目的是,他可以脱离results.json独立展示信息给用户,方便分析漏洞的形成原因.
vulnerability detail
是app的基本信息以及漏洞的基本信息.
data flow
上面的target字段
call stack
污点传播经历了哪些函数.
code detail
详细展示了污点传播的过程. 如果config.json5中指定了javaSource为true,那么还会展示反编译后的函数的java代码.
 

6. 隐私合规问题

隐私数据流分析就是数据流分析的其中一种,但绝大部分时候你不需要编写entry和sanitizer,你应该更关心规则中的source和sink。
具体来说,你可以将source指定为某个获取隐私信息的API,例如:
这个API的return是设备唯一标识IMEI。
同时,将sink指定为你认为会存在隐私数据泄露的方法,例如写文件:
当你所关注的隐私数据来源并非API,而是对象的某个field时,你依然可以按照通用规则编写中field类型source的格式编写规则,例如设备序列号作为source:
最后,你需要使用SliceMode,可以减少分析的时间。
完整的规则文件:

0x02 经典案例

 

1. content provider目录遍历漏洞

我们的app有一个content provider,用来共享sandbox目录下的文件.
作者的意图是只共享sandbox目录,但是他直接把用户path作为参数传递给了File,这意味着,如果path中包含"../",那么就可以绕过sandbox目录限制. 可以轻松构造出一个poc:
notion image
 关键就是定义source,sink以及sanitizer. 明显openFile的参数0也就是uri是用户可控制的,一般把外部用户可直接或间接控制的变量视为source. 而sink点比较合适的一个地方是ParcelFileDescriptor.open的参数0, 因为如果source能够控制ParcelFileDescriptor.open参数0,那么基本上就可以读取任何文件了.
然后给出运行配置
notion image
notion image
假设修复方法如下,通过getLastPathSegment 截取最后一段路径。 return ParcelFileDescriptor.open(new File(root, uri.getLastPathSegment()), ParcelFileDescriptor.MODE_READ_ONLY);
添加sanitizer 检查getLastPathSegment@this ,如果调用了uri.getLastPathSegment(),并且this指针被source污染了,那么可以认为漏洞修复了. 被污染了的准确含义是,可能被控制. 比如c=a+b,那么c就被a和b污染了.
如果我们传输的不是content://slipme2/../../../../../../../../data/data/com.security.bypasspathtraversal/files/file2, 而是content://slipme2/encoded/%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fdata%2Fdata%2Fcom%2Esecurity%2Ebypasspathtraversal%2Ffiles%2Ffile2,VulProvider2也并不是一个有效的修复,仍然有漏洞存在.直接从这个特征入手,如果path中包含了..那么就认为是非法路径即可.
那么这个sanitizer的准确含义是什么呢? 针对一条从source到sink的路径上,如果:
  1. String.contains的this指针被污染了,并且这个函数调用位置的p0参数能够被"..*"这个常量污染到
  1. 并且String.startWith的this指针也被污染了.
校验如果以app的内部路径开头,就抛出异常. 我们可以通过软链接,既不包含..,也不以app的内部路径开头. poc代码如下:
可以存在两种有效的修复方式:
修复方式1
注意这里的startsWith 检查的是sandbox的path,所以我们就没法在自己的目录中创建一个软链接了.
修复方式2
这里通过getCanonicalFile来解析软链接,这样获取到的就是真实的路径了. 所以这里的条件是:
  1. 通过getCanonicalFile获取到真实的路径
  1. 通过startsWith校验真实路径是否以sandbox路径开头.
大家可能有疑问.containsDotDot这个sanitizer存在漏报问题啊,case3中的修复方式明明是无效的,但是仍然会被引擎因为是修复了的,这实际上导致了漏报. 这里只能说一下sanitizer的局限性了,它只能根据source污染到的变量的范围来确定要不要去掉一条路径. 真实的修复方式: path.startsWith(root.getPath())和有问题的修复方式 path.startsWith(internalDir.getCanonicalPath())从形式上看是没什么区别的. 让appshark去识别这种逻辑上的区别,是非常困难的, 这也是appshark的局限.

2. Intent Redirection

LaunchAnywhere是安卓最为经典的漏洞类型之一,现在被Google称为Intent Redirection:support.google.com/faqs/answer… 无恒实验室一直对该类型漏洞有研究,我们把这一类问题比作“安卓上的SSRF”,其中Intent就像一个HTTP请求,而未经验证完全转发了这个请求在安卓上会导致严重的安全问题。关于这类漏洞的逻辑与利用,推荐阅读retme.net/index.php/2… 这篇文章。
基础的规则如下
可以看到这个规则仅仅考虑从 getParcelable 到 startActivity 的数据流,且不考虑 sanitizer。这和我们实际使用的规则有一些差别,但足够说明问题。
 

3. DirtyStream

notion image
 
详见Android安全问题中的DirtyStream 一节。
sanitizer
notion image
加入sanitizer
notion image
无法扫描到漏洞,已修复。
notion image
 

4. MAC 隐私获取

 

5. ZIP 解压缩路径穿越

6. JSInterface_analyzer

请看项目:
 

0x03 总结

静态分析能极大的减小人工分析的精力消耗,能够快速锁定调用链,但是依旧存在一些问题
  • 比如对于复杂调用环境的调用链可能会断,例如需要用户操作触发进一步的调用时,链子就会断掉,这种情况只能进行手动分析。
  • 对于加固的App将可能无法进行静态分析,现有静态分析框架基本基于完整APK进行分析,脱壳后一般会得到dex文件,现有框架无法综合各个DEX进行静态分析,如果可以尝试将脱壳内容重新组包的情况下,说不定可行,请自行拓展尝试。
然还可以尝试一下其他的静态分析框架,这里仅对AppShark进行体验。
 

0x04 参考

  1. https://github.com/bytedance/appshark/blob/main/doc
  1. https://juejin.cn/post/7161660793870614564
  1. Remediation for Intent Redirection Vulnerability
 
Android eBPF Syscall包装器问题小记Android-DirtyStream 漏洞详细说明
  • Twikoo