1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rust 研究:パケット・スニフィング

Last updated at Posted at 2023-07-22

パケット・スニフィングの Rust での実装について調べましたのでメモを残します。

普通だったら tcpdump とか wireshark を使いそうなところですがあえて Rust でやりました。
Rust 修行中ですので。

本稿では Windows10 での実行を前提にしております。

実装例

以下、実装例を示します。
pnet クレイトを使用しました。
さほど難しいコードでもないので解説なしです。

工夫点というほどでもありませんが、

  • windows でのインターフェース名は linux よりも面倒なのでインターフェース名を表示するコマンドラインオプションを用意。
  • なるべく不格好な unwrap の使用を回避し、Rust の流儀でエラー処理。
// main.rs
const VERBOSE:bool = false; // true にすると TCP/UDP/ICMP 以外のパケットの存在も確認できる

use pnet::datalink;
use pnet::datalink::Channel::Ethernet;
use pnet::packet::ethernet::EtherTypes;
use pnet::packet::ethernet::EthernetPacket;
use pnet::packet::ipv4::Ipv4Packet;
use pnet::packet::tcp::TcpPacket;
use pnet::packet::udp::UdpPacket;
use pnet::packet::icmp::IcmpPacket;
use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::Packet;
use pnet::packet::tcp::TcpFlags;
use std::net::Ipv4Addr;
use std::env;

fn main() {
    if let Err(e) = run() {
        eprintln!("[Error] {:?}", e);
    }
}

fn run() -> Result<(), MyError> {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        eprintln!("Usage: {} args", args[0]);
        eprintln!("\tinterfaces ... display interfaces");
        eprintln!("\tsniff interface ... sniff at interface");
        return Ok(());
    }

    let cmd: &str = &args[1];

    match cmd {
        "interfaces" => {
            interfaces();
        },
        "sniff" => {
            if args.len() > 2 {
                let name: &str = &args[2];
                sniff(name)?;
            } else {
                return Err(MyError::TooFewArgumentError);
            }
        },
        _ => {
            return Err(MyError::UnknownCommandError);
        },
    }
    Ok(())
}

// インターフェースを表示する関数
fn interfaces () {
    println!("Interfaces:");
    for interface in datalink::interfaces() {
        println!("{}", interface.name);
        println!("\tdescription: {}", interface.description);
        if let Some(mac) = interface.mac {
            println!("\tmac: {}", mac);
        }
        print!("\tips: ");
        for ip in &interface.ips {
            print!("{} ", ip);
        }
        println!();
    }
}

// パケット・スニッフィングを行う関数
fn sniff(interface_name: &str) -> Result<(), MyError>{
    // 保持するインターフェースの中から該当するものを見つける
    let result = datalink::interfaces().into_iter()
        .find(|interface|interface.name == interface_name);
    // 該当するインターフェースがないときはエラー
    if result.is_none() {
        return Err(MyError::UnknownInterfaceError);
    }
    let interface = result.unwrap();

    println!("Inteface: {}", interface.name);

    // イーサネットフレームを受信するチャネル rx を取得
    if let Ethernet(_, mut rx) = datalink::channel(&interface, 
        Default::default())? {
        loop {
            // rx からイーサネットフレームを取り出して処理する
            let frame = rx.next()?;
            if let Some(packet) = EthernetPacket::new(frame) {
                ethernet_frame_handler(&packet);
            }
        }
    }

    Ok(())
}

// イーサネットフレームを処理する関数
fn ethernet_frame_handler(packet: &EthernetPacket) {
  // イーサタイプにより上にのるプロトコルの種類を確認
  match packet.get_ethertype() {
    EtherTypes::Ipv4 => {
      if let Some(ipv4) = Ipv4Packet::new(packet.payload()) {
        // TCP か UDP か ICMP のときのみ処理
        match ipv4.get_next_level_protocol() {
          IpNextHeaderProtocols::Tcp|IpNextHeaderProtocols::Udp => {
            tcp_udp_handler(ipv4);
          },
          IpNextHeaderProtocols::Icmp => {
            icmp_handler(ipv4);
          },
          _ => {
            if VERBOSE {
                println!("[{}]", ipv4.get_next_level_protocol());
            }
          },
        }
      }
    },
    _ => {
        if VERBOSE {
            println!("[{}]", packet.get_ethertype());
        }
    },
  }
}

// TCP と UDP を処理する関数
fn tcp_udp_handler(ipv4: Ipv4Packet) {
    if let Some(tcp) = TcpPacket::new(ipv4.payload()) {
        print_ip_port("[TCP]", ipv4.get_source(), tcp.get_source(), 
            ipv4.get_destination(), tcp.get_destination(), tcp.get_flags());
    } else if let Some(udp) = UdpPacket::new(ipv4.payload()) {
        print_ip_port("[UDP]", ipv4.get_source(), udp.get_source(), 
            ipv4.get_destination(), udp.get_destination(), 0);
    }
}

// IP と PORT を表示する関数
fn print_ip_port (tag: &str, 
    src_ip:Ipv4Addr, src_port:u16, dst_ip:Ipv4Addr, dst_port:u16, flags:u8) {
    print!("{} {}:{} -> {}:{}", tag, src_ip, src_port, dst_ip, dst_port);
    if flags != 0 {
        print!(" flags:{}", conv_flags(flags));
    }
    println!();
}

// ICMP を処理する関数
fn icmp_handler (ipv4: Ipv4Packet) {
    if let Some(icmp) = IcmpPacket::new(ipv4.payload()) {
        println!("[ICMP] {} -> {} type:{:?}, code:{:?} ", 
            ipv4.get_source(), ipv4.get_destination(),
            icmp.get_icmp_type(), icmp.get_icmp_code()
        );
    }
}

// TCPフラグを文字列化する関数
fn conv_flags (flags: u8) -> String {
    let mut r:Vec<&str> = vec![];
    if flags&TcpFlags::SYN != 0 {
        r.push("SYN");
    }
    if flags&TcpFlags::ACK != 0 {
        r.push("ACK");
    }
    if flags&TcpFlags::RST != 0 {
        r.push("RST");
    }
    if flags&TcpFlags::FIN != 0 {
        r.push("FIN");
    }
    if flags&TcpFlags::PSH != 0 {
        r.push("PSH");
    }
    if flags&TcpFlags::CWR != 0 {
        r.push("CWR");
    }
    if flags&TcpFlags::ECE != 0 {
        r.push("ECE");
    }
    if flags&TcpFlags::URG != 0 {
        r.push("URG");
    }
    r.join(",").to_string()
}

#[derive(thiserror::Error, Debug)]
enum MyError {
    #[error("Unknown Command")]
    UnknownCommandError,

    #[error("Too Few Argument")]
    TooFewArgumentError,

    #[error("Unknown Interface")]
    UnknownInterfaceError,

    #[error("IOError({0})")]
    IOError (#[from] std::io::Error),
}

手順等

  • NPCAP をダウンロードしインストールする
    https://npcap.com/dist/npcap-1.76.exe

  • コンパイルに Packet.lib が必要になるので SDK をダウンロードし、
    https://npcap.com/dist/npcap-sdk-1.13.zip
    この中の "Lib" を適切なフォルダ配下(例:C:\work\pnet)にコピーし、次のように環境変数 LIB を設定する
    set LIB=C:\work\pnet\Lib\x64

  • プロジェクトの作成
    cargo new sniff

  • cd sniff して src/main.rs の内容を上のコードで上書き

  • クレイトの設定
    cargo add pnet thiserror ipnetwork

  • コンパイル
    cargo build

実行例

  • 引数なしで実行
c:\work>sniff.exe
Usage: sniff.exe args
        interfaces ... display interfaces
        sniff interface ... sniff at interface

  • インターフェースの表示
c:\work>sniff.exe interfaces
Interfaces:
\Device\NPF_{8E67E254-B03D-4CF0-B734-2B940458YYYY}
        description: Qualcomm ABCDE 802.11ac Wireless Adapter
        mac: 64:6c:XX:XX:XX:XX
        ips: 192.168.111.7/24
\Device\NPF_{E26BF5DC-AFE0-48DC-9DE6-2552A068ZZZZ}
        description: VirtualBox Host-Only Ethernet Adapter
        mac: 0a:00:XX:XX:XX:XX
        ips: 192.168.56.111/24
…
  • スニフィングする際には使用するインターフェース名を指定する。
c:\work>sniff.exe sniff \Device\NPF_{8E67E254-B03D-4CF0-B734-2B940458YYYY}
Inteface: \Device\NPF_{8E67E254-B03D-4CF0-B734-2B940458YYYY}
[TCP] 64.38.119.27:443 -> 192.168.111.7:56642 flags:ACK
[TCP] 64.38.119.27:443 -> 192.168.111.7:56642 flags:ACK,FIN
[TCP] 192.168.111.7:56642 -> 64.38.119.27:443 flags:ACK
[TCP] 192.168.111.7:56289 -> 107.178.254.65:443 flags:ACK
[TCP] 107.178.254.65:443 -> 192.168.111.7:56289 flags:ACK
[TCP] 192.168.111.7:55240 -> 192.168.111.1:53 flags:FIN
[ICMP] 192.168.111.7 -> 183.181.99.39 type:IcmpType(8), code:IcmpCode(0)
[ICMP] 183.181.99.39 -> 192.168.111.7 type:IcmpType(0), code:IcmpCode(0)
[TCP] 192.168.111.1:53 -> 192.168.111.7:55240 flags:FIN
[TCP] 192.168.111.7:56477 -> 23.106.127.165:443 flags:ACK,FIN
^C ← Ctrl-C で終了

改造メモ

  • TCP / UDP のパケットの処理を変えたいときは tcp_udp_handler 関数を改造する。
  • ICMP の処理を変えたいときは icmp_handler 関数を改造する。
  • ARP とか IPv6 に対応したい場合は ethernet_frame_handler 関数の match 文を改造する。

以上。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?