Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结

无意中在看雪看到一个简单的 CrackMe 应用,正好就着这个例子总结一下逆向过程中基本的常用工具的使用,和一些简单的常用套路。感兴趣的同学可以照着尝试操作一下,过程还是很简单的。APK 我已上传至 Github,下载地址。

首先安装一下这个应用,界面如下所示:

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第1张图片

要求就是通过注册。爆破的方法很多,大致可以归为三类,第一种是直接修改 smali 代码绕过注册,第二种是捋清注册流程,得到正确的注册码。第三种是 hook 。下面就来说说这几种爆破过程。

直接修改 smali 进行爆破

要获取 smali 代码,首先得反编译这个 Apk,通过 ApkTool 就可以完成。ApkTool 的使用过程就不在这里赘述了,执行如下命令:

apktool d creackme.apk
I: Using Apktool 2.3.4-dirty on crackme.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/luyao/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

会在当前目录生成 crackme 文件夹,文件夹目录如下:

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第2张图片

其中的 smali 文件夹就包含了该 Apk 的所有 smali 代码。阅读和修改 smali 代码的工具很多,我个人偏好将整个反编译得到的文件夹导入 IDEA 或者 Android Studio 进行阅读和修改,可能我是 Android 开发,用这两个工具会比较顺手,全局搜索功能也很给力。

导入 Android Studio 之后,看到了所有的 smali 代码,那么我们该从何下手呢?注册失败的时候会弹一个 Toast,“无效用户名或注册码”,这就是突破口。全局搜索这个字符串,

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第3张图片

发现这个字符串定义在 string.xml 中的 unsuccessd ,在写代码的时候就是 R.string.unsuccessd,这是一个 int 值,编译后就直接是一个数字了。我们再来全局搜索 unsuccessd :

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第4张图片

public.xml 中可以看到它的 id,代码中直接使用的就是这个 id了。全局搜索一下 0x7f05000b,看一下这个 Toast 是在哪里弹出的。

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第5张图片

可以看到这个 id 在 MainActivity.smali 中的 433 行使用到了,我们定位到这个文件:

    .line 117
    if-nez v0, :cond_0  # 如果 v0 不等于 0 ,跳转到 cond_0

    .line 119
    const v0, 0x7f05000b

    .line 118
    invoke-static {p0, v0, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;

    move-result-object v0

    .line 119
    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

这段逻辑很简单。判断寄存器 v0 的值是否为 0,不为 0 的话则弹出 “无效用户名或注册码” 。所以最简单的改法,逻辑反一下,v0 为 0 的时候弹出该 Toast,把 if-nez 改为 if-ez 即可。修改之后使用 ApkTool 重打包,重打包命令如下:

apktool b crackme -o crackme_new.apk

会在当前目录生成 crackme_new.apk 文件,注意这个安装包是未签名的,无法直接安装,需要先签名。使用 jarsinger 或者 apksigner 都可以。签名之后安装,输入用户名:

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第6张图片

这样就注册成功了。方法虽然有点 low ,但好歹爆破成功了。下面我们不修改 smali 代码,通过阅读 smali 代码理解其注册码生成逻辑,通过正规方式来注册。

获取注册码爆破

我们之前已经找到了具体的逻辑是在 MainActivity.smali 中,找到这个按钮的 onClick() 事件,来看一下具体逻辑:

.line 116
invoke-direct {p0, v0, v1}, Lcom/droider/crackme0201/MainActivity;->checkSN(Ljava/lang/String;Ljava/lang/String;)Z

move-result v0

.line 117
if-eqz v0, :cond_0

.line 119
const v0, 0x7f05000b

.line 118
invoke-static {p0, v0, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;

move-result-object v0

.line 119
invoke-virtual {v0}, Landroid/widget/Toast;->show()V

goto :goto_0

这里只截取了 onClick 中的部分核心代码,调用 checkSN() 方法获得一个 Boolean 值,根据这个值来判断是否注册成功。这个 checkSN() 方法就是我们需要重点关注的,我对这个方法的 smali 代码逐行添加了注释,还是很容易理解的,感兴趣的同学可以看一下:

.method private checkSN(Ljava/lang/String;Ljava/lang/String;)Z
    .locals 10  # 使用 10 个寄存器
    .param p1, "userName"   # Ljava/lang/String; 参数寄存器 p1 保存的是用户名 userName
    .param p2, "sn"    # Ljava/lang/String; 参数寄存器 p2 保存的是注册码 sn

    .prologue
    const/4 v7, 0x0 # 将 0x0 存入寄存器 v7

    .line 45
    if-eqz p1, :cond_0  # 如果 p1,即 userName 等于 0,跳转到 cond_0

    :try_start_0
    invoke-virtual {p1}, Ljava/lang/String;->length()I # 调用 userName.length()

    move-result v8  # 将 userName.length() 的执行结果存入寄存器 v8

    if-nez v8, :cond_1 # 如果 v8 不等于 0,跳转到 cond_1

    .line 69
    :cond_0
    :goto_0
    return v7

    .line 47
    :cond_1
    if-eqz p2, :cond_0  # 如果 p2,即注册码 sn 等于 0,跳转到 cond_0

    invoke-virtual {p2}, Ljava/lang/String;->length()I  # 执行 sn.length()

    move-result v8  # 将 sn.length() 执行结果存入寄存器 v8

    const/16 v9, 0x10 # 将 0x10 存入寄存器 v9

    if-ne v8, v9, :cond_0   # 如果 sn.length != 0x10 ,跳转至 cond_0

    .line 49
    const-string v8, "MD5"  # 将字符串 "MD5" 存入寄存器 v8

    # 调用静态方法 MessageDigest.getInstance("MD5")
    invoke-static {v8}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;

    move-result-object v1   # 将上一步方法的返回结果赋给寄存器 v1,这里是 MessageDigest 对象

    .line 50
    .local v1, "digest":Ljava/security/MessageDigest;
    invoke-virtual {v1}, Ljava/security/MessageDigest;->reset()V # 调用 digest.reset() 方法

    .line 51
    invoke-virtual {p1}, Ljava/lang/String;->getBytes()[B   # 调用 userName.getByte() 方法

    move-result-object v8   # 上一步得到的字节数组存入 v8

    invoke-virtual {v1, v8}, Ljava/security/MessageDigest;->update([B)V # 调用 digest.update(byte[]) 方法

    .line 52
    invoke-virtual {v1}, Ljava/security/MessageDigest;->digest()[B  # 调用 digest.digest() 方法

    move-result-object v0   # 上一步的执行结果存入 v0,是一个 byte[] 对象

    .line 53
    .local v0, "bytes":[B
    const-string v8, "" # 将字符串 "" 存入 v8

    # 调用 MainActivity 中的 toHexString(byte[] b,String s) 方法
    invoke-static {v0, v8}, Lcom/droider/crackme0201/MainActivity;->toHexString([BLjava/lang/String;)Ljava/lang/String;

    move-result-object v3   # 上一步方法返回的字符串存入 v3

    .line 54
    .local v3, "hexstr":Ljava/lang/String;
    new-instance v5, Ljava/lang/StringBuilder;  # 新建 StringBuilder 对象

    invoke-direct {v5}, Ljava/lang/StringBuilder;->()V    # 执行 StringBuilder 的构造函数

    .line 55
    .local v5, "sb":Ljava/lang/StringBuilder;   # 声明变量 sb 指向刚才创建的 StringBuilder 实例
    const/4 v4, 0x0 # v4 = 0x0

    .local v4, "i":I    # i = 0x0
    :goto_1 # for 循环开始
    invoke-virtual {v3}, Ljava/lang/String;->length()I  # 获取 hexstr 字符串的长度

    move-result v8  # v8 = hexstr.length()

    if-lt v4, v8, :cond_2   # 如果 v4 小于 v8,即 i < hexstr.length(), 跳转到 cond_2

    .line 58
    # 这里已经跳出 for 循环
    invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v6   # v6 = sb.toString()

    .line 63
    .local v6, "userSN":Ljava/lang/String;  # userSN = sb.toString()

    # userSN.equalsIgnoreCase(sn)
    invoke-virtual {v6, p2}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z

    move-result v8  # v8 = userSN.equalsIgnoreCase(sn)

    if-eqz v8, :cond_0 # 如果 v8 等于 0,跳转到 cond_0,即 userSN != sn

    .line 69
    const/4 v7, 0x1

    goto :goto_0    # 跳转到 goto_0,结束 checkSN() 方法并返回 v7

    .line 56
    .end local v6    # "userSN":Ljava/lang/String;
    :cond_2
    invoke-virtual {v3, v4}, Ljava/lang/String;->charAt(I)C # 执行 hexstr.charAt(i)

    move-result v8  # v8 = hexstr.charAt(i)

    # 调用 sb.append(v8)
    invoke-virtual {v5, v8}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
    :try_end_0
    .catch Ljava/security/NoSuchAlgorithmException; {:try_start_0 .. :try_end_0} :catch_0

    .line 55
    add-int/lit8 v4, v4, 0x2    # v4 自增 0x2,即 i+=2

    goto :goto_1    # 跳转到 goto_1,形成 循环

    .line 65
    .end local v0    # "bytes":[B
    .end local v1    # "digest":Ljava/security/MessageDigest;
    .end local v3    # "hexstr":Ljava/lang/String;
    .end local v4    # "i":I
    .end local v5    # "sb":Ljava/lang/StringBuilder;
    :catch_0
    move-exception v2

    .line 66
    .local v2, "e":Ljava/security/NoSuchAlgorithmException;
    invoke-virtual {v2}, Ljava/security/NoSuchAlgorithmException;->printStackTrace()V

    goto :goto_0
.end method

大致逻辑就是对输入的用户名 UserName 作 MD5 运算得到 Hash 值,再转成十六进制字符串就是注册码了。那么,如何获取注册码呢 ?一般有三种方式,打 log,动态调试 smali,自己写注册机。下面逐个说明一下。

打 log 日志

其实在逆向过程中,注入 log 代码是很常见的操作。适当的打 log,可以很好的帮助我们理解代码执行流程。在这里例子中,最终会拿我们输入的注册码和正确的注册码进行比较,在比较的时候我们就可以通过打 log 把正确的注册码打印出来,这样我们就可以直接输入注册码进行注册了。

打 log 的 smali 代码是固定的,一般格式如下:

const-string vX, "TAG"
invoke-static {vX,vX}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

vX 都是指寄存器。把这两行代码加到注册码的检验操作之前就可以了:

.line 63
.local v6, "userSN":Ljava/lang/String;  # userSN = sb.toString()

const-string v8, "TAG"
invoke-static {v8,v6}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

# userSN.equalsIgnoreCase(sn)
invoke-virtual {v6, p2}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z

再次重新打包运行,输入用户名和注册码,就会有如下日志:

这样就拿到正确的注册码了。

动态调试 smali

动态调试 smali 来的更加直截了当。不管是你自己写程序,还是做逆向,debug 永远都是快速理清逻辑的好方法。smali 也是可以进行动态调试的,依赖于 Smalidea 插件,你可以在 Android Studio 的 Plugin 中进行安装,也可以下载下来本地安装。

第一步,我们要保证我们的应用处于 debug 版本,在 AndroidManifest.xml 中加上 android:debuggable="true" 即可,重打包再安装到手机上。

第二步,将之前反编译得到的 smali 文件夹导入 Android Studio 或者 IDEA,并配置远程调试环境。选择 Run -> Edit Configurations,点击左上角 + 号,选择 Remote,弹出配置窗口,如下图所示:

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第7张图片

注意记住自己填写的端口号,端口号不是固定的,只要未被占用即可。配置完成后,记得在合适的地方打上断点,我这里就在 checkSN() 方法内打上断点。

第三步,命令行启动进程调试等待模式。首先执行:

adb shell am start -D -n com.droider.crackme0201/.MainActivity

应用此时会进入等待调试模式,如下图所示:

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第8张图片

然后建立端口转发,输入如下命令:

adb forward tcp:8700 jdwp:pid

用你自己的应用的 pid 替换进去。关于 pid 的获取,可以通过 psgrep 组合:

adb shell ps | grep com.droider.crackme0201
u0_a364   30110 537   2166480 30204 futex_wait 0000000000 S com.droider.crackme0201

我这里的 pid 就是 30010

最后在 Android Studio 或 IDEA 中启动 debug 。 点击 Run -> Debug,应用就进入调试模式了。之后的操作就和我们开发中的 debug 模式一模一样了。我们可以在运行中看到寄存器中的值,运行逻辑一览无遗。运行至注册码校验处的断点,截图如下:

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第9张图片

userName 是用户名,sn 是我输入的注册码,userSN 是正确的注册码。

注册机

注册机其实就是自己重写注册码生成过程了,看懂了 smali 就可以自己写个程序来生成注册码了。这个就不多说了。

Hook

具体的 Hook 操作由于篇幅原因就不在这里演示了。关于 Java 层的 Hook 工具很多,最普遍的就是 Xposed,直接 hook checkSN 方法的返回值,或者打印出正确的注册码。如果你没有 Root 设备,还有一系列基于 VirtualApp 的 hook 框架,例如支持 Xposed 应用的 VirtualXposed 等等,当然 VirtualApp 本身也支持 hook 操作。另外,还有 Frida 等等框架,也可以进行类似的操作。

JADX

最后再介绍一个反编译利器 JADX ,它可以直接将 Apk 反编译成 Java 代码进行查看,毕竟 smali 代码不是那么人性化。我拿到一个 Apk,基本上第一件事就是丢到 JADX 中进行查看,它同时支持命令行操作和图形化界面。我们就用 JADX 打开这个 CrackMe 应用看一下:

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第10张图片

直接就可以看到对应的 Java 代码,理清逻辑之后再去阅读 smali 代码进行修改,事半功倍。支持反编译 Java 代码的工具还有很多,例如基于 Python 实现的 Androgurad 等等,大家也可以尝试去使用一下。

总结

就逆向难度来说,这个 CrackMe 还是很简单的,但本文主旨在于介绍一些逆向相关的知识,实际逆向过程中你面对的任何一个 Apk 肯定都比这复杂的多。看到这里,你应该了解到了下面这些知识点:

  • 使用 ApkTool 反编译以及重打包

  • smali 代码的基本阅读能力

  • smali 代码中注入 log 日志

  • 动态调试 smali 代码

  • 常用 hook 框架

  • jadx 使用

关于 smali 语法我之前也写过几篇文章,往期目录:

Class 文件格式详解

Smali 语法解析——Hello World

Smali —— 数学运算,条件判断,循环

Smali 语法解析 —— 类

Android逆向笔记 —— AndroidManifest.xml  文件格式解析

Android逆向笔记 —— DEX 文件格式解析

下一篇来写写 Android Apk 中资源包文件 resources.arsc 的文件结构,同样会配套思维导图和 Java 源码解析。

文章首发微信公众号: 秉心说 , 专注 Java 、 Android 原创知识分享,LeetCode 题解。

更多 JDK 源码解析,扫码关注我吧!

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_第11张图片

题图 : Pete Linforth

你可能感兴趣的:(Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结)