深入理解 ADB 协议 - 安卓使用 ADB 实现

9,589 阅读10分钟

缝缝补补,还是把这篇文章写完了。

前言

整体感受一下这篇文章研究的东西最后带来了啥。

安卓免 ROOT 实现 ADB 连接另一台安卓,这里的手环是 ow2。

camera1.jpg

安卓免 ROOT 给另一台安卓安装 app

camera2.jpg

ADB简单介绍

ADB 是安卓调试桥(Android Debug Bridge),为了实现分布式(这个分布式的确是官方的词儿),分离出了 ADB server,ADB server 与安卓设备上的 adbd 进程通信。

分离的这层 ADB server 有什么用呢?

例如 PC A 连接了10台安卓,此时 ADB server 运行在 PC A 上,同局域网的其他 PC 只需要通过 PC A 的5037端口即可调试10台安卓,这就是 server 带来的好处。 这个实际参数就是adb -L

-L SOCKET  listen on given socket for adb server [default=tcp:localhost:5037]

并且这个 server 不随 adb 命令的本身退出而退出,就能减少与各个设备的握手过程。

ADB 同时也有一系列自己的协议,负责与设备通信,在 adb 二进制中,第一次使用 adb devices 或者主动执行 adb start-server 的时候,adb 会 fork 出这个 server,绑定端口在5037,随后 adb 作为一个 client 与5037端口的 server 通信,例如执行 adb devices 后,adb 并不直接与设备进行串口通信,而是告诉 server 自己执行 adb devices 命令,server 与设备串口或者网络通信,随后将结果交给 adb client。

adb clientadb server 还有一些通信协议我们不关心,我们关心 adb server 与 adbd(安卓设备上的响应 adb 消息的进程) 之间的通信。

我们需要实现不借助任何编译的 adb 二进制也能实现 adb 的功能

此处看起来需要一个架构图,但是还不会画🤫

为什么安卓需要使用ADB?

这里的意思不是 PC 连接安卓,而是安卓连接安卓,adb 有着 shell 的用户组,这个用户组比普通的 app 权限高很多,可以直接截屏/录屏,冻结/杀死 app,卸载普通/系统的 app。

所有就有了一系列软件如冰箱、小黑屋、Shizuku,他们本身有一个 shell 的 server,但是需要连上 pc 后,用户主动粘贴一行脚本执行,启动自身的服务进程,由于是 adb 启动,所以这个服务进程也有 shell 的能力,如此一来,只要手机不重启,这些 app 就能通过启动的 server 获得 shell 权限。

甚至像 shizuku,可以直接通过 AIDL 通信将自己的 server 给任意 app 赋予 shell 权限(其它app需要引入shizuku api),一些没能力 root 设备的用户,或者一些不能 root 的手机(华为),他们同样可以获得 shell 权限,来更好的管理他们的安卓设备,控制进程。 但是,并不是所有的用户都有电脑!并且想使用这个功能的时候并不在电脑旁边,所以,需求产生了

目前安卓实现 ADB 连接另一台安卓的方案

在 Magisk 的 github.com/Magisk-Modu… ,就是将 adb 交叉编译到安卓的,这类跨平台的移植,肯定是少不了 patch 的,这个过程也不难,在 android-tools 也有现成的方案。

这个库是自己 fork termux package 后维护的。

并且也是我一直在用的方案,此时 adb 可以连接网络设备,但是当另一台安卓设备通过 otg 连接的时候,adb devices 总是查不到这个设备,原因很简单,安卓与 Linux 的内核有差异,在连接串口设备的时候,Linux 会尝试识别并驱动这个设备,最后会在 /dev 下产生一个字符设备,并且普通用户很容易就能读写到这个设备。

举个例子:

crw-rw-rw-   1 root       wheel            9,   0 11 12 10:05 tty.Bluetooth-Incoming-Port

可以看到开头的字符是 c,而不是 d 或者 -,它代表的是字符设备。

而当一个串口设备连接到安卓的时候,产生的字符设备文件,普通用户是不可读的,原因也非常简单,摄像头,麦克风这类设备都是作为串口连接到安卓,如果这些字符设备普通权限就能读写的话,那安卓的权限机制的存在就没有意义了。

那如果通过 root 权限呢?答案是肯定的,在root 权限的加持下,安卓上的 adb 基本就跟 PC 一样了,但是,用户手机可能并没有root!!!需求又产生了

至于内核默认能驱动哪些设备,在编译内核的时候,是可选的,除了常规的键鼠之外,我们还能让内核可以驱动 ch340x/cp210x 这类的驱动,但是这个成本较高,所以 PC 可以支持安装驱动,

用户:我想要使用 SHELL 权限,我甚至比你一个开发者还明白这是为什么,但是我没有电脑,我设备也没有root

开发者:呜呜呜~

突破点分析

如上面的分析,问题的解法也出来了,我们不能通过串口通信去使用摄像头,麦克风,但我们能通过安卓的 api 申请权限使用到那些设备,安卓普通 app 虽然无法直接访问 /dev/ 下的字符设备,那我们同样也能通过安卓的 api,读写 usb 设备。

这部分设计很多 Android USB 知识,本篇不做详细讨论,仓库中有所有代码

我也是狂补了几波 USB 相关的文章,然后去 github 上翻到一个勉强能用的开源修修补补才实现的。

读写 OTG

获取权限

能够识别到安卓设备连接的 device_filter.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
    <usb-device
        class="255"
        protocol="1"
        subclass="66" />
    <usb-device
        class="255"
        protocol="3"
        subclass="66" />
</resources>

这部分写到 manifest

            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>
            <meta-data
                android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />

申请权限

 UsbDevice device = getIntent().getParcelableExtra(UsbManager.EXTRA_DEVICE);
        if (device != null) {
            // 说明是静态广播启动起来的
            // 用户再收到系统弹窗"是否打开ADB工具",用户点击了是
            System.out.println("From Intent!");
            asyncRefreshAdbConnection(device);
        } else {
            System.out.println("From onCreate!");
            for (String k : mManager.getDeviceList().keySet()) {
                UsbDevice usbDevice = mManager.getDeviceList().get(k);
                if (mManager.hasPermission(usbDevice)) {
                    asyncRefreshAdbConnection(usbDevice);
                } else {
                    mManager.requestPermission(usbDevice, PendingIntent.getBroadcast(getApplicationContext(), 0, new Intent(Message.USB_PERMISSION), 0));
                }
            }
        }

这部分就英雄所见略同了~

初始化 OTG


        UsbEndpoint epOut = null;
        UsbEndpoint epIn = null;
        // look for our bulk endpoints
        for (int i = 0; i < intf.getEndpointCount(); i++) {
            UsbEndpoint ep = intf.getEndpoint(i);
            if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
                if (ep.getDirection() == UsbConstants.USB_DIR_OUT) {
                    epOut = ep;
                } else {
                    epIn = ep;
                }
            }
        }
        if (epOut == null || epIn == null) {
            throw new IllegalArgumentException("not all endpoints found");
        }
        mEndpointOut = epOut;
        mEndpointIn = epIn;

这段代码到处都能抄到。

    public void readx(byte[] buffer, int length) throws IOException {

        UsbRequest usbRequest = getInRequest();

        ByteBuffer expected = ByteBuffer.allocate(length).order(ByteOrder.LITTLE_ENDIAN);
        usbRequest.setClientData(expected);

        if (!usbRequest.queue(expected, length)) {
            throw new IOException("fail to queue read UsbRequest");
        }

        while (true) {
            UsbRequest wait = mDeviceConnection.requestWait();

            if (wait == null) {
                throw new IOException("Connection.requestWait return null");
            }

            ByteBuffer clientData = (ByteBuffer) wait.getClientData();
            wait.setClientData(null);

            if (wait.getEndpoint() == mEndpointOut) {
                // a write UsbRequest complete, just ignore
            } else if (expected == clientData) {
                releaseInRequest(wait);
                break;

            } else {
                throw new IOException("unexpected behavior");
            }
        }
        expected.flip();
        expected.get(buffer);
    }

public void writex(byte[] buffer) throws IOException {
        Log.i("Nightmare", ">>>>>>>>" + new String(buffer));
        int offset = 0;
        int transferred = 0;

        while ((transferred = mDeviceConnection.bulkTransfer(mEndpointOut, buffer, offset, buffer.length - offset, defaultTimeout)) >= 0) {
            offset += transferred;
            if (offset >= buffer.length) {
                break;
            }
        }
        if (transferred < 0) {
            throw new IOException("bulk transfer fail");
        }
    }

ADB协议分析

ADB 协议算应用层协议,这种串口通信协议,都会有一些握手的机制,类似于 tcp 的握手,简单说,就是对一些口令,因为会通过 usb 向手机写入东西的不仅是 adb,可能是 MTP 拷贝文件,它不能误识别。

实现 adb connect

在PC第一次连接安卓时,安卓上会有个弹窗,问是否允许调试,其实就是 PC 请求连接安卓设备,点击确认后才算连接结束,之后再次连接就没有这个弹窗了,但是这个请求过程还是在的 所以第一步就是安卓如何实现 otg 成功连接另一台安卓,并唤起调试弹窗。

我将整个协议用聊天的方式表述,右边表示写入内容,代表 PC,左边代表返回内容,代表安卓。 $符号连接的字符串代表变量,AUTH 与 CNXN 等代表字符本身,+符号代表连接,方便表示,不代表协议中有这个字符。 ()间的内容为解释说明

首次连接

                    CNXN + '$protcol_version' + '$maxdata'
                    '$payload'($payload是host::\0)
AUTH
'$token1'
                    AUTH
                    '$signature'(将安卓端发送回来的$token1签名后得到的)
AUTH
'$token2'
                    AUTH
                    '$RSA_PUBLIC_KEY'
CNXN
device::ro.product.name=elish;ro.product.model=M2105K81AC;ro.product.device=elish;
features=sendrecv_v2_brotli,remount_shell,sendrecv_v2,abb_exec,fixed_push_mkdir,
fixed_push_symlink_timestamp,abb,shell_v2,cmd,ls_v2,apex,stat_v2

最后设备点击弹窗的允许后才会返回 device::.*,代表整个握手过程结束。

非首次连接

                    CNXN + '$protcol_version' + '$maxdata'
                    '$payload'
AUTH
'$token1'
                    AUTH
                    '$signature'(将安卓端发送回来的$token1签名后得到的)
CNXN
device::ro.product.name=elish;ro.product.model=M2105K81AC;ro.product.device=elish;features=sendrecv_v2_brotli,remount_shell,sendrecv_v2,abb_exec,fixed_push_mkdir,fixed_push_symlink_timestamp,abb,shell_v2,cmd,ls_v2,apex,stat_v2

少了一个要求发送 RSA 的过程。

以上全部经过代码验证。

as.png

连接成功后,后面想实现什么功能,在 SERVICES.TXTSYNC.TXT 都能很快的找到了。

实现 adb shell

协议文档

shell:command arg1 arg2 ...
    Run 'command arg1 arg2 ...' in a shell on the device, and return
    its output and error streams. Note that arguments must be separated
    by spaces. If an argument contains a space, it must be quoted with
    double-quotes. Arguments cannot contain double quotes or things
    will go very wrong.

    Note that this is the non-interactive version of "adb shell"

完整协议过程。

                OPEN
                shell:cmd
OKAY
CLSE

实际协议内容

箭头表示消息方向,不是具体协议内容,>>>>>>>>代表发送过去的消息,<<<<<<<<代表发送回来的消息

>>>>>>>>OPEN��������������-������+��������
>>>>>>>>shell:settings put system pointer_location 1��
<<<<<<<<OKAY%��������������������������������
<<<<<<<<CLSE%��������������������������������

'�'字符是编码成 String 后出现的,其实就是这个字节没有内容。

实现 adb shell interactive

与上一个不同,这个是可交互式终端。

协议文档

shell:
    Start an interactive shell session on the device. Redirect
    stdin/stdout/stderr as appropriate. Note that the ADB server uses
    this to implement "adb shell", but will also cook the input before
    sending it to the device (see interactive_shell() in commandline.c)

完整协议过程

                OPEN
                shell:
OKAY
WRTE
elish:/ $(根据设备类型打印不同的字符,就是pc终端执行 adb shell 看到的前缀)
                OKAY

实际协议内容

>>>>>>>>OPEN��������������������R��������
>>>>>>>>shell:��
<<<<<<<<OKAY+��������������������������������
<<<<<<<<WRTE+������������
���������������
<<<<<<<<elish:/ $
>>>>>>>>OKAY������+��������������������������

这个时候我敲下 pwd 再按回车。

>>>>>>>>WRTE������'������������p����������
>>>>>>>>p
<<<<<<<<OKAY'��������������������������������
<<<<<<<<WRTE'������������������p����������
<<<<<<<<p
>>>>>>>>OKAY������'��������������������������
>>>>>>>>WRTE������'������������w����������
>>>>>>>>w
<<<<<<<<OKAY'��������������������������������
<<<<<<<<WRTE'������������������w����������
<<<<<<<<w
>>>>>>>>OKAY������'��������������������������
>>>>>>>>WRTE������'������������d����������
>>>>>>>>d
<<<<<<<<OKAY'��������������������������������
<<<<<<<<WRTE'������������������d����������
<<<<<<<<d
>>>>>>>>OKAY������'��������������������������
>>>>>>>>
<<<<<<<<OKAY'��������������������������������
<<<<<<<<WRTE'������������������$����������
<<<<<<<<
>>>>>>>>OKAY������'��������������������������
<<<<<<<<WRTE'������������
<<<<<<<</
elish:/ $
>>>>>>>>OKAY������'��������������������������

实现 adb push

这个比上俩都要复杂一些,毕竟涉及到文件的上传。

协议文档

sync:
    This starts the file synchronization service, used to implement "adb push"
    and "adb pull". Since this service is pretty complex, it will be detailed
    in a companion document named SYNC.TXT
SYNC SERVICES:
Requesting the sync service ("sync:") using the protocol as described in
SERVICES.TXT sets the connection in sync mode. This mode is a binary mode that
differ from the regular adb protocol. The connection stays in sync mode until
explicitly terminated (see below).

After the initial "sync:" command is sent the server must respond with either
"OKAY" or "FAIL" as per usual. 

In sync mode both the server and the client will frequently use eight-byte
packets to communicate in this document called sync request and sync
responses. The first four bytes is an id and specifies sync request is
represented by four utf-8 characters. The last four bytes is a Little-Endian
integer, with various uses. This number will be called "length" below. In fact
all binary integers are Little-Endian in the sync mode. Sync mode is
implicitly exited after each sync request, and normal adb communication
follows as described in SERVICES.TXT
SEND:
The remote file name is split into two parts separated by the last
comma (","). The first part is the actual path, while the second is a decimal
encoded file mode containing the permissions of the file on device.

Note that some file types will be deleted before the copying starts, and if
the transfer fails. Some file types will not be deleted, which allows
  adb push disk_image /some_block_device
to work.

After this the actual file is sent in chunks. Each chunk has the following
format.
A sync request with id "DATA" and length equal to the chunk size. After
follows chunk size number of bytes. This is repeated until the file is
transferred. Each chunk must not be larger than 64k.

When the file is transferred a sync request "DONE" is sent, where length is set
to the last modified time for the file. The server responds to this last
request (but not to chuck requests) with an "OKAY" sync response (length can
be ignored).

完整协议过程

                OPEN
                sync:
OKAY
                WRTE
                SEND+'$length'($length$remote_file_path与,33206字符的总长度)
OKAY
                WRTE
                '$remote_file_path'
OKAY
                WRTE
                ,+'$mode'(mode是文件权限,33206代表权限是0666)
OKAY

> start
                WRTE
                DATA+'$len'($len是即将发送的字节长度)
OKAY
                WRTE
                '$byte'(此次的字节,不能超过16k)
OKAY

< end

start 到 end 循环直到一个文件被完整的发送

                WRTE
                DONE(通知安卓 adbd 这个文件发送完了)
OKAY
WRTE
                OKAY
                WRTE
                QUIT(退出sync模式)
OKAY
CLSE

实际协议内容

>>>>>>>>OPEN�����������������������������
>>>>>>>>sync:��
<<<<<<<<OKAY��������������������������������
>>>>>>>>WRTE������������������F��������
>>>>>>>>SEND������
<<<<<<<<OKAY��������������������������������
>>>>>>>>WRTE���������������������������
>>>>>>>>/sdcard/switch_work.sh
<<<<<<<<OKAY��������������������������������
>>>>>>>>WRTE������������������*��������
>>>>>>>>,33206
<<<<<<<<OKAY��������������������������������
>>>>>>>>WRTE������������������y��������
>>>>>>>>DATA_������
<<<<<<<<OKAY��������������������������������
>>>>>>>>WRTE������������_������3"��������
>>>>>>>>this is file content byte
<<<<<<<<OKAY��������������������������������
>>>>>>>>WRTE���������������������������
>>>>>>>>DONE���
<<<<<<<<OKAY��������������������������������
<<<<<<<<WRTE������������������4��������
<<<<<<<<OKAY��������
>>>>>>>>OKAY��������������������������������
>>>>>>>>WRTE������������������C��������
>>>>>>>>QUIT��������
<<<<<<<<OKAY��������������������������������
<<<<<<<<CLSE��������������������������������

实现 adb install

这个其实还是使用的 adb shell。 首先 push apk 到 /data/local/tmp/ 文件夹,再使用 shell:pm install -r /data/local/tmp/xxx.apk,最后再删掉这个 apk 就完成了。

源代码可以参考otgadb_channel

实现 adb tcpip、adb usb

完整协议过程

                            OPEN
                            tcpip:+'$port'
OKAY
WRTE
restarting in TCP mode port: $port
                            OKAY
                            CLSE

实际协议内容

>>>>>>>>OPEN��������������������.��������
>>>>>>>>tcpip:5555��
<<<<<<<<OKAY/��������������������������������
<<<<<<<<WRTE/������������"������#��������
<<<<<<<<restarting in TCP mode port: 5555
>>>>>>>>OKAY������/��������������������������
<<<<<<<<CLSE/��������������������������������

adb usb 只需要把 tcpip 替换成 usb 即可

开箱即用,拿来吧你

想要更完善的拥有 adb 的功能,依然还是交叉编译 adb binary,客户端就需要对 OTG 的 ADB 与 网络 ADB 做一下区分,简单解释一下实现。

面相抽象

abstract class ADBChannel {
  Future<String> execCmmand(String cmd);

  Future<void> push(String localPath, String remotePath);
  Future<void> install(String file);
  Future<void> changeNetDebugStatus(int port);
}

实现

class BinADBChannel extends ADBChannel {
  BinADBChannel(this.serial);

  final String serial;
  @override
  Future<String> execCmmand(String cmd) async {
    String out = '';
    final List<String> cmds = cmd.split('\n');
    for (final String cmd in cmds) {
      out += await execCmd(cmd);
    }
    return out;
  }

  @override
  Future<void> install(String file) async {
    await execCmmand('adb -s $serial install -t $file');
  }

  @override
  Future<void> push(String localPath, String remotePath) async {
    final String fileName = basename(localPath);
    await execCmmand('adb -s $serial push $localPath $remotePath$fileName');
  }

  @override
  Future<void> changeNetDebugStatus(int port) async {
    if (port == 5555) {
      await execCmmand(
        'adb -s $serial tcpip 5555',
      );
    } else {
      await execCmmand(
        'adb -s $serial usb',
      );
    }
  }
}

execCmmand 是对 Process.run 的封装。

class OTGADBChannel extends ADBChannel {
  @override
  Future<String> execCmmand(String cmd) async {
    // otg 会去掉 adb -s xxx shell
    final String shell = cmd.replaceAll(RegExp('.*shell'), '');
    final String data = await PluginUtil.execCmd(shell);
    Log.e('OTGADBChannel execCmmand -> $data');
    return data;
  }

  @override
  Future<void> install(String file) async {
    final String fileName = basename(file);
    await PluginUtil.pushToOTG(file, '/data/local/tmp/');
    await PluginUtil.execCmd('pm install -r /data/local/tmp/$fileName');
  }

  @override
  Future<void> push(String localPath, String remotePath) async {
    await PluginUtil.pushToOTG(localPath, remotePath);
  }

  @override
  Future<void> changeNetDebugStatus(int port) async {
    final String data = await PluginUtil.changeNetDbugStatus(port);
    Log.w('changeNetDebugStatus ->$data');
  }
}

PluginUtil 是手搓的一个 Plugin 管理类。

体验一下

欢迎大家体验 ADB TOOL 安卓版,有能力者可自行编译体验。

ADB TOOL 酷安下载地址

ADB TOOL 个人服务器下载地址

个人软件快捷下载地址

开源地址

adb_tool 这个工具还在大量开发完善,打磨细节中。后面会有单独介绍的文章。

仓库从建立写上第一行代码的时候,就已经是开源的,也一直当一个玩具在开发,初心都是自己需要一个这样的 app。

con.png

commit.png

参考资料

enjoy!