运动本为一件快乐的事,但当运动变为一项强制性的任务之后,难免会引起人的反感。步道乐跑,一款主打校园运动健康的跑步锻炼 APP,成为了不少大学生手机中的「必备应用」,强制跑步、在限速的同时还要边看地图边寻找打卡点,有何 “乐跑” 可言?
身为一名大学生的我,同样深受其害。作为一名玩机爱好者,自然要尝试一下破解这款软件,做到足不出户,就能完美完成每天的跑步任务。
背景
在 Root 或 Xposed 的环境下,早有人实现了通过修改 GPS 定位信息模拟移动的模块,比如 Fake Location 就是一款非常经典的位置模拟应用。
奈何这些软件或模块大多闭源,有的甚至还收费(比如刚刚提到的 Fake Location),让他们运行在高权限之下多少有些不放心。现如今想要获得一台 Root 的 Android 手机也变得越来越难,于是产生了重新打包乐跑 APP 实现注入的想法。
初步测试
首先初步检查 APK,发现是加固的,导致重新打包 dex 添加 Xposed Hook 变得比较困难,遂采用另一种不需要修改 classes.dex
和 AndroidManifest.xml
的注入方式
下面隆重介绍今天的主角:Frida Gadget。Frida Gadget 是著名轻量级 Hook 框架在无 Root 设备上的一种实现,通过 ELF 注入的方式,将自己的库打包到目标应用的链接库中,从而绕过没有高权限的限制。
首先启动步道乐跑 APP,在终端中查看进程:
1 2 3 4 5
| root@localhost:~ # ps -ef | grep com.lptiyu.tanke u0_a259 2634 791 0 16:08 ? 00:00:02 com.lptiyu.tanke:mult u0_a259 26966 791 8 16:02 ? 00:03:50 com.lptiyu.tanke root 29796 29108 2 16:46 pts/4 00:00:00 grep --color com.lptiyu.tanke
|
发现只有两个进程,传闻绑绑加固使用的是父子进程互相附加的方式防止被 ptrace,于是继续查看两个进程的附加信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| root@localhost:~ # cat /proc/$(pidof com.lptiyu.tanke)/status Name: om.lptiyu.tanke Umask: 0077 State: R (running) Tgid: 26966 Ngid: 0 Pid: 26966 PPid: 791 TracerPid: 0 Uid: 10259 10259 10259 10259 Gid: 10259 10259 10259 10259 ........
root@localhost:~ # cat /proc/$(pidof com.lptiyu.tanke:mult)/status Name: tiyu.tanke:mult Umask: 0077 State: S (sleeping) Tgid: 2634 Ngid: 0 Pid: 2634 PPid: 791 TracerPid: 0 Uid: 10259 10259 10259 10259 Gid: 10259 10259 10259 10259 ........
|
TracePid
值都为 0,似乎并不像传说中的那样通过父子进程相互附加俩防止被 ptrace,这样一来瞬间就变得开朗了许多。接着使用 strace 命令追踪 APP 启动时对各个 so 库的加载情况,执行下面的命令:
1 2
| strace -p $(pidof zygote) -f 2> /sdcard/com.lptiyu.tanke.strace.txt
|
为了避免多余的输出(执行命令后所有由 zygote 进程 fork 出的进程都会被追踪),最好通过 ssh 或在 adb shell 中远程执行此命令,接着启动乐跑 APP,等待 Activity 加载完毕后结束跟踪,然后执行下面的命令获取其 pid:
1 2
| ps -ef | grep com.lptiyu.tanke
|
分析刚刚追踪到的输出文件,虽然文件很长,我们只需要关注其中乐跑 APP 的进程(通过刚刚获取到的 pid)加载链接库的部分就可以了,下面是提取出来的关键记录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libSecShell.so", O_RDONLY|O_LARGEFILE <unfinished ...> [pid 10662] <... openat resumed>) = 45 [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libSecShell.so", O_RDONLY|O_LARGEFILE|O_CLOEXEC <unfinished ...> [pid 10662] <... openat resumed>) = 45 [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libmmkv.so", O_RDONLY|O_LARGEFILE <unfinished ...> [pid 10662] <... openat resumed>) = 56 [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libmmkv.so", O_RDONLY|O_LARGEFILE|O_CLOEXEC <unfinished ...> [pid 10662] <... openat resumed>) = 56 [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libjni_utils.so", O_RDONLY|O_LARGEFILE) = 49 [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libjni_utils.so", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 49 [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libtrace_utils.so", O_RDONLY|O_LARGEFILE) = 49 [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libtrace_utils.so", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 49 [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libsqlcipher.so", O_RDONLY|O_LARGEFILE) = 49 [pid 10662] openat(AT_FDCWD, "/data/app/com.lptiyu.tanke-NtkLRPm8gDOU3fq_rgpZsw==/lib/arm/libsqlcipher.so", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 49
|
尝试注入
既然是要注入,我们肯定希望自己的代码能够尽快执行,可以看到乐跑 APP 首先会加载 libSecShell.so
,然后依次是 libmmkv.so
、libjni_utils.so
、libtrace_utils.so
和 libsqlcipher.so
。首先尝试对 libSecShell.so
进行注入,这里使用我封装的工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import os import re import lief import shutil import subprocess as sp from sys import argv
os.system(f'readelf -d {argv[1]} | grep NEEDED') elf_type = re.search( 'file format elf(32|64)', sp.Popen( f'objdump -a {argv[1]}', stdout=sp.PIPE, shell=True ).stdout.read().decode() ).group(1) shutil.copyfile(f'arm{elf_type}/libgadget.so', './libgadget.so') libso = lief.parse(argv[1]) libso.add_library('libgadget.so') libso.write(argv[1]) os.system(f'readelf -d {argv[1]} | grep NEEDED')
|
执行 python repack.py libSecShell.so
,出现报错信息,似乎是因为 libSecShell.so
没有任何依赖所导致(或许这么说不太准确?),接下来尝试下一个库,将 libmmkv.so
提取到电脑,下载 Frida Gadget,然后使用 LIEF 工具进行重新打包(用上面封装好的也可以,这里为了展示细节在 Terminal 中分步完成):
此时刚刚下载的 libgadget.so
已经被添加到 libmmkv.so
的依赖中了,应用启动时会将其一同加载,我们也就能在它的构造方法里面搞点事情。
接下来编写配置文件,新建一个 libgadget.config.so
注意一定要和刚刚添加的依赖库同名,编辑内容如下:
1 2 3 4 5 6 7
| { "interaction": { "type": "script", "path": "/data/local/tmp/script.js", "on_change": "ignore" } }
|
附 script.js 代码:
1 2 3 4 5 6
| 'use strict'; - Java.perform(function () { let Log = Java.use("android.util.Log"); Log.i("frida-lief", "OHHHHHHHHHHHHHHHHHHHHHHH"); });
|
将 libgadget.so
、libgadget.config.so
和重新打包的 libmmkv.so
一同添加/覆盖到安装包中,重新签名后安装,同时观察到 Logcat 中成功输出:
再看屏幕,却发现 APP 迟迟没有启动,继续查看 Logcat,发现循环输出报错信息,原因尚不明:
参考
[翻译]在未root的设备上使用frida-外文翻译-看雪论坛-安全社区
Gadget | Frida • A world-class dynamic instrumentation framework