Python攻防-APK批量自动反编译与数据分析

前言

日常工作过程中,经常会遇到发现新的攻击模式的情况下,需要全量排查手机上所有 APP 的代码是否存在该类代码缺陷。对于复杂的攻击模式而言,往往需要动用强大静态分析的工具(如 soot 框架或 Codeql),但对于简单的攻击模式而言(比如仅仅是定位某些关键词、关键函数),则通过快速检索即可解决。

但是问题来了,如何在没有 APP 源码的情况下,快速获得所有目标 APP 的代码并进行快速检索?答案当然是借助 jadx 工具对 APK 进行自动化反编译。这涉及几个需要解决的问题:

  1. 如何让以 shell 权限(无需 root)批量从目标手机拉取 APK 文件;
  2. 如何让将拉取到的多个 APK 文件批量、自动化地进行反编译并获得 java 文件;
  3. 如何从获取到的海量数据中快速检索出想要的数据、进行高效率的数据分析?

本文将记录下如何通过编写自定义的 Python 脚本来解决上述场景遇到的测试需求和实现难点。

Pull APK

先来解决第一个问题:批量从目标手机拉取 APK 文件。

1.1 根据包名列表

以下代码是根据 package.txt 文件列出的包名,自动拉取对应的 apk 文件到本地指定路径:

def pullAPK_by_PackageList():
    """
    根据指定的包名列表,批量拉取手机中的APK文件到本地路径
    :return: null
    """
    pkgList = []
    print(Fore.BLUE + "[*]Start pull apk…")
    with open('packageList.txt', 'r', encoding='utf-8') as f:
        for line in f.readlines():
            packageName = line.strip('\n')  # 去除文本中的换行符
            pkgList.append(packageName)
            try:
                pathCmd = "adb shell pm path " + packageName
                result = os.popen(pathCmd).read().strip('\n')  # 去除末尾的换行符,"package:/system/priv-app/aaa.apk"
                pkgPath = result.split(":")[1]  # 截取返回结果中的路径,去除头部多余的"package:"
                pullCmd = "adb pull " + pkgPath + " D:/tmp/Tr0e/TestApk/" + packageName + ".apk"
                os.system(pullCmd)
                print(Fore.GREEN + "[+]Success pull: " + packageName)
            except Exception as e:
                print(Fore.RED + "[-]%s" % e)
                print(Fore.RED + "[-]Pull {0} fail, please check packageName.".format(packageName))
    print(Fore.BLUE + "[*]Done.Enjoy it!")

使用 Android 模拟器来做试验, package.txt 列出的想要 pull 的应用如下:

com.android.email
com.android.calendar
com.android.contacts

代码运行效果如下:
在这里插入图片描述

1.2 根据手机路径

以上的场景局限于在想要 pull 特定的应用列表且已知其 packageName,实际上要批量快速获取手机上所有 APK,那么应该 pull 特定的路径下的整个文件夹,比如:

pathList = ["system/priv-app", "system/app", "hw_product/app"]

代码如下所示:

def pullAPK_by_SystemPath():
    """
    根据手机的app path路径,批量拉取手机中的APK文件到本地路径
    :return: null
    """
    pathList = ["system/priv-app", "system/app"]
    print(Fore.BLUE + "[*]Start pull apk…")
    start = time.time()
    for path in pathList:
        pullCmd = "adb pull " + path + " D:/tmp/Tr0e/pullApk/" + path.replace("/", "_")
        os.system(pullCmd)
        print(Fore.GREEN + "[+]Success pull: " + path)
    end = time.time()
    print(Fore.BLUE + "[*]Done.Totally time is " + str(end - start) + "s.Enjoy it!")

代码运行效果如下所示:
在这里插入图片描述
在这里插入图片描述

以上便解决了批量拉取手机上的 APK 文件的问题。

【技巧】获取手机上所有 apk 文件的路径可以通过如下命令:adb shell find . -iname *.apk 2>/dev/null,不同手机厂商的手机对自研 APK 拥有不同的存储路径。

逆向APK

接下来便可以对 pull 下来的 apk 文件进行自动化的反编译了,以便对 apk 文件的源码进行检索。

2.1 自动化反编译

反编译借助的是 jadx-1.4.5.zip 反编译神器,请自行下载并解压缩到本地文件夹:
在这里插入图片描述
批量反编译的代码如下所示:

apk_list = []  # 递归查询指定文件夹后获得的所有apk文件的路径列表

def apkReverse():
    """
    借助jadx工具,批量反编译指定文件夹下的所有APK(支持文件夹嵌套),输出到outputPath
    :return: null
    """
    apkPath = os.walk("D:/tmp/Android/TestApk/")
    toolPath = "D:/Security/Mobile/jadx/jadx-1.4.4/bin/jadx"
    outputPath = "D:/tmp/Android/Result/"
    find_apk("D:/tmp/Android/TestApk")
    apkTotalNum = len(apk_list)
    num = 1
    start = time.time()
    print(Fore.BLUE + "[*]Start reverse apk…")
    for path, dir_list, file_list in apkPath:  # 反编译apkPath文件夹下所有的apk文件
        for file_name in file_list:
            if file_name.endswith('.apk'):
                print(Fore.GREEN + "*****************************************")
                print("[" + str(num) + "/" + str(apkTotalNum) + "]" + "正在反编译的APK:" + file_name)
                path_apk = os.path.join(path, file_name)
                command = toolPath + " -d " + outputPath + file_name + " -j 4 " + path_apk
                os.system(command)
                num = num + 1
    end = time.time()
    print(Fore.GREEN + "[*]Done.Totally time is " + str(end - start) + "s.Enjoy it!")


def find_apk(file_path):
    """
    递归查询file_path文件夹下的apk文件
    :param file_path: 目标文件夹,如D:/tmp/Android,请留意最后不要加"/"
    :return: 目标文件夹下所有apk文件的列表
    """
    if os.path.isfile(file_path):
        if str(file_path).endswith(".apk"):
            apk_list.append(file_path)
    else:
        for file_ls in os.listdir(file_path):
            find_apk(str(file_path) + "/" + str(file_ls))

为了方便演示,指定待反编译的 APK 文件夹存放如下文件(可以看到,存在子文件夹、非 apk 类型的文件):
在这里插入图片描述在这里插入图片描述
运行脚本进行自动化反编译,效果如下:
在这里插入图片描述
可以看到,程序已经帮我们自动识别出目标文件夹下有哪些 apk 应用并进行了批量反编译,反编译出来的文件结果输出到指定的 outputPath 路径下。

2.2 数据快速检索

完成了 apk 文件的批量拉取和自动化反编译,接下来就可以借助 cmd 命令 findstr 在 Windows 设备上对反编译出来的 APP 代码和资源文件进行快速检索了。

比如在指定文件夹下,忽略大小写、递归搜索所有 java 文件下包含 “getBooleanArrayExtra” 关键词的文件路径:

findstr /I /s "getBooleanArrayExtra" *.java

检索效果如下所示(需要注意的是,jadx 并无法保证一定能反编译成功,比如在应用已加固的情况下将逆向源码失败):
在这里插入图片描述
【注意】以下命令将匹配多个字符串“public”、“ParcelFileDescriptor”、“openFile",而不是完整字符串"public ParcelFileDescriptor openFile":

findstr /s "public ParcelFileDescriptor openFile" *.java

如果想完全匹配搜索 “public ParcelFileDescriptor openFile” 字符串,应使用如下命令:

findstr /s /C:"public ParcelFileDescriptor openFile" *.java

findstr命令参考教程:在Windows中使用Findstr命令搜索文本文件内容

完整命令帮助如下:

Searches for strings in files.

FINDSTR [/B] [/E] [/L] [/R] [/S] [/I] [/X] [/V] [/N] [/M] [/O] [/P] [/F:file]
        [/C:string] [/G:file] [/D:dir list] [/A:color attributes] [/OFF[LINE]]
        strings [[drive:][path]filename[ ...]]

  /B         Matches pattern if at the beginning of a line.
  /E         Matches pattern if at the end of a line.
  /L         Uses search strings literally.
  /R         Uses search strings as regular expressions.
  /S         Searches for matching files in the current directory and all
             subdirectories.
  /I         Specifies that the search is not to be case-sensitive.
  /X         Prints lines that match exactly.
  /V         Prints only lines that do not contain a match.
  /N         Prints the line number before each line that matches.
  /M         Prints only the filename if a file contains a match.
  /O         Prints character offset before each matching line.
  /P         Skip files with non-printable characters.
  /OFF[LINE] Do not skip files with offline attribute set.
  /A:attr    Specifies color attribute with two hex digits. See "color /?"
  /F:file    Reads file list from the specified file(/ stands for console).
  /C:string  Uses specified string as a literal search string.
  /G:file    Gets search strings from the specified file(/ stands for console).
  /D:dir     Search a semicolon delimited list of directories
  strings    Text to be searched for.
  [drive:][path]filename
             Specifies a file or files to search.

Use spaces to separate multiple search strings unless the argument is prefixed
with /C.  For example, 'FINDSTR "hello there" x.y' searches for "hello" or
"there" in file x.y.  'FINDSTR /C:"hello there" x.y' searches for
"hello there" in file x.y.

Regular expression quick reference:
  .        Wildcard: any character
  *        Repeat: zero or more occurrences of previous character or class
  ^        Line position: beginning of line
  $        Line position: end of line
  [class]  Character class: any one character in set
  [^class] Inverse class: any one character not in set
  [x-y]    Range: any characters within the specified range
  \x       Escape: literal use of metacharacter x
  \<xyz    Word position: beginning of word
  xyz\>    Word position: end of word

For full information on FINDSTR regular expressions refer to the online Command
Reference.

数据分析

截至目前为止,我们已经拿到了目标手机里的所有 APK 文件的源码并能够借助文件搜索工具(Windoe 平台的 findstr 或 Linux 平台下的 ag 工具)进行代码关键词的快速检索。接下来还有一个任务要做:如何从海量检索数据中对匹配出来的数据进行进一步的快速固处理和分析?

3.1 txt文本的比较

通过 findstr 检索出来的数据我们可以通过如下命令保存为 txt 格式的文件:

findstr /I /s "getBooleanArrayExtra" *.java >output.txt

实际的应用场景中不免遇到以下需求:

  1. 检索 A、B 关键词依次生成了 output1.txt、output2.txt 文件;
  2. 希望将同时包含 A、B 关键词的 java 文件找出来,即提取 output1.txt、output2.txt 文件检索结果中目标文件路径的交集。

下面将通过代码来快速实现上述需求。此处随机给出一组数据样例,output1.txt:

CeliaKeyboardStub.apk\sources\com\huawei\secure\android\common\intent\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
CeliaKeyboardStub.apk\sources\com\huawei\secure\android\common\intent\SafeIntent.java:            return super.getBooleanArrayExtra(str);
CeliaKeyboardStub.apk\sources\com\huawei\secure\android\common\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
CeliaKeyboardStub.apk\sources\com\huawei\secure\android\common\SafeIntent.java:            return super.getBooleanArrayExtra(str);
Gallery2.apk\sources\com\huawei\gallery\util\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
Gallery2.apk\sources\com\huawei\gallery\util\SafeIntent.java:            return this.mIntent.getBooleanArrayExtra(str);
Gallery2.apk\sources\com\huawei\gallery\util\SafeIntent.java:            GalleryLog.e(TAG, "getBooleanArrayExtra have some exceptions");
Gallery2.apk\sources\com\huawei\himie\vision\Lb.java:    public boolean[] getBooleanArrayExtra(String str) {
Gallery2.apk\sources\com\huawei\himie\vision\Lb.java:            return super.getBooleanArrayExtra(str);
Gallery2.apk\sources\com\huawei\hms\ui\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
Gallery2.apk\sources\com\huawei\hms\ui\SafeIntent.java:            return super.getBooleanArrayExtra(str);
Gallery2.apk\sources\com\huawei\hvi\ability\component\eventbus\EventMessage.java:    public boolean[] getBooleanArrayExtra(String str) {
Gallery2.apk\sources\com\huawei\hvi\ability\component\security\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
Gallery2.apk\sources\com\huawei\hvi\ability\component\security\SafeIntent.java:            return super.getBooleanArrayExtra(str);

output2.txt:

AbilityGallery_entry.apk\sources\androidx\appcompat\app\ActionBar.java:    public void onConfigurationChanged(Configuration configuration) {
AbilityGallery_entry.apk\sources\androidx\appcompat\app\ActionBarDrawerToggle.java:    public void onConfigurationChanged(Configuration configuration) {
AbilityGallery_entry.apk\sources\androidx\appcompat\app\AppCompatActivity.java:    public void onConfigurationChanged(Configuration configuration) {
AbilityGallery_entry.apk\sources\androidx\appcompat\app\AppCompatActivity.java:        super.onConfigurationChanged(configuration);
CeliaKeyboardStub.apk\sources\com\huawei\secure\android\common\SafeIntent.java:    super.onConfigurationChanged(configuration);
AccountKit-base.apk\sources\androidx\appcompat\view\menu\ActionMenuItemView.java:    public void onConfigurationChanged(Configuration configuration) {
AccountKit-base.apk\sources\androidx\appcompat\view\menu\ActionMenuItemView.java:        super.onConfigurationChanged(configuration);
Gallery2.apk\sources\com\huawei\hms\ui\SafeIntent.java:            public void onConfigurationChanged(Configuration configuration) {
AccountKit-base.apk\sources\androidx\appcompat\widget\a.java:    protected void onConfigurationChanged(Configuration configuration) {
AccountKit-base.apk\sources\androidx\appcompat\widget\a.java:        super.onConfigurationChanged(configuration);

程序代码如下所示:

def txtLineList(fileName):
    """
    提取txt文件的每行数据,输出字符串列表
    :param fileName: 待提取数据的txt文件
    :return: 字符串列表
    """
    resultList = []
    with open(fileName, 'r', encoding='utf-8') as f:
        for line in f.readlines():
            resultList.append(line.strip('\n'))  # 去除文本中的换行符
    return resultList


def compareTxtFile(filePath1, filePath2):
    """
    比较两份由不同findstr或ag搜索命令查询出来的txt结果文件,输出共同包含的文件路径列表
    :param filePath1: 待分析的文件1的路径
    :param filePath2: 待分析的文件2的路径
    :return: 输出共同包含的文件路径列表
    """
    print(Fore.BLUE + "[*]Start analyze…")
    fileList1 = txtLineList(filePath1)
    fileList2 = txtLineList(filePath2)
    resultList = []
    middleList = []
    for line in fileList1:
        middleList.append(line.split(":")[0])  # 取每行第一个冒号之前的数据
    for line in fileList2:
        file = line.split(":")[0]
        if file in middleList:
            if file not in resultList:
                resultList.append(file)
                print(Fore.GREEN + "[+]" + file)
    print(Fore.BLUE + "[*]Done.Enjoy it!")
    return resultList

程序运行效果如下:
在这里插入图片描述
可以看到已经正确提取出两个 txt 文件中包含的共同的文件路径的列表。

3.2 txt转换为xlsx

接下来考虑另外一个需求场景:将 txt 文件的数据转换成 xlsx 表格,可以自定义表格需要分成几列数据。将 txt 文件转换成 xlsx 表格的好处在于 excel 具有强大的过滤、检索和数据展示功能。

此处选择数据源如下(检索了包含 “getBooleanArrayExtra” 的代码行):

AbilityGallery_entry.apk\sources\com\huawei\hms\ui\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
AbilityGallery_entry.apk\sources\com\huawei\hms\ui\SafeIntent.java:            return super.getBooleanArrayExtra(str);
AccountKit-base.apk\sources\com\huawei\hms\ui\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
AccountKit-base.apk\sources\com\huawei\hms\ui\SafeIntent.java:            return super.getBooleanArrayExtra(str);
AccountKit-base.apk\sources\com\huawei\secure\android\common\intent\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
AccountKit-base.apk\sources\com\huawei\secure\android\common\intent\SafeIntent.java:            return super.getBooleanArrayExtra(str);
AccountKit-base.apk\sources\com\huawei\secure\android\common\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
AccountKit-base.apk\sources\com\huawei\secure\android\common\SafeIntent.java:            return super.getBooleanArrayExtra(str);
AccountSupplementKit.apk\sources\com\huawei\secure\android\common\b\c.java:    public boolean[] getBooleanArrayExtra(String str) {
AccountSupplementKit.apk\sources\com\huawei\secure\android\common\b\c.java:            return super.getBooleanArrayExtra(str);
adskit-rom.apk\sources\com\huawei\openalliance\ad\ppskit\activity\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
adskit-rom.apk\sources\com\huawei\openalliance\ad\ppskit\activity\SafeIntent.java:            return super.getBooleanArrayExtra(str);
adskit-rom.apk\sources\com\huawei\secure\android\common\intent\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
adskit-rom.apk\sources\com\huawei\secure\android\common\intent\SafeIntent.java:            return super.getBooleanArrayExtra(str);
AirTouch.apk\sources\com\huawei\hms\ui\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
AirTouch.apk\sources\com\huawei\hms\ui\SafeIntent.java:            return super.getBooleanArrayExtra(str);
AirTouch.apk\sources\defpackage\ih.java:    public boolean[] getBooleanArrayExtra(String str) {
AirTouch.apk\sources\defpackage\ih.java:            return super.getBooleanArrayExtra(str);
Audio.apk\sources\com\huawei\secure\android\common\intent\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
Audio.apk\sources\com\huawei\secure\android\common\intent\SafeIntent.java:            return super.getBooleanArrayExtra(str);
Audio.apk\sources\com\huawei\secure\android\common\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
Audio.apk\sources\com\huawei\secure\android\common\SafeIntent.java:            return super.getBooleanArrayExtra(str);
Awareness.apk\sources\com\huawei\hms\nearby\framework\internal\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
Awareness.apk\sources\com\huawei\hms\nearby\framework\internal\SafeIntent.java:            return super.getBooleanArrayExtra(str);
Awareness.apk\sources\com\huawei\hms\ui\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
Awareness.apk\sources\com\huawei\hms\ui\SafeIntent.java:            return super.getBooleanArrayExtra(str);
Awareness.apk\sources\com\huawei\secure\android\common\c.java:    public boolean[] getBooleanArrayExtra(String str) {
Awareness.apk\sources\com\huawei\secure\android\common\c.java:            return super.getBooleanArrayExtra(str);
Awareness.apk\sources\com\huawei\secure\android\common\intent\e.java:            boolean[] booleanArrayExtra = super.getBooleanArrayExtra(str);
Awareness.apk\sources\com\huawei\secure\android\common\intent\e.java:    public boolean[] getBooleanArrayExtra(String str) {
Awareness.apk\sources\com\huawei\secure\android\common\intent\e.java:            return super.getBooleanArrayExtra(str);
BaiduInput_for_Huawei.apk\sources\com\huawei\hms\ui\SafeIntent.java:    public boolean[] getBooleanArrayExtra(String str) {
BaiduInput_for_Huawei.apk\sources\com\huawei\hms\ui\SafeIntent.java:            boolean[] booleanArrayExtra = super.getBooleanArrayExtra(str);

代码如下所示:

def writeTxtToXlsx(txtPath, xlsxPath):
    """
    将txt文件转换成xlsx格式的表格
    :param txtPath: 待转换的txt文件路径
    :param xlsxPath: 输出的xlsx文件路径
    :return: null
    """
    dataSource = {}
    dictCol1List = []
    dictCol2List = []
    lineList = txtLineList(txtPath)
    # 目标行数据样例:“AirTouch.apk\sources\defpackage\ih.java:    public boolean[] getBooleanArrayExtra(String str) {”
    for line in lineList:
        dictCol1List.append(line.split(":")[0])  # 截取每行数据第一个冒号前的数据
        dictCol2List.append(line.split(":")[1].lstrip(" "))  # 截取每行数据第一个冒号后的数据,同时去掉字符串左侧空格
    # 设置xlsx表格每列数据的源数据列表
    dataSource["filePath"] = dictCol1List
    dataSource["codeResult"] = dictCol2List
    # print(dataSource)
    print(Fore.BLUE + "[*]Start write data…")
    writer = pd.ExcelWriter(xlsxPath)
    dataFrame = pd.DataFrame(dataSource)
    dataFrame.to_excel(writer, sheet_name="sheet1")
    writer.close()  # 保存writer中的数据至excel
    print(Fore.BLUE + "[*]Done.Enjoy it!")


if __name__ == '__main__':
    copyRight()
    writeTxtToXlsx("data/output.txt", "data/output.xlsx")
    exit(0)

生成的表格如下:
在这里插入图片描述

总结

工欲善其事,必先利其器。如何将重复的工作通过自动化脚本来完成,是每个安全工程师提高工作效率和漏洞捕获成功率必须面对的问题。同时脚本和工具需要在实战过程中不断改进和优化,最后给各位附上本文的完整代码:GitHub: MyTools

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tr0e

分享不易,望多鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值