这篇博客已摆烂,可能对您的参考价值不大

  运动本为一件快乐的事,但当运动变为一项强制性的任务之后,难免会引起人的反感。步道乐跑,一款主打校园运动健康的跑步锻炼 APP,成为了不少大学生手机中的「必备应用」,强制跑步、在限速的同时还要边看地图边寻找打卡点,有何 “乐跑” 可言?

  身为一名大学生的我,同样深受其害。作为一名玩机爱好者,自然要尝试一下破解这款软件,做到足不出户,就能完美完成每天的跑步任务。


背景

  在 Root 或 Xposed 的环境下,早有人实现了通过修改 GPS 定位信息模拟移动的模块,比如 Fake Location 就是一款非常经典的位置模拟应用。

  奈何这些软件或模块大多闭源,有的甚至还收费(比如刚刚提到的 Fake Location),让他们运行在高权限之下多少有些不放心。现如今想要获得一台 Root 的 Android 手机也变得越来越难,于是产生了重新打包乐跑 APP 实现注入的想法。

初步测试

  首先初步检查 APK,发现是加固的,导致重新打包 dex 添加 Xposed Hook 变得比较困难,遂采用另一种不需要修改 classes.dexAndroidManifest.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
# 跟踪          进程 ID      追踪 fork             重定向输出到文件
strace -p $(pidof zygote) -f 2> /sdcard/com.lptiyu.tanke.strace.txt

  为了避免多余的输出(执行命令后所有由 zygote 进程 fork 出的进程都会被追踪),最好通过 ssh 或在 adb shell 中远程执行此命令,接着启动乐跑 APP,等待 Activity 加载完毕后结束跟踪,然后执行下面的命令获取其 pid

1
2
# 列出进程      查找乐跑 APP
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.solibjni_utils.solibtrace_utils.solibsqlcipher.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

# 重打包 so 文件
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" // 脚本被修改时的行为,若为 "reload" 则会重新加载
}
}

  附 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.solibgadget.config.so 和重新打包的 libmmkv.so 一同添加/覆盖到安装包中,重新签名后安装,同时观察到 Logcat 中成功输出:

  再看屏幕,却发现 APP 迟迟没有启动,继续查看 Logcat,发现循环输出报错信息,原因尚不明:

参考

  [翻译]在未root的设备上使用frida-外文翻译-看雪论坛-安全社区

  Gadget | Frida • A world-class dynamic instrumentation framework