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

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

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


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

image.png

要求就是通过注册。爆破的方法很多,大致可以归为三类,第一种是直接修改 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 文件夹,文件夹目录如下:

image.png

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


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


image.png

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

image.png

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

image.png

可以看到这个 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 都可以。签名之后安装,输入用户名:

image.png

这样就注册成功了。方法虽然有点 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 把正确的注册码打印出来,这样我们就可以直接输入注册码进行注册了。


打 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
复制代码


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

image.png

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


动态调试 smali

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


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


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

image.png

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


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

adb shell am start -D -n com.droider.crackme0201/.MainActivity
复制代码


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

image.png


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

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 模式一模一样了。我们可以在运行中看到寄存器中的值,运行逻辑一览无遗。运行至注册码校验处的断点,截图如下:

image.png

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 应用看一下:

image.png

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


总结


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

  • 使用 ApkTool 反编译以及重打包
  • smali 代码的基本阅读能力
  • smali 代码中注入 log 日志
  • 动态调试 smali 代码
  • 常用 hook 框架
  • jadx 使用



相关文章
|
7月前
|
Linux 编译器 Android开发
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
在Linux环境下,本文指导如何交叉编译x265的so库以适应Android。首先,需安装cmake和下载android-ndk-r21e。接着,下载x265源码,修改crosscompile.cmake的编译器设置。配置x265源码,使用指定的NDK路径,并在配置界面修改相关选项。随后,修改编译规则,编译并安装x265,调整pc描述文件并更新PKG_CONFIG_PATH。最后,修改FFmpeg配置脚本启用x265支持,编译安装FFmpeg,将生成的so文件导入Android工程,调整gradle配置以确保顺利运行。
258 1
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
|
7月前
|
Unix Linux Shell
FFmpeg开发笔记(八)Linux交叉编译Android的FFmpeg库
在Linux环境下交叉编译Android所需的FFmpeg so库,首先下载`android-ndk-r21e`,然后解压。接着,上传FFmpeg及相关库(如x264、freetype、lame)源码,修改相关sh文件,将`SYSTEM=windows-x86_64`改为`SYSTEM=linux-x86_64`并删除回车符。对x264的configure文件进行修改,然后编译x264。同样编译其他第三方库。设置环境变量`PKG_CONFIG_PATH`,最后在FFmpeg源码目录执行配置、编译和安装命令,生成的so文件复制到App工程指定目录。
361 9
FFmpeg开发笔记(八)Linux交叉编译Android的FFmpeg库
|
2月前
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
82 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
2月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
113 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
4月前
|
JavaScript 前端开发 Java
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
IT寒冬使APP开发门槛提升,安卓程序员需转型。选项包括:深化Android开发,跟进Google新技术如Kotlin、Jetpack、Flutter及Compose;研究Android底层框架,掌握AOSP;转型Java后端开发,学习Spring Boot等框架;拓展大前端技能,掌握JavaScript、Node.js、Vue.js及特定框架如微信小程序、HarmonyOS;或转向C/C++底层开发,通过音视频项目如FFmpeg积累经验。每条路径都有相应的书籍和技术栈推荐,助你顺利过渡。
125 3
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
|
4月前
|
编解码 安全 Ubuntu
Android Selinux 问题处理笔记
这篇文章是关于处理Android系统中SELinux权限问题的笔记,介绍了如何通过分析SELinux拒绝的日志、修改SELinux策略文件,并重新编译部署来解决权限问题,同时提供了一些SELinux的背景知识和实用工具。
128 0
|
7月前
|
安全 Linux Android开发
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
该文介绍了如何在Linux服务器上交叉编译Android的FFmpeg库以支持HTTPS视频播放。首先,从GitHub下载openssl源码,解压后通过编译脚本`build_openssl.sh`生成64位静态库。接着,更新环境变量加载openssl,并编辑FFmpeg配置脚本`config_ffmpeg_openssl.sh`启用openssl支持。然后,编译安装FFmpeg。最后,将编译好的库文件导入App工程的相应目录,修改视频链接为HTTPS,App即可播放HTTPS在线视频。
129 3
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
|
6月前
|
Java API Android开发
技术经验分享:Android源码笔记——Camera系统架构
技术经验分享:Android源码笔记——Camera系统架构
67 0
|
7月前
|
Java 测试技术 开发工具
Android 笔记:AndroidTrain , Lint , build(1),只需一篇文章吃透Android多线程技术
Android 笔记:AndroidTrain , Lint , build(1),只需一篇文章吃透Android多线程技术
|
7月前
|
设计模式 缓存 前端开发
真的强!借助阿里技术博主分享的Android面试笔记,我拿到了字节跳动的offer
真的强!借助阿里技术博主分享的Android面试笔记,我拿到了字节跳动的offer