Android网络抓包:使用VpnService完成HTTP请求

2,232 阅读4分钟

目的

在Anroid下完成TCP协议和UDP协议,了解HTTP请求过程。

请求流程

应用HTTP请求流程

下面是客户端发起HTTP请求大概流程,这里的虚拟网络是通过VPNService创建,HTTP的数据包都会写入虚拟网络,最后会根据IP和端口从虚拟网络拿到响应数据,这些操作都是应用那边的,我们不需要管,我们只要知道,他把HTTP请求的数据包写入虚拟网络,我们可以读出来,然后通过Socket连接写入数据,再读取Socket数据,按照规定协议写入虚拟网络中,应用他会自己读取出来。

微信截图_20240412200116.png

JAVA应用流程

下面是JAVA应用大概流程,通过虚拟网络获取数据包,然后通过pcap4j解析数据包,通过源ip和源portMap中获取TCPUDP连接,根据协议不同走不同流程。TCP使用SocketChannelUDP使用DatagramChannel两个NIO,设置非阻塞通过Selector去监听事件。

微信截图_20240413004725.png

TCP协议中的SEQ计算

操作计算
回应SYN+ 1
回应FIN+ 1
回应带data+ data.length
其他+ 0

遇到问题

  1. 多线程使用Selector问题,一个线程先selector.select();(会阻塞线程),另一个线程后再执行channel.register(selector,...)就会阻塞当前线程,可以先使用selector.wakeup()唤醒第一个线程,再注册事件,大量数据执行还是会出现阻塞线程。
  2. 多线程使用FileChannel会出错。
  3. channel.write(buffer)写入数据不一定将buffer的数据写完。

最后效果

我是指定拦截com.android.browser(自带浏览器)应用。

QQ图片202402211520511.jpg

代码操作

使用 Android Studio 创建一个项目,创建一个MyVpnService继承VpnService,setupVpn方法启动,builder设置基本参数,主要创建一个ParcelFileDescriptor对象,这个对象可以拿到数据流,进行读写。

if (descriptor == null) {
    Builder builder = new Builder();
    // 添加IPv4地址
    builder.addAddress("10.0.0.2", 32);
    // 添加IPv4路由,拦截所有ipv4
    builder.addRoute("0.0.0.0", 0);
    // 添加dns服务器
    builder.addDnsServer("114.114.114.114");
    try {
        //指定应用拦截
        //builder.addAllowedApplication("com.example.vpnservicedemo");
        builder.addAllowedApplication("com.android.browser");
    } catch (PackageManager.NameNotFoundException e) {
        throw new RuntimeException(e);
    }

    // ParcelFileDescriptor是一个文件描述符,它是一种程序读写已打开文件、socket的对象
    descriptor = builder.setConfigureIntent(pendingIntent).establish();

    //创建一个读取对象
    readVpn = new ReadVpn(this, descriptor);
    readVpn.start();

}

修改AndroidManifest.xml文件,添加权限声明,MyVpnService的service声明。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.VpnServiceDemo"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".MyVpnService" android:permission="android.permission.BIND_VPN_SERVICE"
            android:exported="true">
            <intent-filter>
                <action android:name="android.net.VpnService" />
            </intent-filter>
        </service>
    </application>

</manifest>

ReadVpn的start方法,读取数据并解析数据,pushPc.pushData()是将数据包通过socket方式推送到pc电脑上,好让Wireshark查看数据包情况,parseData解析数据包。

public void start() {

    //创建一个线程读取channel数据
    readDataThread = new Thread(() -> {

        FileChannel readChannel = null;
        ExecutorService threadPool = null;
        try {


            // 获取一个channel,零拷贝读取数据
            readChannel = new FileInputStream(descriptor.getFileDescriptor()).getChannel();


            ByteBuffer buffer = ByteBuffer.allocate(1024 * 20);


            while (!Thread.interrupted()) {

                int len = readChannel.read(buffer);

                if (len == -1) {
                    break;
                }

                if (len > 0) {
                    buffer.flip();
                    byte[] bt = new byte[len];
                    buffer.get(bt);

                    //计算接收数量
                    receiveLen += len;
                    //发送到ui
                    pushUi();

                    //推送PC
                    pushPc.pushData(bt);

                    parseData(bt);

                    buffer.clear();

                }

            }

        } catch (Exception e) {

            System.out.println("ReadData start 异常: " + e.getMessage());

        } finally {

            if (readChannel != null) {
                try {
                    readChannel.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

            if (threadPool != null) {
                threadPool.shutdown();
            }
        }
    });

    readDataThread.setName("readDataThread");
    readDataThread.start();
}

parseData 方法,IpSelector.newPacket解析数据包,根据源ip源prot判断唯一,如果数据包是tcp就创建TcpPipe,udp就创建UdpPipe,然后再对应执行receive方法。

private void parseData(byte[] array) {
    try {
        Packet packet = IpSelector.newPacket(array, 0, array.length);
        if (packet instanceof IpPacket) {

            IpPacket ipPacket = (IpPacket) packet;
            //通过源ip和源port判断唯一SendData
            String key = getKey(ipPacket);

            if (key != null) {

                if (pipeMap.get(key) == null) {
                    Pipe pipe = null;
                    if (ipPacket.getHeader().getProtocol() == IpNumber.TCP) {
                        pipe = new TcpPipe(vpnService, this);
                    } else if (ipPacket.getHeader().getProtocol() == IpNumber.UDP) {
                        pipe = new UdpPipe(vpnService, this);
                    }
                    if (pipe != null) {
                        //防止多线程问题
                        pipeMap.putIfAbsent(key, pipe);
                    }
                }

                Pipe pipe = pipeMap.get(key);

                int code = pipe.receive(ipPacket);

                if (code == -1) {
                    //说明关闭了
                    pipeMap.remove(key);
                }

            }

        }
    } catch (Exception e) {
        e.printStackTrace();
        System.out.println("ReadData parseData 解析包异常:" + e.getMessage());
    }

}

TcpPipe.receive 方法,syn标志(第一次握手)创建SocketChannel.open()初始化属性,注册连接事件到Selecter,非syn标志有数据就写入SocketChannel,有fin标志就回应fin标志,并且关闭SocketChannel

public int receive(IpPacket ipPacket) throws IOException {

    TcpPacket tcpPacket = (TcpPacket) ipPacket.getPayload();

    boolean syn = tcpPacket.getHeader().getSyn();
    boolean fin = tcpPacket.getHeader().getFin();
    boolean psh = tcpPacket.getHeader().getPsh();
    boolean ack = tcpPacket.getHeader().getAck();
    boolean rst = tcpPacket.getHeader().getRst();

    InetAddress dstAddr = ipPacket.getHeader().getDstAddr();
    int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();

    int sequenceNumber = tcpPacket.getHeader().getSequenceNumber();

    if (rst) {
        close();
        return -1;
    }

    if (sendFin.get() && replyFin.get()) {
        close();
        return -1;
    }

    if (syn) {

        if (socketChannel == null) {

            srcIpPacket = ipPacket;
            replyAck.set(sequenceNumber + 1);

            // 使用nio
            socketChannel = SocketChannel.open();
            // 使连接不被自身拦截
            vpnService.protect(socketChannel.socket());
            // 使用非阻塞io
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress(dstAddr, dstPort));

            //注册事件
            TcpSelector.registerConnect(socketChannel, this);

        }

    } else {

        if (socketChannel != null && !socketChannel.isConnected()) {
            //连接重置
            replyRst(tcpPacket);
            return -1;
        }

        try {
            Packet payload = tcpPacket.getPayload();

            if ((payload != null && payload.getRawData().length > 0 ) || fin) {

                int dataLen = 0;
                if (payload != null && payload.getRawData().length > 0) {
                    dataLen = payload.getRawData().length;
                }

                if (dataLen > 0) {

                    if (socketChannel.isConnected()) {
                        ByteBuffer wrap = ByteBuffer.wrap(payload.getRawData());


                        while (wrap.hasRemaining()){
                            socketChannel.write(wrap);
                        }

                    }

                }

                int add = dataLen + (fin ? 1 : 0);

                replyAck(replyAck.get() + add);

                replyAck.addAndGet(add);


                if (tcpPacket.getHeader().getFin()) {

                    replyFin.set(true);

                    if (sendFin.getAndSet(true)) {
                        replyFin();
                    }

                    socketChannel.close();
                }
            }


        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("sendData 异常:" + e.getMessage());
            close();
            return -1;
        }

    }

    return 0;
}
完整代码,供个人学习

断续/VpnServiceDemo - 码云 - 开源中国 (gitee.com)