返回

Smali代码插桩:Android恶意软件分析与日志记录

Android

Smali 级的代码插桩:Android 恶意软件分析

最近在分析一个针对我的应用用户的安卓恶意软件。这个恶意软件能绕过我的篡改检查机制,并且做了相当程度的混淆,直接分析它如何做到这一点相当困难。我有个想法,就是在 smali 代码里插桩,给每个函数调用都加上日志,打印出函数名,帮助我理解控制流。

在 Java 中,我可以用 ASM 框架很方便地做到这一点。但安卓这边,我要么处理 dex 文件,但 ASM 似乎不支持,要么处理 smali 文件,ASM 也不支持。 我知道有些工具像 dex2jar,但这些工具很久没更新,不像 smali 现在直接由谷歌维护。所以, 如果可以, 我希望基于 smali 实现。 有什么好的办法吗?

我有一些想法, 例如自己写一个简单的处理工具。但是自己处理所有的情况很烦人,例如处理超过16个寄存器的函数的时候。

问题根源分析

本质上,这个问题可以归结为:如何在 smali 这个层次上实现类似 ASM 的字节码操作。 需要找到一种能够解析 smali 代码、允许修改和插入代码、并能将修改后的代码重新打包回 dex 的方法。难点在于:

  • Smali 语法解析: 需要一个库能够正确地解析 smali 语法,将其转换为可操作的数据结构(例如抽象语法树 AST)。
  • Smali 代码生成: 在修改完数据结构后,需要一个能将数据结构转换回 smali 代码的工具。
  • 寄存器处理: Smali 使用寄存器来存储数据,需要注意插入的代码不能破坏原有的寄存器使用。 特别是处理多寄存器的使用方式, 因为超过寄存器可能会出大问题。
  • 代码完整性: 确保插入的代码不会破坏原程序的逻辑和结构。

解决方案

以下是几个可行的解决方案:

方案一:使用 Smali/Baksmali + 自定义脚本

Smali 和 Baksmali 是官方提供的用于汇编和反汇编 dex 文件的工具。可以结合自定义脚本实现代码插桩。

原理:

  1. 反汇编: 使用 Baksmali 将 dex 文件反汇编成 smali 文件。
  2. 解析和修改: 编写脚本(例如 Python 脚本)解析 smali 文件,找到每个方法的开头,插入打印日志的代码。
  3. 重新汇编: 使用 Smali 将修改后的 smali 文件重新汇编成 dex 文件。

代码示例(Python 脚本片段):

import os
import re

def insert_log(smali_file):
    with open(smali_file, 'r+') as f:
        content = f.readlines()
        new_content = []
        for i, line in enumerate(content):
            new_content.append(line)
            if line.strip().startswith('.method'):
                method_name = line.strip().split(' ')[-1]
                # 获取参数的寄存器个数
                locals_directive = None
                j = i + 1
                params_count = 0
                while (j<len(content) and not content[j].strip().startswith(".end method") ):
                    if(content[j].strip().startswith(".locals")):
                        locals_directive = content[j].strip()
                        params_count = int(locals_directive.split(' ')[-1])
                    if content[j].strip().startswith(".param"):
                         params_count+= 1
                    j = j + 1
                if(locals_directive is None):
                    continue

                log_code = f'''
    const-string v0, "{method_name}"
    invoke-static {{v0}}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
                '''
                # 需要进行调整,以使用 v{param_count} 或更大的虚拟寄存器,保证其是有效的虚拟寄存器
                safe_log_code = log_code.replace("v0", f"v{params_count}")
                new_content.extend(safe_log_code.splitlines(keepends=True))

        f.seek(0)
        f.writelines(new_content)
        f.truncate()

# 遍历 smali 文件夹
def process_smali_folder(folder):
    for root, _, files in os.walk(folder):
        for file in files:
            if file.endswith('.smali'):
                insert_log(os.path.join(root, file))

# 示例用法
# 假设 smali 文件在名为 'smali' 的文件夹里
#process_smali_folder('smali') #使用的时候解除注释

命令行指令:

  1. 反汇编: baksmali d input.dex -o smali_folder
  2. 运行脚本: python your_script.py
  3. 重新汇编: smali a smali_folder -o output.dex

安全建议:

  • 插入的日志代码可能会影响性能,特别是在频繁调用的方法中。
  • 在处理大型或复杂的 smali 代码时,要仔细检查脚本的逻辑,避免引入错误。
  • 确保插入的代码不会与原代码产生冲突,特别是寄存器的使用。

进阶使用:

  • 可以使用更强大的正则表达式或解析库来处理复杂的 smali 语法。
  • 可以将脚本进一步扩展,例如支持过滤特定类或方法, 根据配置文件的选项选择开关log功能。
  • 可以根据函数原型进行参数的获取与日志输出。

方案二:使用 Apktool + 自定义脚本

Apktool 是一个更高级的工具,它内部使用了 Smali/Baksmali,并提供了解析和打包整个 APK 的功能。

原理:

  1. 反编译: 使用 Apktool 反编译 APK 文件,这将自动调用 Baksmali 生成 smali 文件。
  2. 解析和修改: 编写脚本(例如 Python)处理 smali 文件,插入日志代码(同方案一)。
  3. 重新打包: 使用 Apktool 重新打包 APK。

代码示例:

同方案一的 Python 脚本片段。

命令行指令:

  1. 反编译: apktool d input.apk -o output_folder
  2. 运行脚本: python your_script.py (修改脚本中的文件夹路径为 output_folder/smali)
  3. 重新打包: apktool b output_folder -o output.apk

安全建议: 同方案一。

进阶使用: 同方案一。 Apktool可以设置frame-path等参数, 应对某些特殊的反编译情况.

方案三:使用 Smalidea 插件 (IntelliJ IDEA / Android Studio)

Smalidea 是一个 IntelliJ IDEA 和 Android Studio 的插件,它提供了 smali 语言的语法高亮、调试等功能。虽然它不能直接进行字节码操作,但可以辅助进行代码分析和修改。

原理:

使用 Smalidea, 用户可以更好理解Smali 代码的结构和流程,从而能够更加清晰、有目标性的定制脚本,并可以在IDE中通过debugger进行测试分析,减少错误。

安装步骤:

在 IDE 的插件市场中搜索 "Smalidea" 并安装。

使用技巧:

  • Smalidea 对Smali 语法进行了高亮,看起来更舒服了.
  • Smalidea 支持调试,可以很方便的打断点测试和验证代码行为, 是分析疑难杂症的好工具.

自定义方案的补充说明:

无论是方案一还是方案二, 都涉及到直接编辑 smali 文件。 务必注意, 对现有寄存器的错误修改可能会导致应用崩溃,或更严重的后果. 在插入 log 的时候, 我们新加了一条指令 const-string vX, "方法名"。 其中寄存器 vX 必须是可用的。 为了减少错误的概率, 一种方式是读取 .locals 指令后的数字, 获取可用的最大寄存器编号+1 作为 vX. 但是这样依然是有问题的, 遇到非标准代码可能存在风险, 例如方法体内又修改了这个数字的情况.

比较好的方案是在插入之前对代码进行完整、正确的寄存器分析。 可以参考的库有 smali-instrumentation. 这个库可以用来扫描所有smali, 分析计算所有函数的最大局部寄存器的编号,然后以此作为依据,选择新的寄存器用于后续的操作。

或者,也可以使用更为复杂的数据流分析和寄存器分配算法来确定可用的寄存器。