Golangで行うポートスキャナ自作ではじめるペネトレーションテスト
はじめに
オライリーでポートスキャナ自作ではじめるペネトレーションテストという本が発売されました。
2章ではScapyを利用して実際にパケットを作成して、nmapのようなポートスキャナ自作します。
パケットのカプセル化などNWの仕組みから丁寧に解説されていてとても良書だと思います。
ただ筆者はPythonよりGolang派なので2章のプログラムをGolangに書き換えてみました。
※オリジナルはこちら
gopacket入門
gopacketはGolangでパケットを読み込んだり作ったりするためのライブラリです。
プログラムを作る前に必要なパッケージをインストールしておきます。
ubuntu 22.04で動作確認をしています。
$ sudo apt install -y gcc libpcap0.8 libpcap-dev
※Dockerimageを用意しているのでDocker環境があれば以下で動作できます。
imageをpullしたらレポジトリをcloneします。cloneしたフォルダをmountして起動してください。
$ docker pull sat0ken/go-port-scanner:latest
$ git clone https://github.com/sat0ken/go-port-scanner
$ cd go-port-scanner
$ docker run --privileged -it -v $(pwd):/work sat0ken/go-port-scanner bash
gopacketをインストールするとgopacket以下のサブパッケージに様々な関数やタイプが定義されています。
・layers: You'll probably use this every time. This contains of the logic built into gopacket for decoding packet protocols. Note that all
・example code below assumes that you have imported both gopacket and gopacket/layers.
・pcap: C bindings to use libpcap to read packets off the wire.
・pfring: C bindings to use PF_RING to read packets off the wire.
・afpacket: C bindings for Linux's AF_PACKET to read packets off the wire.
・tcpassembly: TCP stream reassembly
パケットを読み込んだり、キャプチャするためにはgopacket/pcap
を利用します。
tcpdumpやWiresharkで保存したファイルからパケットを読み込むときは、pcap.OpenOfflineを利用します。
if handle, err := pcap.OpenOffline("/path/to/my/file"); err != nil {
panic(err)
} else {
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
handlePacket(packet) // Do something with a packet here.
}
}
tcpdumpやWiresharkのようにリアルタイムでパケットを受信する場合は、pcap.OpenLiveを利用します。
OpenLive()の引数にeth0
などNWインターフェイスを指定すると、そのNWインターフェイスでパケットを受信します。
SetBPFFilter()を利用すると、合わせてフィルタも行えます。
if handle, err := pcap.OpenLive("eth0", 1600, true, pcap.BlockForever); err != nil {
panic(err)
} else if err := handle.SetBPFFilter("tcp and port 80"); err != nil { // optional
panic(err)
} else {
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
handlePacket(packet) // Do something with a packet here.
}
}
OpenLive()を呼んで取得したHandleに対して、WritePacketData()を呼ぶとパケットを送信することができます。
func (p *Handle) WritePacketData(data []byte) (err error)
パケットの受信や送信はこれでできるようになるので、次はパケットの作成です。
gopacketによるパケットの作成や解析
gopacketではgopacket/layers
に様々なパケットがタイプとして定義されているので、これを利用してパケットの作成や受信したパケットの解析が可能です。
ポートスキャナ自作本でも説明されているOSI参照モデルとパケットのカプセル化は皆さんもご存知でしょう。
L7→L4→L3→L2とパケットを作成して送信すると、受信側ではL2→L3→L4→L7と作るときの逆の順番で取り出されていきます。
gopacketでもこの仕組みは同じです。
例えばgopacketでL2、Ethernetのパケットを作るためには、layers.Ethernet
を利用します。
type Ethernet struct {
BaseLayer
SrcMAC, DstMAC net.HardwareAddr
EthernetType EthernetType
// Length is only set if a length field exists within this header. Ethernet
// headers follow two different standards, one that uses an EthernetType, the
// other which defines a length the follows with a LLC header (802.3). If the
// former is the case, we set EthernetType and Length stays 0. In the latter
// case, we set Length and EthernetType = EthernetTypeLLC.
Length uint16
}
layers.Ethernet
が持つフィールドのSrcMac
は送信元、自分のNWインターフェイスのMACアドレスです。
DstMac
は宛先IPアドレスのMACアドレスです。IPv4ではARPで取得しますね。
EthernetType
は次のL3レイヤのパケットタイプを指定します。
次のパケットがIPv4ならlayers.EthernetTypeIPv4
をセットしますし、IPv6ならlayers.EthernetTypeIPv6
をセットします。
EthernetType
はIEEEで以下のように定義されています。
IPv4は0800
なので、gopacketでもconstで定義されています。
const (
EthernetTypeIPv4 EthernetType = 0x0800
)
このように各レイヤごとにパケットデータを作成していきます。
パケットを作成したら送信する前に各レイヤごとのパケットデータをカプセル化して1つにする必要があります。
以下のようにgopacket.NewSerializeBuffer()
で作成したバッファに、gopacket.SerializeLayers()
で各レイヤのパケットを1つにまとめ、
buf.Bytes()
でbyteのパケットデータが作成されます。
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{}
gopacket.SerializeLayers(buf, opts,
&layers.Ethernet{},
&layers.IPv4{},
&layers.TCP{},
gopacket.Payload([]byte{1, 2, 3, 4}))
packetData := buf.Bytes()
パケットを読み込むときは読み込ませたいレイヤのタイプを指定すると、byteのパケットデータを各レイヤごとに読み込んでくれて
いい感じに構造体にセットしてくれます。
// Decode an ethernet packet
ethP := gopacket.NewPacket(p1, layers.LayerTypeEthernet, gopacket.Default)
// Decode an IPv6 header and everything it contains
ipP := gopacket.NewPacket(p2, layers.LayerTypeIPv6, gopacket.Default)
// Decode a TCP header and its payload
tcpP := gopacket.NewPacket(p3, layers.LayerTypeTCP, gopacket.Default)
PINGの作成
パケットの作成・送信・受信などgopacketの使い方を一通り説明したので実際にコードを書いて動かしてみましょう。
以下はpingコマンドと同じようにICMPのEcho Requestを送信するコードです。
パケットを作成する部分に着目しましょう。
Etherent, IPv4, ICMPとパケットを作成します。EthernetとIPv4の送信元のMACアドレスとIPアドレスは取得したNWインターフェイスの情報からセットします。
宛先のMACアドレスとIPアドレスはプログラムのコマンドライン引数からセットします。
send-icmp.go
package main
import (
"flag"
"fmt"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"log"
"net"
"strings"
)
type nwDevice struct {
macAddr net.HardwareAddr
ipv4Addr net.IP
}
func getInterface(ifname string) nwDevice {
netifs, _ := net.Interfaces()
for _, netif := range netifs {
if netif.Name == ifname {
addrs, _ := netif.Addrs()
for _, addr := range addrs {
if !strings.Contains(addr.String(), ":") && strings.Contains(addr.String(), ".") {
ip, _, _ := net.ParseCIDR(addr.String())
return nwDevice{
macAddr: netif.HardwareAddr,
ipv4Addr: ip,
}
}
}
}
}
return nwDevice{}
}
func parseMac(macaddr string) net.HardwareAddr {
parsedMac, _ := net.ParseMAC(macaddr)
return parsedMac
}
func main() {
// コマンド引数を受け取る
var iface = flag.String("i", "eth0", "Interface to read packets from")
var dstIp = flag.String("dst", "192.168.1.3", "dest ip addr")
var dstMac = flag.String("dstmac", "", "dest ip addr")
flag.Parse()
// 送信元のMACアドレスとIPアドレスを取得する
netif := getInterface(*iface)
// Ethernetのパケットを作成する
ethernet := &layers.Ethernet{
SrcMAC: netif.macAddr,
DstMAC: parseMac(*dstMac),
EthernetType: layers.EthernetTypeIPv4,
}
// IPヘッダを作成する
ip := &layers.IPv4{
Version: 4,
Flags: layers.IPv4DontFragment,
TTL: 64,
Protocol: layers.IPProtocolICMPv4,
SrcIP: netif.ipv4Addr,
DstIP: net.ParseIP(*dstIp),
}
// ICMP Echo Requestのパケットを作成する
icmp := &layers.ICMPv4{
TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0),
Id: 0,
Seq: 0,
}
packetbuf := gopacket.NewSerializeBuffer()
// パケットを作成する
err := gopacket.SerializeLayers(
packetbuf,
gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
},
ethernet,
ip,
icmp)
if err != nil {
log.Fatalf("create packet err : %v", err)
}
handle, err := pcap.OpenLive(*iface, 1600, true, pcap.BlockForever)
if err != nil {
log.Fatal(err)
}
// pingを送信
handle.WritePacketData(packetbuf.Bytes())
// pingを受信
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
icmpLayer := packet.Layer(layers.LayerTypeICMPv4)
reply := icmpLayer.(*layers.ICMPv4)
if reply.TypeCode == layers.ICMPv4TypeEchoReply {
fmt.Printf("recieve echo reply %+v\n", reply)
break
}
}
}
スクリプトでNWを作成して動作確認しましょう。
ポートスキャナ自作本ではdockerを利用していますが、今回はNetwork Namespaceを利用してNetworkを作成します。
$ sudo ./tools/netns1.sh
スクリプトを実行すると以下図のようなNWが作成されるので、確認してみましょう。
$ sudo ip netns exec host0 ip addr show host0-host1
42: host0-host1@if41: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 8a:1c:60:3f:22:78 brd ff:ff:ff:ff:ff:ff link-netns host1
inet 192.168.1.2/24 scope global host0-host1
valid_lft forever preferred_lft forever
inet6 fe80::881c:60ff:fe3f:2278/64 scope link
valid_lft forever preferred_lft forever
$ sudo ip netns exec host1 ip addr show host1-host0
41: host1-host0@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 22:14:dd:c8:61:41 brd ff:ff:ff:ff:ff:ff link-netns host0
inet 192.168.1.3/24 scope global host1-host0
valid_lft forever preferred_lft forever
inet6 fe80::2014:ddff:fec8:6141/64 scope link
valid_lft forever preferred_lft forever
host0からhost1にpingを実行してみると応答があります。
$ sudo ip netns exec host0 ping 192.168.1.3
PING 192.168.1.3 (192.168.1.3) 56(84) bytes of data.
64 bytes from 192.168.1.3: icmp_seq=1 ttl=64 time=0.083 ms
64 bytes from 192.168.1.3: icmp_seq=2 ttl=64 time=0.060 ms
^C
--- 192.168.1.3 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1024ms
rtt min/avg/max/mdev = 0.060/0.071/0.083/0.011 ms
プログラムをビルドしてpingを実行してみます。
$ ./tools/build.sh send-icmp.go
ARPを実装していないので、送信先のhost1のMACアドレスを変数にセットします。
$ export macaddr=$(sudo ip netns exec host0 ./tools/getmac.sh 192.168.1.3)
send-icmpを実行すると、応答が返ってきます。
$ sudo ip netns exec host0 ./send-icmp -i host0-host1 -dst 192.168.1.3 -dstmac $macaddr
recieve echo reply &{BaseLayer:{Contents:[0 0 0 0 0 0 0 0] Payload:[]} TypeCode:EchoReply Checksum:0 Id:0 Seq:0}
ポートスキャナの作成
tcp-syn-scan.go
はポートスキャナ自作本のtcp-syn-scan.pyをGoで書いたファイルです。
ListenしているポートにTCPのSYNパケットを送信するとSYNACKのパケットが返ってくるので、その結果によりポートが開いているかを確認することになります。
TCPの3ハンドシェイクをせずSYNACKの応答があるかないかでスキャンを終了するので、TCPSYNスキャンとなります。
tcp-syn-scan.go
package main
import (
"flag"
"fmt"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"log"
"net"
"strconv"
"strings"
)
type nwDevice struct {
macAddr net.HardwareAddr
ipv4Addr net.IP
}
func parseMac(macaddr string) net.HardwareAddr {
parsedMac, _ := net.ParseMAC(macaddr)
return parsedMac
}
func getInterface(ifname string) nwDevice {
netifs, _ := net.Interfaces()
for _, netif := range netifs {
if netif.Name == ifname {
addrs, _ := netif.Addrs()
for _, addr := range addrs {
if !strings.Contains(addr.String(), ":") && strings.Contains(addr.String(), ".") {
ip, _, _ := net.ParseCIDR(addr.String())
return nwDevice{
macAddr: netif.HardwareAddr,
ipv4Addr: ip,
}
}
}
}
}
return nwDevice{}
}
func main() {
var iface = flag.String("i", "eth0", "Interface to read packets from")
var dstIp = flag.String("dst", "127.0.0.1", "dest ip addr")
var dstMac = flag.String("dstmac", "00:00:00:00:00:00", "dest macc addr")
var dstPortStr = flag.String("p", "22", "dest port")
flag.Parse()
var srcPort = 20
dstPort, _ := strconv.Atoi(*dstPortStr)
netif := getInterface(*iface)
ethernet := &layers.Ethernet{
BaseLayer: layers.BaseLayer{},
SrcMAC: netif.macAddr,
DstMAC: parseMac(*dstMac),
EthernetType: layers.EthernetTypeIPv4,
}
ip := &layers.IPv4{
Version: 4,
Flags: layers.IPv4DontFragment,
TTL: 64,
Protocol: layers.IPProtocolTCP,
SrcIP: netif.ipv4Addr,
DstIP: net.ParseIP(*dstIp),
}
tcp := &layers.TCP{
SrcPort: layers.TCPPort(srcPort),
DstPort: layers.TCPPort(dstPort),
SYN: true,
}
packetbuf := gopacket.NewSerializeBuffer()
tcp.SetNetworkLayerForChecksum(ip)
err := gopacket.SerializeLayers(
packetbuf,
gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
},
ethernet,
ip,
tcp)
if err != nil {
log.Fatalf("create packet err : %v", err)
}
handle, err := pcap.OpenLive(*iface, 1600, true, pcap.BlockForever)
if err != nil {
log.Fatal(err)
}
defer handle.Close()
// SYNパケットを送信
handle.WritePacketData(packetbuf.Bytes())
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
// SYNパケットを受信
for packet := range packetSource.Packets() {
ipLayer := packet.Layer(layers.LayerTypeIPv4)
tcpLayer := packet.Layer(layers.LayerTypeTCP)
synackip := ipLayer.(*layers.IPv4)
synack := tcpLayer.(*layers.TCP)
if synackip.SrcIP.Equal(ip.DstIP) && synack.ACK {
fmt.Printf("TCP %d is open\n", dstPort)
break
} else {
fmt.Printf("TCP %d is close\n", dstPort)
break
}
}
}
次にポートスキャナ自作本ではTCP connectスキャンを自作しています。TCP connectスキャンは3ハンドシェイクの成立をもってポートが開いているかを
確認します。TCP SYNスキャンより時間がかかるので低速になります。
GoでTCPハンドシェイクをする場合、socketを利用することもできますが、netパッケージを利用するのがラクです。
net.Dial()
を呼ぶとTCP接続が実行されるので、その結果を持ってポートが開いているかを確認します。
tcp-connect-scan-by-net.go
package main
import (
"fmt"
"log"
"net"
"os"
)
func main() {
targetIP := os.Args[1]
targetPort := os.Args[2]
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", targetIP, targetPort))
if err != nil {
log.Fatalf("TCP %s is close", targetPort)
}
defer conn.Close()
fmt.Printf("TCP %s is open\n", targetPort)
}
gopacketを利用して、SYNACKに対してACKパケットを送り返してTCPハンドシェイクを成立させることも可能です。
pythonのコードと見比べながら、tcp-syn-scan.go
にコードを追加してみましょう。
コードができたら動作確認をしてみましょう。
NWは先程作成したのと同じ構成を使います。
$ sudo ./tools/netns1.sh
$ sudo ip netns ls
host1 (id: 1)
host0 (id: 0)
プログラムをビルドします。
$ ./tools/build.sh tcp-syn-scan.go
ARPを実装していないので、送信先のhost1のMACアドレスを変数にセットします。
$ export macaddr=$(sudo ip netns exec host0 ./tools/getmac.sh 192.168.1.3)
$ echo $macaddr
22:14:DD:C8:61:41
host1でncコマンドでTCPリッスンをしてポートを開きます。
host1のNetwork Namespaceでは8080ポートが開いています。
$ sudo ip netns exec host1 nc -kl 8080 &
$ sudo ip netns exec host1 ss -antu
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp LISTEN 0 1 0.0.0.0:8080 0.0.0.0:*
ビルドしたtcp-syn-scanを実行すると、SYNパケットに対してSYNACKパケットが返ってくるのでポートが開いていると確認できます。
$ sudo ip netns exec host0 ./tcp-syn-scan -i host0-host1 -dst 192.168.1.3 -dstmac $macaddr -p 8080
TCP 8080 is open
ARPスプーフィング
ポートスキャナ自作本ではAPRスプーフィングを紹介していますので、Goでもやってみます。
Ethernetからパケットを送信するときには、宛先IPアドレスのMACアドレスが必要です。
宛先IPアドレスの取得はIPv4であれば、ARPリクエストをブロードキャストで送信します。
- 「このIPアドレスを持っている人がいたらMACアドレス教えてください」(ARPリクエスト)
- 「そのIPアドレスは私です。私のMACアドレスはxx:xx:xx:xx:xxです」(ARPリプライ)
↑のようにARPリクエストがLAN内にブロードキャストされて、該当のIPアドレスを持つホストがARPリプライを返します。
こうして宛先IPアドレスのMACアドレスが取得されます。
この仕組みを応用して、自分のMACアドレスを他のIPアドレスに置き換えたARPリプライを送信することでなりすましが可能です。
これがARPスプーフィングになります。
ARPスプーフィングのプログラムを作成する前に、gopacketでARPリクエストを送るプログラムを作成してみます。
send-arp.go
package main
import (
"flag"
"fmt"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"log"
"net"
"strings"
)
type nwDevice struct {
macAddr net.HardwareAddr
ipv4Addr net.IP
}
func getInterface(ifname string) nwDevice {
netifs, _ := net.Interfaces()
for _, netif := range netifs {
if netif.Name == ifname {
addrs, _ := netif.Addrs()
for _, addr := range addrs {
if !strings.Contains(addr.String(), ":") && strings.Contains(addr.String(), ".") {
ip, _, _ := net.ParseCIDR(addr.String())
return nwDevice{
macAddr: netif.HardwareAddr,
ipv4Addr: ip,
}
}
}
}
}
return nwDevice{}
}
func parseMac(macaddr string) net.HardwareAddr {
parsedMac, _ := net.ParseMAC(macaddr)
return parsedMac
}
func main() {
var iface = flag.String("i", "eth0", "Interface to read packets from")
var dstIp = flag.String("dst", "127.0.0.1", "dest ip addr")
flag.Parse()
netif := getInterface(*iface)
ethernet := &layers.Ethernet{
SrcMAC: netif.macAddr,
DstMAC: parseMac("FF:FF:FF:FF:FF:FF"),
EthernetType: layers.EthernetTypeARP,
}
arpreq := &layers.ARP{
AddrType: layers.LinkTypeEthernet,
Protocol: layers.EthernetTypeIPv4,
HwAddressSize: 6,
ProtAddressSize: 4,
Operation: layers.ARPRequest,
SourceHwAddress: netif.macAddr,
SourceProtAddress: netif.ipv4Addr.To4(),
DstHwAddress: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
DstProtAddress: net.ParseIP(*dstIp).To4(),
}
packetbuf := gopacket.NewSerializeBuffer()
err := gopacket.SerializeLayers(
packetbuf,
gopacket.SerializeOptions{
FixLengths: true,
},
ethernet,
arpreq)
if err != nil {
log.Fatalf("create packet err : %v", err)
}
handle, err := pcap.OpenLive(*iface, 1600, true, pcap.BlockForever)
if err != nil {
log.Fatal(err)
}
// ARPリクエストを送信
handle.WritePacketData(packetbuf.Bytes())
// ARPリプライを受信
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
ethernetLayer := packet.Layer(layers.LayerTypeEthernet)
ethernetPacket := ethernetLayer.(*layers.Ethernet)
if ethernetPacket.EthernetType.LayerType() == layers.LayerTypeARP {
fmt.Printf("packet is %+v\n", packet)
break
}
}
}
これをビルドして実行すると、ARPリプライの応答があります。
$ sudo ip netns exec host0 ./send-arp -i host0-host1 -dst 192.168.1.3
packet is PACKET: 42 bytes, wire length 42 cap length 42 @ 2023-11-03 11:39:43.447143 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..28..] SrcMAC=22:14:dd:c8:61:41 DstMAC=ce:8c:ad:1d:46:a6 EthernetType=ARP Length=0}
- Layer 2 (28 bytes) = ARP {Contents=[..28..] Payload=[] AddrType=Ethernet Protocol=IPv4 HwAddressSize=6 ProtAddressSize=4 Operation=2 SourceHwAddress=[..6..] SourceProtAddress=[192, 168, 1, 3] DstHwAddress=[..6..] DstProtAddress=[192, 168, 1, 2]}
send-arp.goを参考に、ARPリプライを送りつけるプログラムを作成するとARPスプーフィングが可能になります。
今回は以下のようなNWを作成します。
host0からhost1に通信をする場合、ARPリクエスト要求が送られます。
この時attackerは偽のAPRリプライを送信することで、host0からhost1への通信を自分にも送ることができます。
実際に試してみましょう。
まずNWを作成します。
$ sudo ./tools/netns2.sh
$ sudo ip netns ls
attacker (id: 4)
host1 (id: 3)
host0 (id: 2)
プログラムが作成できたらビルドして実行します。
$ ./tools/build.sh arp-spoof.go
$ sudo ip netns exec attacker ./arp-spoof -i attacker-br0
host0からhost1へpingを送ると、Redirect Host
というメッセージが見えます。
これはARPスプーフィングによる偽のARPリプライにより、attackerにもpingが送信されてしまっているからです。
$ sudo ip netns exec host0 ping 192.168.1.3
PING 192.168.1.3 (192.168.1.3) 56(84) bytes of data.
64 bytes from 192.168.1.3: icmp_seq=1 ttl=64 time=0.111 ms
64 bytes from 192.168.1.3: icmp_seq=2 ttl=64 time=0.083 ms
From 192.168.1.4: icmp_seq=3 Redirect Host(New nexthop: 192.168.1.3)
64 bytes from 192.168.1.3: icmp_seq=3 ttl=64 time=0.223 ms
From 192.168.1.4: icmp_seq=4 Redirect Host(New nexthop: 192.168.1.3)
64 bytes from 192.168.1.3: icmp_seq=4 ttl=64 time=0.087 ms
From 192.168.1.4: icmp_seq=5 Redirect Host(New nexthop: 192.168.1.3)
64 bytes from 192.168.1.3: icmp_seq=5 ttl=64 time=0.156 ms
64 bytes from 192.168.1.3: icmp_seq=6 ttl=64 time=0.112 ms
From 192.168.1.4: icmp_seq=7 Redirect Host(New nexthop: 192.168.1.3)
64 bytes from 192.168.1.3: icmp_seq=7 ttl=64 time=0.122 ms
From 192.168.1.4: icmp_seq=8 Redirect Host(New nexthop: 192.168.1.3)
64 bytes from 192.168.1.3: icmp_seq=8 ttl=64 time=0.094 ms
^C
--- 192.168.1.3 ping statistics ---
8 packets transmitted, 8 received, 0% packet loss, time 7154ms
rtt min/avg/max/mdev = 0.083/0.123/0.223/0.043 ms
curlコマンドでどうなるか試してみましょう。
host1でhttpサーバのプログラムを起動します。
$ go build http-server.go
$ sudo ip netns exec host1 ./http-server
host0でcurlコマンドをhost1に実行すると、正常にHTTPレスポンスが返ってきます。
$ sudo ip netns exec host0 curl http://192.168.1.3:8080
hello
しかしこのHTTPリクエストはARPスプーフィングによりattackerにも送信されているので、キャプチャされてしまいます。
$ sudo ip netns exec attacker ./arp-spoof -i attacker-br0
start ARP spoofing...
ICMP is &{BaseLayer:{Contents:[5 1 188 215 192 168 1 3] Payload:[69 0 0 60 122 24 64 0 63 6 62 78 192 168 1 2 192 168 1 3 213 30 31 144 127 75 36 64 0 0 0 0 160 2 250 240 241 8 0 0 2 4 5 180 4 2 8 10 4 144 59 230 0 0 0 0 1 3 3 7]} TypeCode:Redirect(Host) Checksum:48343 Id:49320 Seq:259}
HTTP Request is GET / HTTP/1.1
Host: 192.168.1.3:8080
User-Agent: curl/7.81.0
Accept: */*
arp-spoof.goは穴埋めになっています。
どのようなメッセージを作成すれば、上記の結果のように騙すことができるでしょうか試してみてください。
おわりに
gopacketよりScapyの方が簡単にパケットを作れますが、面倒な分パケットの構造などよりgopacketの方が勉強になるかなと思います。
いろいろな言語で実装してみるのも楽しみの1つですよね。
ポートスキャナ自作本もぜひ購入してみてください。
Discussion
とありますが、順番に実行するなら、以下のようにexportが必要かもしれません。
ありがとうございます!修正しました!
細かいのですが、-wオプションをつけて
docker run
すると、cdせずにcloneしたディレクトリにいる状態でコンテナに入るので、混乱せずに作業できる気がしました。※wオプションをつけない状態だと、
/go
から始まるので、buildするときなどに若干混乱しました。