今天受好友 nameless 的委托,对一个名叫 nokelock 的 apk 进行插桩,希望在日志中打印出蓝牙加密包的密文,密钥与明文,由于本人是第一次对 apk 进行插桩,于是写了这一篇文章用以学习和记录.
# 0x01 什么是插桩?
依据本人的拙见,插桩就是在原有的代码中插入自己写的代码,用以打印某些想要知道的变量的值。当然了,插入的代码也可以在原有代码的基础上,为 app 增加各种各样的额外功能。
在过去学习 fuzz 的过程中,也有过插桩这一个概念,而过去使用的 afl-fuzz 是在二进制汇编代码中插桩,这一次是需要在 apk 中进行插桩,今日总体的插桩流程体验下来,本人明显感觉到在 apk 中插桩的灵活性明显跟好,而且可操作性更强.
# 0x02 对于 apk 的初步处理
# 需要插桩的代码位置 && 需要打印的变量名称
现在我们给到的是一个 base.apk
如果我们想要对代码进行插桩,那么我们首先需要知道我们要在代码的什么地方去插桩
很幸运当我给到这一个 apk 的时候,nameless 已经找到了需要插桩的代码位于 com.nokelock.blelibrary.b.b
中
我们用 jadx
反编译看一下相关的 java 代码
一看代码,这就是很典型的 AES
加密,而我们需要在日志中打印的变量也一目了然,分别是 bArr
, bArr2
和 instance.doFinal(bArr)
# 得到 apk 的 smali 代码
假如我们想要对 apk 进行插桩,我们无法直接修改反编译出的 java 代码,而是需要修改比 java 代码更加底层的 smali 代码
简单来说,java 和 smali 代码的关系可以简单类比为 c 语言和汇编语言之间的关系
那么如何查看 smali 代码呢?
# 简单查看 smali 代码
如果在 jadx
中,我们可以直接对相关的 java 代码按一下 tab
键,然后就能得到更加原始的 smali 代码
但是需要注意的是,虽然这种方式可以查看其 smali 代码,但是无法重新编译并打包 apk, 就是说你可以看,但是你改不了:]
# 修改 smali 代码并重新打包签名生成 apk
这里我强烈推荐使用 apk easy tool 这个工具,问就是相当的好用;)
下载完成之后打开 apk easy tool
, 然后把我们需要反编译的 apk 拖到这个工具里面,直接先点一下反编译,然后在点一下打开反编译目录,就可以看到 apk 的 smali 代码了
之后根据我们需要插桩的代码的路径 ( com.nokelock.blelibrary.b.b
) 一级一级的点进去,就得到了可以直接编辑的 smali 代码
# 0x03 如何插桩?
当我们打开 b.smali
文件后,通过对相同方法名和形参类型的寻找,我们可以快速定位到如下代码段
.method public static a([B[B)[B | |
.locals 2 | |
:try_start_0 | |
new-instance v0, Ljavax/crypto/spec/SecretKeySpec; | |
const-string v1, "AES" | |
invoke-direct {v0, p1, v1}, Ljavax/crypto/spec/SecretKeySpec;-><init>([BLjava/lang/String;)V | |
const-string p1, "AES/ECB/NoPadding" | |
invoke-static {p1}, Ljavax/crypto/Cipher;->getInstance(Ljava/lang/String;)Ljavax/crypto/Cipher; | |
move-result-object p1 | |
const/4 v1, 0x1 | |
invoke-virtual {p1, v1, v0}, Ljavax/crypto/Cipher;->init(ILjava/security/Key;)V | |
invoke-virtual {p1, p0}, Ljavax/crypto/Cipher;->doFinal([B)[B | |
move-result-object p0 | |
:try_end_0 | |
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 | |
return-object p0 | |
:catch_0 | |
const/4 p0, 0x0 | |
return-object p0 | |
.end method | |
.method public static b([B[B)[B | |
.locals 2 | |
:try_start_0 | |
new-instance v0, Ljavax/crypto/spec/SecretKeySpec; | |
const-string v1, "AES" | |
invoke-direct {v0, p1, v1}, Ljavax/crypto/spec/SecretKeySpec;-><init>([BLjava/lang/String;)V | |
const-string p1, "AES/ECB/NoPadding" | |
invoke-static {p1}, Ljavax/crypto/Cipher;->getInstance(Ljava/lang/String;)Ljavax/crypto/Cipher; | |
move-result-object p1 | |
const/4 v1, 0x2 | |
invoke-virtual {p1, v1, v0}, Ljavax/crypto/Cipher;->init(ILjava/security/Key;)V | |
invoke-virtual {p1, p0}, Ljavax/crypto/Cipher;->doFinal([B)[B | |
move-result-object p0 | |
:try_end_0 | |
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 | |
return-object p0 | |
:catch_0 | |
const/4 p0, 0x0 | |
return-object p0 | |
.end method |
初见这种 smali 代码可能会有手足无措的感觉,但是我自己感觉 smali 代码其实要比汇编更加的简单易懂
回到正题,对于首次插桩,我们可以在 github 上下载现成的插桩代码 Smali 插桩自用代码库
用法很简单,首先将代码下载下来之后,将 SewellDinGLog.smali
直接复制到 apk easy tool
反编译完成后的那个 smail 文件夹中如图所示
# 在哪里插桩?
ok 我们重新回到 b.smali
中
我们可以插桩的代码位置有两处 (为什么是这两处!?没有别的地方吗 ---- 亲身经历,要是插桩在别的地方会无法回编译报错:0)
- 在 invoke-static/invoke-virtual/invoke-direct 指令返回类型是 V 之后可以加入
- 在 invoke-static/invoke-virtual/invoke-direct 指令返回类型不是 V,那么在 move-result-object 命令之后可以加入
# 插桩代码
invoke-static {}, LSewellDinGLog;->Log()V# 无参数,用来判断函数是否执行 | |
invoke-static {v1}, LSewellDinGLog;->Log(Ljava/lang/Object;)V# 打印字符串 | |
invoke-static {v1}, LSewellDinGLog;->Log([Ljava/lang/Object;)V# 打印数组 |
这里的插桩代码我们可以结合 SewellDinGLog.java
来看
import java.util.Arrays; | |
import android.util.Log; | |
public class SewellDinGLog { | |
public static void Log(String tag, String msg) {// 两个参数 | |
Log.d(tag, msg); | |
} | |
public static void Log() {// 无参数 | |
Log("SewellDinG", "DeBug ..."); | |
} | |
public static void Log(Object someObj) {// 一个参数,打印字符串 | |
Log("SewellDinG", someObj.toString()); | |
} | |
public static void Log(Object[] someObj) {// 一个参数,打印数组 | |
Log("SewellDinG", Arrays.toString(someObj)); | |
} | |
} |
代码很简单,就是直接使用 Log 来打印变量
随后我们便可以根据插桩的规则,插入相应的代码,位置类似下图
随后我们再次打开 apk easy tool
, 点一下回编译,在点一下打开回编译目录,就能找到最新编译成功的 apk
# 查看日志
之后使用 adb
命令运行该 apk, 然后使用命令就可以查看打印的日志
adb logcat -s SewellDinG
# 0x04 意料之外的错误
但是当我将插桩后的代码发给 nameless 后,确实有日志打印出来,但是却是长这个样子的
F:\platform-tools>adb logcat -s SewellDinG
--------- beginning of main
03-27 12:38:56.882 14325 14325 D SewellDinG: [B@3ad0a72
03-27 12:38:56.882 14325 14325 D SewellDinG: [B@85ec3
03-27 12:38:56.882 14325 14325 D SewellDinG: [B@e41f340
03-27 12:39:05.392 14325 14325 D SewellDinG: [B@4ee0ab4
03-27 12:39:05.392 14325 14325 D SewellDinG: [B@bd596dd
03-27 12:39:05.392 14325 14325 D SewellDinG: [B@9d6c252
我正疑惑呢难道密钥密文明文都是 [B@
开头的?后来才发现是 SewellDinGLog.smali
代码有问题
首先我需要打印的是一个 byte
类型是数组,而打印数组的 java 代码是长这个样子的
public static void Log(Object[] someObj) {// 一个参数,打印数组 | |
Log("SewellDinG", Arrays.toString(someObj)); | |
} |
这里就有问题了呀,直接使用 toString
方法,对于字节数组来说返回是字节数组的地址而非字节数组的值!
# 0x05 处理 bug, 重新编写代码 SewellDinGLog.smali
在这个过程中,我清楚的认识到我应该修改的是 smali 代码,但是如何修改?
我认为最为简便的方法就是先编写 java 代码,然后再编译成 smali 代码,可是要怎么实现呢?
我 java 的运行平台都是 IDEA, 但是我发现 SewellDinGLog.java
所需要的包 android.util.Log
根本就没有,在网上找了找也找不到这个包,之后通过 google 了很久,才知道我需要安装一个 Android Studio
安装好了之后,就新建一个项目,然后把要运行的代码复制粘贴进去
要注意这里 Language
一定要选 Java, 不要选 Kotlin
, 不然得像我一样下了很久的安装包最后发现代码根本运行不了
第一次使用 Android studio
, 得需要等大约几十分钟安装运行所需要的包
随后点击 File-->Settings-->Plugins
, 然后在插件市场搜索 java2smali
, 安装完之后重启 Android studio
, 然后在 Build-->Compile to smali
就可以直接生成 smali 代码了
为了能够成功 Log 打印字节数组,我们需要对原先的 SewellDinGLog.java
代码进行相应的修改,然后使用 Build-->Compile to smali
生成 smail 代码
修改后的 java 代码如下
package com.example.newlog; | |
import android.util.Log; | |
public class SewellDinGLog { | |
final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); | |
public static String bytesToHex(byte[] bytes) { | |
char[] hexChars = new char[bytes.length * 2]; | |
for ( int j = 0; j < bytes.length; j++ ) { | |
int v = bytes[j] & 0xFF; | |
hexChars[j * 2] = hexArray[v >>> 4]; | |
hexChars[j * 2 + 1] = hexArray[v & 0x0F]; | |
} | |
return new String(hexChars); | |
} | |
public static void Log(String tag, String msg) { | |
Log.d(tag, msg); | |
} | |
public static void Log() { | |
Log("SewellDinG", "DeBug ..."); | |
} | |
public static void Log(byte[] someObj) { | |
String result = bytesToHex(someObj); | |
Log("SewellDinG", result); | |
} | |
} |
随后在同级文件夹下就会生成 SewellDinGLog.smali
这里要注意的是由于我们的 SewellDinGLog.smali
是放在 apk easy tool
反编译后的 smali 文件夹的根文件夹下,所以我们需要对 SewellDinGLog.smali
内的代码进行修改
例如我这里
那么需要将所有的 Lcom/example/newlog/SewellDinGLog
全部替换成 LSewellDinGLog
变成这样
随后和之前的步骤一模一样,把 SewellDinGLog.smali
复制到 smali
文件夹内,然后插桩,回编译就可以了
插桩后的 apk 我们再用 jadx
反编译看看
发现相较之前多了 SewellDinGLog.Log
函数用以打印变量的值
最后我们也是成功打印出变量的值
F:\platform-tools>adb logcat -s SewellDinG
--------- beginning of main
03-27 17:18:08.895 31117 31117 D SewellDinG: 05010630303030303082E3C89616017D
03-27 17:18:08.895 31117 31117 D SewellDinG: 241F632E5907042061014C1A3A45193B
03-27 17:18:08.895 31117 31117 D SewellDinG: 6629B62C88A7E50525E92C328AF258E6
03-27 17:18:10.114 31117 31117 D SewellDinG: 732F5CB22C06B0C2D0D17AD31D165805
03-27 17:18:10.114 31117 31117 D SewellDinG: 241F632E5907042061014C1A3A45193B
03-27 17:18:10.114 31117 31117 D SewellDinG: 05020100E3C896010202000000000000
03-27 17:18:11.770 31117 31117 D SewellDinG: 530B1FCA1467A408A321E71F3D152127
03-27 17:18:11.771 31117 31117 D SewellDinG: 241F632E5907042061014C1A3A45193B
# 0x06 一个自问自答
Q: 为什么不直接使用 android.util.Log 中的 Log 函数来打印,即在需要插桩的位置插入这串代码
invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I |
, 而是调用另一个类中的函数来打印呢?
A: 我认为重写一个 Log 打印方法,灵活性将会更高,我们可以将复杂的代码先用 java 编写,然后再编译成 smali, 这样比直接用 smali 写代码要更简单方便,只需要插桩的位置添加一个函数方法的调用就可以了.