运动本为一件快乐的事,但当运动变为一项强制性的任务之后,难免会引起人的反感。步道乐跑,一款主打校园运动健康的跑步锻炼 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,在终端中查看进程:
| 12
 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,于是继续查看两个进程的附加信息:
| 12
 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 库的加载情况,执行下面的命令:
| 12
 
 | strace    -p $(pidof zygote)    -f     2> /sdcard/com.lptiyu.tanke.strace.txt
 
 | 
  为了避免多余的输出(执行命令后所有由 zygote 进程 fork 出的进程都会被追踪),最好通过 ssh 或在 adb shell 中远程执行此命令,接着启动乐跑 APP,等待 Activity 加载完毕后结束跟踪,然后执行下面的命令获取其 pid:
| 12
 
 | ps -ef | grep com.lptiyu.tanke
 
 | 

  分析刚刚追踪到的输出文件,虽然文件很长,我们只需要关注其中乐跑 APP 的进程(通过刚刚获取到的 pid)加载链接库的部分就可以了,下面是提取出来的关键记录:
| 12
 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 进行注入,这里使用我封装的工具:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 | import osimport 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 注意一定要和刚刚添加的依赖库同名,编辑内容如下:
| 12
 3
 4
 5
 6
 7
 
 | {"interaction": {
 "type": "script",
 "path": "/data/local/tmp/script.js",
 "on_change": "ignore"
 }
 }
 
 | 
  附 script.js 代码:
| 12
 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