往期目录:java
Class 文件格式详解android
Smali —— 数学运算,条件判断,循环github
Smali 语法解析 —— 类shell
无心中在看雪看到一个简单的 CrackMe 应用,正好就着这个例子总结一下逆向过程当中基本的经常使用工具的使用,和一些简单的经常使用套路。感兴趣的同窗能够照着尝试操做一下,过程仍是很简单的。APK 我已上传至 Github,下载地址。微信
首先安装一下这个应用,界面以下所示:app
要求就是经过注册。爆破的方法不少,大体能够归为三类,第一种是直接修改 smali 代码绕过注册,第二种是捋清注册流程,获得正确的注册码。第三种是 hook 。下面就来讲说这几种爆破过程。框架
要获取 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
文件夹,文件夹目录以下:
其中的 smali
文件夹就包含了该 Apk 的全部 smali 代码。阅读和修改 smali 代码的工具不少,我我的偏好将整个反编译获得的文件夹导入 IDEA 或者 Android Studio 进行阅读和修改,可能我是 Android 开发,用这两个工具会比较顺手,全局搜索功能也很给力。
导入 Android Studio 以后,看到了全部的 smali 代码,那么咱们该从何下手呢?注册失败的时候会弹一个 Toast,“无效用户名或注册码”,这就是突破口。全局搜索这个字符串,
发现这个字符串定义在 string.xml
中的 unsuccessd
,在写代码的时候就是 R.string.unsuccessd
,这是一个 int 值,编译后就直接是一个数字了。咱们再来全局搜索 unsuccessd
:
在 public.xml
中能够看到它的 id
,代码中直接使用的就是这个 id了。全局搜索一下 0x7f05000b
,看一下这个 Toast 是在哪里弹出的。
能够看到这个 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
均可以。签名以后安装,输入用户名:
这样就注册成功了。方法虽然有点 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;-><init>()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 的 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 来的更加直截了当。不论是你本身写程序,仍是作逆向,debug 永远都是快速理清逻辑的好方法。smali 也是能够进行动态调试的,依赖于 Smalidea 插件,你能够在 Android Studio 的 Plugin 中进行安装,也能够下载下来本地安装。
第一步,咱们要保证咱们的应用处于 debug 版本,在 AndroidManifest.xml 中加上 android:debuggable="true"
便可,重打包再安装到手机上。
第二步,将以前反编译获得的 smali 文件夹导入 Android Studio 或者 IDEA,并配置远程调试环境。选择 Run -> Edit Configurations,点击左上角 + 号,选择 Remote,弹出配置窗口,以下图所示:
注意记住本身填写的端口号,端口号不是固定的,只要未被占用便可。配置完成后,记得在合适的地方打上断点,我这里就在 checkSN()
方法内打上断点。
第三步,命令行启动进程调试等待模式。首先执行:
adb shell am start -D -n com.droider.crackme0201/.MainActivity
复制代码
应用此时会进入等待调试模式,以下图所示:
而后创建端口转发,输入以下命令:
adb forward tcp:8700 jdwp:pid
复制代码
用你本身的应用的 pid 替换进去。关于 pid 的获取,能够经过 ps
和 grep
组合:
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 模式如出一辙了。咱们能够在运行中看到寄存器中的值,运行逻辑一览无遗。运行至注册码校验处的断点,截图以下:
userName
是用户名,sn
是我输入的注册码,userSN
是正确的注册码。
注册机其实就是本身重写注册码生成过程了,看懂了 smali 就能够本身写个程序来生成注册码了。这个就很少说了。
具体的 Hook 操做因为篇幅缘由就不在这里演示了。关于 Java 层的 Hook 工具不少,最广泛的就是 Xposed,直接 hook checkSN
方法的返回值,或者打印出正确的注册码。若是你没有 Root 设备,还有一系列基于 VirtualApp 的 hook 框架,例如支持 Xposed 应用的 VirtualXposed 等等,固然 VirtualApp 自己也支持 hook 操做。另外,还有 Frida 等等框架,也能够进行相似的操做。
最后再介绍一个反编译利器 JADX ,它能够直接将 Apk 反编译成 Java 代码进行查看,毕竟 smali 代码不是那么人性化。我拿到一个 Apk,基本上第一件事就是丢到 JADX 中进行查看,它同时支持命令行操做和图形化界面。咱们就用 JADX 打开这个 CrackMe 应用看一下:
直接就能够看到对应的 Java 代码,理清逻辑以后再去阅读 smali 代码进行修改,事半功倍。支持反编译 Java 代码的工具还有不少,例如基于 Python 实现的 Androgurad 等等,你们也能够尝试去使用一下。
就逆向难度来讲,这个 CrackMe 仍是很简单的,但本文主旨在于介绍一些逆向相关的知识,实际逆向过程当中你面对的任何一个 Apk 确定都比这复杂的多。看到这里,你应该了解到了下面这些知识点:
关于 smali 语法我以前也写过几篇文章,往期目录:
下一篇来写写 Android Apk 中资源包文件 resources.arsc
的文件结构,一样会配套思惟导图和 Java 源码解析。
文章首发微信公众号:
秉心说
, 专一 Java 、 Android 原创知识分享,LeetCode 题解。更多 JDK 源码解析,扫码关注我吧!