2年前 (2017-09-22) 网友投稿  站长之家
文章评分 0 次,平均分 0.0

目前Android应用代码漏洞扫描工具种类繁多,效果良莠不齐,这些工具有一个共同的特点,都是在应用打包完成后对应用进行解包扫描。这种扫描有非常明显的缺点,扫描周期较长,不能向开发者实时反馈代码中存在的安全问题,并且对于问题代码的定位需要手动搜索匹配源码,这样就更不利于开发者对问题代码进行及时的修改。Code Arbiter正是为解决上述两个问题而开发的,专门对Android Studio中的源码进行安全扫描。

1 背景介绍

为实现对Android Studio中的源码进行扫描,最方便的方式便是将扫描工具以IDE插件的形式进行工作。此时一个很自然的想法便是从头构建一个Android Studio插件,但是进行仔细的评估后会发现,这样做难度并不小:

  1. 工作量大,许多知识需要学习,如IDE开放API接口、插件UI构建等,同时许多底层模块需要从头构建;
  2. 插件的稳定性、检测问题的准确性上都不一定能够达到已有开源工具的效果。

因此我们转而考虑在已有漏洞检测插件的基础上进行扩展,以满足需求。经过调研,最终入围的两款检测插件是PMD和FindBugs,其中PMD是对Java源码进行扫描,而FindBugs则是对Java源码编译后的class文件进行扫描。考虑到可扩展性及检测的准确性,最终选定了FindBugs。FindBugs是一个静态分析工具,它检查类或者JAR文件,将字节码与一组缺陷模式进行对比来发现可能的问题,可以以独立的JAR包形式运行,也可以作为集成开发工具的插件形式存在。

扩展优化

那么,怎么扩展FindBugs呢?调研发现FindBugs插件具有着极强的可扩展性,只需要将扩展的JAR包导入FindBugs插件,重启,即可完成相关功能的扩展。安装JAR包示意图如下所示。

Android漏洞扫描工具Code Arbiter

下面的问题是如何构建可安装的JAR包。继续调研,发现FindBugs有一款专门对安全问题进行检测的扩展插件Find Security Bugs,该插件主要用于对Web安全问题进行检测,也有极少对Android相关安全问题的检测规则。考虑以下几个原因,需要对该插件的源码进行重构。

  1. 对Android安全问题的检测太少,只包含外部文件使用、Webview、Broadcast使用等寥寥几项;
  2. 检测的细粒度上考虑不够完全,会造成大量的误报,无法满足检测精度的要求;
  3. 检测问题的上报只支持英文模式,且问题展示的逻辑性不够严谨,不便于开发者进行问题排查。

基于以上三个原因,我们需要对Find Security Bugs的源码进行重写、优化,通过增加检测项来检测尽可能多的安全问题,通过优化检测规则来减少检测的误报,问题展示使用中文进行描述,同时优化问题描述的逻辑性,使得开发者能够更易理解并修改相关问题,至此插件实现及优化的方案确定。

2 工具实现介绍

FindBugs检测的是class文件,因此当待检测的源码未生成编译文件时,FindBugs会先将源码编译生成.class文件,然后对这个class文件进行分析。FindBugs会完成对class文件的自动建模,在此模型的基础上对代码进行分析。按照在实际编写检测代码过程中的总结,把检测的实现方式分成四种方式,下面分别进行介绍。

2.1 逐行检查

逐行检查主要是针对代码中使用的一些不安全方法或参数进行检测,其实现方式是重写sawOpcode()方法,下面以Android中使用外部存储问题作为示例进行讲解。

Android中获取外部存储文件夹地址的方法主要包括下面这些方法:

<code class="java">getExternalCacheDir()
getExternalCacheDirs()
getExternalFilesDir()
getExternalFilesDirs()
getExternalMediaDirs()
Environment.getExternalStorageDirectory()
Environment.getExternalStoragePublicDirectory()
</code>

检测的方式便是,如果发现存在该方法的调用,则作为一个问题进行上报,实现完整代码如下所示:

<code class="java"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ExternalFileAccessDetector</span> <span class="keyword">extends</span> <span class="title">OpcodeStackDetector</span> </span>{

    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> String ANDROID_EXTERNAL_FILE_ACCESS_TYPE = <span class="string">"ANDROID_EXTERNAL_FILE_ACCESS"</span>;
    <span class="keyword">private</span> BugReporter bugReporter;
    <span class="function"><span class="keyword">public</span> <span class="title">ExternalFileAccessDetector</span><span class="params">(BugReporter bugReporter)</span> </span>{
        <span class="keyword">this</span>.bugReporter = bugReporter;
    }

    <span class="annotation">@Override</span>
 <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">sawOpcode</span><span class="params">(<span class="keyword">int</span> seen)</span> </span>{
        <span class="comment">//printOpCode(seen);</span>
 <span class="keyword">if</span> (seen == Constants.INVOKEVIRTUAL &amp;&amp; (
        getNameConstantOperand().equals(<span class="string">"getExternalCacheDir"</span>) ||
        getNameConstantOperand().equals(<span class="string">"getExternalCacheDirs"</span>) ||
        getNameConstantOperand().equals(<span class="string">"getExternalFilesDir"</span>) ||
        getNameConstantOperand().equals(<span class="string">"getExternalFilesDirs"</span>) ||
        getNameConstantOperand().equals(<span class="string">"getExternalMediaDirs"</span>)
            )) {
<span class="comment">// System.out.println(getSigConstantOperand());</span>
 bugReporter.reportBug(<span class="keyword">new</span> BugInstance(<span class="keyword">this</span>, ANDROID_EXTERNAL_FILE_ACCESS_TYPE, Priorities.NORMAL_PRIORITY).addClass(<span class="keyword">this</span>).addMethod(<span class="keyword">this</span>).addSourceLine(<span class="keyword">this</span>));
        }
        <span class="keyword">else</span> <span class="keyword">if</span>(seen == Constants.INVOKESTATIC &amp;&amp; getClassConstantOperand().equals(<span class="string">"android/os/Environment"</span>) &amp;&amp; (getNameConstantOperand().equals(<span class="string">"getExternalStorageDirectory"</span>) || getNameConstantOperand().equals(<span class="string">"getExternalStoragePublicDirectory"</span>))) {
            bugReporter.reportBug(<span class="keyword">new</span> BugInstance(<span class="keyword">this</span>, ANDROID_EXTERNAL_FILE_ACCESS_TYPE, Priorities.NORMAL_PRIORITY).addClass(<span class="keyword">this</span>).addMethod(<span class="keyword">this</span>).addSourceLine(<span class="keyword">this</span>));
        }
    }
}
</code>

该类的实现是继承OpcodeStackDetector类,是FindBugs中的一个抽象类,封装了对于获取代码特定参数的方法调用。sawOpcode方法参数可以理解为待检测代码行的行号,通过printOpCode(seen)可以打印该代码行的具体内容。Constants.INVOKEVIRTUAL表示该行调用类的实例方法,Constants.INVOKESTATIC表示调用类的静态方法。getNameConstantOperand方法表示获取被调用方法的名称,getClassConstantOperand方法表示获取调用类的名称,getSigConstantOperand方法表示获取方法的所有参数。bugReporter.reportBug用于上报检测到的漏洞信息,其中BugInstance的三个参数分别表示:检测器、漏洞类型、漏洞等级,其中漏洞等级分为五个级别,如下表所示:

名称 参数 含义
HIGH_PRIORITY 1 高危风险
NORMAL_PRIORITY 2 中危风险
LOW_PRIORITY 3 低危风险
EXP_PRIORITY 4 安全提醒
IGNORE_PRIORITY 5 可忽略风险

addClass、addMethod、addSourceLine用于指定该漏洞所在的类、方法、行,方便报告漏洞时定位关键代码。

2.2 逐方法检查

逐方法检查首先获取待检测类的所有内容,然后对类中的方法进行逐个检查,多用于对方法体进行检测,其实现的方法主要是通过重写visitClassContext方法,下面以对Android TrustManager的空实现的检测为例进行说明。
TrustManager的空实现,主要是指对于检测Server端证书是否可信的方法checkServerTrusted,是否是空实现。下面展示问题代码,如果是空实现那么将导致客户端接收任意证书,从而造成加密后的HTTPS消息被中间人解密。

<code class="java"><span class="annotation">@Override</span>
<span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">checkServerTrusted</span><span class="params">(X509Certificate[] x509Certificates, String s)</span> <span class="keyword">throws</span> CertificateException </span>{

}
</code>

检测的方式是通过遍历类中的所有方法,找到checkServerTrusted方法,对方法整体进行检测,确定其是否为空实现,部分代码如下所示:

<code class="java"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">WeakTrustManagerDetector</span> <span class="keyword">implements</span> <span class="title">Detector</span> </span>{
...
<span class="function"><span class="keyword">public</span> <span class="title">WeakTrustManagerDetector</span><span class="params">(BugReporter bugReporter)</span> </span>{
        <span class="keyword">this</span>.bugReporter = bugReporter;
    }

    <span class="annotation">@Override</span>
 <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">visitClassContext</span><span class="params">(ClassContext classContext)</span> </span>{
        JavaClass javaClass = classContext.getJavaClass();

        <span class="comment">//The class extends X509TrustManager</span>
  <span class="keyword">boolean</span> isTrustManager = InterfaceUtils.isSubtype(javaClass,<span class="string">"javax.net.ssl.X509TrustManager"</span>);
        <span class="keyword">boolean</span> isHostnameVerifier = InterfaceUtils.isSubtype(javaClass,<span class="string">"javax.net.ssl.HostnameVerifier"</span>);

<span class="comment">// if (!isTrustManager &amp;&amp; !isHostnameVerifier) return;</span>
 <span class="keyword">if</span> (!isTrustManager &amp;&amp; !isHostnameVerifier){
            <span class="keyword">for</span> (Method m : javaClass.getMethods()) {
                allow_All_Hostname_Verify(classContext, javaClass, m);
            }
        }

        Method[] methodList = javaClass.getMethods();

        <span class="keyword">for</span> (Method m : methodList) {
            MethodGen methodGen = classContext.getMethodGen(m);

            <span class="keyword">if</span> (DEBUG) System.out.println(<span class="string">"&gt;&gt;&gt; Method: "</span> + m.getName());

            <span class="keyword">if</span> (isTrustManager &amp;&amp;
                    (m.getName().equals(<span class="string">"checkClientTrusted"</span>) ||
                     m.getName().equals(<span class="string">"checkServerTrusted"</span>) ||
                     m.getName().equals(<span class="string">"getAcceptedIssuers"</span>))) {
                <span class="keyword">if</span>(isEmptyImplementation(methodGen)) {
                    bugReporter.reportBug(<span class="keyword">new</span> BugInstance(<span class="keyword">this</span>, WEAK_TRUST_MANAGER_TYPE, Priorities.NORMAL_PRIORITY).addClassAndMethod(javaClass, m));
                }
......
</code>

classContext.getJavaClass用于获取整个类的所有内容;javaClass.getMethods用于获取该类中的所有方法,以一个方法列表的形式返回;classContext.getMethodGen用于获取该方法的内容;isEmptyImplementation将方法的内容导入该函数进行检测,用于确定方法是否是空实现,该方法的代码如下所示:

<code class="java"><span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">isEmptyImplementation</span><span class="params">(MethodGen methodGen)</span></span>{
    <span class="keyword">boolean</span> invokeInst = <span class="keyword">false</span>;
    <span class="keyword">boolean</span> loadField = <span class="keyword">false</span>;

    <span class="keyword">for</span> (Iterator itIns = methodGen.getInstructionList().iterator();itIns.hasNext();) {
        Instruction inst = ((InstructionHandle) itIns.next()).getInstruction();
        <span class="keyword">if</span> (DEBUG)
            System.out.println(inst.toString(<span class="keyword">true</span>));

        <span class="keyword">if</span> (inst <span class="keyword">instanceof</span> InvokeInstruction) {
            invokeInst = <span class="keyword">true</span>;
        }
        <span class="keyword">if</span> (inst <span class="keyword">instanceof</span> GETFIELD) {
            loadField = <span class="keyword">true</span>;
        }
    }
    <span class="keyword">return</span> !invokeInst &amp;&amp; !loadField;
}
</code>

该方法主要用于检测方法中是否包含方法调用、域操作,如果没有包含则认为是一个空实现的方法。因此该方法对于只包含 return true/false 语句的方法体同样认为是一个空实现。

2.3 污点分析

数据流分析主要用于分析特定方法加载的参数是否能够被用户控制,即进行污点分析。做污点分析首先需要定义污染源(source点),污染源可以理解为能够被用户控制的输入数据,这里定义的Android污染源主要包括用户的输入、Intent传入的数据,下面展示定义的部分污染源(source点):

<code>- EditText
android/widget/EditText.getText()Landroid/text/Editable;:TAINTED
- Intent
android/content/Intent.getAction()Ljava/lang/String;:TAINTED
android/content/Intent.getStringExtra(Ljava/lang/String;)Ljava/lang/String;:TAINTED
......
- Bundle
android/os/Bundle.get(Ljava/lang/String;)Ljava/lang/Object;:TAINTED
android/os/Bundle.getString(Ljava/lang/String;)Ljava/lang/String;:TAINTED
......
</code>

定义好污染源后就需要确定污染的触发点(sink点),可以理解为会触发危险操作的函数。定义sink点的方式有两种,一种是直接从文件中导入,以命令注入为示例,代码如下:

<code class="java"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CommandInjectionDetector</span> <span class="keyword">extends</span> <span class="title">BasicInjectionDetector</span> </span>{

    <span class="function"><span class="keyword">public</span> <span class="title">CommandInjectionDetector</span><span class="params">(BugReporter bugReporter)</span> </span>{
        <span class="keyword">super</span>(bugReporter);
        loadConfiguredSinks(<span class="string">"command.txt"</span>, <span class="string">"COMMAND_INJECTION"</span>);
 }
</code>

从代码中可以清楚的看到其导入方式是继承BasicInjectionDetector类,然后再该类的构造方法中通过loadConfiguredSinks方法,导入包含sink点的文件,下面展示该示例文件中的内容:

<code>java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process;:0
java/lang/Runtime.exec([Ljava/lang/String;)Ljava/lang/Process;:0
java/lang/Runtime.exec(Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/Process;:0,1
java/lang/Runtime.exec([Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/Process;:0,1
java/lang/Runtime.exec(Ljava/lang/String;[Ljava/lang/String;Ljava/io/File;)Ljava/lang/Process;:1,2
java/lang/Runtime.exec([Ljava/lang/String;[Ljava/lang/String;Ljava/io/File;)Ljava/lang/Process;:1,2
java/lang/ProcessBuilder.&lt;init&gt;([Ljava/lang/String;)V:0
java/lang/ProcessBuilder.&lt;init&gt;(Ljava/util/List;)V:0
java/lang/ProcessBuilder.command([Ljava/lang/String;)Ljava/lang/ProcessBuilder;:0
java/lang/ProcessBuilder.command(Ljava/util/List;)Ljava/lang/ProcessBuilder;:0
dalvik/system/DexClassLoader.loadClass(Ljava/lang/String;)Ljava/lang/Class;:0
</code>

另一种是自定义导入,其实现是通过覆盖BasicInjectionDetector类中的getInjectionPoint方法,以WebView.loadurl方法为例,示例代码如下所示:

<code class="java"><span class="annotation">@Override</span>
 <span class="function"><span class="keyword">protected</span> InjectionPoint <span class="title">getInjectionPoint</span><span class="params">(InvokeInstruction invoke, ConstantPoolGen cpg, InstructionHandle handle)</span> </span>{
        <span class="keyword">assert</span> invoke != <span class="keyword">null</span> &amp;&amp; cpg != <span class="keyword">null</span>;
        String method = invoke.getMethodName(cpg);
        String sig    = invoke.getSignature(cpg);
<span class="comment">// System.out.println(invoke.getClassName(cpg));</span>
 <span class="keyword">if</span>(sig.contains(<span class="string">"Ljava/lang/String;"</span>)) {
            <span class="keyword">if</span>(<span class="string">"loadUrl"</span>.equals(method)){
                <span class="keyword">if</span>(sig.contains(<span class="string">"Ljava/util/Map;"</span>)){
                    <span class="keyword">return</span> <span class="keyword">new</span> InjectionPoint(<span class="keyword">new</span> <span class="keyword">int</span>[]{<span class="number">1</span>}, WEBVIEW_LOAD_DATA_URL_TYPE);
                }<span class="keyword">else</span>{
                    <span class="keyword">return</span> <span class="keyword">new</span> InjectionPoint(<span class="keyword">new</span> <span class="keyword">int</span>[]{<span class="number">0</span>}, WEBVIEW_LOAD_DATA_URL_TYPE);
                }
            }<span class="keyword">else</span> <span class="keyword">if</span>(<span class="string">"loadData"</span>.equals(method)){
                <span class="keyword">return</span> <span class="keyword">new</span> InjectionPoint(<span class="keyword">new</span> <span class="keyword">int</span>[]{<span class="number">2</span>}, WEBVIEW_LOAD_DATA_URL_TYPE);
            }<span class="keyword">else</span> <span class="keyword">if</span>(<span class="string">"loadDataWithBaseURL"</span>.equals(method)){
                <span class="comment">//BUG</span>
 <span class="keyword">return</span> <span class="keyword">new</span> InjectionPoint(<span class="keyword">new</span> <span class="keyword">int</span>[]{<span class="number">4</span>}, WEBVIEW_LOAD_DATA_URL_TYPE);
            }
        }
        <span class="keyword">return</span> InjectionPoint.NONE;
    }
</code>

通过实例化InjectionPoint类构造新的sink点,其构造方法中的第一个参数表示该方法接收污染数据参数的位置,如方法为webView.loadUrl(url),其第一个参数就是new int[]{0},其它的以此类推。

上报发现漏洞的情况,则通过覆盖getPriorityFromTaintFrame方法的实现,示例代码如下所示:

<code class="java"><span class="annotation">@Override</span>
 <span class="function"><span class="keyword">protected</span> <span class="keyword">int</span> <span class="title">getPriorityFromTaintFrame</span><span class="params">(TaintFrame fact, <span class="keyword">int</span> offset)</span>
            <span class="keyword">throws</span> DataflowAnalysisException </span>{
        Taint stringValue = fact.getStackValue(offset);
<span class="comment">// System.out.println(stringValue.getConstantValue());</span>
 <span class="keyword">if</span> (stringValue.isTainted() || stringValue.isUnknown()) {
            <span class="keyword">return</span> Priorities.NORMAL_PRIORITY;
        } <span class="keyword">else</span> {
            <span class="keyword">return</span> Priorities.IGNORE_PRIORITY;
        }
    }
</code>

通过fact.getStackValue获取检测的函数变量,如果该变量被污染(isTainted)或 变量是否被污染未知(但是是可控制变量),那么作为一个中危风险(Priorities.NORMAL_PRIORITY)进行上报,其它的情况则上报为可忽略风险(Priorities.IGNORE_PRIORITY)。

2.4 自定义代码检测

自定义代码检测实现的前半部分同2.2的逐方法检测类似,均是获取类的内容,然后遍历所有的方法,对方法的内容进行检测,但是在具体代码检测实现上是通过自定义分析进行。目前自定义检测只应用到Android中本地拒绝服务的检测。本地拒绝服务的被触发的重要原因在于对通过Intent获取的参数未进行异常捕获,因此检测实现的方式便是检测获取参数的代码行是否被try catch包裹(这个存在误差,待改进)。对于其代码分析,不能使用FindBugs模型进行分析,而是使用最原始的class代码进行分析,原始class代码的形式通过javap命令进行查看,下图展示示例代码。

Android漏洞扫描工具Code Arbiter

对原始class文件进行分析存在的缺陷是无法定位具体的代码行,那么在进行问题上报时无法将问题定位到代码行,因此第一步需要在原有模型的基础上对所有包含Intent获取参数的方法的位置存储到一个Map结构中,方便后面对方法的定位,代码实现如下所示,获取方法所在的行,然后以方法名作为Key值,以代码行相关信息作为Value值,存储到Map中。

<code class="java"><span class="keyword">private</span> Map&lt;String, List&lt;Location&gt;&gt; get_line_location(Method m, ClassContext classContext){
        HashMap&lt;String, List&lt;Location&gt;&gt; all_line_location = <span class="keyword">new</span> HashMap&lt;&gt;();
        ConstantPoolGen cpg = classContext.getConstantPoolGen();
        CFG cfg = <span class="keyword">null</span>;
        <span class="keyword">try</span> {
            cfg = classContext.getCFG(m);
        } <span class="keyword">catch</span> (CFGBuilderException e) {
            e.printStackTrace();
            <span class="keyword">return</span> all_line_location;
        }
        <span class="keyword">for</span> (Iterator&lt;Location&gt; i = cfg.locationIterator(); i.hasNext(); ) {
            Location loc = i.next();
            Instruction inst = loc.getHandle().getInstruction();
            <span class="keyword">if</span>(inst <span class="keyword">instanceof</span> INVOKEVIRTUAL) {
                INVOKEVIRTUAL invoke = (INVOKEVIRTUAL) inst;
 <span class="keyword">if</span>(all_line_location.containsKey(invoke.getMethodName(cpg))){
                        all_line_location.get(invoke.getMethodName(cpg)).add(loc);
                    }<span class="keyword">else</span> {
                        LinkedList&lt;Location&gt; loc_list = <span class="keyword">new</span> LinkedList&lt;&gt;();
                        loc_list.add(loc);
                        all_line_location.put(invoke.getMethodName(cpg), loc_list);
                    }
<span class="comment">// }</span>
 }
        }
        <span class="keyword">return</span> all_line_location;
    }
</code>

之后获取Exception包裹的范围,FindBugs中包含对Exception的建模,因此能够通过其模型能够直接获取其范围并存储到一个列表中,代码如下所示,其中exceptionTable[i].getStartPC用于获取try catch 的起始代码行,exceptionTable[i].getEndPC用于获取try catch 的结束代码行。

<code class="java"><span class="keyword">public</span> <span class="keyword">int</span>[] getExceptionScope(){
        <span class="keyword">try</span> {
            CodeException[] exceptionTable = <span class="keyword">this</span>.code.getExceptionTable();
            <span class="keyword">int</span>[] exception_scop = <span class="keyword">new</span> <span class="keyword">int</span>[exceptionTable.length * <span class="number">2</span>];
            <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; exceptionTable.length; i++) {
                exception_scop[i * <span class="number">2</span>] = exceptionTable[i].getStartPC();
                exception_scop[i * <span class="number">2</span> + <span class="number">1</span>] = exceptionTable[i].getEndPC();
            }
            <span class="keyword">return</span> exception_scop;
        }<span class="keyword">catch</span> (Exception e){
 }
        <span class="keyword">return</span> <span class="keyword">new</span> <span class="keyword">int</span>[<span class="number">0</span>];
    }
</code>

在对代码进行逐行检查时,因为使用的是最原始class文件形式,因此需要限定其遍历的范围,限定的方式是通过代码的行号,即上图中每行代码的第一个数值。首先需要获取代码总行数的大小,获取的方式便是解析FindBugs建模后的第一行代码,找到关键词code-length后面的数值,即为代码的行数,解析代码如下所示:

<code class="java"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">get_Code_Length</span><span class="params">(String firstLineCode)</span></span>{
        <span class="keyword">try</span>{
            String[] split1 = firstLineCode.split(<span class="string">"code_length"</span>);
<span class="comment">// System.out.println(split1[split1.length-1]);</span>
 <span class="keyword">byte</span>[] code_length_bytes = split1[split1.length-<span class="number">1</span>].getBytes();
            <span class="keyword">byte</span>[] new_code_bytes = <span class="keyword">new</span> <span class="keyword">byte</span>[code_length_bytes.length];
            <span class="keyword">for</span>(<span class="keyword">int</span> i=<span class="number">0</span>; i&lt;code_length_bytes.length; i++){
<span class="comment">// System.out.println();</span>
 <span class="keyword">if</span>(code_length_bytes[i]&lt;<span class="number">48</span> || code_length_bytes[i]&gt;<span class="number">57</span>){
                    new_code_bytes[i] = <span class="number">32</span>;
                }<span class="keyword">else</span>{
                    new_code_bytes[i] = code_length_bytes[i];
                }
            }
            <span class="keyword">return</span> Integer.parseInt(<span class="keyword">new</span> String(new_code_bytes).trim());
        }<span class="keyword">catch</span>(Exception e){
            e.printStackTrace();
        }
        <span class="keyword">return</span> <span class="number">0</span>;
    }
</code>

最后对代码进行逐行遍历,遍历中为防止try catch块被遍历到,使用行号来限制遍历的范围。检测代码行是否包含通过Intent获取参数,及该行是否被try catch 包裹,如果上述两个条件均被触发,那么就作为一个问题进行上报。示例代码如下,其中get_code_line_index方法用于获取代码的行号,获取的方式是截取代码行的首字符的数值,以确定是否在代码包裹的范围内。

<code class="java"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">analyzeMethod</span><span class="params">(JavaClass javaClass, Method m, ClassContext classContext)</span> <span class="keyword">throws</span> CFGBuilderException </span>{
        HashMap&lt;String, List&lt;Location&gt;&gt; all_line_location = (HashMap&lt;String, List&lt;Location&gt;&gt;) get_line_location(m, classContext);
        Code code = m.getCode();
        StringCodeAnalysis sca = <span class="keyword">new</span> StringCodeAnalysis(code);
        String[] codes = sca.codes_String_Array();
        <span class="keyword">int</span> code_length = sca.get_Code_Length(sca.get_First_Code(codes));
        <span class="keyword">int</span>[] exception_scop = sca.getExceptionScope();
        <span class="keyword">for</span>(<span class="keyword">int</span> i=<span class="number">1</span>; i&lt;codes.length; i++){
            <span class="keyword">int</span> line_index = sca.get_code_line_index(codes[i]);
            <span class="keyword">if</span> (line_index &lt; code_length){
                <span class="keyword">if</span>(codes[i].toLowerCase().contains(<span class="string">"invokevirtual"</span>) &amp;&amp;
                        (codes[i].contains(<span class="string">"android.content.Intent.get"</span>)  || codes[i].contains(<span class="string">"android.os.Bundle.get"</span>))){
                    <span class="keyword">if</span>(exception_scop.length == <span class="number">0</span>){
                        ......
                    }<span class="keyword">else</span>{
                        <span class="keyword">boolean</span> is_scope = <span class="keyword">false</span>;
                        <span class="keyword">for</span>(<span class="keyword">int</span> j=<span class="number">0</span>; j&lt;exception_scop.length; j+=<span class="number">2</span>){
                            <span class="keyword">int</span> start = exception_scop[j];
                            <span class="keyword">int</span> end = exception_scop[j+<span class="number">1</span>];
                            <span class="keyword">if</span>(line_index &gt;= start &amp;&amp; line_index &lt;= end){
                                is_scope = <span class="keyword">true</span>;
                            }
                            <span class="keyword">if</span>(is_scope){
                                <span class="keyword">break</span>;
                            }
                        }
                        <span class="keyword">if</span>(!is_scope){
                            String method_name = get_method_name(codes[i]);
                            <span class="keyword">if</span>(all_line_location.containsKey(method_name)){
                                <span class="keyword">for</span>(Location loc : all_line_location.get(method_name)){
                                    bugReporter.reportBug(<span class="keyword">new</span> BugInstance(<span class="keyword">this</span>, LOCAL_DENIAL_SERVICE_TYPE, Priorities.NORMAL_PRIORITY).addClass(javaClass).addMethod(javaClass, m).addSourceLine(classContext, m, loc));
                                }
                            }<span class="keyword">else</span> {
                                bugReporter.reportBug(<span class="keyword">new</span> BugInstance(<span class="keyword">this</span>, LOCAL_DENIAL_SERVICE_TYPE, Priorities.NORMAL_PRIORITY).addClass(javaClass).addMethod(javaClass, m));
 }
                        }
                    }
                }
            }
        }
    }
</code>

3 注册打包

上面详细叙述了如何构造自己的问题检测代码,完成检测方法的书写后,下一步就是在配置文件中对检测方法进行注册,才能使检测代码运转起来。

需要在两个文件中进行注册,第一个是findbugs.xml,注册示例如下:

<code>&lt;Detector class="com.h3xstream.findsecbugs.android.LocalDenialOfServiceDetector" reports="LOCAL_DENIAL_SERVICE"/&gt;
&lt;BugPattern type="LOCAL_DENIAL_SERVICE" abbrev="SECLDOS" category="Android安全问题" cweid="276"/&gt;
</code>

其中Detector用于注册该检测方法的位置及其唯一标识,BugPattern用于对检测出的问题进行归类,方便展示,如此处归类到"Android安全问题"中,那么在生成报告的时候问题也将被归类到"Android安全问题"中。

第二个是messages.xml注册,注册示例如下,该注册主要是对该问题进行说明,包括问题的危害及修复方法。

<code>&lt;Detector class="com.h3xstream.findsecbugs.android.LocalDenialOfServiceDetector"&gt;
&lt;Details&gt;Local Denial of Service.&lt;/Details&gt;
&lt;/Detector&gt;
&lt;BugPattern type="LOCAL_DENIAL_SERVICE"&gt;
&lt;ShortDescription&gt;本地拒绝服务&lt;/ShortDescription&gt;
&lt;LongDescription&gt;通过Intent接收的参数未进行异常捕获,导致出现异常使得应用崩溃&lt;/LongDescription&gt;
&lt;Details&gt;
&lt;![CDATA[
    &lt;p&gt;
        &lt;b&gt;危害:&lt;/b&gt;&lt;br/&gt;
        &lt;pre&gt;
            应用崩溃无法使用,影响用户体验;
            被竞争对手利用,进行点对点攻击。
        &lt;/pre&gt;
    &lt;/p&gt;
    &lt;p&gt;
        &lt;b&gt;错误代码:&lt;/b&gt;&lt;br/&gt;
        &lt;pre&gt;
            bundle.getString(""); //未try/catch
            intent.getStringExtra(""); //未try/catch
        &lt;/pre&gt;
    &lt;/p&gt;
    &lt;p&gt;
        &lt;b&gt;解决方案:&lt;/b&gt;&lt;br/&gt;
        &lt;pre&gt;
            对通过Intent接收的参数处理时,进行严格的异常捕获。
            try {
                bundle.getString("");
                intent.getStringExtra(""); 
            }catch (Exception e){}
        &lt;/pre&gt;
    &lt;/p&gt;
]]&gt;
&lt;/Details&gt;
&lt;/BugPattern&gt;
&lt;BugCode abbrev="SECLDOS"&gt;本地拒绝服务&lt;/BugCode&gt;
</code>

一切完成就绪后使用Maven进行打包,就生产了供FindBugs集成开发工具插件使用的JAR包,完成安装并重启,即可使用自定义插件对特定问题进行检测。
最终分析的效果图如下图所示:

Android漏洞扫描工具Code Arbiter

4 结语

本文介绍了Android集成开发环境Android Studio的代码实时检测工具Code Arbiter的产生原因及代码实现,最后展示了分析的效果。通过Code Arbiter在生产环境中的应用,其检测效果还是相当不错,能够发现很多编码过程中存在的问题。但是Code Arbiter仍然存在许多不足,需要优化。后续将在以下两个方面对工具进行改进:

  1. 扩大漏洞检测范围,使Code Arbiter能够囊括Android编码常见安全问题;
  2. 优化漏洞检测规则,提高检测的准确性,减少误报。

5 作者简介

建弋,2016年加入美团点评,目前主要负责金融部门相关的安全工作。对于代码审计/漏洞扫描感兴趣的同学,可以阅读本人Freebuf上发表的相关文章,期待与大家共同学习共同提高。


//下面这个css和插件后台设置的主题有关系,如果需要换样式,则需要修改以下CSS名称

 

除特别注明外,本站所有文章均为铁匠运维网原创,转载请注明出处来自http://www.tiejiang.org/19044.html

切换注册

登录

忘记密码 ?

您也可以使用第三方帐号快捷登录

切换登录

注册

扫一扫二维码分享