Environment Setup Using Container

Sơ đồ mạng của lab có dạng như sau:

Dùng lệnh docker compose up -d để chạy các container. Kết quả sau khi chạy:

$  docker ps
CONTAINER ID   IMAGE                               COMMAND                  CREATED              STATUS              PORTS     NAMES
475609f1d671   handsonsecurity/seed-ubuntu:large   "/bin/sh -c /bin/bash"   About a minute ago   Up About a minute             seed-attacker
e05c5d32b66e   handsonsecurity/seed-ubuntu:large   "bash -c ' /etc/init…"   About a minute ago   Up About a minute             hostB-10.9.0.6
7d2a11f03dbb   handsonsecurity/seed-ubuntu:large   "bash -c ' /etc/init…"   About a minute ago   Up About a minute             hostA-10.9.0.5

Với địa chỉ IP của các host là:

  • Attacker: 10.9.0.1
  • Host A: 10.9.0.5
  • Host B: 10.9.0.6

About the Attacker Container

Attacker container mount thư mục /volumes vào thư mục ./src của hosting machine (máy chạy Docker container):

volumes:
		- ./src:/volumes

Chúng ta sẽ viết code và đặt ở trong thư mục này.

Container được gắn vào một virtual switch nên nó chỉ thấy các traffic của chính nó. Để một container thấy được traffic của các container khác nhằm mục đích sniffing thì ta cần dùng host mode:

network_mode: host

Khi một container ở trong host mode, IP của nó sẽ trùng với IP của hosting machine và nó còn thấy được tất cả các network interface của hosting machine.

Getting the Network Interface Name

Các container được kết nối với nhau thông qua network sau:

networks:
    net-10.9.0.0:
        name: net-10.9.0.0
        ipam:
            config:
                - subnet: 10.9.0.0/24

Attacker container sẽ có một interface với IP là 10.9.0.1. Chúng ta cần biết tên của interface này để viết script.

Tên interface là sự kết hợp của prefix br- và network ID:

$ docker exec -it seed-attacker ifconfig
 
br-88ab64b587ba: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.9.0.1  netmask 255.255.255.0  broadcast 10.9.0.255

Cũng có thể dùng lệnh sau để liệt kê các network:

$ docker network ls
 
NETWORK ID     NAME           DRIVER    SCOPE
88ab64b587ba   net-10.9.0.0   bridge    local

Task 1: Using Scapy to Sniff and Spoof Packets

Scapy là một thư viện của Python dùng để thao tác với các gói tin. Cụ thể hơn, nó cho phép chúng ta đánh hơi (sniffing) và giả mạo (spoofing) các gói tin.

Các yêu cầu của task:

  1. Capture các gói tin ICMP.
  2. Capture các gói tin TCP từ một địa chỉ IP cụ thể đến port 23.
  3. Capture các gói tin từ hoặc đến một subnet cụ thể chẳng hạn như 128.230.0.0/16 (ngoại trừ subnet được tạo giữa các container ở trên).

Task 1.1: Sniffing Packets

Task 1.1A: Sniffing ICMP Packets

Dùng Scapy để đánh hơi các gói tin ICMP như sau:

from scapy.all import *
 
def print_pkt(pkt):
	pkt.show()
	
pkt = sniff(iface='br-88ab64b587ba', filter='icmp', prn=print_pkt)

Với br-88ab64b587ba là network interface của attacker container.

Nếu ta muốn lắng nghe trên nhiều interface, ta có thể truyền vào tham số iface một mảng các tên interface:

iface=['br-88ab64b587ba', 'enp0s3']

Scapy sử dụng cú pháp của BPF (Berkeley Packet Filter) để lọc các gói tin. Một số ví dụ:

BPF filter exampleDescription
udp dst port not 53UDP not bound for port 53.
host 10.0 .0.1 && host 10.0 .0.2Traffic between these hosts.
tcp dst port 80 or 8080Packets to either of the specified TCP ports.

Chạy script trên ở trong attacker container:

$ docker exec -it seed-attacker bash
root@docker-desktop:/# cd volumes/
root@docker-desktop:/volumes# python3 ./imcp_sniff.py

Sau đó ping đến host A:

$ docker exec seed-attacker ping 10.9.0.5
PING 10.9.0.5 (10.9.0.5) 56(84) bytes of data.
64 bytes from 10.9.0.5: icmp_seq=1 ttl=64 time=0.062 m
...

Ta sẽ bắt được các gói tin ICMP ở phía attacker machine:

###[ Ethernet ]###
  dst       = 02:42:0a:09:00:05
  src       = 02:42:20:d6:4d:39
  type      = IPv4
###[ IP ]###
     version   = 4
     ihl       = 5
     tos       = 0x0
     len       = 84
     id        = 49940
     flags     = DF
     frag      = 0
     ttl       = 64
     proto     = icmp
     chksum    = 0x637d
     src       = 10.9.0.1
     dst       = 10.9.0.5
     \options   \
###[ ICMP ]###
        type      = echo-request
        code      = 0
        chksum    = 0xc505
        id        = 0x22
        seq       = 0x1
###[ Raw ]###
           load      = '\xae\x1f2f\x00\x00\x00\x00\x90~\x03\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'

Task 1.1B: Sniffing TCP Packets

Ta viết script như sau để bắt các gói tin TCP từ 10.9.0.5 (host A) đến port 23:

from scapy.all import *
 
def print_pkt(pkt):
	pkt.show()
	
pkt = sniff(iface='br-88ab64b587ba', filter='src 10.9.0.5 && dst port 23', prn=print_pkt)

Chạy script trên ở trong attack container.

Port 23 là port của Telnet, mở kết nối Telnet từ host A đến 10.9.0.6 (host B):

$ docker exec -it hostA-10.9.0.5 telnet 10.9.0.6
Trying 10.9.0.6...
Connected to 10.9.0.6.
Escape character is '^]'.
Ubuntu 20.04.1 LTS
807c88fd7ac7 login:

Ta sẽ capture được các gói tin TCP ở phía attacker container:

###[ Ethernet ]###
  dst       = 02:42:0a:09:00:06
  src       = 02:42:0a:09:00:05
  type      = IPv4
###[ IP ]###
     version   = 4
     ihl       = 5
     tos       = 0x10
     len       = 60
     id        = 2157
     flags     = DF
     frag      = 0
     ttl       = 64
     proto     = tcp
     chksum    = 0x1e23
     src       = 10.9.0.5
     dst       = 10.9.0.6
     \options   \
###[ TCP ]###
        sport     = 48812
        dport     = telnet
        seq       = 3192565319
        ack       = 0
        dataofs   = 10
        reserved  = 0
        flags     = S
        window    = 64240
        chksum    = 0x144b
        urgptr    = 0
        options   = [('MSS', 1460), ('SAckOK', b''), ('Timestamp', (568141210, 0)), ('NOP', None), ('WScale', 7)]

Info

Khi mở kết nối Telnet từ một địa chỉ IP khác 10.9.0.5 chẳng hạn như 10.9.0.6 (host B) hoặc 10.9.0.1 (attacker container) thì sẽ không bắt được gói tin TCP.

Task 1.1C: Sniffing a Subnet

Bắt các gói tin đi vào hoặc đi ra subnet 192.168.1.0/24:

from scapy.all import *
 
def print_pkt(pkt):
	pkt.show()
	
pkt = sniff(iface='br-88ab64b587ba', filter='src net 192.168.1.0/24 or dst net 192.168.1.0/24', prn=print_pkt)

Ping từ attacker container đến 192.168.1.1 (gateway router) nhưng không bắt được gói tin nào. Nếu chuyển filter thành src net 10.9.0.0/24 or dst net 10.9.0.0/24 thì vẫn bắt được các gói tin ICMP từ các host trong mạng 10.9.0.0/24.

Important

Không thể bắt các gói tin ở đường mạng khác với đường mạng của interface đang lắng nghe.

Sử dụng interface eth0 để lắng nghe traffic trên đường mạng 192.168.1.0/24:

from scapy.all import *
 
def print_pkt(pkt):
        pkt.show()
 
pkt = sniff(iface='eth0', filter='src net 192.168.1.0/24 or dst net 192.168.1.0/24', prn=print_pkt)

Ping từ attacker container đến gateway router một lần nữa thì bắt được các gói tin ICMP có dạng như sau:

###[ Ethernet ]###
  dst       = 5a:94:ef:e4:0c:dd
  src       = 3e:d3:69:51:28:3d
  type      = IPv4
###[ IP ]###
     version   = 4
     ihl       = 5
     tos       = 0x0
     len       = 84
     id        = 49925
     flags     = DF
     frag      = 0
     ttl       = 64
     proto     = icmp
     chksum    = 0xb44e
     src       = 192.168.65.3
     dst       = 192.168.1.1
     \options   \
###[ ICMP ]###
        type      = echo-request
        code      = 0
        chksum    = 0x5459
        id        = 0x61
        seq       = 0x1
###[ Raw ]###
           load      = '\x99#2f\x00\x00\x00\x00\x13\xe8\x05\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'

Với 192.168.65.3 là địa chỉ IP của attacker container trên interface eth0.

Task 1.2: Spoofing ICMP Packets

Scapy cho phép chúng ta xây dựng và gửi gói tin. Task này yêu cầu:

  1. Xây dựng một gói tin echo request (ICMP).
  2. Gửi gói tin echo request cho một host ở trong network.
  3. Dùng Wireshark để theo dõi gói tin echo request.

Xây dựng và gửi gói tin echo request bằng script như sau:

from scapy.all import *
 
ipPacket = IP()
ipPacket.dst = '10.9.0.5'
icmpPacket = ICMP()
packet = ipPacket/icmpPacket
send(packet)

Giải thích script trên:

  • Dòng ipPacket = IP tạo ra một instance của lớp IP đại diện cho một gói tin IP. Có thể dùng câu lệnh ls(ipPacket) hoặc ls(IP) để liệt kê các thuộc tính của lớp IP:

    version    : BitField  (4 bits)                  = ('4')
    ihl        : BitField  (4 bits)                  = ('None')
    tos        : XByteField                          = ('0')
    len        : ShortField                          = ('None')
    id         : ShortField                          = ('1')
    flags      : FlagsField                          = ('<Flag 0 ()>')
    frag       : BitField  (13 bits)                 = ('0')
    ttl        : ByteField                           = ('64')
    proto      : ByteEnumField                       = ('0')
    chksum     : XShortField                         = ('None')
    src        : SourceIPField                       = ('None')
    dst        : DestIPField                         = ('None')
    options    : PacketListField                     = ('[]')
  • Dòng ipPacket.dst = '10.9.0.5' thiết lập địa chỉ IP đích đến. Nếu không thiết lập, giá trị mặc định sẽ là '127.0.0.1'.

  • Dòng icmpPacket = ICMP() tạo ra một instance của lớp ICMP đại diện cho một gói tin ICMP. Loại gói tin ICMP mặc định là echo request.

  • Dòng packet = ipPacket/icmpPacket ghép ipPacketicmpPacket lại với nhau để tạo thành một gói tin ICMP hoàn chỉnh. Toán tử / đã được overload bởi lớp IP và được dùng để chồng các gói tin thuộc các tầng lại với nhau.

Sau khi chạy script thì ta bắt được gói tin echo-request và echo-reply bằng Scapy (do không có giao diện nên không thể dùng Wireshark) như sau:

root@docker-desktop:/volumes# python3 icmp_sniff.py
###[ Ethernet ]###
  dst       = 02:42:0a:09:00:05
  src       = 02:42:8d:86:fa:d3
  type      = IPv4
###[ IP ]###
     version   = 4
     ihl       = 5
     tos       = 0x0
     len       = 28
     id        = 1
     flags     =
     frag      = 0
     ttl       = 64
     proto     = icmp
     chksum    = 0x66c9
     src       = 10.9.0.1
     dst       = 10.9.0.5
     \options   \
###[ ICMP ]###
        type      = echo-request
        code      = 0
        chksum    = 0xf7ff
        id        = 0x0
        seq       = 0x0
 
###[ Ethernet ]###
  dst       = 02:42:8d:86:fa:d3
  src       = 02:42:0a:09:00:05
  type      = IPv4
###[ IP ]###
     version   = 4
     ihl       = 5
     tos       = 0x0
     len       = 28
     id        = 18465
     flags     =
     frag      = 0
     ttl       = 64
     proto     = icmp
     chksum    = 0x1ea9
     src       = 10.9.0.5
     dst       = 10.9.0.1
     \options   \
###[ ICMP ]###
        type      = echo-reply
        code      = 0
        chksum    = 0x0
        id        = 0x0
        seq       = 0x0

Có thể thấy, Scapy bắt được gói tin echo request và gói tin echo reply tương ứng.

Task 1.3: Traceroute

Trong task này, chúng ta sẽ sử dụng Scapy để tìm ra các router giữa host nguồn và host đích (tương tự như Traceroute).

Ý tưởng như sau:

  1. Xây dựng một gói tin ICMP với trường TTL (Time-To-Live) có giá trị là 1 và đích đến là host đích. Gói tin này sẽ được hủy bởi router đầu tiên và router đó cũng sẽ gửi lại cho ta gói tin ICMP error message. Khi đó, ta sẽ biết được địa chỉ IP của router đầu tiên trong đường đi từ host nguồn đến host đích.
  2. Kế tiếp, ta tăng TTL lên thành 2 và gửi lại gói tin để nhận địa chỉ IP của router thứ 2.
  3. Lặp lại quá trình này cho đến khi gói tin đến được host đích.

Dựa vào sơ đồ mạng của lab, ta thấy rằng giữa các host không có router nên trường TTL có thể có giá trị là 1. Xây dựng script như sau:

from scapy.all import *
 
ipPacket = IP()
ipPacket.dst = '10.9.0.5'
ipPacket.ttl = 1
icmpPacket = ICMP()
send(ipPacket/icmpPacket)

Chạy script trên trong attacker container và ta bắt được cái gói tin ICMP thông qua Scapy:

###[ Ethernet ]###
  dst       = 02:42:0a:09:00:05
  src       = 02:42:8d:86:fa:d3
  type      = IPv4
###[ IP ]###
     version   = 4
     ihl       = 5
     tos       = 0x0
     len       = 28
     id        = 1
     flags     =
     frag      = 0
     ttl       = 1
     proto     = icmp
     chksum    = 0xa5c9
     src       = 10.9.0.1
     dst       = 10.9.0.5
     \options   \
###[ ICMP ]###
        type      = echo-request
        code      = 0
        chksum    = 0xf7ff
        id        = 0x0
        seq       = 0x0
 
###[ Ethernet ]###
  dst       = 02:42:8d:86:fa:d3
  src       = 02:42:0a:09:00:05
  type      = IPv4
###[ IP ]###
     version   = 4
     ihl       = 5
     tos       = 0x0
     len       = 28
     id        = 13256
     flags     =
     frag      = 0
     ttl       = 64
     proto     = icmp
     chksum    = 0x3302
     src       = 10.9.0.5
     dst       = 10.9.0.1
     \options   \
###[ ICMP ]###
        type      = echo-reply
        code      = 0
        chksum    = 0x0
        id        = 0x0
        seq       = 0x0

Cũng có thể dùng Termshark (là một công cụ TUI tương tự như Wireshark):

root@docker-desktop:/# termshark -i br-88ab64b587ba

Sau khi chạy script thì công cụ hiển thị kết quả như sau:

Task 1.4: Sniffing and Spoofing

Task này yêu cầu ta viết script để gửi gói tin echo reply giả mạo bất cứ khi nào có một gói tin echo request được gửi từ các host trong LAN (trừ attacker container) đến một host nào đó, kể cả khi nó không tồn tại.

Có thể chạy các lệnh ping sau để thử nghiệm:

ping 1.2.3.4 # a non-existing host on the Internet
ping 10.9.0.99 # a non-existing host on the LAN
ping 8.8.8.8 # an existing host on the Internet

Hint

Đề bài gợi ý rằng ta cần hiểu về giao thức ARP và định tuyến để giải thích các kết quả.

1.2.3.4

Thử dùng host A ping đến 1.2.3.4, ta thu được các gói tin sau trong Termshark của attacker container:

Với 10.9.0.502:42:0a:09:00:05 lần lượt là địa chỉ IP và địa chỉ MAC được gán cho interface eth0 của host A:

$ docker exec -it hostA-10.9.0.5 ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.9.0.5  netmask 255.255.255.0  broadcast 10.9.0.255
        inet6 fe80::42:aff:fe09:5  prefixlen 64  scopeid 0x20<link>
        ether 02:42:0a:09:00:05  txqueuelen 0  (Ethernet)

Có thể thấy, host A có gửi một gói tin ARP request để phân giải địa chỉ IP 10.9.0.1 (của attacker container) thành địa chỉ MAC 02:42:4b:60:b2:90. Ngoài ra, các gói tin echo request đều được gửi đến địa chỉ 1.2.3.4 thông qua địa chỉ MAC của attacker container. Như vậy, attacker container đóng vai trò như là default gateway của host A. Kiểm tra bảng định tuyến của host A thì quả thật như vậy:

$ docker exec -it hostA-10.9.0.5 ip route
default via 10.9.0.1 dev eth0
10.9.0.0/24 dev eth0 proto kernel scope link src 10.9.0.5

Tuy nhiên, do IP 1.2.3.4 không tồn tại nên host A không nhận được gói tin echo reply nào.

Xây dựng script để lắng nghe và gửi gói tin echo reply giả mạo:

from scapy.all import *
from scapy.layers.inet import IP, ICMP
 
def spoof(pkt):
    src = pkt.getlayer(IP).src
    dst = pkt.getlayer(IP).dst
    type = pkt.getlayer(ICMP).type
    if(type == 8):
        print(f'{src} sent an echo request packet, spoofing...')
        ipPacket = IP()
        ipPacket.src = dst
        ipPacket.dst = src
        icmpPacket = ICMP()
        icmpPacket.type = 0
        packet = ipPacket/icmpPacket
        send(packet)
 
pkt = sniff(iface='br-88ab64b587ba', filter='icmp', prn=spoof)

Giải thích script trên:

  • Xây dựng gói tin IP với địa chỉ nguồn và đích bị hoán đổi.
  • Nếu gói tin là echo request (có ICMP type là 8) thì ta sẽ gửi gói tin echo reply (có ICMP type là 0).

Chạy script trên trong attacker container và thực hiện ping từ host A đến 1.2.3.4, ta thu được các gói tin sau trong Termshark của attacker container:

Có thể thấy, đã có các gói tin echo reply gửi lại cho host A.

Tuy nhiên, host A không xem các gói tin echo reply mà nó nhận được là hợp lệ. Output của câu lệnh ping:

$ docker exec -it hostA-10.9.0.5 ping 1.2.3.4
PING 1.2.3.4 (1.2.3.4) 56(84) bytes of data.
^C
--- 1.2.3.4 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3128ms

Important

Gói tin echo reply bắt buộc phải chứa payload nếu gói tin echo request có. Trích từ RFC792:

The data received in the echo message must be returned in the echo reply message.

Như vậy, ta cần sao chép phần payload từ gói tin echo request vào gói tin echo reply để tạo thành một gói tin ICMP hoàn chỉnh. Ngoài ra, giá trị idseq trong ICMP header của gói tin echo reply cũng phải khớp với gói tin echo request.

from scapy.all import *
from scapy.layers.inet import IP, ICMP
 
def spoof(pkt):
    src = pkt.getlayer(IP).src
    dst = pkt.getlayer(IP).dst
    type = pkt.getlayer(ICMP).type
    id = pkt.getlayer(ICMP).id
    seq = pkt.getlayer(ICMP).seq
    payload = pkt.getlayer(Raw).load
    if(type == 8):
        print(f'{src} sent an echo request packet, spoofing...')
        ipPacket = IP()
        ipPacket.src = dst
        ipPacket.dst = src
        icmpPacket = ICMP()
        icmpPacket.type = 0
        icmpPacket.id = id
        icmpPacket.seq = seq
        packet = ipPacket/icmpPacket/payload
        send(packet)
 
pkt = sniff(iface='br-88ab64b587ba', filter='icmp', prn=spoof)

Chạy lại script và ping từ host A đến 1.2.3.4, ta thu được traffic sau trong Termshark:

Output của câu lệnh ping cho thấy các gói tin echo reply giả mạo đã được host A chấp nhận:

$ docker exec -it hostA-10.9.0.5 ping 1.2.3.4
PING 1.2.3.4 (1.2.3.4) 56(84) bytes of data.
64 bytes from 1.2.3.4: icmp_seq=1 ttl=64 time=121 ms
64 bytes from 1.2.3.4: icmp_seq=2 ttl=64 time=50.9 ms
64 bytes from 1.2.3.4: icmp_seq=3 ttl=64 time=38.0 ms
64 bytes from 1.2.3.4: icmp_seq=4 ttl=64 time=36.8 ms

10.9.0.99

Dùng host A để ping đến 10.9.0.99, ta thu được các gói tin sau ở trong Termshark của attacker container:

Có thể thấy, host A liên tục gửi các gói tin ARP đến địa chỉ broadcast trong mạng nội bộ nhằm phân giải địa chỉ IP 10.9.0.99 nhưng không có phản hồi. Do không có địa chỉ MAC, gói tin ICMP sẽ không được gửi đi và do đó mà ta không thể bắt được ở trong Termshark.

Output của lệnh ping:

$ docker exec -it hostA-10.9.0.5 ping 10.9.0.99
PING 10.9.0.99 (10.9.0.99) 56(84) bytes of data.
From 10.9.0.5 icmp_seq=1 Destination Host Unreachable
From 10.9.0.5 icmp_seq=5 Destination Host Unreachable
From 10.9.0.5 icmp_seq=6 Destination Host Unreachable
From 10.9.0.5 icmp_seq=7 Destination Host Unreachable
From 10.9.0.5 icmp_seq=8 Destination Host Unreachable
From 10.9.0.5 icmp_seq=9 Destination Host Unreachable
^C
--- 10.9.0.99 ping statistics ---
12 packets transmitted, 0 received, +6 errors, 100% packet loss, time 11412ms

8.8.8.8

Dùng host A ping đến địa chỉ 8.8.8.8, ta thu được các gói tin echo request và echo reply ở trong Termshark của attacker container như sau:

Có thể thấy, địa chỉ MAC đích đến trong các gói tin ICMP vẫn là của attacker container (do nó là default gateway của host A).

Ta sử dụng script ở phần 1.2.3.4 để lắng nghe và gửi các gói tin echo reply giả mạo. Thu được traffic sau trong Termshark của attack container:

Có thể thấy, ứng với mỗi gói tin echo request thì có tới 2 gói tin echo reply.

Output của câu lệnh ping cho thấy host A nhận diện được gói tin echo reply bị trùng lặp:

$ docker exec -it hostA-10.9.0.5 ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=63 time=29.3 ms
64 bytes from 8.8.8.8: icmp_seq=1 ttl=64 time=176 ms (DUP!)
64 bytes from 8.8.8.8: icmp_seq=2 ttl=63 time=29.9 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=64 time=35.5 ms (DUP!)
64 bytes from 8.8.8.8: icmp_seq=3 ttl=63 time=29.8 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=64 time=47.6 ms (DUP!)
64 bytes from 8.8.8.8: icmp_seq=4 ttl=63 time=28.7 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=64 time=41.2 ms (DUP!)
^C
--- 8.8.8.8 ping statistics ---
4 packets transmitted, 4 received, +4 duplicates, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 28.747/52.270/176.026/47.204 ms

Task 2: Writing Programs to Sniff and Spoof Packets

Trong task này, ta sẽ viết các chương trình C để lắng nghe và làm giả gói tin.

Định nghĩa một số alias và một cấu trúc dữ liệu để lưu gói tin:

typedef unsigned char byte;
typedef unsigned short word;
typedef unsigned int dword;
typedef unsigned long qword;
typedef struct
{
    byte *data;
    size_t len;
} Packet;

Task 2.1: Writing Packet Sniffing Program

Để viết chương trình lắng nghe traffic mạng bằng C, ta có thể dùng thư viện pcap.

Task 2.1A: Understanding How a Sniffer Works

Task này yêu cầu viết chương trình để in ra địa chỉ IP nguồn và đích của mỗi gói tin bắt được.

Xây dựng hàm main() gọi đến hàm sniff() để lắng nghe các gói tin ICMP:

#include "sniffer.h"
#define FILTER "icmp"
#define PARSE_IP 0
#define PARSE_TCP 0
 
int main(int argc, char *argv[])
{
	char *dev = argv[1] != NULL ? argv[1] : "br-88ab64b587ba";
	sniff(dev, FILTER);
}
 
void sniff(char *if_name, char *filter)
{
	/* Open device */
	char errbuf[PCAP_ERRBUF_SIZE];
	pcap_t *handle = pcap_open_live(if_name, BUFSIZ, 1, 1000, errbuf);
 
	/* Compile and apply the filter */
	compile_and_apply_filter(handle, filter);
 
	/* Start sniffing */
	pcap_loop(handle, -1, parse_packet, NULL);
 
	/* Close the handle */
	pcap_close(handle);
}

Ta sẽ lắng nghe các gói tin ICMP ở promiscuous mode với read timeout là 1 giây. Interface mà ta lắng nghe được truyền vào thông qua đối số dòng lệnh. Nếu không có đối số, interface sẽ là br-88ab64b587ba.

Hàm compile_and_apply_filter() giúp biên dịch và áp dụng filter:

void compile_and_apply_filter(pcap_t *handle, char *filter_exp)
{
	struct bpf_program fp;
	bpf_u_int32 net;
	pcap_compile(handle, &fp, filter_exp, 0, net);
	if (pcap_setfilter(handle, &fp) != 0)
	{
		pcap_perror(handle, "Error:");
		exit(EXIT_FAILURE);
	}
}

Note

Tập tin sniffer.h chứa định nghĩa của các cấu trúc tương ứng với các thành phần trong gói tin.

Xây dựng callback để phân tách gói tin:

void parse_packet(byte *args, const struct pcap_pkthdr *header, const byte *packet)
{
	Packet p = {
		.data = (byte *)packet,
		.len = header->len};
	print_buff(p);
 
	/* Ethernet */
	const struct sniff_ethernet *ethernet = (struct sniff_ethernet *)(packet);
 
	/* IP */
	const struct sniff_ip *ip = (struct sniff_ip *)(packet + sizeof(struct sniff_ethernet));
	dword size_ip = parse_ip(ip);
}

Hàm parse_ip(ip) giúp phân tách và in ra các thông tin liên quan đến gói tin IP:

dword parse_ip(const struct sniff_ip *ip)
{
	dword size_ip = IP_HL(ip) * 4;
	if (size_ip >= 20 && PARSE_IP)
	{
		printf("IP header size: %u\n", size_ip);
		printf("Source IP: %s\n", ip_str(ip->ip_src.s_addr));
		printf("Dest IP: %s\n", ip_str(ip->ip_dst.s_addr));
	}
	return size_ip;
}

Thuộc tính ip->ip_srcip->ip_dst tương ứng với IP nguồn và IP đích của gói tin. Hai thuộc tính này có kiểu struct in_addr thuộc netinet/in.h, có định nghĩa như sau1:

struct in_addr {
    unsigned long s_addr;  // load with inet_aton()
};

Để chuyển giá trị long về chuỗi IP có dấu chấm phân cách, ta cần dùng hàm inet_ntoa() của netinet/in.h:

char *ip_str(dword addr)
{
    struct in_addr ip_addr = {.s_addr = addr};
    char *ip_addr_buff = inet_ntoa(ip_addr);
    char *ip = (char *)malloc(16);
    memcpy(ip, ip_addr_buff, 16);
    return ip;
}

Biên dịch và lắng nghe trên interface br-88ab64b587ba:

root@docker-desktop:/volumes# gcc -o sniffer sniffer.c -lpcap
root@docker-desktop:/volumes# ./sniffer br-88ab64b587ba

Ping từ host A đến 1.1.1.1, ta nhận được các output sau:

root@docker-desktop:/volumes# gcc -o sniffer sniffer.c -lpcap
root@docker-desktop:/volumes# ./sniffer br-88ab64b587ba
Packet captured!
Source IP: 10.9.0.5
Dest IP: 1.1.1.1
Packet captured!
Source IP: 1.1.1.1
Dest IP: 10.9.0.5
Packet captured!
Source IP: 10.9.0.5
Dest IP: 1.1.1.1
Packet captured!
Source IP: 1.1.1.1
Dest IP: 10.9.0.5
Packet captured!
Source IP: 10.9.0.5
Dest IP: 1.1.1.1
Packet captured!
Source IP: 1.1.1.1
Dest IP: 10.9.0.5

Task 2.1B: Writing Filters

Task này yêu cầu chúng ta viết các filter để capture các gói tin:

  • ICMP giữa hai host cụ thể.
  • TCP có port đích trong khoảng từ 10 đến 100.

Sử dụng filter sau để capture các gói tin ICMP giữa host A và host B:

ip proto 1 and ((ip src host 10.9.0.5 and ip dst host 10.9.0.6) or (ip src host 10.9.0.6 and ip dst host 10.9.0.5))

Chạy code ở Task 2.1A Understanding How a Sniffer Works và rồi ping từ host A đến host B, ta thu được các gói tin có địa chỉ IP đích và địa chỉ IP nguồn như sau:

root@docker-desktop:/volumes# gcc -o sniffer sniffer.c -lpcap
root@docker-desktop:/volumes# ./sniffer br-88ab64b587ba
Packet captured!
Source IP: 10.9.0.5
Dest IP: 10.9.0.6
Packet captured!
Source IP: 10.9.0.6
Dest IP: 10.9.0.5
Packet captured!
Source IP: 10.9.0.5
Dest IP: 10.9.0.6
Packet captured!
Source IP: 10.9.0.6
Dest IP: 10.9.0.5

Sau đó ping từ host B đến host A, ta cũng thu được các gói tin tương tự:

Packet captured!
Source IP: 10.9.0.6
Dest IP: 10.9.0.5
Packet captured!
Source IP: 10.9.0.5
Dest IP: 10.9.0.6
Packet captured!
Source IP: 10.9.0.6
Dest IP: 10.9.0.5
Packet captured!
Source IP: 10.9.0.5

Khi ping từ host A đến 1.1.1.1 thì chương trình không lắng nghe được.

Dùng filter sau để capture các gói tin TCP đến port trong khoảng 10 đến 100:

tcp and dst portrange 10-100

Hàm xử lý gói tin TCP có dạng như sau:

dword parse_tcp(const struct sniff_tcp *tcp)
{
	dword size_tcp = TH_OFF(tcp) * 4;
	if (size_tcp >= 20 && PARSE_TCP)
	{
		printf("TCP header size: %u\n", size_tcp);
		printf("Source port: %0x\n", tcp->th_sport);
		printf("Dest port: %0x\n", tcp->th_dport);
	}
	return size_tcp;
}

Gọi hàm này trong callback parse_packet() như sau:

void parse_packet(byte *args, const struct pcap_pkthdr *header, const byte *packet)
{
	Packet p = {
		.data = (byte *)packet,
		.len = header->len};
	print_buff(p);
 
	/* Ethernet */
	const struct sniff_ethernet *ethernet = (struct sniff_ethernet *)(packet);
 
	/* IP */
	const struct sniff_ip *ip = (struct sniff_ip *)(packet + sizeof(struct sniff_ethernet));
	dword size_ip = parse_ip(ip);
 
	/* TCP */
	const struct sniff_tcp *tcp = (struct sniff_tcp *)(packet + sizeof(struct sniff_ethernet) + size_ip);
	dword size_tcp = parse_tcp(tcp);
}

Ngoài ra, ta chỉ capture đúng một gói tin TCP duy nhất bằng cách chỉ định tham số thứ 2 của pcap_loop()1 như sau:

pcap_loop(handle, 1, sniff, NULL);

Khi mở kết nối Telnet từ host A đến host B:

$ docker exec -it hostA-10.9.0.5 telnet 10.9.0.6
Trying 10.9.0.6...
Connected to 10.9.0.6.
Escape character is '^]'.
Ubuntu 20.04.1 LTS
e05c5d32b66e login:

Ta nhận được kết quả như sau:

root@docker-desktop:/volumes# gcc -o sniffer sniffer.c -lpcap
root@docker-desktop:/volumes# ./sniffer br-88ab64b587ba
Packet captured!
IP header size: 20
Total length in IP: 15360
Source IP: 10.9.0.5
Dest IP: 10.9.0.6
TCP header size: 40
Source port: 90c5
Dest port: 1700

Với 1700 là dạng small endian của 0x0017 (giá trị thập phân là 23) còn 90c5 là dạng small endian của 0xc590 (giá trị thập phân là 50576).

Info

Các gói tin mạng sử dụng big endian.

Để chuyển đổi qua lại giữ thứ tự byte của host và thứ tự byte của gói tin mạng (network byte order), ta có thể sử dụng các hàm sau:

  • htonl(): chuyển số unsigned int từ thứ tự byte của host sang thứ tự byte của gói tin mạng.
  • ntohl(): ngược lại của htonl().
  • htons(): chuyển số unsigned short từ thứ tự byte của host sang thứ tự byte của gói tin mạng.
  • ntohs() ngược lại của htons().

Đối với các giá trị mà chỉ có 1 byte thì không cần thực hiện việc chuyển đổi.

Kiểm tra bằng Termshark, ta thấy quả thật có một gói tin TCP SYN gửi từ port 50576 của IP 10.9.0.5 đến port 23 của IP 10.9.0.6 như sau:

Thử host một HTTP server ở port 8000 trên attacker container:

$ docker exec -it seed-attacker python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Sau đó gửi một request bất kỳ từ host A đến attacker container:

$ docker exec -it hostA-10.9.0.5 curl http://10.9.0.1:8000/sniffer.c

Attacker container có nhận được request:

10.9.0.5 - - [02/May/2024 10:56:46] code 404, message File not found
10.9.0.5 - - [02/May/2024 10:56:46] "GET /sniffer.c HTTP/1.1" 404 -

Termshark cũng capture được quá trình bắt tay 3 bước và HTTP request:

Do chỉ lắng nghe các gói tin TCP gửi đến port trong khoảng 10-100 nên chương trình lắng nghe không capture được gói tin nào.

Task 2.1C: Sniffing Passwords

Sử dụng filter là tcp and (src port 23 or dst port 23) để lắng nghe tất cả các gói tin TCP gửi từ hoặc gửi đến port 23 của Telnet.

Chúng ta không dùng hai hàm xử lý gói tin IP và TCP:

#define PARSE_IP 0
#define PARSE_TCP 0

Thay vào đó, ta chỉ đọc nội dung có trong phần payload:

void parse_packet(byte *args, const struct pcap_pkthdr *header, const byte *packet)
{
	Packet p = {
		.data = (byte *)packet,
		.len = header->len};
	print_buff(p);
 
	/* Ethernet */
	const struct sniff_ethernet *ethernet = (struct sniff_ethernet *)(packet);
 
	/* IP */
	const struct sniff_ip *ip = (struct sniff_ip *)(packet + sizeof(struct sniff_ethernet));
	dword size_ip = parse_ip(ip);
 
	/* TCP */
	const struct sniff_tcp *tcp = (struct sniff_tcp *)(packet + sizeof(struct sniff_ethernet) + size_ip);
	dword size_tcp = parse_tcp(tcp);
 
	/* Payload */
	const char *payload = (byte *)(packet + sizeof(struct sniff_ethernet) + size_ip + size_tcp);
	printf("%s", payload);
}

Hàm trên sẽ liên tục in ra payload của các gói tin Telnet.

Chạy chương trình rồi mở kết nối Telnet từ host A đến host B:

$ docker exec -it hostA-10.9.0.5 telnet 10.9.0.6
Trying 10.9.0.6...
Connected to 10.9.0.6.
Escape character is '^]'.
Ubuntu 20.04.1 LTS
e05c5d32b66e login: seed
Password: 
Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.15.146.1-microsoft-standard-WSL2 x86_64)
 
 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage
 
This system has been minimized by removing packages and content that are
not required on a system that users do not log into.
 
To restore this content, you can run the 'unminimize' command.
Last login: Thu May  2 11:13:03 UTC 2024 from hostA-10.9.0.5.net-10.9.0.0 on pts/1
seed@e05c5d32b66e:~$ 

Output của chương trình:

root@docker-desktop:/volumes# ./sniffer br-88ab64b587ba
�������� ��!��"��'������ ��#��'�������!��"����#�� ����'���������� ��������Ubuntu 20.04.1 LTS
�e05c5d32b66e login: sseeeedd
Password: dees
Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.15.146.1-microsoft-standard-WSL2 x86_64)
 
 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage
 
This system has been minimized by removing packages and content that are
�not required on a system that users do not log into.
 
To restore this content, you can run the 'unminimize' command.
Last login: Thu May  2 11:13:03 UTC 2024 from hostA-10.9.0.5.net-10.9.0.0 on pts/1

Có thể thấy, ta thu được username và password. Đối với username, do giá trị được hiển thị lại cho người dùng nên ta thấy các ký tự bị lặp lại.

Task 2.2: Spoofing

Khi một người dùng thông thường gửi một gói tin, hệ điều hành không cho phép người dùng thiết lập giá trị cho đa số các trường trong các header của các giao thức chẳng hạn như TCP, UDP và IP. Người dùng chỉ có thể thiết lập giá trị cho một vài trường chẳng hạn như địa chỉ IP đích, port đích, … Tuy nhiên, nếu như người dùng có đặc quyền root thì có thể thiết lập giá trị cho bất kỳ trường nào trong header của gói tin.

Để làm được điều đó, ta cần sử dụng raw socket.

Task 2.2A: Write a Spoofing Program

Xây dựng chương trình để gửi gói tin sử dụng raw socket. Hàm main() của chương trình:

#define LAYER 2
 
int main()
{
    char *if_name = "br-88ab64b587ba";
    char *src = "10.9.0.1";
    char *dst = "10.9.0.5";
    char *dst_mac = "02:42:0a:09:00:05";
    char *ping_data = "Hello There!";
 
    if (LAYER == 2)
    {
        send_through_layer2_socket(if_name, dst, dst_mac, ping_data);
    }
    else if (LAYER == 3)
    {
        send_through_layer3_socket(src, dst, ping_data);
    }
}

Hàm send_through_layer2_socket() được dùng để gửi gói tin thông qua raw socket ở layer 2:

void send_through_layer2_socket(char *if_name, char *dst, char *dst_mac, char *ping_data)
{
    /* Open layer-2 raw socket*/
    int layer2_socket_descriptor = open_raw_socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
 
    /* Get layer 2 interface information */
    struct ifreq if_req = initialize_ifreq(if_name);
    int if_index = get_if_index(layer2_socket_descriptor, if_req);
    printf("Source interface index: %d\n", if_index);
    char *src_mac = get_if_mac(layer2_socket_descriptor, if_req);
    printf("Source MAC address: %s\n", src_mac);
    char *src = get_if_ip(layer2_socket_descriptor, if_req);
    printf("Source IP address: %s\n", src);
 
    /* Make ICMP payload */
    Packet payload = {
        .data = (byte *)ping_data,
        .len = strlen(ping_data)};
 
    /* Make ping packet */
    Packet ping_packet = make_ping_packet(payload, ICMP_ECHO, 1, 1);
 
    /* Make ip packet */
    Packet ip_packet = make_ip_packet(src, dst, IPPROTO_ICMP, ping_packet);
    print_buff(ip_packet);
 
    /* Make ethernet frame */
    Packet ethernet_frame = make_ethernet_frame(src_mac, dst_mac, ip_packet);
    print_buff(ethernet_frame);
 
    /* Send frame */
    send_frame(if_index, layer2_socket_descriptor, dst_mac, ethernet_frame);
}

Chạy chương trình để gửi gói tin echo request:

root@docker-desktop:/volumes# ./spoofer
Source interface index: 5
Source MAC address: 02:42:cc:5a:fd:37
Source IP address: 10.9.0.1
Packet data: 45 00 00 28 27 0f 00 00 40 01 3f af 0a 09 00 01 0a 09 00 05 08 00 95 11 00 00 00 00 48 65 6c 6c 6f 20 74 68 65 72 65 21 
Packet length: 40
Packet data: 02 42 0a 09 00 05 02 42 cc 5a fd 37 08 00 45 00 00 28 27 0f 00 00 40 01 3f af 0a 09 00 01 0a 09 00 05 08 00 95 11 00 00 00 00 48 65 6c 6c 6f 20 74 68 65 72 65 21 
Packet length: 54
Success: spoofed Ethernet frame sent

Termshark bắt được các gói tin như sau:

Task 2.2B: Spoof an ICMP Echo Request

Gán cứng địa chỉ IP nguồn là của host A và địa chỉ IP đích là 1.1.1.1 rồi sử dụng raw socket ở layer 3 để gửi gói tin:

#define LAYER 3
 
int main()
{
    char *src = "10.9.0.5";
    char *dst = "1.1.1.1";
    char *ping_data = "Hello There!";
 
    if (LAYER == 2)
    {
        send_through_layer2_socket(if_name, dst, dst_mac, ping_data);
    }
    else if (LAYER == 3)
    {
        send_through_layer3_socket(src, dst, ping_data);
    }
}

Hàm send_through_layer3_socket() có dạng như sau:

void send_through_layer3_socket(char *src, char *dst, char *ping_data)
{
    /* Open layer-3 raw socket */
    int layer3_socket_descriptor = open_raw_socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
 
    /* Create ICMP payload */
    Packet payload = {
        .data = (byte *)ping_data,
        .len = strlen(ping_data)};
 
    /* Create ping packet */
    Packet ping_packet = make_ping_packet(payload, ICMP_ECHO, 1, 1);
 
    /* Create ip packet */
    Packet ip_packet = make_ip_packet(src, dst, IPPROTO_ICMP, ping_packet);
    print_buff(ip_packet);
 
    /* Send packet */
    send_packet(layer3_socket_descriptor, dst, ip_packet);
}

Chạy chương trình:

root@docker-desktop:/volumes# ./spoofer
Packet data: 45 00 00 28 27 0f 00 00 40 01 47 b7 0a 09 00 05 01 01 01 01 08 00 95 11 00 00 00 00 48 65 6c 6c 6f 20 74 68 65 72 65 21 
Packet length: 40
Success: spoofed IP packet sent

Termshark bắt được gói tin echo reply như sau:

Note

Nếu gói tin không có các header của Ethernet, ta sẽ không capture được gói tin echo request ở trong Termshark hoặc Scapy

Can you set the IP packet length field to an arbitrary value, regardless of how big the actual packet is?

Hàm tạo gói tin IP:

Packet make_ip_packet(char *src, char *dst, byte proto, Packet payload)
{
    /* Create packet */
    Packet ip_packet = {
        .data = malloc(sizeof(struct iphdr) + payload.len),
        .len = sizeof(struct iphdr) + payload.len};
 
    /* Point to the IP header */
    struct iphdr *iph = (struct iphdr *)ip_packet.data;
 
    /* Set IP header fields */
    iph->version = 4;                                            // version
    iph->ihl = 5;                                                // IP header length (5 means 5 words, 1 word is 4 bytes, so it will be 5x4=20 bytes in total)
    iph->tos = 0;                                                // priority
    iph->tot_len = htons(ip_packet.len);                         // total length (includes header and data)
    iph->id = htons(9999);                                       // identification (anything is valid)
    iph->frag_off = 0;                                           // fragment offset (0  means no fragmentation)
    iph->ttl = 64;                                               // time to live
    iph->protocol = proto;                                       // protocol
    iph->check = 0;                                              // set to 0 for now (will be calculated later)
    iph->saddr = inet_addr(src);                                 // source IP address
    iph->daddr = inet_addr(dst);                                 // destination IP address
    iph->check = checksum(ip_packet.data, sizeof(struct iphdr)); // calculate checksum
 
    /* Copy packet payload */
    memcpy(&ip_packet.data[sizeof(struct iphdr)], payload.data, payload.len);
 
    return ip_packet;
}

Giả sử giá trị đúng của tot_len phải là 40. Có các trường hợp sau xảy ra:

Trường hợp đầu tiên, giá trị của tot_len nhỏ hơn 20: không gửi được gói tin echo request. Thay vào đó, sẽ có một gói tin IPv4 được gửi đi như sau:

Trường hợp thứ 2, giá trị của tot_len lớn hơn 40: có gói tin echo request được gửi đi nhưng không có gói tin echo reply trả về và Termshark thông báo “IPv4 total length exceeds packet length (40 bytes)“.

Trường hợp thứ 3, giá trị của tot_len nhỏ hơn 28: có gói tin echo request được gửi đi nhưng không có gói tin echo reply trả về và Termshark thông báo “Malformed Packet”.

Trường hợp thứ 4, giá trị của tot_len lớn hơn hoặc bằng 28 và nhỏ hơn 40: có gói tin echo request được gửi đi nhưng không có gói tin echo reply trả về.

Why do you need the root privilege to run the programs that use raw sockets? Where does the program fail if executed without the root privilege?

Do sử dụng đến những tính năng low-level nên ta cần dùng quyền root. Nếu không sử dụng thì sẽ gặp lỗi khi mở socket:

seed@docker-desktop:/volumes$ ./icmp_spoofer
Error: socket() failed: Operation not permitted
Error: ioctl() failed: Bad file descriptor
Source interface index: -1
Error: ioctl() failed: Bad file descriptor
Source MAC address: (null)
Error: ioctl() failed: Bad file descriptor
Source IP address: (null)
Segmentation fault

Task 2.3: Sniff and Then Spoof

Task này tương tự với task Task 1.4 Sniffing and Spoofing nhưng ta cần dùng C. Cụ thể hơn, ta cần kết hợp pcapraw socket.

Xây dựng hàm main() sử dụng pcap để lắng nghe traffic như sau:

int main(int argc, char *argv[])
{
    char *dev = "br-88ab64b587ba";
    char *filter = "icmp";
    char errbuf[PCAP_ERRBUF_SIZE];
    struct bpf_program fp;
    bpf_u_int32 mask;
 
    pcap_t *handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
 
    pcap_compile(handle, &fp, filter, 0, mask);
    if (pcap_setfilter(handle, &fp) != 0)
    {
        pcap_perror(handle, "Error:");
        exit(EXIT_FAILURE);
    }
 
    pcap_loop(handle, 1, sniff_then_spoof, dev);
}

Với dev là đối số truyền vào callback sniff_then_spoof().

Trong callback, đầu tiên ta sẽ tạo ra các con trỏ để trỏ đến các phân vùng gói tin tương ứng:

void sniff_then_spoof(byte *args, const struct pcap_pkthdr *header, const byte *raw_packet)
{
    printf("\nREQUEST PACKET: \n");
    Packet packet = {
        .data = (byte *)raw_packet,
        .len = header->len,
    };
    print_buff(packet);
 
    struct ether_header *eth = (struct ether_header *)(raw_packet);                                                                     // Ethernet header
    struct iphdr *iph = (struct iphdr *)(raw_packet + sizeof(struct ether_header));                                                     // IP header
    struct icmphdr *icmph = (struct icmphdr *)(raw_packet + sizeof(struct ether_header) + sizeof(struct iphdr));                        // ICMP header
    struct timeval *tsh = (struct timeval *)(raw_packet + sizeof(struct ether_header) + sizeof(struct iphdr) + sizeof(struct icmphdr)); // ICMP timestamps
	//...
}

Sau đó mở một raw socket ở layer 2 và lấy thông tin của interface:

int layer2_socket_descriptor = open_raw_socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP)); // Open layer 2 socket
struct ifreq if_req = initialize_ifreq(args);                                         // Get layer 2 interface information

Kế đến, lấy ra các thông tin quan trọng của gói tin echo request:

char *src_mac = get_if_mac(layer2_socket_descriptor, if_req); // src_mac is of the sniffing machine
char *dst_mac = mac_str(eth->ether_shost);                    // dst_mac is of the machine that sent the packet
char *src_ip = ip_str(iph->daddr);                            // src_ip is the destination ip in captured packet
char *dst_ip = ip_str(iph->saddr);                            // dst_ip is the source ip in captured packet
uint8_t icmp_type = icmph->type;
unsigned long icmp_payload_len = ntohs(iph->tot_len) - sizeof(struct iphdr) - sizeof(struct icmphdr) - sizeof(struct timeval);

Ta bỏ qua các gói tin ICMP không phải là echo request:

 if (icmp_type != ICMP_ECHO)
        return;

Xây dựng gói tin dựa trên các thông tin vừa lấy được ở trên:

printf("\nREPLY PACKET: \n");
/* Make ICMP payload */
Packet icmp_payload = make_icmp_payload(raw_packet, icmp_payload_len);
 
/* Make ping packet */
Packet ping_packet = make_ping_packet(icmp_payload, ICMP_ECHOREPLY, ntohs(icmph->un.echo.id), ntohs(icmph->un.echo.sequence));
 
/* Make ip packet */
Packet ip_packet = make_ip_packet(src_ip, dst_ip, IPPROTO_ICMP, ping_packet);
 
/* Make ethernet frame */
Packet ethernet_frame = make_ethernet_frame(src_mac, dst_mac, ip_packet);
print_buff(ethernet_frame);

Hàm make_icmp_payload() giúp tạo ra payload của gói tin ICMP:

Packet make_icmp_payload(const byte *raw_packet, uint32_t icmp_payload_len)
{
    byte *icmp_payload = malloc(icmp_payload_len);
    memcpy(
        icmp_payload,
        raw_packet + sizeof(struct ether_header) + sizeof(struct iphdr) + sizeof(struct icmphdr) + sizeof(struct timeval),
        icmp_payload_len);
 
    Packet payload = {
        .data = icmp_payload,
        .len = icmp_payload_len};
 
    return payload;
}

Cuối cùng, gửi gói tin thông qua raw socket ở layer 2:

/* Send packet */
int if_index = get_if_index(layer2_socket_descriptor, if_req);
send_frame(if_index, layer2_socket_descriptor, dst_mac, ethernet_frame);

Biên dịch:

root@docker-desktop:/# cd volumes/
root@docker-desktop:/volumes# gcc -o sniff_then_spoof.out *.c -lpcap

Ta sẽ biên dịch file sniffer.c (chứa các hàm để lắng nghe traffic), spoofer.c (chứa các hàm để tạo gói tin) và sniff_then_spoof.c (chứa các hàm trên).

Chạy chương trình:

root@docker-desktop:/volumes# ./sniff_then_spoof.out

Khi ping từ host A đến 1.2.3.4 (host không tồn tại), chương trình có output như sau:

REQUEST PACKET: 
Packet data: 02 42 a5 14 fb dc 02 42 0a 09 00 05 08 00 45 00 00 54 30 1d 40 00 40 01 fc 78 0a 09 00 05 01 02 03 04 08 00 5a 2a 00 bb 00 01 d2 65 36 66 00 00 00 00 d0 7a 05 00 00 00 00 00 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 
Packet length: 98
 
REPLY PACKET: 
Packet data: 02 42 0a 09 00 05 02 42 a5 14 fb dc 08 00 45 00 00 54 27 0f 00 00 40 01 45 87 01 02 03 04 0a 09 00 05 00 00 fa 1d 00 bb 00 01 d2 65 36 66 00 00 00 00 31 87 0c 00 00 00 00 00 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 
Packet length: 98
Success: spoofed Ethernet frame sent

Gói tin echo reply được host A chấp thuận:

PING 1.2.3.4 (1.2.3.4) 56(84) bytes of data.
64 bytes from 1.2.3.4: icmp_seq=1 ttl=64 time=0.087 ms
 
--- 1.2.3.4 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.087/0.087/0.087/0.000 ms

Output của Termshark chạy trên interface br-88ab64b587ba trong attacker container:

list
from outgoing([[SEED Lab - Sniffing and Spoofing]])
sort file.ctime asc

Resources

Footnotes

  1. tham khảo struct sockaddr_in, struct in_addr (ufrj.br)