2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ろうとるがPythonを扱う、、(その21:ScapyでPort Forward)

Posted at

ScapyでPort Forwardを実現

今回のPythonもScapyのネタ。Port Forwardを実現してみる。

ネットワーク

今回も、VirtualBoxのVM利用。
image.png
NAT環境下とみなし、Port Forwardを行うNATルーターが中央に位置する。左側がNATの外側、右側がNATの内側。左側のPCからNATルーターのWAN側IPアドレス(10.1.1.254)の特定TCPポート(ここでは、1111と2222)にアクセスすると、設定したNAT内側PCに転送する(1111:192.168.10.1、2222:192.168.10.2)ものである。

ソースコード

参考情報は下記であり、多くの部分を利用させて頂いた。

import

from scapy.all import *
import select
import os

ここは特記事項なし。

IPパケット再作成

# Recreate IP Packet #
def recreate_ip_packet(ip_pkt):
    # Removal Check-Sum
    if ip_pkt.haslayer(TCP):
        del ip_pkt.getlayer(TCP).chksum
    if ip_pkt.haslayer(UDP):
        del ip_pkt.getlayer(UDP).chksum
    if ip_pkt.haslayer(ICMP):
        del ip_pkt.getlayer(ICMP).chksum
    del ip_pkt.chksum

    result = IP(ip_pkt.build())
    return result

IPアドレス変更などによるIPパケット再作成。ここは、以前の記事「ろうとるがPythonを扱う、、(その20:Scapyで改ざん)」参照。

定義

### Main ###
# Inside Terminal Information #
in_ip_addr1 = '192.168.10.1'
in_ip_port1 = 1111
in_ip_addr2 = '192.168.10.2'
in_ip_port2 = 2222

# NAT Table List
nat_table1 = {}
nat_table2 = {}

# Transform List: IP address, Dest Port, NAT Table
trans_list = [[in_ip_addr1, in_ip_port1, nat_table1],\
              [in_ip_addr2, in_ip_port2, nat_table2]]

# NAT Outside & Inside Mac Address & IP Address
out_eth_mac = get_if_hwaddr('enp0s3')
out_eth_ip = get_if_addr('enp0s3')	# 10.1.1.254
in_eth_mac = get_if_hwaddr('enp0s8')
in_eth_ip = get_if_addr('enp0s8')	# 192.168.10.254
  • 転送先IPアドレスおよびポート
  • NATテーブル
  • NATルーターのMacアドレスおよびIPアドレス取得

ソケット定義、パケット受信

# Layer2 socket
out_sock = conf.L2socket(iface='enp0s3')
in_sock = conf.L2socket(iface='enp0s8')

# Not send RST in order not to disconnect from NAT router
os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP')

while True:
    # select()
    read_sockets, _, _ = select.select([out_sock, in_sock], [], [])

    for s in read_sockets:
        # Data receive
        p = s.recv()

        # Drop non TCP packet
        if not p or not p.haslayer(TCP):
            #print("Not TCP packet")
            continue
  • NAT外側・内側のソケット
  • TCPセッションを維持するため、NATルーターからRSTを送信しない
    • これについても、上述した参考情報を参照
  • selectによるパケット待受け
  • データ受信(TCP以外は捨てる(何もしない))

外側からのパケット処理

        # From Outside to Inside (Initiate from Outside)
        if p.dst == out_eth_mac:
            ip_pkt_out_to_in = p.getlayer(IP)
            found = 0
            for i in range(len(trans_list)):
                if ip_pkt_out_to_in.dport == trans_list[i][1]:
                    found = 1
                    # Keep Source IP and Source Port at NAT Table
                    trans_list[i][2][ip_pkt_out_to_in.src] = ip_pkt_out_to_in.sport
                    print(trans_list[i][2])
                    # Replace IP address with local IP adress
                    ip_pkt_out_to_in.dst = trans_list[i][0]
                    # Recreate IP address
                    pkt_to_ip_addr = recreate_ip_packet(ip_pkt_out_to_in)
                    print(pkt_to_ip_addr)
                    send(pkt_to_ip_addr, verbose=0)
                    break
            if found == 0:
                print("Closed Port: " + str(ip_pkt_out_to_in.dport))
  • 宛先MacアドレスがNATルーターの外側
  • IPパケットを取得(ip_pkt_out_to_int)
  • 受信したパケットの宛先ポートがNATテーブルに含まれている場合
    • 送信側IPアドレスと送信ポートをテーブルに格納
    • 宛先IPアドレスを転送先IPアドレスに置き換え
    • 前述した関数recreate_ip_packet()によりIPパケットを再生成(pkt_to_ip_addr)
    • 送信
  • 受信したパケットの宛先ポートがNATテーブルに含まれていない場合
    • ポートCloseのメッセージ表示

内側からのパケット処理

        # From Inside to Outside
        elif p.dst == in_eth_mac:
            ip_pkt_in_to_out = p.getlayer(IP)
            found = 0
            for i in range(len(trans_list)):
                if ip_pkt_in_to_out.src == trans_list[i][0]:
                    found = 1
                    # Get destination port from destination IP at NAT Table
                    dport = trans_list[i][2].get(ip_pkt_in_to_out.dst)
                    #print(dport)
                    if dport is None:
                        print("No data in NAT Table")
                        continue
                    # Replace IP address with send IP address
                    ip_pkt_in_to_out.src = out_eth_ip
                    # Recreate IP address
                    pkt_from_ip_addr = recreate_ip_packet(ip_pkt_in_to_out)
                    print(pkt_from_ip_addr)
                    send(pkt_from_ip_addr, verbose=0)
                    break
            if found == 0:
                print("Not allowed inside IP address")
  • 宛先MacアドレスがNATルーターの内側
  • IPパケットを取得(ip_pkt_in_to_out)
  • 送信先IPアドレスがNATテーブルに含まれている場合
    • 宛先ポートがテーブルに含まれているか確認
      • 内側起動のデータ通信はここでは考慮していない
    • 送信IPアドレスをNATルーター外側IPアドレスに置き換え
    • IPパケットを再生成(pkt_from_ip_addr)
    • 送信
  • 送信先IPアドレスがNATテーブルに含まれていない場合
    • 登録されていないメッセージを表示

全体

from scapy.all import *
import select
import os

# Recreate IP Packet #
def recreate_ip_packet(ip_pkt):
    # Removal Check-Sum
    if ip_pkt.haslayer(TCP):
        del ip_pkt.getlayer(TCP).chksum
    if ip_pkt.haslayer(UDP):
        del ip_pkt.getlayer(UDP).chksum
    if ip_pkt.haslayer(ICMP):
        del ip_pkt.getlayer(ICMP).chksum
    del ip_pkt.chksum

    result = IP(ip_pkt.build())
    return result

### Main ###
# Inside Terminal Information #
in_ip_addr1 = '192.168.10.1'
in_ip_port1 = 1111
in_ip_addr2 = '192.168.10.2'
in_ip_port2 = 2222

# NAT Table List
nat_table1 = {}
nat_table2 = {}

# Transform List: IP address, Dest Port, NAT Table
trans_list = [[in_ip_addr1, in_ip_port1, nat_table1],\
              [in_ip_addr2, in_ip_port2, nat_table2]]

# NAT Outside & Inside Mac Address & IP Address
out_eth_mac = get_if_hwaddr('enp0s3')
out_eth_ip = get_if_addr('enp0s3')	# 10.1.1.254
in_eth_mac = get_if_hwaddr('enp0s8')
in_eth_ip = get_if_addr('enp0s8')	# 192.168.10.254

# Layer2 socket
out_sock = conf.L2socket(iface='enp0s3')
in_sock = conf.L2socket(iface='enp0s8')

# Not send RST in order not to disconnect from NAT router
os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP')

while True:
    # select()
    read_sockets, _, _ = select.select([out_sock, in_sock], [], [])

    for s in read_sockets:
        # Data receive
        p = s.recv()

        # Drop non TCP packet
        if not p or not p.haslayer(TCP):
            #print("Not TCP packet")
            continue

        # From Outside to Inside (Initiate from Outside)
        if p.dst == out_eth_mac:
            ip_pkt_out_to_in = p.getlayer(IP)
            found = 0
            for i in range(len(trans_list)):
                if ip_pkt_out_to_in.dport == trans_list[i][1]:
                    found = 1
                    # Keep Source IP and Source Port at NAT Table
                    trans_list[i][2][ip_pkt_out_to_in.src] = ip_pkt_out_to_in.sport
                    print(trans_list[i][2])
                    # Replace IP address with local IP adress
                    ip_pkt_out_to_in.dst = trans_list[i][0]
                    # Recreate IP address
                    pkt_to_ip_addr = recreate_ip_packet(ip_pkt_out_to_in)
                    print(pkt_to_ip_addr)
                    send(pkt_to_ip_addr, verbose=0)
                    break
            if found == 0:
                print("Closed Port: " + str(ip_pkt_out_to_in.dport))

        # From Inside to Outside
        elif p.dst == in_eth_mac:
            ip_pkt_in_to_out = p.getlayer(IP)
            found = 0
            for i in range(len(trans_list)):
                if ip_pkt_in_to_out.src == trans_list[i][0]:
                    found = 1
                    # Get destination port from destination IP at NAT Table
                    dport = trans_list[i][2].get(ip_pkt_in_to_out.dst)
                    #print(dport)
                    if dport is None:
                        print("No data in NAT Table")
                        continue
                    # Replace IP address with send IP address
                    ip_pkt_in_to_out.src = out_eth_ip
                    # Recreate IP address
                    pkt_from_ip_addr = recreate_ip_packet(ip_pkt_in_to_out)
                    print(pkt_from_ip_addr)
                    send(pkt_from_ip_addr, verbose=0)
                    break
            if found == 0:
                print("Not allowed inside IP address")

検証

事前準備

Scapyによる転送を実施するため、NATルーターにてルーティングを無効化する。

$ sudo sysctl -w net.ipv4.ip_forward=0

Port 1111へのフォワード

netcatによる確認。Server(192.168.10.1)にて、ループバックを実現する下記コマンド実行。

$ rm -f /tmp/f; mkfifo /tmp/f
$ cat /tmp/f | nc -l -p 1111 > /tmp/f

Client(10.1.1.1)で、相手先としてNATルーター外側IPアドレスを指定した時の実行の様子。

$ nc 10.1.1.254 1111
test
test
123
123

NATルーターで、指定された宛先である192.168.10.1へPort Forwardされ、"test"と入力して"test"がエコーバックしている。このときのNATルーターのログ(ソースコード:print)の状況は下記となる。

{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 S
IP / TCP 10.1.1.254:1111 > 10.1.1.1:52600 SA
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 A
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 PA / Raw
IP / TCP 10.1.1.254:1111 > 10.1.1.1:52600 A
IP / TCP 10.1.1.254:1111 > 10.1.1.1:52600 PA / Raw
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 A
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 PA / Raw
IP / TCP 10.1.1.254:1111 > 10.1.1.1:52600 PA / Raw
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 A

Port 2222へのフォワード

httpサーバーによる確認。Server(192.168.10.2)にて、待受けポート2222を指定し、httpサーバーを起動。Client(10.1.1.1および10.1.1.2)のブラウザにてアクセス。NATルーターで、指定された宛先である192.168.10.2へPort Forwardされ、HTTPレスポンスが得られる。
BrowserAccess.png
NATルーターのログの状況は下記となる。

{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 S
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 SA
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 A
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 PA / Raw
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 A
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 PA / Raw
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 A / Raw
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 A
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 FPA / Raw
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 A
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 FA
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 A

{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 S
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 SA
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 A
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 PA / Raw
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 A
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 PA / Raw
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 A / Raw
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 A
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 FPA / Raw
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 A
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 FA
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 A

Port Forwardの対象ではないポートへのアクセス

Client(10.1.1.1)で下記実行。

$  nc 10.1.1.254 3333

NATルーターのログ。

Closed Port: 3333

NAT内側からのアクセス

未対象のIPアドレス(192.168.10.3)からの送信

$ echo | nc 10.1.1.1 3333

NATルーターのログ。

Not allowed inside IP address

未登録状況でのIPアドレス(192.168.10.2)からの送信

$ echo 123 | nc 10.1.1.1 1234

NATルーターのログ。

No data in NAT Table

EOF

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?