0%

需要

  • android 10 ROM:Android 10.0.0 (QP1A.191005.007.A3, Dec 2019)
  • simg2img: Android sparse imageLinux image;用来将system.img, vendor.img转为可挂载的image
  • img2simg: Linux imageAndroid sparse image;将可挂载的image转为可刷写的image
  • X-Ways Forensics: 以16进制编辑文件内容,并支持ext4文件系统格式挂载系统映像文件
  • IDA Pro:反汇编支持
  • ARM 架构参考手册:汇编指令支持
  • mount, umount:挂载和卸载指定分区

解压谷歌官方映像压缩包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ pwd
/home/ccint3/Desktop
$ tree sailfish
sailfish # 根目录,将谷歌官方镜像压缩包存放至该目录中
├── sailfish-qp1a.191005.007.a3-factory-d4552659 # 解压谷歌官方镜像压缩包后得到的目录
│   └── sailfish-qp1a.191005.007.a3 # 解压谷歌官方镜像压缩包后得到的目录
│   ├── bootloader-sailfish-8996-012001-1908071822.img # bootloader image 文件
│   ├── flash-all.bat # windows刷机脚本
│   ├── flash-all.sh # linux刷机脚本
│   ├── flash-base.sh # 刷机脚本,只刷bootloader和基带
│   ├── image-sailfish-qp1a.191005.007.a3 # 解压image压缩包得到的目录
│   │   ├── android-info.txt # 描述文件,描述当前目录下的image适用于那种设备和基带版本要求
│   │   ├── boot.img # boot image,包含内核以及recovery分区(android Q及以上版本将boot中的root分区移动至了system.img中)
│   │   ├── system.img # 包含system分区以及root分区 + avb校验元数据
│   │   ├── system_other.img # 附属的system分区内容(其中的数据不是非常重要)
│   │   └── vendor.img # 包含vendor分区 + avb校验元数据
│   ├── image-sailfish-qp1a.191005.007.a3.zip # image压缩包,包含system和vendor image
│   └── radio-sailfish-8996-130361-1905270421.img ## 基带 image 文件
└── sailfish-qp1a.191005.007.a3-factory-d4552659.zip # 谷歌官方镜像压缩包

3 directories, 12 files
1
2
3
4
5
6
7
8
9
10
11
$ pwd
/home/ccint3/Desktop/sailfish/sailfish-qp1a.191005.007.a3-factory-d4552659/sailfish-qp1a.191005.007.a3/image-sailfish-qp1a.191005.007.a3
$ tree
.
├── android-info.txt
├── boot.img
├── system.img
├── system_other.img
└── vendor.img

0 directories, 5 files

关闭avb

Android 启动时验证

该功能是为了防止恶意程序对系统映像文件的恶意修改,它对系统映像文件进行每4kb字节的数据生成校验值,最后将avb metadata附加到系统映像文件的结尾,一同刷写进android设备。android设备启动时,会对avb metadata进行校验。

实现流程:

  1. 选择一个随机盐(十六进制编码)
  2. 将系统映像拆分成 4k 大小的块
  3. 获取每个块的加盐 SHA256 哈希
  4. 组合这些哈希以形成层
  5. 在层中填充 0,直至达到 4k 块的边界
  6. 将层组合到哈希树中
  7. 重复第 2-6 步(使用前一层作为下一层的来源),直到最后只有一个哈希

该过程的结果是一个哈希,也就是根哈希。在构建 dm-verity 映射表时会用到该哈希和您选择的盐

dm-verity hash table

如果不关闭avb功能,对分区的修改会无法生效 或者 导致设备bootloop。

例如对分区中的某个文件进行了修改,重新刷写并重启后发现该文件的内容并未发生任何变化,这是因为android7以上开启了FEC向前纠错功能,简而言之该功能对系统的文件有简单的修复功能。另外如果设备bootloop,那么说明FEC向前纠错无法对更改的文件进行纠错,那么就会进入bootloop。

avb metadata头部的magic number可以控制avb是否生效。通过修改avb metadata magic number可以关闭avb功能;

校验元数据Magic number的定义如下:

#define VERITY_METADATA_MAGIC_NUMBER 0xb001b001

#define VERITY_METADATA_MAGIC_DISABLE 0x46464f56 // “VOFF”

Verity结构

字段 说明 大小 默认值
magic number 供 fs_mgr 用作一个健全性检查项目 4 bytes 0xb001b001
version 用于为Verity数据块添加版本号 4 bytes android 10为0
signature PKCS1.5 填充形式的表签名 256 bytes
table length dm-verity 表的长度(以字节数计) 4 bytes
table 上文介绍的 dm-verity 表 table length bytes
padding 此结构会通过填充 0 达到 32k 长度 0
关闭android 8以下的avb:

android 8(包含)以下可以直接修改boot.img/fstab.sailfish文件,删除其中的verity字段即可;这是因为boot.img中的root分区并没有被avb保护,所以可以直接修改root分区中的文件后重新打包root分区到boot.img中进行刷写。

关闭android 10的avb:

android 10(包含)以上boot.img中的root分区被移动到system.img中,因此修改boot.img并不能影响到root分区。因此需要通过以下的方式关闭

  1. 重命名

    system.imgvendor.img分别命名为system.img.orgvendor.img.org以备份

  2. android sparse image转linux image

    使用simg2img将system.img.orgvendor.img.org分别转为system.img.rawvendor.img.raw

    1
    2
    $ simg2img system.img.org system.img.raw
    $ simg2img vendor.img.org vendor.img.raw
  3. 修改avb校验元数据Magic Number

    使用16进制编辑器X-Ways Forensics分别修改system.img.rawvendor.img.raw中的校验元数据,将0xb001b001修改为0x46464f56

    搜索16进制字符串01B001B000000000定位到校验元数据头Magic number的位置

    • 修改system.img.raw,偏移:0x7EFE5000

      system.img.raw avbmeta magic enabled

      system.img.raw avbmeta magic disabled

    • 修改vendor.img.raw,偏移:0x1299b000

      vendor.img.raw avbmeta magic enabled

      vendor.img.raw avbmeta magic disabled

关闭 EXT4_FEATURE_RO_COMPAT_SHARED_BLOCKS

该功能会导致android 10以上的系统映像文件在ubuntu等系统上只能被挂载为只读模式。

关闭该功能是通过修改映像文件中的Superblock结构体来实现的。关闭该功能后虽然可挂载为可读写模式,但是不要试图去删除映像中的文件,这是因为该功能类似于通过引用计数的技术来共享公共文件,如果删除了某个被引用到的文件,那么需要额外的修复它的引用计数。但是不影响修改文件和新增文件。

以system.img.raw举例,superblock结构体的起始位置位于该文件0x400的位置。我们需要修改superblock.s_feature_ro_compat位置的4bytes数据,那么文件偏移为0x400 + 0x64 = 0x464;下面通过一段python代码展示如何关闭它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from io import SEEK_SET


def main():
image = open('system.img.raw', 'rb+')
image.seek(0x464, SEEK_SET)
s_feature_ro_compat = int.from_bytes(image.read(4), byteorder='little')
print('old s_feature_ro_compat: 0x%08X' % s_feature_ro_compat)
if s_feature_ro_compat & 0x4000:
s_feature_ro_compat = s_feature_ro_compat ^ 0x4000
image.seek(0x464, SEEK_SET)
image.write(s_feature_ro_compat.to_bytes(4, byteorder='little'))
print('new s_feature_ro_compat: 0x%08X' % s_feature_ro_compat)


if __name__ == '__main__':
main()

向selinux中注入策略

google提供的官方系统映像中selinux策略文件被保存在vendor/etc/selinux/precompiled_sepolicy文件中,该文件是经过*.te使用宏扩展为*.cil文件后再由selinux策略编译器生成的最终binary文件。因此我们直接向该文件中注入一些策略即可影响到操作系统的selinux。

magiskpolicy:该工具是Magisk工程中提供的一个小工具,它可以读取cli或者selinux binary并向其中注入自定义domain,因此从 Magisk 守护进程产生的所有进程,包括 root shell 及其所有分支,都在上下文 u:r:magisk:s0 中运行,以此来实现Magisk su。

那么我们可以使用该工具向vendor/etc/selinux/precompiled_sepolicy中注入我们自定义的domain;通过修改selinux.hpp中的几个宏即可自定义domain名称,当然这一步不是必须的。

1
2
3
4
5
6
7
8
// Unconstrained domain the daemon and root processes run in
#define SEPOL_PROC_DOMAIN "magisk"
// Highly constrained domain, sole purpose is to connect to daemon
#define SEPOL_CLIENT_DOMAIN "magisk_client"
// Unconstrained file type that anyone can access
#define SEPOL_FILE_TYPE "magisk_file"
// Special file type to allow clients to transit to client domain automatically
#define SEPOL_EXEC_TYPE "magisk_exec"

如果上一步中修改了domain,那么需要重新编译magiskpolicy工具,并执行以下命令使magiskpolicy向目标binary中注入SEPOL_PROC_DOMAIN domain

1
2
3
4
$ adb push magiskpolicy /data/local/tmp/magiskpolicy
$ adb shell chmod a+x /data/local/tmp/magiskpolicy
$ adb shell /data/local/tmp/magiskpolicy --magisk --load-split --save /sdcard/magisk.sepolicy
$ adb pull /sdcard/magisk.sepolicy magisk.sepolicy

得到magisk.sepolicy文件之后,挂载vendor.img.raw为可读写模式,并替换vendor/etc/selinux/precompiled_sepolicy文件。

1
2
3
4
$ mkdir ./vendor
$ sudo mount vendor.img.raw ./vendor
$ cp magisk.sepolicy ./vendor/etc/selinux/precompiled_sepolicy
$ sudo umount ./vendor

adbd修改

上一步中我们向selinux环境中注入了自定义的context以此来达到可以创建拥有root权限的进程。

adbd root

adbd root有多种方式:patch adbd binary、修改aosp源码

其中patch adbd binary的方式在另外一篇文章中已经讲过,这里主要讲一下修改aosp源码的方式

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
project system/core/
diff --git a/adb/daemon/main.cpp b/adb/daemon/main.cpp
index 620d078..a23e127 100644
--- a/adb/daemon/main.cpp
+++ b/adb/daemon/main.cpp
@@ -62,16 +62,17 @@ static inline bool is_device_unlocked() {
return "orange" == android::base::GetProperty("ro.boot.verifiedbootstate", "");
}

-static bool should_drop_capabilities_bounding_set() {
- if (ALLOW_ADBD_ROOT || is_device_unlocked()) {
- if (__android_log_is_debuggable()) {
- return false;
- }
- }
- return true;
-}
+//static bool should_drop_capabilities_bounding_set() {
+// if (ALLOW_ADBD_ROOT || is_device_unlocked()) {
+// if (__android_log_is_debuggable()) {
+// return false;
+// }
+// }
+// return true;
+//}

static bool should_drop_privileges() {
+ return false;
// "adb root" not allowed, always drop privileges.
if (!ALLOW_ADBD_ROOT && !is_device_unlocked()) return true;

@@ -128,46 +129,47 @@ static void drop_privileges(int server_port) {
// Don't listen on a port (default 5037) if running in secure mode.
// Don't run as root if running in secure mode.
if (should_drop_privileges()) {
- const bool should_drop_caps = should_drop_capabilities_bounding_set();
+ //const bool should_drop_caps = should_drop_capabilities_bounding_set();

- if (should_drop_caps) {
- minijail_use_caps(jail.get(), CAP_TO_MASK(CAP_SETUID) | CAP_TO_MASK(CAP_SETGID));
- }
+ //if (should_drop_caps) {
+ // minijail_use_caps(jail.get(), CAP_TO_MASK(CAP_SETUID) | CAP_TO_MASK(CAP_SETGID));
+ //}

- minijail_change_gid(jail.get(), AID_SHELL);
- minijail_change_uid(jail.get(), AID_SHELL);
+ //minijail_change_gid(jail.get(), AID_SHELL);
+ //minijail_change_uid(jail.get(), AID_SHELL);
// minijail_enter() will abort if any priv-dropping step fails.
- minijail_enter(jail.get());
+ //minijail_enter(jail.get());

// Whenever ambient capabilities are being used, minijail cannot
// simultaneously drop the bounding capability set to just
// CAP_SETUID|CAP_SETGID while clearing the inheritable, effective,
// and permitted sets. So we need to do that in two steps.
- using ScopedCaps =
- std::unique_ptr<std::remove_pointer<cap_t>::type, std::function<void(cap_t)>>;
- ScopedCaps caps(cap_get_proc(), &cap_free);
- if (cap_clear_flag(caps.get(), CAP_INHERITABLE) == -1) {
- PLOG(FATAL) << "cap_clear_flag(INHERITABLE) failed";
- }
- if (cap_clear_flag(caps.get(), CAP_EFFECTIVE) == -1) {
- PLOG(FATAL) << "cap_clear_flag(PEMITTED) failed";
- }
- if (cap_clear_flag(caps.get(), CAP_PERMITTED) == -1) {
- PLOG(FATAL) << "cap_clear_flag(PEMITTED) failed";
- }
- if (cap_set_proc(caps.get()) != 0) {
- PLOG(FATAL) << "cap_set_proc() failed";
- }
-
- D("Local port disabled");
+ //using ScopedCaps =
+ // std::unique_ptr<std::remove_pointer<cap_t>::type, std::function<void(cap_t)>>;
+ //ScopedCaps caps(cap_get_proc(), &cap_free);
+ //if (cap_clear_flag(caps.get(), CAP_INHERITABLE) == -1) {
+ // PLOG(FATAL) << "cap_clear_flag(INHERITABLE) failed";
+ //}
+ //if (cap_clear_flag(caps.get(), CAP_EFFECTIVE) == -1) {
+ // PLOG(FATAL) << "cap_clear_flag(PEMITTED) failed";
+ //}
+ //if (cap_clear_flag(caps.get(), CAP_PERMITTED) == -1) {
+ // PLOG(FATAL) << "cap_clear_flag(PEMITTED) failed";
+ //}
+ //if (cap_set_proc(caps.get()) != 0) {
+ // PLOG(FATAL) << "cap_set_proc() failed";
+ //}
+
+ //D("Local port disabled");
} else {
// minijail_enter() will abort if any priv-dropping step fails.
minijail_enter(jail.get());

if (root_seclabel != nullptr) {
+ D("root_seclabel: %s", root_seclabel);
if (selinux_android_setcon(root_seclabel) < 0) {
LOG(FATAL) << "Could not set SELinux context";
- }
+ }
}
std::string error;
std::string local_name =
adbd 取消认证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
diff --git a/adb/daemon/main.cpp b/adb/daemon/main.cpp
index 72bad5f..79b1780 100644
--- a/adb/daemon/main.cpp
+++ b/adb/daemon/main.cpp
@@ -208,12 +208,13 @@ int adbd_main(int server_port) {

#if defined(ALLOW_ADBD_NO_AUTH)
// If ro.adb.secure is unset, default to no authentication required.
- auth_required = android::base::GetBoolProperty("ro.adb.secure", false);
+ //auth_required = android::base::GetBoolProperty("ro.adb.secure", false);
#elif defined(__ANDROID__)
- if (is_device_unlocked()) { // allows no authentication when the device is unlocked.
- auth_required = android::base::GetBoolProperty("ro.adb.secure", false);
- }
+ //if (is_device_unlocked()) { // allows no authentication when the device is unlocked.
+ // auth_required = android::base::GetBoolProperty("ro.adb.secure", false);
+ //}
#endif
+ auth_required = false;

adbd_auth_init();

应用adbd修改

重新编译adbd之后需要挂载system.img.raw为可读写模式并将系统映像中的adbd替换掉

1
2
3
4
$ mkdir system
$ sudo mount system.img.raw system
$ cp adbd ./system/system/bin/adbd
$ sudo umount system

init 在启动adbd时使用的selinux context是u:r:adbd:s0,再经过selinux_android_setcon函数将context切换到u:r:su:s0;因为我们是向sepolicy中注入了自定的domain,因此我们需要将 u:r:magisk:s0 应用至 system.img.raw/init.usb.rc

1
2
3
4
$ mkdir system
$ sudo mount system.img.raw system
$ vim ./system/init.usb.rc
$ sudo umount system

例如:

1
2
3
4
5
6
# adbd is controlled via property triggers in init.<platform>.usb.rc
service adbd /system/bin/adbd --root_seclabel=u:r:magisk:s0
class core
socket adbd seqpacket 660 system system
disabled
seclabel u:r:magisk:s0
adbd开机启动

修改init.usb.rc,在on boot阶段加入persist.sys.usb.config adb

1
2
3
4
$ mkdir system
$ sudo mount system.img.raw system
$ vim ./system/init.usb.rc
$ sudo umount system

例如:

1
2
3
on boot
setprop sys.usb.configfs 0
setprop persist.sys.usb.config adb

刷写修改后的系统映像

1
2
3
4
5
6
$ img2simg system.img.raw system.new.img
$ img2sigm vendor.img.raw vendor.new.img
$ adb reboot bootloader
$ fastboot flash system system.new.img
$ fastboot flash vendor vendor.new.img
$ fastboot reboot

需要

  • android系统镜像:10.0.0 (QP1A.191005.007.A3, Dec 2019) - d455265945bb936a653730031af7d7a4aba70dc0c775024666a53491c9833b61
  • simg2img: Android sparse image转Linux image;用来将system.img, vendor.img转为可挂载的image
  • img2simg: Linux image转Android sparse image;将可挂载的image转为可刷写的image
  • X-Ways Forensics: 以16进制编辑文件内容,并支持ext4文件系统格式挂载文件
  • IDA Pro:反汇编支持
  • ARM 架构参考手册:汇编指令支持

解压谷歌官方镜像压缩包后得到以下文件树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ pwd
/home/ccint3/Desktop
$ tree sailfish
sailfish # 根目录,将谷歌官方镜像压缩包存放至该目录中
├── sailfish-qp1a.191005.007.a3-factory-d4552659 # 解压谷歌官方镜像压缩包后得到的目录
│   └── sailfish-qp1a.191005.007.a3 # 解压谷歌官方镜像压缩包后得到的目录
│   ├── bootloader-sailfish-8996-012001-1908071822.img # bootloader image 文件
│   ├── flash-all.bat # windows刷机脚本
│   ├── flash-all.sh # linux刷机脚本
│   ├── flash-base.sh # 刷机脚本,只刷bootloader和基带
│   ├── image-sailfish-qp1a.191005.007.a3 # 解压image压缩包得到的目录
│   │   ├── android-info.txt # 描述文件,描述当前目录下的image适用于那种设备和基带版本要求
│   │   ├── boot.img # boot image,包含内核以及recovery分区(android Q及以上版本将boot中的root分区移动至了system.img中)
│   │   ├── system.img # 包含system分区以及root分区 + avb校验元数据
│   │   ├── system_other.img # 附属的system分区内容(其中的数据不是非常重要)
│   │   └── vendor.img # 包含vendor分区 + avb校验元数据
│   ├── image-sailfish-qp1a.191005.007.a3.zip # image压缩包,包含system和vendor image
│   └── radio-sailfish-8996-130361-1905270421.img ## 基带 image 文件
└── sailfish-qp1a.191005.007.a3-factory-d4552659.zip # 谷歌官方镜像压缩包

3 directories, 12 files

工作目录和其文件树

下文的操作如无特殊说明,均在sailfish/sailfish-qp1a.191005.007.a3-factory-d4552659/sailfish-qp1a.191005.007.a3/image-sailfish-qp1a.191005.007.a3中进行

1
2
3
4
5
6
7
8
9
10
11
$ pwd
/home/ccint3/Desktop/sailfish/sailfish-qp1a.191005.007.a3-factory-d4552659/sailfish-qp1a.191005.007.a3/image-sailfish-qp1a.191005.007.a3
$ tree
.
├── android-info.txt
├── boot.img
├── system.img
├── system_other.img
└── vendor.img

0 directories, 5 files

关闭avb

avb功能首先对分区进行每4kb字节的数据生成校验值,并将这些校验值合并到一起生成一个校验值数据块,校验值数据块的大小取决于分区大小并将这个数据块称作avb metadata,最后将avb metadata附加到image结尾,一同刷写进android设备。

android设备启动时,首先对avb metadata的root节点进行校验,如果通过则继续启动,否则将陷入bootloop.

dm-verity hash table

如果不关闭avb功能,对分区的修改会导致android设备bootloop或者修改的内容无法生效(无法生效是通过FEC向前纠错来实现的)。

avb metadata头部的magic number可以控制avb是否生效。通过修改avb metadata magic number可以关闭avb功能;

校验元数据Magic number的定义如下:

#define VERITY_METADATA_MAGIC_NUMBER 0xb001b001

#define VERITY_METADATA_MAGIC_DISABLE 0x46464f56 // “VOFF”

  • android O上可以直接修改boot.img/fstab.sailfish文件,删除其中的verity字段即可;

    这是因为boot.img中的root分区并没有被avb保护,所以可以直接修改root分区中的文件后重新打包root分区到boot.img中进行刷写。

  • android Q上boot.img中的root分区被移动到system.img中,所以修改boot.img是不生效的。

关闭android Q的avb:

  1. 重命名

    system.imgvendor.img分别命名为system.img.orgvendor.img.org以备份

  2. android sparse image转linux image

    使用simg2img将system.img.orgvendor.img.org分别转为system.img.rawvendor.img.raw

    1
    2
    $ simg2img system.img.org system.img.raw
    $ simg2img vendor.img.org vendor.img.raw
  3. 修改avb校验元数据Magic number

    使用16进制编辑器X-Ways Forensics分别修改system.img.rawvendor.img.raw中的校验元数据,将0xb001b001修改为0x46464f56

    搜索16进制字符串01B001B000000000定位到校验元数据头Magic number的位置

    • 修改system.img.raw,偏移:0x7EFE5000

      system.img.raw avbmeta magic enabled

      system.img.raw avbmeta magic disabled

    • 修改vendor.img.raw,偏移:0x1299b000

      vendor.img.raw avbmeta magic enabled

      vendor.img.raw avbmeta magic disabled

关闭selinux

  • android O

    修改aosp源码init.cpp: selinux_is_enforcing函数,使该函数直接返回false。

    重新编译init可执行文件。

    boot.img解包后替换init可执行文件。

    重新打包boot.img后刷写到android设备中。

  • android Q

    boot.img中的根分区被移动到system.img中,init可执行文件也同样被移动到system.img中。

    那么就需要对system.img中的init可执行文件进行修改,但是遇到一个问题就是system.img无法挂载为可读可写。并且remount时提示“写保护”。

    1
    2
    3
    4
    5
    $ sudo mount system.img.raw system_raw
    mount: /home/ccint3/Desktop/sailfish/sailfish-qp1a.191005.007.a3-factory-d4552659/sailfish-qp1a.191005.007.a3/image-sailfish-qp1a.191005.007.a3/system_raw: wrong fs type, bad option, bad superblock on /dev/loop6, missing codepage or helper program, or other error.
    $ sudo mount -r system.img.raw system_raw
    $ sudo mount -o rw,remount system_raw
    mount: /home/ccint3/Desktop/sailfish/sailfish-qp1a.191005.007.a3-factory-d4552659/sailfish-qp1a.191005.007.a3/image-sailfish-qp1a.191005.007.a3/system_raw: cannot remount /dev/loop6 read-write, is write-protected.

    那么只能对system.img进行16进制编辑并保存,因此就需要X-Ways Forensics工具提供支持,这个工具支持以16进制编辑system.img,并且可以将system.img转为ext4的文件系统并编辑。

    1. 阅读android Q的源码,了解如何关闭selinux

      android Q上开启selinux的源码发生了变化,对selinux进行了独立的源码管理

      selinux.cpp: IsEnforcing

      查看IsEnforcing的调用关系,并定位到selinux.cpp: SelinuxInitialize

    2. 修改system.img中的init可执行文件

      X-Ways Forensics打开system.img,并将文件转换为磁盘

      system.img.raw winhex convert to disk

      将system.img磁盘中的system/bin/init可执行文件复制出来

      system.img.raw disk pull init

      使用IDA Pro x64打开init可执行文件进行分析,因为从源码中我们知道SelinuxInitialize函数最终启动了selinux,因此我们在IDA中定位SelinuxInitialize函数,在SelinuxInitialize中有一些日志字符串,我们通过这些字符串就可以定位SelinuxInitialize的位置,例如:Loading SELinux policy;在IDA中shift + F12打开字符串窗口,并搜索Loading SELinux policy获取以下结果

      init.SelinuxInitialize string location

      对字符串参考引用后,最终定位到下图的位置

      init.SelinuxInitialize string location

      反汇编后,看到下图中的代码

      init.SelinuxInitialize security_getenforce

      关键代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 因为编译器内联汇编的原因,IsEnforcing函数直接被替换为了常量 1
      // 与 security_getenforce 的返回值做比较
      if ( (unsigned int)security_getenforce() != 1 )
      {
      // 因为编译器内联汇编的原因,所以security_setenforce函数的参数被替换为了常量1
      // 通过 security_setenforce(1) 来启动selinux
      // 因此我们需要修改security_setenforce函数的参数,将参数该为0,表示关闭selinux
      v6 = (android::base *)security_setenforce(1LL);
      if ( (_DWORD)v6 )
      {
      // ...
      }
      }

      我们的最终目的是修改security_setenforce(1)security_setenforce(0)即可关闭selinux;

      因为默认请情况下security_getenforce返回1,表示默认启动selinux;那么security_getenforce() != 1则表示不会调用security_setenforce函数。

      至此我们明确了需要修改的两处内容:

      1:将security_getenforce() != 1修改为security_getenforce() != 0

      2:将security_setenforce(1LL)修改为security_setenforce(0LL)

      转至汇编代码,查看对应的汇编代码并修改,汇编代码如下图所示:

      init.SelinuxInitialize security_getenforce asm

      偏移6A8FC处的汇编CMP W0, #1修改为CMP W0, #0

      CMP W0, #0的汇编指令:0x7100001F

      偏移6A904处的汇编MOV W0, #1修改为MOV W0, #0

      MOV W0, #0的汇编指令:0x52800000

      修改后如图所示

      init.SelinuxInitialize security_getenforce asm modify

      将修改的内容写到init文件中,查看代码偏移6A8FC6A904对应的文件偏移6A8FC6A904

      使用X-Ways Forensics打开init文件,并跳转至文件偏移后,将汇编指令写入该位置:

      init.SelinuxInitialize security_getenforce file

      修改后:

      init.SelinuxInitialize security_getenforce file midify

关闭adbd指纹认证

  • android O

    修改aosp adbd源码,重新编译之后,解包system.img,替换system.img中的adbd可执行文件,然后对system.img重新打包,之后刷写进android设备即可。

  • android Q

    实现方式同关闭selinux类似,定位adbd源码位置,查看如何在源码级关闭指纹认证。之后使用IDA Pro修改对应的汇编,最后在X-Ways Forensics中修改system.img中adbd的文件内容。

    源码位置:adb/daemon/main.cpp:adbd_main

    关键代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int adbd_main(int server_port) {
    // ...
    #if defined(ALLOW_ADBD_NO_AUTH)
    // If ro.adb.secure is unset, default to no authentication required.
    auth_required = android::base::GetBoolProperty("ro.adb.secure", false);
    #elif defined(__ANDROID__)
    if (is_device_unlocked()) { // allows no authentication when the device is unlocked.
    auth_required = android::base::GetBoolProperty("ro.adb.secure", false);
    }
    #endif
    adbd_auth_init();
    // ...
    return 0;
    }

    全局变量auth_requiredtrue时,开启adbd指纹认证,为false时则关闭指纹认证。

    该变量通过ro.adb.secure来控制,在default.prop中设置该属性,或者修改GetBoolProperty函数的返回值。

    在system.img中复制出system/bin/adbd可执行文件,并使用IDA Pro x64打开。
    通过ro.adb.secure字符串参考引用来定位adbd_main函数的位置。最终定位到如下图所示:

    adbd.adbd_main

    关键代码的反汇编如下:

    adbd.adbd_main disasm

    查看对应的汇编代码如下:

    adbd.adbd_main asm

    偏移20F0处的汇编AND W8, W0, #1修改为MOV W8, #0

    MOV W8, #0的汇编指令:0x52800008

    修改如下:

    adbd.adbd_main asm modify

    最后通过X-Ways Forensics将修改的内容应用到system.img system/bin/adbd

禁止adbd root权限降级

修改方式同关闭adbd指纹认证

源码位置:

adb/daemon/main.cpp:adbd_main

adb/daemon/main.cpp:drop_privileges

adb/daemon/main.cpp:should_drop_privileges

关键代码如下:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
int adbd_main(int server_port) {
// ...
#if defined(__ANDROID__)
drop_privileges(server_port);
#endif
// ...
return 0;
}

static void drop_privileges(int server_port) {
// ...
if (should_drop_privileges()) {
// ...
} else {
// minijail_enter() will abort if any priv-dropping step fails.
minijail_enter(jail.get());
if (root_seclabel != nullptr) {
if (selinux_android_setcon(root_seclabel) < 0) {
LOG(FATAL) << "Could not set SELinux context";
}
}
std::string error;
std::string local_name =
android::base::StringPrintf("tcp:%d", server_port);
if (install_listener(local_name, "*smartsocket*", nullptr, 0, nullptr, &error)) {
LOG(FATAL) << "Could not install *smartsocket* listener: " << error;
}
}
}

static bool should_drop_privileges() {
// "adb root" not allowed, always drop privileges.
if (!ALLOW_ADBD_ROOT && !is_device_unlocked()) return true;
// The properties that affect `adb root` and `adb unroot` are ro.secure and
// ro.debuggable. In this context the names don't make the expected behavior
// particularly obvious.
//
// ro.debuggable:
// Allowed to become root, but not necessarily the default. Set to 1 on
// eng and userdebug builds.
//
// ro.secure:
// Drop privileges by default. Set to 1 on userdebug and user builds.
bool ro_secure = android::base::GetBoolProperty("ro.secure", true);
bool ro_debuggable = __android_log_is_debuggable();
// Drop privileges if ro.secure is set...
bool drop = ro_secure;
// ... except "adb root" lets you keep privileges in a debuggable build.
std::string prop = android::base::GetProperty("service.adb.root", "");
bool adb_root = (prop == "1");
bool adb_unroot = (prop == "0");
if (ro_debuggable && adb_root) {
drop = false;
}
// ... and "adb unroot" lets you explicitly drop privileges.
if (adb_unroot) {
drop = true;
}
return drop;
}

读取ro.secureservice.adb.root属性并判断是否需要降级。

我们需要将should_drop_privileges的返回值改为false;通过字符串ro.secure使用IDA Pro x64定位到目标代码位置。并反汇编后代码如下:

adbd.adbd_main adbroot disasm

需要修改的汇编位置如下:

偏移2270处汇编 AND W8, W10, #1修改为MOV W8, #0

adbd.adbd_main adbroot asm modify 2270

偏移22C8处汇编ORR W21, W9, W8修改为MOV W21, #0

adbd.adbd_main adbroot asm modify 22C8

偏移22D0处汇编AND W8, W8, #1修改为MOV W8, #0

adbd.adbd_main adbroot asm modify 22D0

偏移22E8处汇编AND W21, W21, W9修改为MOV W21, #0

adbd.adbd_main adbroot asm modify 22E8

偏移245C处汇编550400948000F8364B0400941F1800710D420054修改为0xD503201F*5

adbd.adbd_main adbroot asm modify 245C

开机启动adbd,无需打开开发者模式并勾选USB调试

android Q上通过修改vendor.img中的etc/init/init.usb.rc来影响adbd的启动

init.usb.rc修改为:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
on property:sys.usb.config=mtp
setprop sys.usb.config adb

on property:sys.usb.config=mtp,adb
write /sys/class/android_usb/android0/enable 0
write /sys/class/android_usb/android0/idVendor 18D1
write /sys/class/android_usb/android0/idProduct 4EE2
write /sys/class/android_usb/android0/bDeviceClass 0
write /sys/class/android_usb/android0/bDeviceSubClass 0
write /sys/class/android_usb/android0/bDeviceProtocol 0
write /sys/class/android_usb/android0/functions ${sys.usb.config}
write /sys/class/android_usb/android0/enable 1
start adbd
setprop sys.usb.state ${sys.usb.config}

on property:sys.usb.config=rndis
setprop sys.usb.config adb

on property:sys.usb.config=rndis,adb
write /sys/class/android_usb/android0/enable 0
write /sys/class/android_usb/android0/idVendor 18D1
write /sys/class/android_usb/android0/idProduct 4EE4
write /sys/class/android_usb/android0/bDeviceClass 239
write /sys/class/android_usb/android0/bDeviceSubClass 2
write /sys/class/android_usb/android0/bDeviceProtocol 1
write /sys/class/android_usb/android0/functions ${sys.usb.config}
write /sys/class/android_usb/android0/enable 1
start adbd
setprop sys.usb.state ${sys.usb.config}

on property:sys.usb.config=ptp
setprop sys.usb.config adb

on property:sys.usb.config=ptp,adb
write /sys/class/android_usb/android0/enable 0
write /sys/class/android_usb/android0/idVendor 18D1
write /sys/class/android_usb/android0/idProduct 4EE6
write /sys/class/android_usb/android0/bDeviceClass 0
write /sys/class/android_usb/android0/bDeviceSubClass 0
write /sys/class/android_usb/android0/bDeviceProtocol 0
write /sys/class/android_usb/android0/functions ptp,adb
write /sys/class/android_usb/android0/enable 1
start adbd
setprop sys.usb.state ${sys.usb.config}

on property:sys.usb.config=midi
setprop sys.usb.config adb

on property:sys.usb.config=midi,adb
write /sys/class/android_usb/android0/enable 0
write /sys/class/android_usb/android0/idVendor 18D1
write /sys/class/android_usb/android0/idProduct 4EE9
write /sys/class/android_usb/android0/bDeviceClass 0
write /sys/class/android_usb/android0/bDeviceSubClass 0
write /sys/class/android_usb/android0/bDeviceProtocol 0
write /sys/class/android_usb/android0/functions ${sys.usb.config}
write /sys/class/android_usb/android0/enable 1
start adbd
setprop sys.usb.state ${sys.usb.config}

修改后的init.usb.rc相比源文件有所缩短,为了不破坏文件,我们还需要维护这个文件在ext4文件系统inode块文件大小。通过X-Ways Forensics跳转至该文件的inode数据块,统计修改后这个文件的真实大小并修改inode数据库记录即可。

vendor.img.raw goto inode

vendor.img.raw modify inode

最后保存修改并刷写至手机即可

通过ro.boot.vbmeta.digest属性确定当前系统是否正在使用AVB。如果没有设置该属性,那么当前系统运行在VB1.0模式

简介

该框架是一个网络框架,一般是作为okhttp的上层框架;请求先经过该框架的处理之后再交给okhttp;它的功能实际上是将java interface提取成request请求接口。之后交给 okhttp将发送request。

下面以美团9.8.0作为例子来分析。

分析Retrofit框架

Retrofit类

Retrofit类是框架的核心类,管理所有的interface;只要获得该对象,就可以调用任意一个已经创建好的interface接口;

Retrofit$Builder

Retrofit类有一个Builder内部类,用来创建Retrofit类的对象。

通过hook Retrofit$Builder.build可以获取到Retrofit的对象,也可以通过调用栈继续追踪调用源头(建议)。美团的Retrofit类采用单例设计模式;不同的app Retrofit对象的数量可能不同。

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
28
29
30
31
32
public final Retrofit build() {
// ...

if(this.baseUrl != null) {
Factory v2 = this.callFactory;
if(v2 != null) {
Executor v0 = this.httpExecutor;
if(v0 == null) {
v0 = Retrofit.defaultHttpExecutor;
if(v0 == null) {
v0 = this.platform.defaultHttpExecutor();
}
}

Executor v7 = v0;
Executor v0_1 = this.callbackExecutor;
if(v0_1 == null) {
v0_1 = this.platform.defaultCallbackExecutor();
}

ArrayList v5 = new ArrayList(this.adapterFactories);
v5.add(this.platform.defaultCallAdapterFactory(v0_1));

// 构造 Retrofit 对象
return new Retrofit(v2, this.baseUrl, new ArrayList(this.converterFactories), v5, this.interceptors, v7, v0_1, this.validateEagerly, this.cache);
}

throw new IllegalStateException("RawCall.Factory required.");
}

throw new IllegalStateException("Base URL required.");
}
Retrofit对象的源头

通过Retrofit$Builder.build调用栈追踪,定位到com.sankuai.waimai.platform.capacity.network.retrofit.c.a(java.lang.Class): com.sankuai.meituan.retrofit2.Retrofit方法

该方法传入一个interface后返回Retrofit对象。从代码上来看它是全局保存的

1
2
3
4
5
6
7
8
9
10
11
12
public static Retrofit a(Class arg11) {
// ...

// 尝试从缓存中读取Retrofit对象
q v11 = (q)c.c.get(arg11);
if(v11 == null) {
v11 = c.b;
}

// 创建Retrofit对象,最终调用到 Retrofit$Builder.build 和 Retrofit.create
return (Retrofit)v11.c();
}
Retrofit.create

Retrofit类的create方法传入一个interface,然后为这个interface实现代理类;也就是说Retrofit已经为这个接口创建好了request,等待外部调用即可。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public final Object create(Class arg13) {
// ...

// 校验arg13,必须是interface才行
Utils.validateServiceInterface(arg13);
if(this.validateEagerly) {
// 解析 interface 中声明的method(解析注解),并为方法创建代理
this.eagerlyValidateMethods(arg13);
}

// 为 interface 创建代理类,并等待外部调用
return Proxy.newProxyInstance(arg13.getClassLoader(), new Class[]{arg13}, new InvocationHandler() {
public static ChangeQuickRedirect changeQuickRedirect;
public final Platform platform;
public final Retrofit this$0;
public final Class val$service;

{
Class arg2 = arg13; // captured argument (potential naming conflict with outer method variables - use N to rename)
this.platform = Platform.get();
}

@Override
public Object invoke(Object arg12, Method arg13, Object[] arg14) throws Throwable {
// ...

if(arg13.getDeclaringClass() == Object.class) {
return arg13.invoke(this, arg14);
}

if(this.platform.isDefaultMethod(arg13)) {
return this.platform.invokeDefaultMethod(arg13, arg13, arg12, arg14);
}

// 解析 interface 中声明的 method(解析注解),并为 method 创建代理
ServiceMethod v12 = Retrofit.this.loadServiceMethod(arg13);

// 创建 ClientCall 对象并根据 method 的参数动态生成 request
return v12.callAdapter.adapt(new ClientCall(v12, arg14, Retrofit.this.interceptors, Retrofit.defInterceptors, Retrofit.this.httpExecutor, Retrofit.this.cache));
}
});
}
HomePageApi interface

HomePageApi interface 是美团定义的,需要被Retrofit.create注册为动态代理类的interface,它的内部定义了一些method,当Retrofit注册完成后,就可以被外部所调用以完成request。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public interface HomePageApi {
@FormUrlEncoded
@POST("v6/home/dynamic/tabs")
d getDynamicTabInfo(@Field("last_time_actual_latitude") String arg1, @Field("last_time_actual_longitude") String arg2);

@FormUrlEncoded
@POST("v7/product/list")
d getFoodReunionPoilist(@Field("page_index") String arg1, @Field("session_id") String arg2, @Field("rank_trace_id") String arg3, @Field("rank_list_id") String arg4, @Field("filter_tab_list") String arg5, @Field("behavioral_characteristics") String arg6, @Field("navigate_type") long arg7);

@FormUrlEncoded
// 通过注解定义 url
@POST("v7/poi/homepage")
d getHomePagePoiList(
// getHomePagePoiList 的参数列表,当调用该参数时需要传入这些参数。
@Field("seq_num") int arg1,
@Field("offset") int arg2,
@Field("dynamic_page") boolean arg3,
@Field("page_index") long arg4,
@Field("page_size") long arg5,
@Field("sort_type") long arg6,
@Field("activity_filter_codes") String arg7,
@Field("slider_select_data") String arg8,
@Field("load_type") int arg9,
@Field("rank_trace_id") String arg10,
@Field("session_id") String arg11,
@Field("union_id") String arg12,
@Field("rank_list_id") String arg13,
@Field("category_type") int arg14,
@Field("second_category_type") int arg15,
@Field("behavioral_characteristics") String arg16
);

@FormUrlEncoded
@POST("v7/product/recommend/entrance")
d getRecommendEntrance(@FieldMap Map arg1);

@FormUrlEncoded
@POST("v6/poi/dynamicrecommend")
d getRecommendPoiCard(@FieldMap Map arg1);

@FormUrlEncoded
@POST("v8/home/gettopbanner")
d getTopBanner(@Field("topbanner_refresh_poi_ids") String arg1, @Field("topbanner_refresh_activity_ids") String arg2);

@FormUrlEncoded
@POST("v6/product/tag")
d optimizationFeedbackReport(@Field("poi_id") long arg1, @Field("tag_type") int arg2, @Field("entry_id") int arg3, @Field("reason_type") int arg4, @Field("extend") String arg5);

@FormUrlEncoded
@POST("v6/product/tag")
d tagProduct(@Field("poi_id") long arg1, @Field("spu_id") long arg2, @Field("dpc_id") long arg3, @Field("tag_type") int arg4, @Field("entry_id") int arg5, @Field("reason_type") int arg6, @Field("extend") String arg7);
}

ServiceMethod 类

ServiceMethod 类用来关联 interface method, 每个ServiceMethod对象关联一个interface method。

通过ServiceMethod$Builder.build来创建ServiceMethod 对象。ServiceMethod 记录了url,请求类型,参数个数,参数类型等等request信息;最终由ClientCall使用ServiceMethod 对象生成和发送request。

可以理解为一个interface method对应一个ServiceMethod;一个ServiceMethod对应一个request;

生成逻辑:interface method生成ServiceMethod,ServiceMethod生成request。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public final ServiceMethod build() {
Type v4;
int v2;
// ...

this.callAdapter = this.createCallAdapter();
this.responseType = this.callAdapter.responseType();
if(this.responseType != Response.class && this.responseType != RawResponse.class) {
this.responseConverter = this.createResponseConverter();
Annotation[] v1 = this.methodAnnotations;
int v3;
for(v3 = 0; v3 < v1.length; ++v3) {
// 解析 method 的注解,可以解析出request.method request.url, request.params等等
this.parseMethodAnnotation(v1[v3]);
}

if(this.httpMethod != null) {
if(!this.hasBody) {
// 这个request不包含body
if(this.isMultipart) {
throw this.methodError("Multipart can only be specified on HTTP methods with request body (e.g., @POST).", new Object[0]);
}

if(this.isFormEncoded) {
throw this.methodError("FormUrlEncoded can only be specified on HTTP methods with request body (e.g., @POST).", new Object[0]);
throw this.methodError("Multipart can only be specified on HTTP methods with request body (e.g., @POST).", new Object[0]);
}
}

// request.headers
this.parameterHandlers = new ParameterHandler[this.parameterAnnotationsArray.length];
v2 = 0;
goto label_58;
}

throw this.methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).", new Object[0]);
while(true) {
label_58:
// 解析参数注解
if(v2 >= this.parameterAnnotationsArray.length) {
goto label_81;
}

v4 = this.parameterTypes[v2];
if(Utils.hasUnresolvableType(v4)) {
throw this.parameterError(v2, "Parameter type must not include a type variable or wildcard: %s", new Object[]{v4});
}

Annotation[] v3_1 = this.parameterAnnotationsArray[v2];
if(v3_1 == null) {
throw this.parameterError(v2, "No Retrofit annotation found.", new Object[0]);
}

this.parameterHandlers[v2] = this.parseParameter(v2, v4, v3_1);
++v2;
}

throw this.parameterError(v2, "No Retrofit annotation found.", new Object[0]);
throw this.parameterError(v2, "Parameter type must not include a type variable or wildcard: %s", new Object[]{v4});
label_81:
if(this.relativeUrl == null && !this.gotUrl) {
throw this.methodError("Missing either @%s URL or @Url parameter.", new Object[]{this.httpMethod});
}

if(!this.isFormEncoded && !this.isMultipart && !this.hasBody && (this.gotBody)) {
throw this.methodError("Non-body HTTP method cannot contain @Body.", new Object[0]);
}

if((this.isFormEncoded) && !this.gotField) {
throw this.methodError("Form-encoded method must contain at least one @Field.", new Object[0]);
}

if((this.isMultipart) && !this.gotPart) {
throw this.methodError("Multipart method must contain at least one @Part.", new Object[0]);
}

return new ServiceMethod(this);
}

StringBuilder v1_1 = new StringBuilder("\'");
v1_1.append(Utils.getRawType(this.responseType).getName());
v1_1.append("\' is not a valid response body type. Did you mean ResponseBody?");
throw this.methodError(v1_1.toString(), new Object[0]);
}

ClientCall 类

ClientCall对象由Retrofit生成的动态代理类创建,ClientCall用来生成okhttp.Request对象,并发送request。

如果在Retrofit + okhttp的app中,ClientCall表示为okhttp3.RealCall;但是美团app的核心请求没有使用okhttp框架,而是自己实现的一套ClientCall代码;所以美团ClientCall的功能对标okhttp3.RealCall

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// 构造
public ClientCall(ServiceMethod arg19, Object[] arg20, List arg21, List arg22, Executor arg23, Cache arg24) {
// ...

this.convertElapse = -1L;

// interface method, serviceMethod
this.serviceMethod = arg19;

// request.param
this.args = arg20;

// 拦截器
this.clientInterceptors = arg21;

// 默认拦截器
this.defInterceptors = arg22;

// 发送http请求的执行器
this.httpExecutor = arg23;
this.cache = arg24;
}

// 执行请求,获取响应
public final Response execute() throws IOException {
Request v2;
// ...

long v0 = System.currentTimeMillis();
__monitor_enter(this);
try {
if(this.executed) {
throw new IllegalStateException("Already executed.");
}

this.executed = true;
if(this.creationFailure != null) {
if((this.creationFailure instanceof IOException)) {
throw (IOException)this.creationFailure;
}

throw (RuntimeException)this.creationFailure;
}

v2 = this.originalRequest;
if(v2 == null) {
try {
// serviceMethod 生成 request
v2 = this.serviceMethod.toRequest(this.args);
this.originalRequest = v2;
goto label_42;
}
catch(IOException | RuntimeException v0_2) {
}

this.creationFailure = v0_2;
throw v0_2;
}

label_42:
__monitor_exit(this);
}
catch(Throwable v0_1) {
goto label_67;
}

try {
// 进入拦截器链(对request进行签名,添加公共头,公共参数等等操作),发送请求,获得响应
Response v0_4 = this.parseResponse(this.getResponseWithInterceptorChain(v2.newBuilder().addHeader("retrofit_exec_time", String.valueOf(v0)).build()));
Retrofit.RequestCallbackDispatcher.onSuccess(this, this.realRequest, v0_4, this.convertElapse);
return v0_4;
}
catch(Throwable v0_3) {
}

Retrofit.RequestCallbackDispatcher.onError(this, this.realRequest, v0_3);
throw v0_3;
try {
throw new IllegalStateException("Already executed.");
label_67:
__monitor_exit(this);
}
catch(Throwable v0_1) {
goto label_67;
}

throw v0_1;
}

总结与hook

我们现在需要调用v7/poi/homepage这个request,并拦截response。

首先确定这个url 属于 HomePageApi.getHomePagePoiList interface method:

1
2
3
4
5
6
7
8
9
public interface HomePageApi {
// ...

@FormUrlEncoded
@POST("v7/poi/homepage")
d getHomePagePoiList(@Field("seq_num") int arg1, @Field("offset") int arg2, @Field("dynamic_page") boolean arg3, @Field("page_index") long arg4, @Field("page_size") long arg5, @Field("sort_type") long arg6, @Field("activity_filter_codes") String arg7, @Field("slider_select_data") String arg8, @Field("load_type") int arg9, @Field("rank_trace_id") String arg10, @Field("session_id") String arg11, @Field("union_id") String arg12, @Field("rank_list_id") String arg13, @Field("category_type") int arg14, @Field("second_category_type") int arg15, @Field("behavioral_characteristics") String arg16);

// ...
}

hook com.sankuai.waimai.platform.capacity.network.retrofit.c.a(java.lang.Class)方法,传入需要的interface后获取到全局唯一的Retrofit对象;使Retrofit框架为HomePageApi interface注册动态代理;

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
let rObject = Java.use('java.lang.Object');
let rThread = Java.use('java.lang.Thread');
let rString = Java.use('java.lang.String');
let rInteger = Java.use('java.lang.Integer');
let rBoolean = Java.use('java.lang.Boolean');
let rLong = Java.use('java.lang.Long');
let rArray = Java.use('java.lang.reflect.Array');

let rRetrofit = Java.use('com.sankuai.meituan.retrofit2.Retrofit');
let rClientCall = Java.use('com.sankuai.meituan.retrofit2.ClientCall');
let rRetrofit$Builder = Java.use('com.sankuai.meituan.retrofit2.Retrofit$Builder');

// 我们将retrofit.c类视为Retrofit的创建器,用来生成Retrofit对象
let rRetrofit$Creator = Java.use('com.sankuai.waimai.platform.capacity.network.retrofit.c');

// 我们需要注册HomePageApi这个interface
let rHomePageApi = Java.use('com.sankuai.waimai.business.page.home.net.request.HomePageApi');

// 当类中的方法出现重载时,需要通过.overload().call()的方式来调用
let retrofit = rRetrofit$Creator.a.overload('java.lang.Class').call(
// .overload().call()的第一个参数必须时类本身,也可能时类对象本身,这个需要自己确定。
rRetrofit$Creator,

// 注册HomePageApi这个interface
rHomePageApi.class
);
console.log(`retrofit: ${retrofit}`);

// 通过getDeclaredMethods找到 rHomePageApi.getHomePagePoiList method
let methods = rHomePageApi.class.getDeclaredMethods();
let method;
for (let i = 0; i < methods.length; i++) {
if (methods[i].getName().indexOf('getHomePagePoiList') !== -1) {
method = methods[i];
console.log(`method: ${method}`);
break;
}
}

// 通过retrofit对象和interface method生成ServiceMethod对象
let serviceMethod = retrofit.loadServiceMethod(method);
console.log(`serviceMethod: ${serviceMethod}`);

// 构造 rHomePageApi.getHomePagePoiList 所需的参数
// 需要注意的是 frida 不能将javascrip中的类型自动转换为java中的基础数据类型。
// 例如javascript中的0和true不能自动转换到java中的int和boolean。
// 需要先Java.use数值类型,之后调用xxx.$new方法手动构造
// javascript中的字符串类型可以直接转到java中的java.lang.String类型,所以不需要手动构造
let params = Java.array('java.lang.Object', [
rInteger.$new(0),
rInteger.$new(0),
rBoolean.$new(true),
rInteger.$new(0),
rInteger.$new(20),
rInteger.$new(0),
"",
"",
rInteger.$new(1),
"",
'1884e2f6-0fd6-4254-9555-9e701faf1a771604177571387562',
'77c5243a18ca439c9e3eb05ee894707ea159474625289990347',
'9a44b234332e43a69a6bb06d6de1863a',
rInteger.$new(0),
rInteger.$new(0),
''
]);
console.log(`ary: ${params}, ary.length: ${params.length}`);

// 构造ClientCall对象
let call = rClientCall.$new(serviceMethod, params, retrofit.interceptors.value, retrofit.defInterceptors.value, retrofit.httpExecutor.value, retrofit.cache.value);
console.log(`call: ${call}`);

// 调用ClientCall.execute 执行request并获取response。
let response = call.execute();
console.log(`response: ${response}`);

通过python -m pip install frida-toolspython -m pip install frida 安装frida时经常出现假死状态,应该和当前网络环境和frida的编译比较复杂有关。

我们可以手动下载frida的pypi包,并在本地使用easy_install安装它。建议使用python3frida 12的版本,最新的frida 14bug比较多。可以等作者发布几个新版本或修复bug后再使用。

安装frida

前往frida pypi页面下载需要的版本,这里选择frida 12.11.18

下载对应版本和平台的egg包后,使用以下命令进行安装:

1
$ python -m easy_install frida-12.11.18-py3.8-win-amd64.egg

铁头娃可以通过以下命令进行安装:

1
$ python -m pip install frida

安装frida-tools

前往frida-tools pypi页面下载需要的版本,因为frida和frida-tools的版本是有依赖关系的,frida 12.11.18 对应frida-tools 8.2.0

下载frida-tools的zip包并解压后,通过以下命令安装:

1
2
3
$ cd frida-tools-8.2.0
$ python setup.py build
$ python setup.py install

如果已经安装好了frida,那么可以直接通过以下命令来安装frida-tools

1
$ python -m pip install frida-tools

需要注意的是,你可能需要指定frida-tools的版本

1
$ python -m pip install frida-tools==8.2.0

校验frida和frida-tools的安装

通过以下命令来校验:

1
2
3
4
5
$ python -m pip list
$ frida --version
$ frida-ps
# 执行下面的命令时必须保证连接了android或ios设备,并且运行了frida-server。
$ frida-ps -U

iptables 对 ipv4 数据包的重定向

可以使用这个功能将android ipv4的流量转发给中间人代理。

简介

更多关于iptables的内容请看:iptables详解

Netfilter-packet-flow

iptables是一个命令行工具,可以用来增删改查netfilter内核组件维护的表、链和规则。

iptables/netfilter(以下统称为iptables)组成了Linux平台下的IPv4包过滤器,可以使用它提供的功能来实现软件防火墙,这也是iptables的最初用意。

它可以代替昂贵的商业防火墙解决方案,它可以做到修改数据包、包过滤、包重定向和网络地址转换(NAT)等功能。

iptables使用表、链和规则的概念来管理网络流量。表包含若干链;链包含若干规则;用户可以在表中新增自定义的链和规则来管理网络流量。

每个表,每个链的功能都是不同的。

iptables提供的内置表如下:

  • filter

filter表中包含的内置链:INPUT 、FORWARD 和 OUTPUT

  • nat

nat表中包含的内置链:PREROUTING 、OUTPUT 和 POSTROUTING

  • mangle

mangle表中包含的内置链:PREROUTING 、OUTPUT 、INPUT 、FORWARD 和 POSTROUTING

  • raw

raw表中包含的内置链:PREROUTING 和 OUTPUT

  • security

security表中包含的内置链:INPUT 、OUTPUT 和 FORWARD

流量转发

nat.OUTPUT链用来管理本机进程向外发送的数据包,为了做到控制本机所有流量,我们在这个表中配置规则即可。

启用iptables流量转发
  1. 创建nat.REDSOCKS链:

在nat表中创建REDSOCKS规则链

1
2
$ iptables -t nat -N REDSOCKS
$ iptables --line -t nat -nvxL REDSOCKS

结果如下图所示:

create REDSOCKS chain in nat table.

  1. nat.REDSOCKS链中忽略本地地址和保留地址:

不要处理发往本地地址或保留地址的数据包。我们RETURN即可,表示nat.REDSOCKS链不处理它们,它们将按原本的链继续执行;

关于本地地址或保留地址的更多信息请参阅以下网址:Reserved_IP_addressesrfc5735

1
2
3
4
5
6
7
8
9
10
$ iptables -t nat -A REDSOCKS -d 10.0.0.0/8     -j RETURN
$ iptables -t nat -A REDSOCKS -d 100.64.0.0/10 -j RETURN
$ iptables -t nat -A REDSOCKS -d 127.0.0.0/8 -j RETURN
$ iptables -t nat -A REDSOCKS -d 169.254.0.0/16 -j RETURN
$ iptables -t nat -A REDSOCKS -d 172.16.0.0/12 -j RETURN
$ iptables -t nat -A REDSOCKS -d 192.168.0.0/16 -j RETURN
$ iptables -t nat -A REDSOCKS -d 198.18.0.0/15 -j RETURN
$ iptables -t nat -A REDSOCKS -d 224.0.0.0/4 -j RETURN
$ iptables -t nat -A REDSOCKS -d 240.0.0.0/4 -j RETURN
$ iptables --line -t nat -nvxL REDSOCKS

结果如下图所示:

add rules to nat.REDSOCKS chain.

  1. 将其它数据包转发到android设备的 8989 端口上
1
2
$ iptables -t nat -A REDSOCKS -p tcp -j REDIRECT --to-ports 8989
$ iptables --line -t nat -nvxL REDSOCKS

结果如下图所示,从图中可以看出我们过滤了发往本地或保留地址的数据包,对于发给其它网络设备的数据包我们全部转发至android设备的8989端口

add redirect rule to nat.REDSOCKS chain.

  1. nat.OUTPUT链中应用nat.REDSOCKS

iptables支持在一个链中挂上其它链

1
2
$ iptables -t nat -A OUTPUT   -p tcp -j REDSOCKS
$ iptables --line -t nat -nvxL OUTPUT

结果如下图所示:

apply nat.REDSOCKS on nat.OUTPUT chain.

  1. 使用第三方转发工具或adb将8989端口接收到的数据包转发给中间人代理。
  • 第三方工具redsocks转发流量:

    redsocks可以在android设备的任意端口(这里我们选择的是8989)创建监听,并将该端口接收到的数据包转发给其它网络设备。使用方式不做过多介绍,详见redsocks官网

  • adb方式转发流量:

    需要注意的是当使用adb方式转发流量时,必须开启系统代理,否则中间人代理将收不到数据包,该问题目前无法解决。

    adb reverse [--no-rebind] REMOTE LOCAL命令提供了反向代理的功能

    可以使adbd(运行在android设备上的adb守护进程)在android设备的REMOTE端口上创建监听,并将REMOTE端口接收到的数据包转发给adb server(无特殊情况下一般是执行adb reverse的计算机)设备上的LOCAL端口

    adb reverse提供了反向代理的功能,它可以在android设备的任意端口(这里我们选择的是8989)创建监听,并将该端口接收到的数据包转发给adb server(一般是执行adb reverse的计算机)

    adb reverse功能如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    reverse --list           list all reverse socket connections from device
    reverse [--no-rebind] REMOTE LOCAL
    reverse socket connection using:
    tcp:<port> (<remote> may be "tcp:0" to pick any open port)
    localabstract:<unix domain socket name>
    localreserved:<unix domain socket name>
    localfilesystem:<unix domain socket name>
    reverse --remove REMOTE remove specific reverse socket connection
    reverse --remove-all remove all reverse socket connections from device

使用adb reverse反向代理将8989端口转发到计算机的8888端口,在计算机上启动fiddler或charles中间人代理并咋计算机上监听8888端口。

1
2
$ adb reverse tcp:8989 tcp:8888
$ adb reverse --list

adb reverse

移除iptables的流量转发
  1. 关闭adb的反向代理,使用adb reverse --remove-alladb reverse --remove REMOTE命令
1
2
$ adb reverse --remove-all
$ adb reverse --remove tcp:8989
  1. 重新刷写nat表中的REDSOCKS链,这一操作将清空nat.REDSOCKS链下的所有规则
1
$ iptables -t nat -F REDSOCKS
  1. 删除nat.OUTPUT链下的REDSOCKS规则链
1
$ iptables -t nat -D OUTPUT -p tcp -j REDSOCKS
  1. 查看 nat.OUTPUTnat.REDSOCKS 链的规则是否生效
1
2
$ iptables --line -t nat -nvxL OUTPUT
$ iptables --line -t nat -nvxL REDSOCKS

部分安卓系统会出现安装了 fiddler 证书,设置了证书信任,甚至把证书移动到系统目录时,https 网页依旧提示证书不可信;

使用 monitor 检查 log 信息发现提示:

E/chromium(8753): [ERROR:ssl_client_socket_impl.cc(941)] handshake failed; returned -1, SSL error code 1, net_error -213

搜索报错信息,找到 chromium project 的源码,发现似乎是 MapLastOpenSSLError 函数返回的 -213 错误,错误代码的宏格式为 ERR_SSL_CLIENT_AUTH_NO_COMMON_ALGORITHMS

搜索 “ssl error code list” 找到 net_error_list.h,发现 -213 错误的详细宏定义为:

1
2
// The certificate's validity period is too long.
NET_ERROR(CERT_VALIDITY_TOO_LONG, -213)

继续搜索 “fiddler The certificate’s validity period is too long.” 在 Fiddler 论坛找到该帖子,看起来有人碰到过类似问题且已有解决方案:

在 Fiddler 插件页面 找到并下载
CertMaker for iOS and Android
双击下载好的 exe,重启 Fiddler,在 Fiddler https 界面重置证书并重新生成;
再按照其他帖子的介绍将证书导入到系统(未测试直接安装为用户证书),现在就能正常抓 https 网页不提示错误。

使用Frida自动补全和TypeScript模块化脚本

简介

配合使用集成开发环境编辑器(例如:JetBrains IDEA或Visual Code)编写TypeScript脚本来规范化、模块化并提供自动补全你的代码,让你的开发效率大大提升。

部署

1
2
3
4
$ git clone git://github.com/oleavr/frida-agent-example.git
$ cd frida-agent-example/
$ npm install
$ npm run watch
  • 集成开发环境打开frida-agent-example目录

编辑frida-agent-example/index.ts文件并保存

npm run watch会监控index.ts文件并在项目根目录下生成_agent.js目标脚本。

  • Frida注入
1
$ frida -U -f com.example.android --runtime=v8 --no-pause -l _agent.js

遍历Java List

requestHeaders是一个Java的List对象,遍历其内容如下:

1
2
3
log += "  request.headers: " + rObject.$methods.find("toString").call(rObject, requestHeaders) + "\n";
for (var i = 0; i < requestHeaders.size(); i++)
log += " headers[" + i + "]: " + requestHeaders.get(i) + "\n";

输出:

1
2
request.headers: [com.sankuai.meituan.retrofit2.Header@a671e8]
headers[0]: com.sankuai.meituan.retrofit2.Header@a671e8

Native

静态注册或动态注册的Native函数,第二个参数总是一个jobject;如果native函数是静态函数,那么该参数是jclass,如果native函数是成员函数,那么该参数是当前类的对象;

例如Java层有以下声明代码:

1
private native String TestNative(int arg1, int arg2, String arg3);

在Frida hook时:

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
const nativeHook_TestNative = {
onEnter: function (args) {
this.env = args[0];
//this.caller = args[1]; // jobject of the class.
this.arg1 = args[2];
this.arg2 = args[3];
this.arg3 = args[4];
this.tag = "nativeHook_TestNative";
this.log = "";
this.log += "> - - - - - - - - <\n";
this.log += this.tag + " Enter.\n";
this.log += " JNIEnv: " + this.env + "\n";
this.log += " arg1: " + this.arg1 + "\n";
this.log += " arg2: " + this.arg2 + "\n";
this.log += " arg3: " + this.arg3 + ", " + GetStringUTFChars(this.env, this.arg3, NULL).readCString() + "\n";
console.log(this.log);
},
onLeave: function (ret) {
const tid = gettid();
this.log = this.tag + " Leave.\n";
this.log += " ret: " + ret + ", str:" + GetStringUTFChars(this.env, ret, NULL).readCString() + "\n";
this.log += "^ - - - - - - - - ^\n";
console.log(this.log);
}
};

华丽的hook代码

以下代码均运行在v8引擎环境中,请使用frida --runtime=v8来指定

gettid, getpid, getuid

定义gettid, getpid, getuid三个frida.NativeFunction供外部直接调用。

1
2
3
const gettid = new NativeFunction(Module.getExportByName(null, 'gettid'), 'uint32', []);
const getpid = new NativeFunction(Module.getExportByName(null, 'getpid'), 'uint32', []);
const getuid = new NativeFunction(Module.getExportByName(null, 'getuid'), 'uint32', []);

Hook libart.RegisterNatives

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function nativeHook_RegisterNatives_onEnter(args) {
let _this = this;
_this.tag = 'RegisterNatives';
_this.env = args[0];
_this.clazz = args[1];
_this.methods = args[2];
_this.nMethods = parseInt(args[3]);

let log = `> - - - - - - - - - - - - - - - - - - tid:[${gettid()}] - - - - - - - - - - - - - - - - - - <\n`;
log += `${_this.tag} Enter.\n`;
log += ` env: ${_this.env}\n`;
log += ` clazz: ${Java.vm.getEnv().getClassName(_this.clazz)}\n`;
log += ` methods: ${_this.methods}\n`;
log += ` nMethods: ${_this.nMethods}\n`;
for (let i = 0; i < _this.nMethods; i++) {
let methodName = _this.methods.add(i * (Process.pointerSize * 3)).readPointer().readCString();
let methodSig = _this.methods.add(i * (Process.pointerSize * 3) + (Process.pointerSize)).readPointer().readCString();
let methodPtr = _this.methods.add(i * (Process.pointerSize * 3) + (Process.pointerSize * 2)).readPointer();
let methodMod = Process.findModuleByAddress(methodPtr);
log += ` ${i + 1}:\n`;
log += ` methodName: ${methodName}\n`;
log += ` methodSig: ${methodSig}\n`;
log += ` methodPtr: ${methodPtr}, off: ${methodPtr.sub(methodMod.base)}\n`;
log += ` methodLib: ${methodMod.name}, base: ${methodMod.base}\n`;
log += `\n`;
}
console.log(log);
}

function nativeHook_RegisterNatives_onLeave(retval) {
let log = `${this.tag} Leave.\n`;
log += ` retval: ${retval}\n`;
log += `> - - - - - - - - - - - - - - - - - - tid:[${gettid()}] - - - - - - - - - - - - - - - - - - <\n`;
console.log(log);
}

setImmediate(function () {
Java.perform(function () {
let JNIEnv = Java.vm.getEnv().handle;
let JNIEnv_vtable = JNIEnv.readPointer();
let registerNativesPtr = JNIEnv_vtable.add(215 * Process.pointerSize).readPointer();
Interceptor.attach(registerNativesPtr, {
onEnter: nativeHook_RegisterNatives_onEnter,
onLeave: nativeHook_RegisterNatives_onLeave
});
});
console.log('end');
});

Java.use增加一些功能

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
let classCache = {};

// hook before
Function.prototype.before = function (beforeFunction) {
let _this = this;
return function () {
this._continue = true;
let ret = beforeFunction(this, arguments);
if (!ret) {
//调用原函数
ret = _this.apply(this, arguments);
}
return ret;
}
};

// hook after
Function.prototype.after = function (afterFunction) {
let _this = this;
return function () {
// 先执行原函数
let ret = _this.apply(this, arguments);
// 再执行新函数
return afterFunction(this, ret, arguments);
}
};

// 在调用Java.use前,先调用我们指定的匿名函数。
// 该匿名函数先通过classCache全局变量判断是否已经Java.use过这个类
// 如果已经Java.use过,那么直接返回;否则继续执行原本的Java.use
Java.use = Java.use.before(function (_this, args) {
let className = args[0];
if (classCache[className]) {
console.log('[+] Java use:', className, 'is cached.');
return classCache[className];
}
return false;
})

// 在调用Java.use后,再调用我们指定的匿名函数。
// 为Java.use的返回值增加两个属性:$fields 和 $methods
// 可以通过以下方式来使用这两个属性:
// 获取SecretKeySpec类的key字段
// let rSecretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec');
// rSecretKeySpec.$fields.get("key", secKey);
//
// 调用Reference类的get方法
// let rReference = Java.use('java.lang.ref.Reference');
// rReference.$methods.find("get").call(rReference, uObj.get(i))
Java.use = Java.use.after(function (_this, ret, args) {
let className = args[0];

if (!classCache[className]) {
ret.$fields = new class Fields {
wrapper;

constructor(wrapper) {
this.wrapper = wrapper;
}

find(name) {
let field;
try {
field = this.wrapper.getDeclaredField(name);
} catch (e) {
field = this.wrapper.getField(name);
}
return field;
};

findall() {
let fields;
fields = this.wrapper.getDeclaredFields();
fields.concat(this.wrapper.getFields());
return fields;
};

get(name, obj) {
let notAccessible;
let ret;
let field = this.find(name);
notAccessible = !field.isAccessible();
if (notAccessible) field.setAccessible(true);
ret = field.get.overload('java.lang.Object').call(field, obj);
if (!notAccessible) field.setAccessible(false);
return ret;
};

set(name, obj, val) {
let isAccessible;
let field = this.find(name);
isAccessible = field.isAccessible();
if (!isAccessible) field.setAccessible(true);
field.set.overload('java.lang.Object', 'java.lang.Object').call(field, obj, val);
if (!isAccessible) field.setAccessible(false);
};
}(ret.class);
ret.$methods = new class Methods {
wrapper;

constructor(wrapper) {
this.wrapper = wrapper;
}

find(name, ...args) {
let method;
try {
method = this.wrapper.getDeclaredMethod(name, args);
} catch (e) {
method = this.wrapper.getMethod(name, args);
}
return function (obj, ...args) {
let isAccessible;
let ret;
isAccessible = method.isAccessible();
if (!isAccessible) method.setAccessible(true);
ret = method.invoke(obj, args);
if (!isAccessible) method.setAccessible(false);
return ret;
};
};
}(ret.class);
classCache[className] = ret;
}
return ret;
});

打印堆栈

根据指定的tab数量转为空格

1
2
3
4
5
6
7
8
9
function genTableString(tableSize) {
let tableString = '';
tableSize = Math.max(Math.min(tableSize, 10), 0);
for (let i = 0; i < tableSize; i++) {
// One table character maps to two space characters.
tableString += ' ';
}
return tableString;
}

Java层

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function nativeThreadTraceToString(traces, tableSize) {
let module;
let table = genTableString(tableSize);
const stackTraces = [];
for (let j = 0; j < traces.length; j++) {
if (!traces.hasOwnProperty(j)) continue;
const stackTrace = new class {
index;
moduleName;
moduleBase;
offset;
address;

constructor(index) {
this.index = index;
this.moduleName = '';
this.moduleBase = NULL;
this.offset = '';
this.address = NULL;
}

toString() {
return `idx: ${this.index}` +
`, name: ${this.moduleName}` +
`, base: ${this.moduleBase}` +
`, offset: ${this.offset}` +
`, address: ${this.address}`;
};
}(j);
module = Process.findModuleByAddress(traces[j]);
if (module) {
stackTrace.moduleName = module.name;
stackTrace.moduleBase = module.base;
}
stackTrace.address = traces[j];
stackTrace.offset = `0x${(parseInt(stackTrace.address) - parseInt(stackTrace.moduleBase)).toString(16)}`;
stackTraces.push(stackTrace);
}
return `${table} ${stackTraces.join(`\n${table} `)}`
}

// 调用代码丢了,后面再补叭。

native层

1
2
3
4
5
6
7
8
9
10
function javaThreadTraceToString(thread, tableSize) {
let table = genTableString(tableSize);
let tag = `${table}Thread Trace:\n`;
return `${tag}${table} ${thread.getStackTrace().join(`\n${table} `)}`;
}

// 调用代码
let rThread = Java.use('java.lang.Thread');
let thread = rThread.currentThread();
console.log(javaThreadTraceToString(thread, 1));

关闭SELinux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
project system/core/
diff --git a/init/init.cpp b/init/init.cpp
index 93fe944..acbcf66 100644
--- a/init/init.cpp
+++ b/init/init.cpp
@@ -913,6 +913,7 @@ static bool selinux_is_disabled(void)

static bool selinux_is_enforcing(void)
{
+ return false;
if (ALLOW_DISABLE_SELINUX) {
return selinux_status_from_cmdline() == SELINUX_ENFORCING;
}

adbd 取消授权

ALLOW_ADBD_NO_AUTHro.adb.secure是且的关系,编译release版时ALLOW_ADBD_NO_AUTH被置为0;这将导致release版的ro.adb.secure无论置1还是0都是不起作用的;

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/adb/adb_main.cpp b/adb/adb_main.cpp
index 45a2158..b526c1b 100644
--- a/adb/adb_main.cpp
+++ b/adb/adb_main.cpp
@@ -239,9 +239,9 @@ int adb_main(int is_daemon, int server_port)
// descriptor will always be open.
adbd_cloexec_auth_socket();

- if (ALLOW_ADBD_NO_AUTH && property_get_bool("ro.adb.secure", 0) == 0) {
+
auth_required = false;
- }
+

adbd ROOT

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
28
29
30
31
32
project system/core/
diff --git a/adb/adb_main.cpp b/adb/adb_main.cpp
index 45a2158..99050db 100644
--- a/adb/adb_main.cpp
+++ b/adb/adb_main.cpp
@@ -109,6 +109,7 @@ static void drop_capabilities_bounding_set_if_needed() {
}

static bool should_drop_privileges() {
+ return false;
#if defined(ALLOW_ADBD_ROOT)
char value[PROPERTY_VALUE_MAX];

@@ -287,12 +288,12 @@ int adb_main(int is_daemon, int server_port)

D("Local port disabled\n");
} else {
- if ((root_seclabel != NULL) && (is_selinux_enabled() > 0)) {
- // b/12587913: fix setcon to allow const pointers
- if (setcon((char *)root_seclabel) < 0) {
- exit(1);
- }
- }
+
+
+
+
+
+
std::string local_name = android::base::StringPrintf("tcp:%d", server_port);
if (install_listener(local_name, "*smartsocket*", NULL, 0)) {
exit(1);

简介

以下内容如无特殊说明均基于IPv4的TCP/IP方式通讯。

因各种原因Android设备的WIFI功能无法进行网络连接或连接不稳定,经常出现掉线的情况。导致无法稳定持久的让Android设备对外提供服务。

例如:

Android设备A(以下简称设备A)原本以WIFI形式连接到局域网中,并且监听8082端口等待局域网内的其他设备访问并提供HTTP服务。但是设备AWIFI模块不稳定。那么可以通过adb forward + Rinetd使设备A通过USB的方式向外提供持久可靠的HTTP服务。

它是如何工作的

  • 设备A:监听A端口并等待连接。

  • adb forward:作为路由的功能,将计算机AA端口接收的数据以USB传输给设备AA端口

  • 计算机A:监听B端口并允许来自局域网的连接,将B端口上接收的数据转发给它自己的A端口

  • 通讯方式:

    1
    设备A <==设备A的A端口==> adb forward <==计算机A的A端口==> rinetd <==计算机A的B端口==> 局域网内的其他设备

要求

  • Rinetd:端口转发工具

部署

  • adb转发端口

假设设备A正在监听8082端口,那么就需要将计算机A18082端口接收来的数据转发到设备A8082端口。这里的端口都可以自定义的,不需要一定是808218082

1
adb -s 84B7N16620000115 forward tcp:18082 tcp:8082

也可以

1
adb -s 84B7N16620000115 forward tcp:0 tcp:8082

-s选项:可以指定设备序列号,在多个Android设备连接到同一台计算机时,这个选项是非常有用的。

tcp:18082 或 tcp:0:adb将在计算机A18082或任意一个可用的端口上开启监听。如果使用tcp:0,那么adb forwad成功后将输出成功开启监听的端口号。

tcp:8082:adb将计算机A18082端口或任意一个可用端口上接收到的数据转发给序列号为84B7N166200001158082端口去。

你需要确保计算机A18082端口是空闲的,否则adb会提示你端口被占用。

到这一步,你已经可以成功执行telnetcurl来测试adb的端口转发了:

1
2
telnet 127.0.0.1 18082
curl 127.0.0.1 18082

但是局域网内的其他计算机仍然不能通过局域网IP访问到计算机A18082端口,这是因为adb只监听回环IP,不允许来自局域网内的连接。

  • 安装并配置Rinetd

Rinetd端口转发工具允许来自局域网内的连接,所以还需要通过Rinetd计算机A18082端口做一次转发,以对局域网提供设备A的服务。

1
2
sudo apt-get install rinetd
sudo vim /etc/rinetd.conf

在该文件的最后一行添加以下内容并保存退出
0.0.0.0 10082 127.0.0.1 18082

该配置指出,监听计算机A10082端口并将接收到的数据转发给本机的18082端口;

  • 添加防火墙规则或关闭防火墙

关闭防火墙

至此,计算机A上开启了1808210082两个端口,并且局域网内的其他计算机可以通过局域网ip:10082访问设备A8082端口。并可以通过以下命令测试:

1
2
3
4
telnet 127.0.0.1 18082 // 测试本机18082端口是否能正常向手机的8082端口发送数据
telnet 127.0.0.1 10082 // 测试本机10082端口是否能正常向本机的18082端口发送数据
telnet 192.168.14.30 18082 // 该命令将失败,因为无法通过局域网访问adb转发的端口
telnet 192.168.14.30 10082 // 测试本机10082端口是否能正常向本机的18082端口发送数据