0%

需要

  • 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](https://raw.githubusercontent.com/MakeThings-team/picgo-library/main/modify-android-q-for-sailfish/init.SelinuxInitialize.security_getenforce.asm.png)

 偏移`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](https://raw.githubusercontent.com/MakeThings-team/picgo-library/main/modify-android-q-for-sailfish/init.SelinuxInitialize.security_getenforce.asm.modify.png)

 将修改的内容写到init文件中,查看代码偏移`6A8FC`和`6A904`对应的文件偏移`6A8FC`和`6A904`;

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

 ![init.SelinuxInitialize security_getenforce file](https://raw.githubusercontent.com/MakeThings-team/picgo-library/main/modify-android-q-for-sailfish/init.SelinuxInitialize.security_getenforce.file.png)

 修改后:

 ![init.SelinuxInitialize security_getenforce file midify](https://raw.githubusercontent.com/MakeThings-team/picgo-library/main/modify-android-q-for-sailfish/init.SelinuxInitialize.security_getenforce.file.midify.png)

关闭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

使用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端口发送数据

APK安装失败[INSTALL_FAILED_VERIFICATION_FAILURE]

1
2
adb shell su -c settings put global package_verifier_enable 0
adb shell su -c settings put global verifier_verify_adb_installs 0

不插拔USB恢复offline状态的设备

1
2
$ adb devices
$ adb -s serial reconnect

adb查看apk信息

1
2
$ adb shell pm list packages | grep aweme
$ adb shell dumpsys package com.ss.android.ugc.aweme

armeabi-v7a系统调用表

https://android.googlesource.com/platform/external/kernel-headers/+/refs/tags/android-6.0.1_r66/original/uapi/asm-arm/asm/unistd.h

Android Studio中指定ABI

编辑app -> build.gradle

1
2
3
4
5
6
defaultConfig {
...
ndk {
abiFilters 'armeabi-v7a' //只生成armv7的so
}
}

相关ABI连接:

https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.NdkOptions.html

https://developer.android.com/ndk/guides/abis.html#sa

调整屏幕亮度

1
2
3
4
5
6
$ adb shell su
// 屏幕最亮
# echo 255 > /sys/class/leds/lcd-backlight/brightness

// 屏幕全黑,没有一点亮度;不影响screencap命令
# echo 0 > /sys/class/leds/lcd-backlight/brightness

截图

1
$ adb shell screencap -p /sdcard/01.png & adb pull /sdcard/01.png

获取Application和Context

1
android.app.ActivityThread.currentApplication().getApplicationContext()

adb logcat中搜索指定进程的日志正则表达式

1
7679\s+\d+\s+\w

android logcat原理:http://gityuan.com/2018/01/27/android-log/

打开Url Scheme协议链接

1
$ adb shell am start -a android.intent.action.VIEW -d "snssdk1128://user/profile/3733569708763603"

将用户证书修改为系统证书

1
2
3
4
5
6
7
$ adb shell
$ su
# mount -o remount,rw /system
# ls -al /data/misc/user/0/cacerts-added/
# mv /data/misc/user/0/cacerts-added/* /etc/security/cacerts/
# chmod 644 /etc/security/cacerts/*
# chown root:root /etc/security/cacerts/*

查找端口占用情况

  • 通过端口号查找占用该端口的uid

例如需要查找8081端口,对应的16进制为:0x1f91

1
2
3
$ adb shell
$ cat /proc/net/tcp6 | grep -i 1f91
$ cat /proc/net/tcp | grep -i 1f91

结果如下:

1
53: 00000000000000000000000000000000:1F91 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 10080        0 300399 1 0000000000000000 99 0 0 10 -1
  • 通过uid查找该uid对应的进程号

其中10080uidUID(10080) - 10000 = 80 = u0_a80

1
$ adb shell su -c ps | grep u0_a80

结果如下:

结果有多个进程都属于u0_a80用户的,那么8081端口就在其中一个进程中

1
2
3
4
5
u0_a80    9732  530   1828844 114996 SyS_epoll_ 00f734acb8 S com.ss.android.ugc.aweme:push
u0_a80 9907 530 2520916 507228 SyS_epoll_ 00f734acb8 S com.ss.android.ugc.aweme
u0_a80 10029 530 1831376 118660 SyS_epoll_ 00f734acb8 S com.ss.android.ugc.aweme:pushservice
u0_a80 10956 530 1799376 102028 SyS_epoll_ 00f734acb8 S com.ss.android.ugc.aweme:downloader
u0_a80 10968 530 1989384 173844 SyS_epoll_ 00f734acb8 S com.ss.android.ugc.aweme:miniapp0
  • 根据进程id确定uid
1
$ adb shell su -c cat /proc/9907/cgroup

结果如下:

1
2
3
3:cpuset:/foreground
2:cpu:/
1:cpuacct:/uid_10080/pid_9907

查找顶级Activity

1
$ adb shell dumpsys activity activities > activity_activities.log

输出格式如下:其中每个Hist代表一个Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)

Display #0 (activities from top to bottom):

Stack #4:

Task id #144

* TaskRecord{e98d78a #144 A=com.tencent.karaoke U=0 sz=4}
...
* Hist #3: ActivityRecord{6da6fab u0 com.tencent.karaoke/.module.detail.ui.GiftBillboardActivity t144}
...

* Hist #2: ActivityRecord{cea57ec u0 com.tencent.karaoke/.module.live.ui.LiveActivity t144}
...

* Hist #1: ActivityRecord{840f95 u0 com.tencent.karaoke/.module.webview.ui.WebViewContainerActivity t144}
...

* Hist #0: ActivityRecord{ea80b18 u0 com.tencent.karaoke/.module.main.ui.MainTabActivity t144}
...

更改adbd的监听端口

1
2
3
4
$ adb shell su
# setprop service.adb.tcp.port 5555
# stop adbd
# start adbd

启用/关闭开发者选项 - USB debugging

启用:setprop persist.sys.usb.config mtp,adb

关闭:setprop persist.sys.usb.config mtp
相关代码:

https://developer.android.com/reference/android/provider/Settings.Global.html#ADB_ENABLED

https://android.googlesource.com/platform/frameworks/base/+/android-4.4_r1.2/services/java/com/android/server/usb/UsbDeviceManager.java

调试启动APK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
:: Example:
:: run_dbg.bat org.github.testsignal .MainActivity
@ECHO OFF
CALL :DeQuote %1
SET PACKAGE_NAME=%return%

CALL :DeQuote %2
SET ACTIVITY_NAME=%return%

adb shell am start -D %PACKAGE_NAME%/%ACTIVITY_NAME%
adb shell ps | FINDSTR %PACKAGE_NAME%
ECHO please input process id:
SET /P PID=
adb forward tcp:12346 jdwp:%PID%
jdb -connect com.sun.jdi.SocketAttach:port=12346

PAUSE
GOTO :EOF

:DeQuote
SET return=%~1
GOTO :EOF

TWRP模式下挂载指定分区

1
2
3
4
adb shell
# make /test
# ls -la /dev/block/platform/soc.0/f9824900.sdhci/by-name/boot
# mount /dev/block/mmcblk0p43 /test

Ubuntu 上使用 Android-SDK

  • 安装platforms时需要注意引号

    1
    2
    root@github:/opt/android_sdk# ./tools/bin/sdkmanager --install "platforms;android-23"
    [=======================================] 100% Unzipping... android-6.0/source.p

logcat

android的Log.d系列日志是写在/dev/log_xxx文件中的;而/dev是挂载在tmpfs文件系统上,所以重启之后日志就消失了。参考:system/core/liblog/logd_write.csystem/core/liblog/logd_write_kern.c

1
2
# adb shell su -c mount | grep /dev
tmpfs /dev tmpfs rw,seclabel,nosuid,relatime,size=1424720k,nr_inodes=356180,mode=755 0 0

Hide all methods with CMAKE

在 CMAKE 中设置隐藏所有方法(不显示他们的符号);

Hide all methods without add “__attribute__(visibility(“default”))” for everyone of them:

1
2
set_target_properties(YOUR_TARGET_NAME PROPERTIES CXX_VISIBILITY_PRESET hidden)
set_target_properties(YOUR_TARGET_NAME PROPERTIES C_VISIBILITY_PRESET hidden)

Fucking knack of use ollvm edition clang to compile so on Android Studio

在 Android Studio 中使用 ollvm 版本的 clang 编译 so;

​ As you know, if you just overwrite the compiler executable file with ollvm edition, then you will get “‘xxx.h’ file not found” error, actually I am not understand this error explicitly, because the file exactly is there, and it will perform like you want when execute that compile command on shell, so here is my approach to avoid that:

​ Open “YOUT_PROJECT_PATH\app.externalNativeBuild\cmake\debug\TARGET_ABI\rules.ninja”, find the line of “rule C_COMPILER__TARGET”(TARGET mean the target name you specified by “add_library” on “CMakeLists.txt”), and you will find the path of clang executable file under it, modify it with your ollvm edition clang, then build your project as normal, you will get the ollvm compiled file.

​ 如果简单的使用 ollvm 版本的 clang.exe 等可执行文件替换掉原版 ndk toolchain 中的 exe,那么将会报一些头文件查找不到的错误,网上说的原因似乎是不同版本的 clang 将会使用的头文件有差异,然而如果在控制台中直接使用 ollvm 版本 clang 去手动执行编译命令,是可以正常编译成功得到 .o 文件的,以下是我避免该坑的方法:

​ 打开 “YOUT_PROJECT_PATH\app.externalNativeBuild\cmake\debug\TARGET_ABI\rules.ninja”,找到 “rule C_COMPILER__TARGET” 这一行(TARGET 指你在 CMakeLists.txt 中使用 add_library 指定的库名),然后你会在下面几行找到编译使用的 clang 路径,把它替换为你 ollvm 版本 clang 的路径,然后正常编译即可得到你想要的 so。

项目简介

Gnirehtet是一个非常优秀的项目。该项目为Android设备提供了通过adb方式的反向代理。它允许Android设备通过USB使用所接入计算机的Internet网络,并且计算机和Android设备都不需要任何root权限。它的服务器可以运行在GNU/LinuxWindowsMac OS上,客户端需要运行在Android Lollipop API 21 及以上。

Gnirehtet的当前版本(v2.4)支持通过IPv4传输TCPUDP协议的数据,但不支持IPv6

它是如何工作的

客户端(Client):android设备视为客户端。注册VPN服务,以拦截整个设备的网络流量。

服务器(Relay Server):计算机(WindowsGNU/LinuxMac OS)视为服务器。也称作中继服务器

客户端仅建立与服务器之间的TCP连接;并通过该TCP连接以字节数组的形式交换原生IPv4数据包

客户端和服务器之间的TCP连接在开始反向端口重定向后由adb建立,反向端口重定向命令如下:

1
adb reverse localabstract:gnirehtet tcp:31416

这意味着服务器必须监听31416端口,并且客户端的所有的sockets连接都将由adb重定向到服务器的31416端口上。所以务必确保服务器的31416端口未被其他程序占用,务必确保服务器和客户端之间的USB连接是正常且稳定的。

服务器从连接的客户端上接收IPv4数据包并且根据数据包与对应的目标IP建立sockets连接,然后开始双向中继传输数据。

这就需要服务器在OSI model上以Level 3(客户端一边)Level 5(外网一边)之间传输数据包;

获取更多详情请点我

为什么选择它

当Android设备的WIFI不够稳定时,或想获得更高的下载速度,或其他一些原因;

要求

  • Android Lollipop API 21 及以上;

  • Android设备需要启动adb debugging

  • Java 8的运行时环境,在Debian-based发行版上,需要openjdk-8-jre

  • adb 1.0.36及以上,因为需要adb reverse支持;

  • 服务器的31416端口未被占用且网络正常;

下载

根据需要下载对应的版本,建议下载最新版本:Last Release

Rust 版本

  • Linux: gnirehtet-rust-linux64-XXX.zip
  • Windows: gnirehtet-rust-win64-XXX.zip
  • Mac OS: gnirehtet-rust-macos64-XXX.zip

LinuxMac OS zip文件解压后包含以下文件:

  • gnirehtet.apk:安装在客户端上。
  • gnirehtet:安装在服务器上。

Windows zip文件解压后包含以下文件:

  • gnirehtet.apk:安装在客户端上。
  • gnirehtet.exe:安装在Windows服务器上。
  • gnirehtet-run.cmd:快速启动gnirehtet的批处理文件。

Java 版本

  • 全平台: gnirehtet-java-XXX.zip

解压后包含以下文件:

  • gnirehtet.apk:安装在客户端上。
  • gnirehtet.jar:部署在服务器上
  • gnirehtet:部署在服务器上
  • gnirehtet.cmd:部署在Windows服务器上
  • gnirehtet-run.cmd:快速启动gnirehtet的批处理文件。

运行

在服务器上启动服务,该服务不提供用户界面,以控制台终端的形式呈现:

1
./gnirehtet relay

Ubuntu上使Gnirehtet不占用命令行终端在后台运行:

1
sudo nohup ./gnirehtet relay &

在客户端上安装APK

1
adb install -r gnirehtet.apk

设置反向端口重定向并启动APP,该APP不提供用户界面,以Android Service的方式运行在系统后台:

1
2
adb reverse localabstract:gnirehtet tcp:31416
adb shell am start -a com.genymobile.gnirehtet.START -n com.genymobile.gnirehtet/.GnirehtetActivity

停止客户端:

1
adb shell am start -a com.genymobile.gnirehtet.STOP -n com.genymobile.gnirehtet/.GnirehtetActivity

停止服务器:

1
在gnirehtet的命令行终端上按下Ctrl + C组合键