移动测试基础 分享一个 Android 混淆检测脚本

Heyniu · 2016年11月28日 · 最后由 songz 回复于 2016年12月15日 · 3754 次阅读

背景

项目中打包经常遇到混淆导致的各种 bug(崩溃、闪退、无厘头),提醒过开发数次,问题还是时而发生。这时,人是不可靠的,错误时而发生,所以做了这么个脚本,旨在抛砖引玉。

思路

目的在于扫描项目,判断是否有实体被混淆。

说明:我们项目中出现最多的问题就是实体被混淆,所以这个脚本只是扫描实体的混淆情况。不排除其他混淆导致的问题,这里只是抛砖引玉,希望通过这个思路,让有被混淆困扰的童鞋能够做好这一块。

  • 传入项目路径,扫描出所有路径
  • 符合实体类的路径存入列表
  • 与混淆配置文件对比
  • 打印被混淆的实体类

实例

检测通过:

Checked pass.
Time: 0.01 s

检测不通过(有实体被混淆):

Errors, has some entities be garble.
com.s.toutiao.entity
Time: 0.01 s

实体被混淆导致的闪退:

FATAL EXCEPTION: main
Process: com.s.d, PID: 30138
java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.String java.lang.CharSequence.toString()' on a null object reference
at com.s.widget.PagerSlidingTabStrip.a(Unknown Source)
at com.s.widget.PagerSlidingTabStrip.setViewPager(Unknown Source)
at com.s.toutiao.fragment.ToutiaoFragment$4.onComplete(Unknown Source)
at com.y.http.async.HttpRequest$1.handleMessage(Unknown Source)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:158)
at android.app.ActivityThread.main(ActivityThread.java:7225)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)

脚本加入 Android Studio 编译环境

  • build.gradle加入下列代码

    android.applicationVariants.all { variant ->
    if (variant.name.contains("release")) {
        def cmd ="python ScanProguard.py 1"
        Process p = cmd.execute()
        def result =  p.text
        if (result.contains("Errors")) {
            throw new RuntimeException("下列实体被混淆了:\n" + result)
        }
    }
    }
    
  • ScanProguard.py 脚本放在 build.gradle 同级目录

  • 编译失败的截图

实体为什么不能混淆?

当然不能混淆了,实体混淆后就变成 abc 之流了,与接口返回的字段就对应不上,肯定会有问题。

代码

#!/usr/bin/evn python
# -*- coding:utf-8 -*-

# FileName ScanProguard.py
# Author: HeyNiu
# Created Time: 2016/11/28
"""
扫描项目实体是否被混淆
1.传入项目路径,扫描出所有路径
2.符合实体类的路径存入列表
3.与混淆配置文件对比
4.打印被混淆的实体类
"""


import os
import time
import sys

temp = sys.argv[1]


class ScanProguard(object):

    def __init__(self, project):
        if not isinstance(project, dict):
            raise TypeError('Project object must be dict.')
        for value in project.values():
            if not os.path.exists(value):
                raise FileNotFoundError('%s' % value)
        self.project_path = project['project_path']
        self.proguard_path = project['proguard_path']
        self.nim_path = ''
        if 'nim_path' in project.keys():
            self.nim_path = project['nim_path']

    @staticmethod
    def __filter_dirs(target_path, dir_name):
        """
        获取目标地址目录下的文件夹列表
        :param dir_name:过滤文件夹名字
        :return:
        """
        return (os.path.join(root, d) for root, dirs, files in os.walk(target_path) for d in dirs if
                dir_name.upper() in d.upper())

    def __get_project_entity(self):
        """
        获取将要进行匹配的项目实体
        :return:
        """
        files = self.__filter_dirs(self.project_path, 'Entity')
        return [str(i.split('\java\\')[-1]).replace('\\', '.') for i in files]

    def __get_nim_entity(self):
        """
        获取将要进行匹配的nim实体
        :return:
        """
        files = self.__filter_dirs(self.nim_path, 'Entity')
        return [str(i.split('\java\\')[-1]).replace('\\', '.') for i in files]

    def __get_proguard_entity(self):
        """
        获取将要进行匹配的项目混淆文件实体
        :return:
        """
        l = open(self.proguard_path, encoding='utf-8').readlines()
        return [i.replace('-keep class ', '').replace('.** {*;}', '').replace('.**  {*;}', '').strip() for i in l
                if 'ENTITY' in i.upper() and '-KEEP CLASS' in i.upper()]

    def print_entity(self):
        """
        打印被混淆的实体类
        :return:
        """
        project_entity = self.__get_project_entity()
        proguard_entity = self.__get_proguard_entity()
        nim_entity = self.__get_nim_entity()
        for i in nim_entity:
            project_entity.append(i)
        l = set(project_entity).difference(set(proguard_entity))
        if len(l) > 0:
            print('Errors, has some entities be garble.')
            for i in l:
                print(i)
        else:
            print('Checked pass.')


class Project:
    """
    这里传的就是项目路径,为了方便写成了这样
    """
    document_path = os.path.join(os.path.expanduser('~'), 'Documents')
    jz = ''
    zf = ''
    zx = ''
    pro = 'proguard-project.pro'
    java = 'src\main\java'
    J = {
        'proguard_path': os.path.join(document_path, jz, jz, 'proguard-rules.pro'),
        'project_path': os.path.join(document_path, jz, jz, java, 's'),
        'nim_path': os.path.join(document_path, jz, 'im', java, 'com')
    }
    ZF = {
        'proguard_path': os.path.join(document_path, zf, zf, pro),
        'project_path': os.path.join(document_path, zf, zf, java, 'com'),
        'nim_path': os.path.join(document_path, zf, 'im', java, 'com')
    }
    XIU = {
        'proguard_path': os.path.join(document_path, zx, 'D', 'proguard-project.txt'),
        'project_path': os.path.join(document_path, zx, 'D', java, 'com')
    }


if __name__ == '__main__':
    start = time.clock()
    if '1' == temp:
        print('A项目')
        s = ScanProguard(Project.J)
        s.print_entity()
    if '2' == temp:
        print('B项目')
        s = ScanProguard(Project.ZF)
        s.print_entity()
    if '3' == temp:
        print('C项目')
        s = ScanProguard(Project.XIU)
        s.print_entity()
    end = time.clock()
    print("Time: %.02f s" % (end - start))

最后

如果有更好的方法或者其他建议,欢迎评论,请轻拍。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 9 条回复 时间 点赞

不错 赞一个

还能这么玩,长知识了!对这方面不了解,请问下,你这里是对开发已经混淆的代码进行检查吗?

#2 楼 @lose 已经混淆的代码就是安装包了,所以当然是源代码的检查了

一般都跑 DEBUG。。。RELEASE 跑了才知道混淆有没有问题的飘过。。。。

#4 楼 @yangchengtest 就是跑 release 之前检测

—— 来自 TesterHome 官方 安卓客户端

好思路,多谢分享

—— 来自 TesterHome 官方 安卓客户端

这里的 jz、zf 和 xiu 是什么意思
jz = ''
zf = ''
zx = ''

ZF = {

}
XIU = {

}

#7 楼 @ady 就是地址吧,你不用管具体内容是什么,只要传入项目路径,混淆文件路径就好

好思路,学习了👍

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册