文章目录
- 分析 Android 程序是开发 Android 程序的逆过程,要想分析一个 Android 程序,先要了解其开发流程、程序结构、语句分支、解密原理等
编写第一个 Android 程序
- IDE:Android Studio
创建 Android 工程
-
新建工程
-
选择“Empty Activity”
-
命名为“Crackme0201”,其他选项如图:
-
打开工程布局文件 activity_main.xml,修改如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/info"
android:textSize="20sp"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/username"/>
<EditText
android:id="@+id/edit_username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:hint="@string/hint_username"
android:ems="10"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sn"/>
<EditText
android:id="@+id/edit_sn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:ems="10"/>
</LinearLayout>
<Button
android:id="@+id/button_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:layout_gravity="right"
android:text="@string/register"/>
</LinearLayout>
- 打开 strings.xml,修改如下:
<resources>
<string name="app_name">Crackme0201</string>
<string name="menu_settings">Settings</string>
<string name="title_activity_main">Crackme0201</string>
<string name="info">Android程序破解演示实例</string>
<string name="username">用户名:</string>
<string name="sn">注册码:</string>
<string name="register">注 册</string>
<string name="hint_username">请输入用户名</string>
<string name="hint_sn">请输入16位的注册码</string>
<string name="unregister">程序未注册</string>
<string name="registered">程序已注册</string>
<string name="unsuccessed">无效用户名或注册码</string>
<string name="successed">恭喜您!注册成功</string>
</resources>
- 编写 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 中输入命令,如下图:
- 可以看到,输出了项目目录
- 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:
- 从输出可看出,支持的 task 有很多,这里只关注编译时使用的 Build tasks 和安装 APK 时使用的 Install tasks
- 要想生成 Debug 调试版本的 APK,只需执行 gradle assembleDebug 命令。编译后会在 Crackme0201/app/build/outputs/apk/debug 目录下生成 app-debug.apk 文件
- 安装 Debug 调试版本的 APK,只需执行 gradle installDebug 命令。安装时,gradlew 会将 APK 安装到与系统相连的设备或模拟器上。若系统没有与 Android 设备连接,或没有配置 Android 模拟器,则安装失败,输出异常信息“com.android.builder.testing.api.DeviceException:No connected devices!”
通过 Android Studio 编译生成
- 写好程序代码逻辑后,在 AS 上点击 “运行”按钮,会自动编译 APK,并安装到连接的设备后运行程序
破解第一个 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 版本,如图:
- 输入命令:gradle assembleRelease
- 执行完毕即可在当前项目的 app/build/outputs/apk/release 目录下生成 app-release.apk 文件
- 打开命令行,执行如下命令:
apktool d APK路径 -o 生成反编译文件的路径
- 如无意外会在指定目录生成反编译文件(包括一系列目录和文件),如下图:
- 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 程序时,strings.xml 文件中的所有字符串资源都在 gen//R.java 文件的 String 类中标识,每个字符串都有唯一的 int 类型的索引值。使用 ApkTool 反编译 APK 后,所有的索引值保存在 strings.xml 同一目录的 public.xml 中。上图的代码中,“无效用户名或注册码”的字符串名称为“unsuccessed”
- 输出 public.xml 的内容,如下:
- “unsuccessed” 的 id 值为 0x7f0c0028
- 搜索 0x7f0c0028:
- 除了 public.xml 和 R$string.smali 资源文件,只有 MainActivity$1.smali 文件一处进行了调用,代码如下:
- 通过阅读这些代码,发现下面这行代码调用了 checkSN() 方法进行注册码核验:
- 接着是这三行代码:
- checkSN() 方法返回 Boolean 类型的值(“V”)。上图中第一行代码将返回结果保存到 p1 寄存器中,第二行代码现在不去管,第三行代码对 p1 进行判断,如果其值不为 0(即条件为真)就跳转到 cond_0 处,反之程序继续执行
- 如果不跳转,就执行下列代码:
- 可以看出,执行这些代码会弹出“无效用户名或注册码”的 Toast,可见不能往这儿执行
- 如果跳转,就执行下列代码:
- 可以看出,执行这些代码会弹出注册成功的 Toast,可见应该往这儿跳转
- 因此,关键代码即是:
修改 smali 文件的代码
- 找到了关键代码,现在将其修改,即可破解程序:
- if-eqz 表示比较结果为 0 或相等时跳转
- 保存
重新编译 APK 文件并签名
- 修改 smali 文件后,要将该文件重新编译,打包成 APK:
- 回编译完成后,会在指定的目录(此处为当前目录)生成指定名称的 APK(此处为 new.apk)
- 但是,此时的 new.apk 是没有签名的,要用 signapk 对 APK 文件签名
- 将下图中的三个文件(可从 Android 系统源码中提取)和要签名的文件放在同一个目录
- 执行如下命令,即可生成签名后的 APK:
安装和测试
- 启动 Android 模拟器或 Android 设备
- 执行如下命令,卸载之前安装的 Crackme0201 程序
- 执行如下命令,安装签名后的 APK
- 运行新安装的 Crackme0201,任意输入,点击“注册”:
- 至此,破解完成
小结
- 破解 Android 程序的流程为:反编译 -> 分析 -> 修改 -> 回编译 -> 签名
- 除了命令行操作外,还可用集成工具破解,如 Android Killer 等