Hôm nay chúng ta sẽ cùng tìm hiểu về scapy và cách sử dụng scapy và netfilterqueue để tạo một con firewall vui vui.
Việc cài đặt và chạy sẽ được tiến hành trên ubuntu 20.04 brandnew.
# Scapy
[Scapy](https://scapy.net/) là một thư viện của python hỗ trợ việc decode, điều chỉnh, làm giả các gói tin và nhiều hơn thế nữa.
Việc cài đặt scapy cũng khá là đơn giải khi nó đã là thư viện của python:
```cmd
sudo python3 -m pip install scapy
```
Ta sẽ cài scapy với quyền root vì scapy cần quyền root mới có thể chạy được:

# IPtables ([Tham khảo](https://wiki.matbao.net/kb/bao-mat-may-chu-linux-huong-dan-cau-hinh-iptables-can-ban/#2-tao-rule-iptables))
IPtables là cấu hình firewall của linux được sử dụng rộng rãi hiện nay nhưng khá khó làm quen với những người mới tiếp cận linux. Ta có thể chỉnh cách gói tin được gửi đến và gửi đi, hoặc có thể cấu hình routing như một con router thực thụ.
Để kiểm tra các rule đang được thiết lập, ta dùng lệnh sau:
```cmd
iptables -L -v
```
Kết quả trả về giống như ảnh bên dưới:

Ở đây có tổng cộng 3 rule là `INPUT`, `FORWARD` và `OUTPUT`. Thứ tự chuỗi gói tin được thực hiện như sau:
```
INPUT --> FORWARD --> OUTPUT
```
Ngoài ra còn có các rule khác cho NAT nhưng trong giới hạn bài viết này, ta sẽ chủ yếu focus vào 2 rule chính là `INPUT` và `OUTPUT`. Rule `INPUT` sẽ được kích hoạt trước khi có một kết nối tới server của mình. Khi rule `INPUT` process xong thì sẽ được truyền tiếp tới rule `OUTPUT` để xử lý.
Để thiết lập 1 rule, giả sử rule đó chặn toàn bộ các kết nối tcp tại `INPUT`, ta có thể dùng lệnh sau:
```cmd
sudo iptables -A INPUT -p tcp -j DROP
```
Hoặc cho phép toàn bộ kết nối udp tại `INPUT`:
```cmd
sudo iptables -A INPUT -p udp -j ACCEPT
```
Chú thích:
- `-A` là append rule mới
- `-p` là protocol
- `-j` là jump tới việc thực hiện `DROP` hoặc `ACCEPT` hoặc các lệnh khác
Việc thiết lập cho `FORWARD` và `OUTPUT` cũng tương tự như vậy.
Để xóa các rule của `INPUT`, `FORWARD` và `OUTPUT`, ta có thể dùng lệnh sau:
```cmd
sudo iptables --flush
```
Để lưu các thiết lập hiện tại của IPtables, ta dùng lệnh sau:
```cmd
sudo iptables-save > "tên-file"
```
Chẳng hạn như sau:
```cmd
sudo iptables-save > rules.ip
```
Để load cấu hình từ file, ta dùng lệnh sau:
```cmd
sudo iptables-restore < "tên-file"
```
Chẳng hạn ta muốn load lại file `rules.ip` vừa nãy, ta có thể chạy:
```cmd
sudo iptables-restore < rules.ip
```
# NetfilterQueue ([Tham khảo](https://stackoverflow.com/a/63875318))
NetfilterQueue là một thư viện của python dùng để thiết lập việc truyền packet cũng như đưa packet vào script để ta có thể thực hiện việc điều chỉnh. Để cài đặt `NetfilterQueue`, trước tiên ta cần cài đặt các dependencies:
```cmd
sudo apt-get install build-essential python3-dev libnetfilter-queue-dev
```

Sau khi cài xong, ta sẽ tiến hành cài `NetfilterQueue` với quyền root:
```cmd
sudo python3 -m pip install NetfilterQueue
```
Khi đã cài đủ scapy và NetfilterQueue, ta sẽ mở python3 lên để kiểm tra xem python đã nhận diện chưa:
```python
from scapy.all import *
from netfilterqueue import NetfilterQueue
```
Không báo lỗi tức là ta đã cài đặt thành công:

# Firewall cơ bản
## Bắt gói tin
Để có thể bắt được gói tin khi có người dùng kết nối tới server, ta sẽ phải thiết lập IPtables để truyền gói tin đó vào cái NetfilterQueue. Do đó IPtables ta phải thiết lập như sau:
```cmd!
sudo iptables -A INPUT -j NFQUEUE --queue-num 0
```
Ta sẽ dùng `INPUT` cho tiện vì nó sẽ là thằng nhận packet đầu tiên. Tham số `-j` là để nhảy tới cái queue của Netfilter mà chúng ta đã cài đặt. Tham số `--queue-num` sẽ là cái queue mà ta muốn packet đi vào, ở đây ta chọn queue 0 (có thể chọn số khác tùy ý).
Tuy nhiên để thuận tiện, ta sẽ chạy lệnh đó trong script python:
```python
from scapy.all import *
from netfilterqueue import NetfilterQueue
import os
if __name__=='__main__':
if os.geteuid() != 0:
exit("Run with root permission!")
os.system('iptables -A INPUT -j NFQUEUE --queue-num 0')
```
Kết quả sẽ như thế này:

Bây giờ ta sẽ tạo một object NetfilterQueue, cho nó bind với queue number là 0 và callback là hàm `process_packet`, cuối cùng là cho nó tiến hành việc bind:
```python
try:
queue = NetfilterQueue()
queue.bind(0, process_packet)
queue.run()
except:
os.system('iptables --flush')
```
Khi thiết lập IPtables, chúng ta phải đảm bảo rằng script chạy vì mọi kết nối sau khi thiết lập IPtables sẽ được redirect trực tiếp vào script. Nếu script vì một lỗi nào đó mà crash, ta cần phải recover lại thiết lập của IPtables để kết nối có thể hoạt động bình thường, nếu không ta sẽ mất khả năng truy cập lại vào server.
Vì chúng ta chưa define hàm `process_packet` nên ta sẽ đi định nghĩa nó. Hàm `process_packet` sẽ nhận một tham số là packet từ NetfilterQueue, chúng ta sẽ accept và cho packet đó được lưu thông tiếp trên đường truyền:
```python
def process_packet(packet):
packet.accept()
```
Full script kết nối bình thường:
```python
from scapy.all import *
from netfilterqueue import NetfilterQueue
import os
def process_packet(packet):
packet.accept()
if __name__=='__main__':
if os.geteuid() != 0:
exit("Run with root permission!")
os.system('iptables -A INPUT -j NFQUEUE --queue-num 0')
queue = NetfilterQueue()
queue.bind(0, process_packet)
queue.run()
```
## In gói tin
Bây giờ ta sẽ muốn xem coi gói tin đó thực chất, hình hài của nó như thế nào, ta sẽ add thêm vào hàm `process_packet` lệnh để convert từ packet của NetfilterQueue thành packet của Scapy và cho script in ra gói tin đó:
```python
def process_packet(packet):
scapy_packet = IP(packet.get_payload())
print(scapy_packet.show())
packet.accept()
```
Và khi chạy script, ta sẽ thu được một gói tin tcp có hình dáng như thế này:

Nhìn vào một gói tin đó, ta thấy có 2 lớp là lớp `IP` và lớp `TCP`. Trong 2 lớp này sẽ có những thuộc tính con và ta hoàn toàn có thể thay đổi các thuộc tính con đó. Hãy cùng nhìn ngắm một gói tin khác là kết quả của việc thực hiện lệnh sau:

Gói tin bắt được:
```cmd
###[ IP ]###
version = 4
ihl = 5
tos = 0xc0
len = 61
id = 21230
flags =
frag = 0
ttl = 64
proto = icmp
chksum = 0x2910
src = 127.0.0.1
dst = 127.0.0.1
\options \
###[ ICMP ]###
type = dest-unreach
code = port-unreachable
chksum = 0xfb1d
reserved = 0
length = 0
nexthopmtu= 0
unused = ''
###[ IP in ICMP ]###
version = 4
ihl = 5
tos = 0x0
len = 33
id = 34648
flags = DF
frag = 0
ttl = 64
proto = udp
chksum = 0xb571
src = 127.0.0.1
dst = 127.0.0.1
\options \
###[ UDP in ICMP ]###
sport = 43003
dport = 1234
len = 13
chksum = 0x852a
###[ Raw ]###
load = 'asdf\n'
None
```
Nhìn vào ta thấy có tổng cộng 3 layer chính là `IP`, `ICMP` và `Raw`. Giả sử bây giờ ta muốn kiểm tra xem coi gói tin đó có layer `ICMP` hay không và nếu có thì in ra toàn bộ dữ liệu của layer `ICMP` cũng như in ra một vài thông tin của layer đó:
```python
def process_packet(packet):
scapy_packet = IP(packet.get_payload())
if scapy_packet.haslayer(ICMP):
print(scapy_packet[ICMP].show())
print(scapy_packet[ICMP].src)
print(scapy_packet[ICMP].dst)
print(scapy_packet[ICMP].sport)
print(scapy_packet[ICMP].dport)
packet.accept()
```
Lệnh `haslayer()` giúp kiểm tra coi trong gói tin đó có layer yêu cầu hay không. Truy cập layer của một gói tin thông qua chỉ mục index nhưng là tên của layer đó:
```python
scapy_packet[ICMP]
```
Để in ra dữ liệu của layer `ICMP` thì vẫn là dùng lệnh `show()`. Để truy cập thuộc tính con của layer thì ta dùng toán tử `.` để lấy dữ liệu:
```python
scapy_packet[ICMP].src
scapy_packet[ICMP].dst
scapy_packet[ICMP].sport
scapy_packet[ICMP].dport
```
Ta có output như này:

Với các layer khác thì việc truy cập các phần tử cũng tương tự!
## Sửa gói tin
Ta đã biết packet trông như thế nào trong scapy, giờ là lúc ta chỉnh một vài tham số cho nó! Các tham số ở phần `TCP` hay `UDP` này nọ ta phải chỉnh đúng thì mới giữ được kết nối, nếu không kết nối sẽ bị corrupt. Để đơn giản hơn, ta sẽ chỉ chỉnh sửa layer `Raw` tức là sửa dữ liệu người dùng nhập vào.
Giả sử ta tạo một server tcp trên cổng 1234 , mong muốn của ta là nếu client kết nối tới server và gửi dòng chữ `flag` thì script sẽ thay dòng chữ đó thành `Helo` và tiếp tục gửi tới server (lưu ý nhớ giữ nguyên độ dài của `load`). Ta sẽ tạo thêm một hàm là `modify_packet` dùng để thay đổi dữ liệu đó. Ta đã biết cách truy cập dữ liệu `load` của `Raw` như sau:
```python
scapy_packet[Raw].load
```
Vậy ta chỉ việc replace nó thôi:
```python
def modify_packet(scapy_packet):
scapy_packet[Raw].load = scapy_packet[Raw].load.replace(b'flag', b'Helo')
del scapy_packet[IP].len
del scapy_packet[IP].chksum
del scapy_packet[TCP].chksum
return scapy_packet
```
Lưu ý rằng ta phải thay đổi lại thuộc tính `len` và `chksum` trong layer `IP` và `TCP` sau khi đã thay đổi dữ liệu vì 2 thuộc tính này sẽ được kiểm tra lại để đảm bảo tính toàn vẹn của gói tin.
Ta thêm một chút điều chỉnh trong hàm `process_packet` để kiểm tra nếu packet có layer `TCP` và có dữ liệu truyền vào, tức có layer `Raw` thì sẽ nhảy vào hàm `modify_packet`. Sau khi modify xong, ta sẽ lấy packet scapy đã chỉnh sửa đó và gửi đi:
```python
def process_packet(packet):
scapy_packet = IP(packet.get_payload())
if scapy_packet.haslayer(TCP) and scapy_packet.haslayer(Raw):
scapy_packet = modify_packet(scapy_packet)
packet.set_payload(bytes(scapy_packet))
packet.accept()
```
Lệnh `set_payload` là lệnh của NetfilterQueue được dùng để cập nhật nội dung của gói tin. Nếu mọi thứ chạy tốt (không gõ sai hoặc thiếu code) thì khi client gửi dòng chữ `flag`, server sẽ nhận được chữ `Helo`:

Hy vọng bài viết này giúp các bạn có cái nhìn sơ qua về scapy, IPtables và NetfilterQueue!