Android 静态代码扫描流程及工具说明

作者: MarcusMa | 来源:发表于2017-06-16 17:31 被阅读2972次

1. 静态扫描流程

1.1 版本发布流程

大致分为5个阶段,静态代码扫描的工作在第3步进行,如图:

版本发布流程图

1.2 典型案例分析

  • [空指针]空指针引用
  • [内存泄露]Stream资源关闭
  • [性能]使用indexOf(字符)
  • [兼容]系统API兼容性隐患
  • [越界]数组下标越界隐患
  • [异常] 使用除法或求余没有判断分母长度隐患
  • [SQL]注入风险
  • [应用安全] AndroidMannifest.xml文件中allowBackup设置为true时会导致数据泄露

更多的错误检查示例请查看各检查工具的检查规则说明文档。

1.2.1 [空指针]空指针引用

错误位置:4

public class StringUtil {
  public static final String queryParams(String param) {
    String ret = "";
    if (param != null || param.length() > 2) {
      ret = param.substring(1, param.length() - 1);
    }
  return ret;
}

存在空指针引用,会导致空指针异常。解决方案:

public class StringUtil {
  public static final String queryParams(String param) {
    String ret = "";
    if (param != null && param.length() > 2) {
      ret = param.substring(1, param.length() - 1);
    }
  return ret;
}

1.2.2 [内存泄露]Stream资源关闭

错误位置:17

private static void write2logfile(String msg) {
    try {

        File sdCardDir = android.os.Environment
                .getExternalStorageDirectory();

        File logfile = new File(sdCardDir.getAbsolutePath()
                + File.separator + logfileName);

        if (!logfile.exists()) {
            logfile.createNewFile();
        }

        msg += "\n";

        FileOutputStream outputStream = new FileOutputStream(logfile, true);
        outputStream.write(msg.getBytes());
        outputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

资源对象在被关闭或者Return之前可能出现异常,导致无法正常关闭或Return。比如连续关闭多个资源对象时没有进行异常捕获,或者资源对象在Return之前进行了未捕获异常的操作。解决方案:

private static void write2logfile(String msg) {
    try {

        File sdCardDir = android.os.Environment
                .getExternalStorageDirectory();

        File logfile = new File(sdCardDir.getAbsolutePath()
                + File.separator + logfileName);

        if (!logfile.exists()) {
            logfile.createNewFile();
        }

        msg += "\n";

        FileOutputStream outputStream = new FileOutputStream(logfile, true);
        outputStream.write(msg.getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    } finally{
      if(null != outputStream){
        outputStream.close();
      }
    }
}

1.2.3 [性能]使用indexOf(字符)

错误位置:340

338                         int index = result.indexOf(Keyword);
339                         line = result.substring(index + Keyword.length());
340                         index = line.indexOf(" ");
341                         kernelVersion = line.substring(0, index);
342                     }
343                 } catch (IndexOutOfBoundsException e) {

当你检测单个字符的位置时使用String.indexOf(字符),它执行的很快。
解决方案:不要使用indexOf(字符串)。

340                         index = line.indexOf(' ');

1.2.4 [兼容]系统API兼容性隐患

public static String getSupportMap(Context context, String seInfo) {
    StringBuffer support = new StringBuffer("000");
    if (!"000".equals(seInfo)) {
        support.setCharAt(2, '1');
    }

    if (VERSION.SDK_INT < 10) {
        return support.toString();
    }

    NfcManager manager = (NfcManager) context
            .getSystemService(Context.NFC_SERVICE);
    NfcAdapter adapter = manager.getDefaultAdapter();
    if (null == adapter) {
        return support.toString();
    } else {
        if (adapter.isEnabled()) {
            support.setCharAt(0, '1');
        } else {
            support.setCharAt(0, '2');
        }

        if (VERSION.SDK_INT >= 19) {
            PackageManager pm = context.getPackageManager();
            boolean hasNfcHce = pm
                    .hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
            if (hasNfcHce) {
                support.setCharAt(1, '1');
            }
        }
    }

    return support.toString();
}

getDefaultAdapter方法不支持:10(android2.3.3) 以下的版本。
解决方案:加入对版本的系统版本的判别

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) {
       // 包含新API的代码块
else
{
// 包含旧的API的代码块
}

1.2.5 [越界]数组下标越界隐患

private void doSelectItem(int pos) {
    mButtonViews[mSelectedButtonIndex].line.setVisibility(View.GONE);
    mButtonViews[mSelectedButtonIndex].buttonText.setTextColor(Color.BLACK);
    mButtonViews[mSelectedButtonIndex].contentView.setVisibility(View.GONE);
    mButtonViews[pos].line.setVisibility(View.VISIBLE);
    mButtonViews[pos].buttonText
            .setTextColor(KDimens.K_COLOR_PROM_INDICATOR);
    mButtonViews[pos].contentView.setVisibility(View.VISIBLE);
    mSelectedButtonIndex = pos;
}

采用下标的方式获取数组元素时,如果下标越界,将产生java.lang.ArrayIndexOutOfBoundsException的异常,导致app出现Crash。
解决方案:在使用下标的方式获取数组元素时,需判断下标的有效性。

1.2.6 [异常] 使用除法或求余没有判断分母长度隐患

public static Drawable zoomDrawable(Context context, Drawable in, int scaledW, int scaledH) {
  Drawable zoomed = null;
  if (in instanceof BitmapDrawable) {
      Bitmap bm = ((BitmapDrawable) in).getBitmap();
      if (scaledH != -1 && scaledW == -1) {
          scaledW = (int) ((float) (bm.getWidth() / bm.getHeight()) * scaledH);
      } else if (scaledH == -1 && scaledW != -1) {
          scaledH = (int) ((float) (bm.getHeight() / bm.getWidth()) * scaledW);
      }

      Bitmap sbm = Bitmap.createScaledBitmap(bm, scaledW, scaledW, true);
      zoomed = new BitmapDrawable(context.getResources(), sbm);
  }
  return zoomed;
}

使用除法或者求余运算时,如果分母是通过调用函数返回的int,未对返回值进行判断,当返回值为0时,会出现java.lang.ArithmeticException: / by zero异常。
解决方案:调用函数前,对函数的返回值的长度进行判断。

1.2.6 [SQL]注入风险

描述:对Content Provider进行增删改查操作时,程序没有对用户的输入进行过滤,未采用参数化查询的方式,可能导致sql注入攻击。

代码示例:

private SQLiteDatabase db;
db.rawQuery("select * from person", null);//触发规则

推荐写法:

  1. 服务端充分校验参数
  2. 使用参数化查询,比如SQLiteStatement
  3. 避免使用rawQuery()方法
  4. 对用户输入进行过滤
SQLiteStatement sqLiteStatement = db.compileStatement("insert into msgTable(uid, msg) values(?, ?)");
sqLiteStatement.bindLong(1, 12);
sqLiteStatement.bindString(3, "text");
long newRowId = sqLiteStatement.executeInsert();

1.2.7 [应用安全] AndroidMannifest.xml文件中allowBackup设置为true时会导致数据泄露

描述:建议将AndroidMannifest.xml文件android:allowBackup属性设置为false。当allowBackup标志值为true时,攻击者可通过adb backup和adb restore来备份和恢复应用程序数据。

推荐写法:

描述:建议将AndroidMannifest.xml文件android:allowBackup属性设置为false。当allowBackup标志值为true时,攻击者可通过adb backup和adb restore来备份和恢复应用程序数据。

推荐写法:

  1. minSdkVersion不低于9。
  2. android:allowBackup属性显示设置为false。

1.3 Android 客户端扫描流程

分为3个阶段:

Android客户端扫描流程图

1.3.1 基础内容扫描

使用 Android Lint 对项目进行扫描,该工具已将扫描到的问题进行了分组,同时定义了问题的严重级别: errorwarning。在 Android StudioInspection Results 视图窗中,点击问题标题即可在右边的详情视图中查看该问题的具体解释等内容,针对部分内容还有直接进行自动修复的按钮,如图:

Android Lint

关注点

由于检查的内容繁多,我们重点关注以下几个问题组的相关内容:

  • Android 开头的组,例如

    • Android > Lint > Correctness (可能影响程序正确性)
    • Android > Lint > Performance (可能影响程序性能)
    • Android > Lint > Security (可能影响程序安全性)
    • 等等
  • Class structure 组:指出类的设计上可能存在的问题

  • Code style issues 组:有助于提供代码书写规范

  • Probable bugs 组:有助于发现隐藏的问题

检查通过标准

上述的列出的问题组别不出现或者出现但只包含warning类型的问题

1.3.2 可能引起Crash的问题扫描

使用360火线Godeyes对项目进行扫描,下面将分别说明二者扫描时的关注点和检查通过标准:

360火线

360火线共有61个检查项,按级别分为Block风险建议优化,检查报告以html文件输出,
按规则分类查看的Tab中,可以查看具体问题位置及示例代码。如图

360 fireline 360 fireline
关注点

重点关注Block风险两类标记的问题

检查通过标准

扫描结果中不出现Block风险两类问题

Godeyes

Godeyes共检查23个错误,扫描结果以html文档的方式输出,文档中包含了检查问题的描述示例以及推荐方案,方便理解。值得注意的是在扫描结果的显示上,报告只会给出问题所在的行号。如图

godeyes
关注点

所有列表检查出的问题。

检查通过标准

各扫描项扫描结果为0

1.3.3 空指针和资源泄露扫描

使用Infer工具对可能的空指针可能的资源泄露进行扫描,Infer工具会在项目的根文件夹下生成infer-out的文件夹,重点关注bugs.txt这个文件,文件中会详细指出可能存在的问题的代码片段及相应的解释,示例如下:

Found 70 issues

219: error: RESOURCE_LEAK
   resource of type `java.io.DataInputStream` acquired to `dis` by call to `new()` at line 159 is not released after line 219
**Note**: potential exception at line 164
  217.                  dis.close();
  218.                  is.close();
  219. >            } catch (IOException e) {
  220.                  e.printStackTrace();
  221.                  dr = null;

error: NULL_DEREFERENCE
  object returned by `getItemByName("instalment")` could be null and is dereferenced at line 402
  400.           } else {
  401.               ((UPDropDownWidget) getItemByName(Rules.TYPE_INSTALMENT))
  402. >                     .setmCanShow(true);
  403.               ((UPDropDownWidget) getItemByName(Rules.TYPE_INSTALMENT))
  404.                       .onCheckBoxStatusChanged(true);
关注点

所有列表检查出的问题。

检查通过标准

对检查的问题尽量修复或者编写保护语句避免抛出异常。

2. 工具使用

以下内容均以 Android Studio 为默认的开发环境

2.1 Android Lint

该工具已经默认集成 Android Studio

使用方法:

Android Stuido -> 菜单栏 -> Analyze -> Inspect Code -> 根据需要选择相应的扫描范围 -> OK -> 启动扫描

如图:

Android Lint
Android Lint
Android Lint

参考资料

2.2 360 火线

使用方法

详情参考 官方使用方法
火线插件目前可以在Android Studio中进行在线搜索安装。

  • jar包版本使用

    java -jar D:\test\fireline.jar -s=D:\test\TestCase -r=E:\RedlineReport
    // 参数解释:
    //【必填项】-s或scanSrcDir为被扫描的项目工程路径
    //【必填项】-r或reportSaveDir为火线报告输出路径
    
360 fireline
  • Android stuido版本
    1. Android Studio -> 菜单栏 -> File -> Settings... -> Plugins
    2. 搜索框 -> 搜索fireline -> install -> 重启
    3. 使用 -> Project视图 -> 鼠标右键 -> fireline -> run 生成报告
360 fireline 360 fireline

参考资料

2.3 Godeyes

使用方法

详情参考 官方使用方法

  1. 下载 Android Studio版本插件 下载地址

  2. Android Studio-> 菜单栏 -> File -> Settings -> Plugins

  3. 选择Install plugin from disk -> 选择已下载的Godeyes_Android_Vx.x_(for_AndroidStudio).zip -> OK -> 安装完成 -> 重启

  4. Project视图 -> 鼠标右键 -> Run Godeyes -> 生成报告

godeyes godeyes
参考资料

2.4 Infer

注意:

  1. 仅支持 Mac 和 Linux 环境
  2. 需要Python 且 Python >= 2.7

使用方法

cd {项目的根目录}
./gradlew clean
infer -- ./gradlew build

一段时间后会在项目的根目录下生成infer-out这个文件夹,里面的bugs.txt文档里记录的就是扫描出的问题。

参考资料

3. 疑问解答

3.1 Android Studio 按照教程安装完Godeyes后扫描项目发现没有得到的报告没有任何错误,说明项目没有任何问题么?

不一定。Godeyes插件使用前,需要设置输出报告类型,若都两种类型都没选择,则生成的html报告中就会是0错误。如果这里也设置了但是html报告中还是0个错误则说明你的代码没有问题。

godeyes bug

3.2 Android Lint 工具检查的项目太多了,只关注error就可以了吧?

并不是,Android Lint 检查项目繁琐是由于他自身也集成了一些检查工具。例如FindBugs,单独用Findbugs检查的内容基本都涵盖在了 Android Lint检查的 Probable bugs 分组中,但Android Lint 只将这些错误视为warning,而在FindBugs则可能是Scary(严重问题)的,所以除了errorwarning也是必须关注的。

相关文章

网友评论

    本文标题:Android 静态代码扫描流程及工具说明

    本文链接:https://www.haomeiwen.com/subject/obemqxtx.html