Java网络抓包: 使用wintun创建dns服务拦截tcp流量

目的

如何让网络流量走自己的网络适配器呢,浏览器又不能设置选择网络适配器,在windows下可以修改hosts添加域名和临时ip映射,临时ip在网络适配器子网掩码内,就会走该网络适配器,在java中你还的弄个映射表去记录你的实际ip, 几个还好,但是多起来就麻烦。

具体实现

自己实现个dns服务器,记录实际ip返回临时ip,临时ip会走自建适配器,java socket时将临时ip变成实际ip,完成数据传输。

wintun创建适配器设置ip:172.29.1.1,子网掩码:255.255.0.0,dns地址:172.29.1.2

子网掩码范围255*255 个做临时ip够用,dns地址会走自建适配器,dns默认走udp端口53,我们在适配器中拦截就行了,就不用在弄个dns服务器。

udp协议

udp报文

sql 复制代码
 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
|     Source      |   Destination   |
|      Port       |      Port       |
+--------+--------+--------+--------+
|     Length      |    Checksum     |
+--------+--------+--------+--------+
|                                     
|          Data octets (if any)        
|                                     
+-------------------------------------+
Source Port (16 bits): 源端口号,占据UDP头部的前两个字节,指示发送方应用程序所使用的端口号。

Destination Port (16 bits): 目标端口号,占据UDP头部的第三和第四字节,指示接收方应用程序所使用的端口号。

Length (16 bits): 长度字段指示UDP数据报的长度,包括头部和数据部分的长度。长度字段的最小值是8(头部的长度),最大值是65535字节。

Checksum (16 bits): 校验和字段用于检测UDP数据报在传输过程中是否发生了错误。如果校验和字段为0,表示未使用校验和。

Data (Variable): 数据字段包含UDP数据报的实际数据内容。它的长度可以从Length字段中得知。

UDP报文相对于TCP报文而言非常简单,没有像TCP那样的序列号、确认号、窗口大小等字段,也没有复杂的连接管理机制。

pcap4j可以帮我计算Length和Checksum,没什么好关注字段,比tcp简单多了。

wintu构建一个dns服务器

dns可以使用udp协议实现,端口53 。如果udp 53端口超时,他会发送tcp 53端口的。

pcap4j 解析wintun获取的数据包,使用 UdpData 类解析udp

scss 复制代码
//解析数据包
IpPacket packet = (IpPacket) IpSelector.newPacket(packetBytes, 0, packetBytes.length);

if(packet.getPayload() instanceof TcpPacket){
    TcpData.parse(packet,sessionHandle);
}else if(packet.getPayload() instanceof UdpPacket){
    UdpData.parse(packet,sessionHandle);
}

过滤一些端口协议和广播,防止数据包在网络一直循环

scss 复制代码
InetAddress dstAddr = ipPacket.getHeader().getDstAddr();
UdpPacket udpPacket = (UdpPacket) ipPacket.getPayload();
int dstPort = udpPacket.getHeader().getDstPort().valueAsInt();

byte[] rawData = udpPacket.getPayload().getRawData();

//过滤一些协议
if (dstPort == 1900) {
    //ssdp协议
    remove(ipPacket);
    return;
} else if (dstPort == 5355) {
    //LLMNR
    remove(ipPacket);
    return;
} else if (dstPort == 5353) {
    //MDNS
    remove(ipPacket);
    return;
} else if (dstAddr.getAddress()[3] == (byte) 255) {
    //广播
    remove(ipPacket);;
    return;
}

双重校验DatagramChannel channel,没有创建,最后使用channel写入数据。复用channel 同一个源ip和源端口用同一个channel。使用本地网关192.168.1.1来替换172.29.1.2来访问dns服务。udp的DatagramChannel连接还得判断ip4和ip6,不知道为什么不通过connect时自己去判断。开启一个线程去读取udp那边返回的数据,然后写入wintun的网络适配器中(如果想减少线程可以使用Selector,会麻烦些,会有多线程问题),构建数据包使用pcap4j方便很多。

ini 复制代码
//双重校验 volatile添加线程可见性
if (channel == null) {
    synchronized (this) {
        if (channel == null) {
            try {
                //判断是ip4还ip6
                ProtocolFamily protocolFamily = StandardProtocolFamily.INET;
                if (ipPacket instanceof IpV6Packet) {
                    protocolFamily = StandardProtocolFamily.INET6;
                }

                channel = DatagramChannel.open(protocolFamily);

                if(dstAddr.getHostAddress().equals("172.29.1.2")){
                    dstAddr = InetAddress.getByName("192.168.1.1");
                }

                channel.connect(new InetSocketAddress(dstAddr, dstPort));

                srcPacket = ipPacket;

                readUdpThread = new Thread(() -> {
                    ByteBuffer buffer = ByteBuffer.allocate(1024);

                    while (!Thread.interrupted()) {
                        try {
                            int len = channel.read(buffer);
                            if (len > 0) {
                                buffer.flip();
                                byte[] array = new byte[buffer.limit()];
                                buffer.get(array);

                                //构建upd包
                                byte[] bytes = buildUdpPacket(ipPacket, array);

                                //写入网络适配器
                                writeData(bytes);

                            } else {
                                //认为连接关闭
                                close();
                            }

                        } catch (IOException e) {
                            e.printStackTrace();
                            close();
                        }
                    }

                });

                readUdpThread.setName(dstAddr + ":" + dstPort + " readUdpThread");
                readUdpThread.start();

            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}
try {
    if(channel.isConnected()){
        //写入数据
        channel.write(ByteBuffer.wrap(rawData));
    }

} catch (IOException e) {
   e.printStackTrace();
   close();
}

可以使用一下 dnsjava 测试一dns服务,java自带InetAddress不能设置dns服务器ip。

mavne依赖

xml 复制代码
<dependency>
    <groupId>dnsjava</groupId>
    <artifactId>dnsjava</artifactId>
    <version>3.5.3</version>
</dependency>

查询dns服务器

ini 复制代码
public void dnsQuery() throws Exception {
    Record queryRecord = Record.newRecord(Name.fromString("www.baidu.com."), Type.A, DClass.IN);
    Message queryMessage = Message.newQuery(queryRecord);
    Resolver r = new SimpleResolver("172.29.1.2");
    r.sendAsync(queryMessage)
            .whenComplete(
                    (answer, ex) -> {
                        if (ex == null) {
                            System.out.println(answer);
                        } else {
                            ex.printStackTrace();
                        }
                    })
            .toCompletableFuture()
            .get();
}

结果看没有问题

缓存临时ip和实际ip

生成临时ip:ip是4个byte组成,4个byte就是一个int,我们可以将172.29.1.3变成int开始,每次生成就+1,generateIp方法生成,bytesToInt方法和intToBytes方法由gpt生成。

scss 复制代码
private static byte[] initIp = new byte[]{(byte)172,(byte)29,(byte)1,(byte) 3};
private static AtomicInteger number = new AtomicInteger(0);

public int generateIp(){
    return bytesToInt(initIp)+number.getAndIncrement();
}

private static int bytesToInt(byte[] bytes) {
    if (bytes.length != 4) {
        throw new IllegalArgumentException("Byte array must have length of 4");
    }
    int result = 0;
    for (int i = 0; i < 4; i++) {
        result = (result << 8) | (bytes[i] & 0xFF);
    }
    return result;
}

private static byte[] intToBytes(int value) {
    byte[] bytes = new byte[4];
    bytes[0] = (byte) ((value >> 24) & 0xFF);
    bytes[1] = (byte) ((value >> 16) & 0xFF);
    bytes[2] = (byte) ((value >> 8) & 0xFF);
    bytes[3] = (byte) (value & 0xFF);
    return bytes;
}

使用 guava 中的Cache 缓存ip,key用ip转成的int

scss 复制代码
static Cache<Integer, DnsData> dnsCache = CacheBuilder.newBuilder()
        .maximumSize(1000) // 设置缓存最大容量
        .expireAfterWrite(3, TimeUnit.MINUTES) // 设置写入后的过期时间
        .build();
 

public static void putCache(InetAddress inetAddress){
    int i = generateIp();
    ipCache.put(i,inetAddress);
}

public static InetAddress getCache(int i){
    return ipCache.getIfPresent(i);
}

在读取udp数据时判断一下ip是172.29.1.2,然后修改一下ip,具体updateDnsIp方法

scss 复制代码
while (!Thread.interrupted()) {
    try {
        int len = channel.read(buffer);
        if (len > 0) {
            buffer.flip();
            byte[] array = new byte[buffer.limit()];
            buffer.get(array);

            //判断是dns 服务
            if (srcPacket.getHeader().getDstAddr().getHostAddress().equals("172.29.1.2")) {
                array = updateDnsIp(array);
            }

            //构建upd包
            byte[] bytes = buildUdpPacket(ipPacket, array);

            //写入网络适配器
            writeData(bytes);

        } else {
            //认为连接关闭
            close();
        }

    } catch (Exception e) {
        e.printStackTrace();
        close();
    }
}

我们主要修改的是dnsAnswers 或者Additional Info 的 type = A

updateDnsIp方法看起来代码好多,实际上就是修改type=A的数据(A表示的是ip4地址),然后重新构造数据包

scss 复制代码
private byte[] updateDnsIp(byte[] array) throws IllegalRawDataException {

    DnsPacket dnsPacket = DnsPacket.newPacket(array, 0, array.length);
    DnsPacket.DnsHeader header = dnsPacket.getHeader();
    String domain = null;
    if (header.getQdCount() > 0) {
        DnsQuestion dnsQuestion = header.getQuestions().get(0);
        domain = dnsQuestion.getQName().getName();
        System.out.println(domain);
    }


    //(附加信息)部分
    List<DnsResourceRecord> answersList = new ArrayList<>();
    for (DnsResourceRecord answer : header.getAnswers()) {
        if (answer.getDataType() == DnsResourceRecordType.A) {

            DnsRDataA dnsRDataA = (DnsRDataA) answer.getRData();

            byte[] bytes = IpCache.putCache(dnsRDataA.getAddress());
            DnsRDataA dnsRDataA1 = DnsRDataA.newInstance(bytes, 0, bytes.length);


            DnsResourceRecord build = new DnsResourceRecord.Builder()
                    .name(answer.getName())
                    .dataType(answer.getDataType())
                    .ttl(answer.getTtl())
                    .rData(dnsRDataA1)
                    .dataClass(answer.getDataClass())
                    .correctLengthAtBuild(true).build();
            answersList.add(build);

        } else {
            answersList.add(answer);
        }

    }
    //(附加信息)部分
    List<DnsResourceRecord> additionalList = new ArrayList<>();
    for (DnsResourceRecord info : header.getAdditionalInfo()) {
        if (info.getDataType() == DnsResourceRecordType.A) {

            DnsRDataA dnsRDataA = (DnsRDataA) info.getRData();

            byte[] bytes = IpCache.putCache(dnsRDataA.getAddress());
            DnsRDataA dnsRDataA1 = DnsRDataA.newInstance(bytes, 0, bytes.length);


            DnsResourceRecord build = new DnsResourceRecord.Builder()
                    .name(info.getName())
                    .dataType(info.getDataType())
                    .ttl(info.getTtl())
                    .rData(dnsRDataA1)
                    .dataClass(info.getDataClass())
                    .correctLengthAtBuild(true).build();
            additionalList.add(build);

        } else {
            additionalList.add(info);
        }
    }


    DnsPacket build = new DnsPacket.Builder()
            .id(dnsPacket.getHeader().getId())
            .response(dnsPacket.getHeader().isResponse())
            .opCode(dnsPacket.getHeader().getOpCode())
            .authoritativeAnswer(dnsPacket.getHeader().isAuthoritativeAnswer())
            .truncated(dnsPacket.getHeader().isTruncated())
            .recursionDesired(dnsPacket.getHeader().isRecursionDesired())
            .recursionAvailable(dnsPacket.getHeader().isRecursionAvailable())
            .reserved(dnsPacket.getHeader().getReservedBit())
            .authenticData(dnsPacket.getHeader().isAuthenticData())
            .checkingDisabled(dnsPacket.getHeader().isCheckingDisabled())
            .rCode(dnsPacket.getHeader().getrCode())
            .questions(dnsPacket.getHeader().getQuestions())
            .answers(answersList)
            .authorities(dnsPacket.getHeader().getAuthorities())
            .additionalInfo(additionalList)
            .qdCount(dnsPacket.getHeader().getQdCount())
            .anCount(dnsPacket.getHeader().getAnCount())
            .nsCount(dnsPacket.getHeader().getNsCount())
            .arCount(dnsPacket.getHeader().getArCount()).build();


    return build.getRawData();
}

使用dnsjava测试一下

调整了一下之前的tcp那块代码,socket阻塞io换成 AsynchronousSocketChannel 异步io,在TcpData的 receive方中替换一下实际ip

ini 复制代码
if(dstAddr instanceof  Inet4Address){
    InetAddress cache = IpCache.getCache(dstAddr);
    if(cache!=null){
        //更换实际ip
        dstAddr = cache;
    }
}

具体代码:wintun-03 · 断续/learn-demo - 码云 - 开源中国 (gitee.com)

测试结果

我这边使用Hyper-V创建一个ubuntu虚拟机,修改dns服务器为:172.29.1.2

使用浏览器访问测试一下,发现没有问题。具体数据包信息可以通过Wireshark查看。

相关推荐
Jarlen几秒前
将本地离线Jar包上传到Maven远程私库上,供项目编译使用
java·maven·jar
蓑 羽6 分钟前
力扣438 找到字符串中所有字母异位词 Java版本
java·算法·leetcode
Reese_Cool13 分钟前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言
严文文-Chris38 分钟前
【设计模式-享元】
android·java·设计模式
Flying_Fish_roe1 小时前
浏览器的内存回收机制&监控内存泄漏
java·前端·ecmascript·es6
c#上位机1 小时前
C#事件的用法
java·javascript·c#
【D'accumulation】2 小时前
典型的MVC设计模式:使用JSP和JavaBean相结合的方式来动态生成网页内容典型的MVC设计模式
java·设计模式·mvc
试行2 小时前
Android实现自定义下拉列表绑定数据
android·java
茜茜西西CeCe2 小时前
移动技术开发:简单计算器界面
java·gitee·安卓·android-studio·移动技术开发·原生安卓开发