第二章 如何分析 Android 程序

文章目录

  • 编写第一个 Android 程序
    • 创建 Android 工程
    • 编译生成 APK 文件
      • 通过命令行方式生成
      • 通过 Android Studio 编译生成
  • 破解第一个 Android 程序
    • 破解入手
    • 反编译 APK 文件
    • 分析 APK 文件
    • 修改 smali 文件的代码
    • 重新编译 APK 文件并签名
    • 安装和测试
    • 小结

  • 分析 Android 程序是开发 Android 程序的逆过程,要想分析一个 Android 程序,先要了解其开发流程、程序结构、语句分支、解密原理等

编写第一个 Android 程序

  • IDE:Android Studio

创建 Android 工程

  • 新建工程
    第二章 如何分析 Android 程序_第1张图片

  • 选择“Empty Activity”
    第二章 如何分析 Android 程序_第2张图片

  • 命名为“Crackme0201”,其他选项如图:
    第二章 如何分析 Android 程序_第3张图片

  • 打开工程布局文件 activity_main.xml,修改如下:




    

    

        
        

    

    

        
        

    

        
  • 打开 strings.xml,修改如下:

    Crackme0201
    Settings
    Crackme0201
    Android程序破解演示实例
    用户名:
    注册码:
    注    册
    请输入用户名
    请输入16位的注册码
    程序未注册
    程序已注册
    无效用户名或注册码
    恭喜您!注册成功

  • 编写 MainActivity 类的代码,具体如下:
package com.droider.crackme0201;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MainActivity extends AppCompatActivity {

    private EditText edit_userName;
    private EditText edit_sn;
    private Button btn_register;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        setTitle(R.string.unregister);
        edit_userName = (EditText)findViewById(R.id.edit_username);
        edit_sn = (EditText)findViewById(R.id.edit_sn);
        btn_register = (Button)findViewById(R.id.button_register);
        btn_register.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // trim():去掉原始字符串头部和尾部的空格
                if (!checkSN(edit_userName.getText().toString().trim(),
                        edit_sn.getText().toString().trim())) {
                    // 验证失败后弹出注册失败的 Toast
                    Toast.makeText(MainActivity.this,
                    R.string.unsuccessed,
                    Toast.LENGTH_SHORT).show();
                }
                else {
                    // 验证成功后弹出注册成功的 Toast
                    Toast.makeText(MainActivity.this,
                        R.string.successed,
                        Toast.LENGTH_SHORT).show();
                    // 设置按钮不可点击
                    btn_register.setEnabled(false);
                    // 设置标题
                    setTitle(R.string.registered);
                }
            }
        });
    }

    // 验证注册码函数
    private boolean checkSN(String userName, String sn) {
        try {
            if ((userName == null) || (userName.length() == 0)) {
                return false;
            }
            if ((sn == null) || (sn.length() != 16)) {
                return false;
            }
            // MessageDigest 类为应用程序提供信息摘要算法的功能,
            // 如 MD5 或 SHA 算法。信息摘要是安全的单向哈希函数,
            // 它接收任意大小的数据,输出固定长度的哈希值。
            // MessageDigest 对象开始被初始化
            MessageDigest digest = MessageDigest.getInstance("MD5");
            // 调用 reset 方法重置摘要
            digest.reset();
            // 使用 update 方法处理数据
            digest.update(userName.getBytes());
            // 所有要更新的数据都被更新后,
            // 应调用 digest 方法之一完成哈希计算
            byte[] bytes = digest.digest();
            String hexstr = toHexString(bytes, "");
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < hexstr.length(); i += 2) {
                sb.append(hexstr.charAt(i));
            }
            String userSN = sb.toString();
            // 不区分大小写
            if (!userSN.equalsIgnoreCase(sn)) {
                return false;
            }
        }
        catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return false;
        }
        return false;
    }

    // 加密函数
    private static String toHexString(byte[] bytes, String separator) {
        StringBuilder hexString = new StringBuilder();
        for (byte b: bytes) {
            String hex = Integer.toHexString(b & 0xff);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex).append(separator);
        }
        return hexString.toString();
    }
}

编译生成 APK 文件

  • 方法:
  • 用命令行的方式生成
  • 用 Android Studio 的 Build 菜单构建

通过命令行方式生成

  • 在 Android Studio 底部的 Terminal 中输入命令,如下图:
    第二章 如何分析 Android 程序_第4张图片
  • 可以看到,输出了项目目录
  • Crackme0201.iml:Android Studio 生成的项目配置文件
  • local.properties:本地配置文件,记录了当前系统中安装 Android SDK 和 Android NDK 的路径
  • app:项目目录,存放了程序的代码和资源
  • settings.gradle:指定了包含的项目模块的路径
  • build(暂无):命令执行后输出文件的存放目录
  • build.gradle:项目的构建脚本
  • gradlew:macOS 和 Ubuntu 系统用户使用的脚本编译命令
  • gradlew.bat:与 gradle 的功能相同,专门供 Windows 用户使用
  • gradlew 是基于 task 来编译项目的,可在命令行执行 gradlew tasks 命令,列出所有支持的 task:
    第二章 如何分析 Android 程序_第5张图片
    第二章 如何分析 Android 程序_第6张图片
    第二章 如何分析 Android 程序_第7张图片
    第二章 如何分析 Android 程序_第8张图片
  • 从输出可看出,支持的 task 有很多,这里只关注编译时使用的 Build tasks 和安装 APK 时使用的 Install tasks
  • 要想生成 Debug 调试版本的 APK,只需执行 gradle assembleDebug 命令。编译后会在 Crackme0201/app/build/outputs/apk/debug 目录下生成 app-debug.apk 文件
    第二章 如何分析 Android 程序_第9张图片
  • 安装 Debug 调试版本的 APK,只需执行 gradle installDebug 命令。安装时,gradlew 会将 APK 安装到与系统相连的设备或模拟器上。若系统没有与 Android 设备连接,或没有配置 Android 模拟器,则安装失败,输出异常信息“com.android.builder.testing.api.DeviceException:No connected devices!”
    第二章 如何分析 Android 程序_第10张图片
    第二章 如何分析 Android 程序_第11张图片

通过 Android Studio 编译生成

  • 写好程序代码逻辑后,在 AS 上点击 “运行”按钮,会自动编译 APK,并安装到连接的设备后运行程序
    第二章 如何分析 Android 程序_第12张图片
    第二章 如何分析 Android 程序_第13张图片
    第二章 如何分析 Android 程序_第14张图片

破解第一个 Android 程序

  • 破解刚刚写好的 Crackme0201 程序

破解入手

  • 通常的方法:
  • 使用 ApkTool 反编译 APK 文件,生成 smali 格式的反汇编代码
  • 阅读 smali 代码理解程序运行机制,找到突破口,并对代码进行修改
  • 使用 ApkTool 重新编译生成 APK 文件并对其签名
  • 运行测试
  • 如若没成功,则循环上述步骤,直到程序被破解
  • 还可使用 IDA Pro 直接分析 APK 文件,使用 dex2jar 与 jd-gui 配合进行 Java 源码级的分析等(暂不涉及)

反编译 APK 文件

  • ApkTool 是一款跨平台的 APK 反编译工具
  • jar 包下载地址:https://bitbucket.org/iBotPeaches/apktool/downloads/
  • 针对不同的系统,还要下载相关的运行时包装脚本(Windows 版本):
    https://github.com/iBotPeaches/Apktool/blob/master/scripts/windows/apktool.bat
  • 将 jar 包和脚本放在同一个目录,然后将该路径添加到系统的 Path 环境变量中,即完成安装
  • 生成 Crackme0201 的 Release 版本,如图:
    第二章 如何分析 Android 程序_第15张图片
    第二章 如何分析 Android 程序_第16张图片
    第二章 如何分析 Android 程序_第17张图片
    第二章 如何分析 Android 程序_第18张图片
    第二章 如何分析 Android 程序_第19张图片
    第二章 如何分析 Android 程序_第20张图片
  • 输入命令:gradle assembleRelease
    第二章 如何分析 Android 程序_第21张图片
  • 执行完毕即可在当前项目的 app/build/outputs/apk/release 目录下生成 app-release.apk 文件
  • 打开命令行,执行如下命令:
apktool d APK路径 -o 生成反编译文件的路径

第二章 如何分析 Android 程序_第22张图片
第二章 如何分析 Android 程序_第23张图片

  • 如无意外会在指定目录生成反编译文件(包括一系列目录和文件),如下图:
    第二章 如何分析 Android 程序_第24张图片
  • smali 目录:存放了程序的所有反汇编代码
  • res 目录:存放了程序中所有的资源文件
  • 这些目录的子目录和文件的组织结构与开发时源码目录的组织结构是一致的

分析 APK 文件

  • 如何寻找突破口是分析一个程序的关键。对大部分 Android 程序来说,错误提示信息是指引我们找到关键代码的明灯。错误提示代码附近通常就是程序的核心验证代码,可通过阅读这些代码理解程序的注册流程
  • 错误提示属于 Android 程序中的字符串资源。开发 Android 程序时,这些字符串可能会被硬编码到源码中,也可能引用自 res/values 目录下的 strings.xml 文件。APK 文件打包时,strings.xml 中的字符串被加密存储为 resources.arsc 文件并保存到 APK 程序包中;若 APK 文件被成功反编译,这个文件就解密了
  • 在我们编写的程序逻辑中,若注册失败,会以 Toast 的形式弹出提示信息,那我们大可以此为线索寻找关键代码。res/values/strings.xml 文件的内容如下,除了系统默认生成的一系列以“abc_”开头的字符串,都是 Crackme0201 使用的字符串:
    第二章 如何分析 Android 程序_第25张图片
  • 在开发 Android 程序时,strings.xml 文件中的所有字符串资源都在 gen//R.java 文件的 String 类中标识,每个字符串都有唯一的 int 类型的索引值。使用 ApkTool 反编译 APK 后,所有的索引值保存在 strings.xml 同一目录的 public.xml 中。上图的代码中,“无效用户名或注册码”的字符串名称为“unsuccessed”
  • 输出 public.xml 的内容,如下:
    第二章 如何分析 Android 程序_第26张图片
  • “unsuccessed” 的 id 值为 0x7f0c0028
  • 搜索 0x7f0c0028:
    第二章 如何分析 Android 程序_第27张图片
  • 除了 public.xml 和 R$string.smali 资源文件,只有 MainActivity$1.smali 文件一处进行了调用,代码如下:
    第二章 如何分析 Android 程序_第28张图片
    第二章 如何分析 Android 程序_第29张图片
    第二章 如何分析 Android 程序_第30张图片
  • 通过阅读这些代码,发现下面这行代码调用了 checkSN() 方法进行注册码核验:
    在这里插入图片描述
  • 接着是这三行代码:
    第二章 如何分析 Android 程序_第31张图片
  • checkSN() 方法返回 Boolean 类型的值(“V”)。上图中第一行代码将返回结果保存到 p1 寄存器中,第二行代码现在不去管,第三行代码对 p1 进行判断,如果其值不为 0(即条件为真)就跳转到 cond_0 处,反之程序继续执行
  • 如果不跳转,就执行下列代码:
    在这里插入图片描述
  • 可以看出,执行这些代码会弹出“无效用户名或注册码”的 Toast,可见不能往这儿执行
  • 如果跳转,就执行下列代码:
    第二章 如何分析 Android 程序_第32张图片
  • 可以看出,执行这些代码会弹出注册成功的 Toast,可见应该往这儿跳转
  • 因此,关键代码即是:
    在这里插入图片描述

修改 smali 文件的代码

  • 找到了关键代码,现在将其修改,即可破解程序:
    第二章 如何分析 Android 程序_第33张图片
  • if-eqz 表示比较结果为 0 或相等时跳转
  • 保存

重新编译 APK 文件并签名

  • 修改 smali 文件后,要将该文件重新编译,打包成 APK:
    第二章 如何分析 Android 程序_第34张图片
  • 回编译完成后,会在指定的目录(此处为当前目录)生成指定名称的 APK(此处为 new.apk)
  • 但是,此时的 new.apk 是没有签名的,要用 signapk 对 APK 文件签名
  • 将下图中的三个文件(可从 Android 系统源码中提取)和要签名的文件放在同一个目录
    第二章 如何分析 Android 程序_第35张图片
  • 执行如下命令,即可生成签名后的 APK:
    第二章 如何分析 Android 程序_第36张图片

安装和测试

  • 启动 Android 模拟器或 Android 设备
  • 执行如下命令,卸载之前安装的 Crackme0201 程序
    第二章 如何分析 Android 程序_第37张图片
  • 执行如下命令,安装签名后的 APK
    第二章 如何分析 Android 程序_第38张图片
  • 运行新安装的 Crackme0201,任意输入,点击“注册”:
    第二章 如何分析 Android 程序_第39张图片
  • 至此,破解完成

小结

  • 破解 Android 程序的流程为:反编译 -> 分析 -> 修改 -> 回编译 -> 签名
  • 除了命令行操作外,还可用集成工具破解,如 Android Killer 等

你可能感兴趣的:(《Android,软件安全权威指南》学习笔记,android,反编译,android,studio,安全)