Skip to content

Commit

Permalink
Merge pull request #1 from Cyphysecurity/master
Browse files Browse the repository at this point in the history
Major updates
  • Loading branch information
lsalab authored Feb 18, 2022
2 parents 3634c64 + d33f678 commit b239c24
Show file tree
Hide file tree
Showing 36 changed files with 3,059 additions and 1,250 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# IDE workspace metadata
.vscode/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand All @@ -18,7 +21,7 @@ lib/
lib64/
parts/
sdist/
var/
#var/
wheels/
pip-wheel-metadata/
share/python-wheels/
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "nefics/IEC61850"]
path = nefics/IEC61850
url = [email protected]:lsalab/iec61850_mms_scapy.git
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Network Emulation Framework for Industrial Control Systems (NEFICS)
51 changes: 0 additions & 51 deletions autotopo.py

This file was deleted.

25 changes: 18 additions & 7 deletions commander.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,29 @@
from threading import Thread
import socket
from time import sleep
import scapy.all as scapy
import netifaces as nic
from prompt_toolkit.shortcuts import run_application
from PyInquirer.prompts.list import question
from netifaces import AF_LINK, AF_INET, ifaddresses, interfaces
from scapy.sendrecv import sr1
from scapy.layers.l2 import ARP
import ipaddress
from IEC104_Raw.dissector import APDU

# NEFICS imports
from nefics.IEC104.dissector import APDU
from iec104 import IEC104, get_command

IEC104_PORT = 2404

if __name__ == '__main__':
iface = scapy.get_working_if()
print('[+] Using ' + iface)
address = nic.ifaddresses(iface)[nic.AF_INET][0]
iface = run_application(
question(
'Choose an interface ',
choices=[f'{x:s} ({ifaddresses(x)[AF_INET][0]["addr"]:s})' for x in interfaces() if AF_INET in ifaddresses(x)]
)
)
iface = iface.split(' ')[0]
print('[+] Using ' + str(iface))
address = ifaddresses(iface)[AF_INET][0]
subnet = ipaddress.ip_network(address['addr'] + '/' + address['netmask'], strict=False)
nethosts = list(subnet.hosts())
print('[+] Searching for live hosts in {0:s} ...'.format(str(subnet)))
Expand All @@ -26,7 +37,7 @@ def arpscan(hosts: list):
for host in hosts:
if str(host) != address['addr']:
print('[-] Trying {0:s} ...\r'.format(str(host)), end='')
response = scapy.sr1(scapy.ARP(op=0x1, psrc=address['addr'], pdst=str(host)), iface=iface, retry=0, timeout=1, verbose=0)
response = sr1(ARP(op=0x1, psrc=address['addr'], pdst=str(host)), retry=0, timeout=1, verbose=0)
if response is not None and response.haslayer('ARP') and response['ARP'].op == 0x2:
print(' [!] {0:s} is alive'.format(str(host)))
alive.append(str(host))
Expand Down
12 changes: 12 additions & 0 deletions conf/Load.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"module": "simplepowergrid",
"class": "Load",
"guid": 3,
"in": [
2
],
"out": [],
"parameters": {
"load": 12.5
}
}
12 changes: 12 additions & 0 deletions conf/Source.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"module": "simplepowergrid",
"class": "Source",
"guid": 1,
"in": [],
"out": [
2
],
"parameters": {
"voltage": 526315.79
}
}
19 changes: 19 additions & 0 deletions conf/Transmission.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"module": "simplepowergrid",
"class": "Transmission",
"guid": 2,
"in": [
1
],
"out": [
3
],
"parameters": {
"loads": [
0.394737,
0.394737,
0.394737
],
"state": 7
}
}
45 changes: 45 additions & 0 deletions conf/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"devices": [
{
"module": "simplepowergrid",
"class": "Source",
"guid": 1,
"in": [],
"out": [
2
],
"parameters": {
"voltage": 526315.79
}
},
{
"module": "simplepowergrid",
"class": "Transmission",
"guid": 2,
"in": [
1
],
"out": [
3
],
"parameters": {
"loads": [
0.394737,
0.394737,
0.394737
],
"state": 7
}
},
{
"module": "simplepowergrid",
"class": "Load",
"guid": 3,
"in": [2],
"out": [],
"parameters": {
"load": 12.5
}
}
]
}
98 changes: 98 additions & 0 deletions iec104_poller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env python3

from nefics.IEC104.const import DPI_ENUM
import sys
from netaddr import valid_ipv4
from types import FrameType
from socket import AF_INET, IPPROTO_TCP, SOCK_STREAM, socket, timeout

# NEFICS imports
from nefics.IEC104.dissector import APDU, APCI

IEC104_PORT = 2404
IEC104_T1 = 15
BUFFER_SIZE = 65536

IOA_ADDR_MAP = {
1001: 'Voltage',
1002: 'Current'
}

class IEC104Poller(object):

def __init__(self, address:str):
super().__init__()
assert valid_ipv4(address)
self._terminate = False
self._sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
self._sock.settimeout(IEC104_T1)
try:
self._sock.connect((address, IEC104_PORT))
except timeout:
print('[!] Socket connection timeout')
sys.exit()
print('[+] Connection established')

def terminate(self, signum:int, stack_frame:FrameType):
self._terminate = True

def loop(self):
try:
print('[*] Sending STARTDT U-Frame ... ', end='')
apdu = APDU()/APCI(ApduLen=4, Type=0x03, UType=0x01)
self._sock.send(apdu.build())
data = self._sock.recv(BUFFER_SIZE)
apdu = APDU(data)
if apdu['APCI'].Type != 0x03 or apdu['APCI'].UType != 0x02:
print(f'ERROR\r\n[!] Unexpected Frame: {repr(apdu)}')
print('Confirmed')
while not self._terminate:
data = self._sock.recv(BUFFER_SIZE)
apdu = APDU(data)
if apdu['ASDU'].TypeId == 36:
print(f"[+] Received type 36 ASDU :: [{IOA_ADDR_MAP[apdu['IOA36'].IOA]}] Value: {apdu['IOA36'].Value}")
elif apdu['ASDU'].TypeId == 3:
print(f"[+] Received type 3 ASDU :: [Breaker status] Breaker ID: {apdu['IOA3'].IOA} Status: {DPI_ENUM[apdu['DIQ'].DPI]} (0x{apdu['DIQ'].DPI:02x})")
else:
print(f'[!] Received an unknown ASDU :: {repr(apdu)}')
print('[*] Sending TESTFR U-Frame ... ', end='')
apdu = APDU()/APCI(ApduLen=4, Type=0x03, UType=0x10)
self._sock.send(apdu.build())
data = self._sock.recv(BUFFER_SIZE)
apdu = APDU(data)
if apdu['APCI'].Type != 0x03 or apdu['APCI'].UType != 0x20:
print(f'FATAL\r\n[!] Unexpected frame: {repr(apdu)}')
self._sock.close()
sys.exit()
print('Confirmed')
print('[*] Sending STOPDT U-Frame ... ')
apdu = APDU()/APCI(ApduLen=4, Type=0x03, UType=0x04)
self._sock.send(apdu.build())
data = self._sock.recv(BUFFER_SIZE)
apdu = APDU(data)
while apdu['APCI'].Type != 0x03 or apdu['APCI'].UType != 0x08:
print('[!] Received pending Frame:', repr(apdu))
data = self._sock.recv(BUFFER_SIZE)
apdu = APDU(data)
print('[*] STOPDT confirmed')
print('[*] Closing connection ...')
self._sock.close()
except timeout:
print('Socket timeout')
sys.exit()


if __name__ == '__main__':
import argparse
import signal
aparser = argparse.ArgumentParser(description='IEC 60870-4-104 device poller')
aparser.add_argument('address', action='store', type=str, metavar='IPv4_ADDRESS')
args = aparser.parse_args()
try:
poller = IEC104Poller(args.address)
except AssertionError:
print(f'Invalid IPv4 address: "{args.address}"')
sys.exit()
signal.signal(signal.SIGINT, poller.terminate)
signal.signal(signal.SIGTERM, poller.terminate)
poller.loop()
37 changes: 0 additions & 37 deletions launch_load.py

This file was deleted.

36 changes: 0 additions & 36 deletions launch_src.py

This file was deleted.

Loading

0 comments on commit b239c24

Please sign in to comment.