介绍

从本文开始将对 Pcap4J(下文简称为 p4)提供的样例代码进行注释讲解,期间还包括了对 Pcap 原理的解读


每篇文章讲解一个样例,目录如下:

目录


SendArpRequest

原理

此样例代码示范如何发送 ARP 请求

在代码之前,本文将对 ARP 协议规范、ARP 欺骗原理及中间人攻击(MITM)做必要的介绍,也是为了项目做准备

ARP 协议

ARP 欺骗

MITM

步骤

  • 初始化 ARP 请求的参数,初始化网卡
  • 构造 ARP 数据包及以太网帧
  • 发送 ARP 请求并捕获 ARP响应
  • 完成释放资源

实现

package org.pcap4j.sample;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.pcap4j.core.BpfProgram.BpfCompileMode;
import org.pcap4j.core.NotOpenException;
import org.pcap4j.core.PacketListener;
import org.pcap4j.core.PcapHandle;
import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.core.PcapNetworkInterface.PromiscuousMode;
import org.pcap4j.core.Pcaps;
import org.pcap4j.packet.ArpPacket;
import org.pcap4j.packet.EthernetPacket;
import org.pcap4j.packet.Packet;
import org.pcap4j.packet.namednumber.ArpHardwareType;
import org.pcap4j.packet.namednumber.ArpOperation;
import org.pcap4j.packet.namednumber.EtherType;
import org.pcap4j.util.ByteArrays;
import org.pcap4j.util.MacAddress;
import org.pcap4j.util.NifSelector;

@SuppressWarnings("javadoc")
public class SendArpRequest {

  private static final String COUNT_KEY = SendArpRequest.class.getName() + ".count";
  private static final int COUNT = Integer.getInteger(COUNT_KEY, 1);

  private static final String READ_TIMEOUT_KEY = SendArpRequest.class.getName() + ".readTimeout";
  private static final int READ_TIMEOUT = Integer.getInteger(READ_TIMEOUT_KEY, 10); // [ms]

  private static final String SNAPLEN_KEY = SendArpRequest.class.getName() + ".snaplen";
  private static final int SNAPLEN = Integer.getInteger(SNAPLEN_KEY, 65536); // [bytes]

  // 发送 ARP 请求的源 MAC地址, 需填写正确, 否则接收不到 ARP 响应, 格式为: "-" 或 ":" 分隔开
  // D0-C6-37-3E-7A-fB, d0-c6-37-3e-7a-fb, d0:c6:37:3e:7a:fb 均可, 不区分大小写
  private static final MacAddress SRC_MAC_ADDR = MacAddress.getByName("d0:c6:37:3e:7a:fb");

  // 响应的 MAC 地址
  private static MacAddress resolvedAddr;

  private SendArpRequest() {}

  public static void main(String[] args) throws PcapNativeException, NotOpenException {
    // 源 IP 地址, 需填写正确
    String strSrcIpAddress = "192.168.1.121"; // for InetAddress.getByName()
    // 目的 IP 地址, 需填写正确
    String strDstIpAddress = args[0]; // for InetAddress.getByName()

    System.out.println(COUNT_KEY + ": " + COUNT);
    System.out.println(READ_TIMEOUT_KEY + ": " + READ_TIMEOUT);
    System.out.println(SNAPLEN_KEY + ": " + SNAPLEN);
    System.out.println("\n");

    PcapNetworkInterface nif;
    try {
      nif = new NifSelector().selectNetworkInterface();
    } catch (IOException e) {
      e.printStackTrace();
      return;
    }

    if (nif == null) {
      return;
    }

    System.out.println(nif.getName() + "(" + nif.getDescription() + ")");

    // 为什么需要使用两个 Handle 呢? 之前说过同一个 Handle 只可以操作一张网卡, 现在做一下补充
    // 由于此程序既要发包, 还要抓这个自己发的包, 所以需要单独使用一个 Handle
    // 不过就算不是这样, 也推荐, 发包和抓包尽量不要使用同一个 Handle
    PcapHandle handle = nif.openLive(SNAPLEN, PromiscuousMode.PROMISCUOUS, READ_TIMEOUT);
    PcapHandle sendHandle = nif.openLive(SNAPLEN, PromiscuousMode.PROMISCUOUS, READ_TIMEOUT);

    // 定义了一个不可重用的单一的线程池, 专门用来捕获 ARP 数据包, 此线程池是一个无界的线程队列(队列大小无限制)
    // 且线程池中同时只会有一个线程运行
    // 关于那几种线程池等到开始做项目用到时再来探究
    ExecutorService pool = Executors.newSingleThreadExecutor();

    try {
      // 设置用于捕获 ARP 响应的 BPF 过滤器规则
      handle.setFilter(
          "arp and src host "
              + strDstIpAddress
              + " and dst host "
              + strSrcIpAddress
              + " and ether dst "
              + Pcaps.toBpfString(SRC_MAC_ADDR),
          BpfCompileMode.OPTIMIZE);

      // listener 的回调函数, 逻辑很简单
      // 先判断是否是 ARP 包, 再判断是不是 ARP 响应, 如果是则将 ARP 响应体包括的 MAC 作为响应得到的 MAC
      PacketListener listener =
          packet -> {
            if (packet.contains(ArpPacket.class)) {
              ArpPacket arp = packet.get(ArpPacket.class);
              if (arp.getHeader().getOperation().equals(ArpOperation.REPLY)) {
                SendArpRequest.resolvedAddr = arp.getHeader().getSrcHardwareAddr();
              }
            }
            // 输出响应包
            System.out.println(packet);
          };

      // Task 为继承自 Runnable 的任务线程, 用于捕获 ARP 响应
      Task t = new Task(handle, listener);
      // 将 Task 放入线程池并运行线程池
      pool.execute(t);

      // 以下是构造 ARP 数据包的过程
      // 初始化一个 ArpBuilder 对象用于操作 ARP 数据包
      ArpPacket.Builder arpBuilder = new ArpPacket.Builder();
      try {
        // 添加参数
        arpBuilder
            .hardwareType(ArpHardwareType.ETHERNET) // 硬件类型为以太网, 如果电脑连接的是 WiFi 热点, 也可改为 IEEE.802, 总之必须对应上
            .protocolType(EtherType.IPV4) // 协议类型为 IPV4
            .hardwareAddrLength((byte) MacAddress.SIZE_IN_BYTES) // MAC 长度
            .protocolAddrLength((byte) ByteArrays.INET4_ADDRESS_SIZE_IN_BYTES) // IP 长度
            .operation(ArpOperation.REQUEST) // ARP 类型为: 请求
            .srcHardwareAddr(SRC_MAC_ADDR) // 源 MAC
            .srcProtocolAddr(InetAddress.getByName(strSrcIpAddress)) // 源 IP
             // 目的MAC: 广播地址, 也可改为 MacAddress.getByName("ff-ff-ff-ff-ff-ff")
            .dstHardwareAddr(MacAddress.ETHER_BROADCAST_ADDRESS)
             // 目的 IP
            .dstProtocolAddr(InetAddress.getByName(strDstIpAddress));
      } catch (UnknownHostException e) {
        throw new IllegalArgumentException(e); // 参数错误异常
      }

      // 以下是构造以太网帧的过程
      // 初始化一个 etherBuilder 对象用于操作帧
      EthernetPacket.Builder etherBuilder = new EthernetPacket.Builder();
      etherBuilder
          .dstAddr(MacAddress.ETHER_BROADCAST_ADDRESS)
          .srcAddr(SRC_MAC_ADDR)
          .type(EtherType.ARP) // 帧类型
          .payloadBuilder(arpBuilder) // 由于 ARP 请求是包含在帧里的, 故需要做一个 payload
          .paddingAtBuild(true); // 是否填充至以太网的最小帧长, 必须为 true, 否则对方不会接受请求

      // 发送 count 个请求, 请注意如果将 count 改为无限, 每隔一定时间向目的战点发送特定的 ARP 请求, 即可达到 ARP 欺骗的作用
      for (int i = 0; i < COUNT; i++) {
        Packet p = etherBuilder.build();
        System.out.println(p);
        sendHandle.sendPacket(p);
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          break;
        }
      }// 最后, 回收资源
    } finally {
      if (handle != null && handle.isOpen()) {
        handle.close();
      }
      if (sendHandle != null && sendHandle.isOpen()) {
        sendHandle.close();
      }
      if (pool != null && !pool.isShutdown()) {
        pool.shutdown();
      }

      System.out.println(strDstIpAddress + " was resolved to " + resolvedAddr);
    }
  }

  // 下面是 Task 子类的定义, 重载了 Runnable 的默认回调函数 run
  private static class Task implements Runnable {

    private PcapHandle handle;
    private PacketListener listener;

    public Task(PcapHandle handle, PacketListener listener) {
      this.handle = handle;
      this.listener = listener;
    }

    @Override // 将 run 重载为使用 loop 方法捕获数据包
    public void run() {
      try {
        handle.loop(COUNT, listener);
      } catch (PcapNativeException e) {
        e.printStackTrace();
      } catch (InterruptedException e) {
        e.printStackTrace();
      } catch (NotOpenException e) {
        e.printStackTrace();
      }
    }
  }
}

总结

本文详细介绍了 ARP 相关的原理,并进一步说明了 ARP 欺骗、MITM 及两者之间的关系

在此基础上详细地注释了样例代码,此时读者应该可以自己编写 ARP 欺骗等等进阶程序了