Pages

Saturday, February 16, 2019

Hacking a Crouzet PLC (Zelio, Schneider)



I was given a Crouzet PLC, which turned out to be the same as other more popular brands like Zelio. Some time ago I successfully built a cable with a TTL-USB converter. I got the instructions from these links:

http://plc-blog.com.ua/cable-zelio-sr2cbl01
http://www.hexperiments.com/?page_id=11

However after establishing a good connection with the PC I couldn't do anything with it. I come up with a solution in this post.

The problem
My PLC is a Crouzet CD12 model 88970823. For some stupid reason this PLC with this particular model number is a custom one, made (or locked) for one specific customer. I am completely convinced it's the exact same one as the readily available 88970042, however because of the difference in model number, the programming software (M3 soft) says it is unsupported and so, it don't allow me to do any damn thing to this piece of shit. So that was the problem.



Fail attempts
I thought of several approaches to solve this. Starting for the simpler, I tried to hack the M3 soft program itself at first but there was no file that I could easily edit to add my model to the supported list. Moreover, I couldn't easily disassemble the code.

The hack
The second easiest thing I came up with was to develop a simple man-in-the-middle program that controls the communications through the COM port, so when the time came I could fake the identity of my PLC and make the computer believe it's a supported device. To do this, first I used the free tool "null modem emulator (com0com)" that allowed me to create a virtual pair of COM ports so I can connect M3 soft to my program. Then, I wrote a Python script that did the work and talk directly to the PLC.

That was one cool and fun approach btw. The diagram is like this:



Sniffing the communication
I programmed a sniffer in Python to log the traffic of the COM port, which also was a a man-in-the-middle but for then, it was a transparent logger. The COM parameters as baudrate, data bits... where found in the internet. To be exact, in the following link, which is the datasheet of a compatible HMI for the Zelio (which is the same one under another brand).

Baudrate: 115200
Data bits: 7
Parity: even
Stop bits: 1

https://euroec.by/assets/files/weintek/plc_connection_guide/Schneider_Zelio.pdf

Playing with the M3 soft I could see how the traffic flowed.


Data from M3 soft at 14:30:49.385069 -> b':010300006D00018E\r\n'
Data from PLC at 14:30:49.433699 -> b':01030100FB\r\n'
Data from M3 soft at 14:30:49.455736 -> b':011000006C00020E0073\r\n'
Data from PLC at 14:30:49.493760 -> b':011000006C000281\r\n'
Data from M3 soft at 14:30:49.506650 -> b':0103000066004056\r\n'
Data from PLC at 14:30:49.558686 ->':010340434431325330373032302020383839373038
323301000208040401001D07000032343632303031353036303730373039313902000200010C000
20201020C03F2AD\r\n'
Data from M3 soft at 14:30:49.572890 -> b':0103000066404016\r\n'
Data from PLC at 14:30:49.617998 -> b':01034004D44700000000000020202020202020202020202
000000000000000000000000000000000000000000020202020202020202020202000000000000
0000000009D\r\n'
Data from M3 soft at 14:30:49.629034 -> b':01030000668040D6\r\n'
Data from PLC at 14:30:49.678148 -> b':010340000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000BC\r\n'
Data from M3 soft at 14:30:49.689183 -> b':0103000066C004D2\r\n'
Data from PLC at 14:30:49.733299 -> b':01030400000000F8\r\n'
Data from M3 soft at 14:30:49.744307 -> b':011000006C00020F0072\r\n'
Data from PLC at 14:30:49.793846 -> b':011000006C000281\r\n'
Data from M3 soft at 14:30:49.804854 -> b':011000006C00020E0073\r\n'
Data from PLC at 14:30:49.853793 -> b':011000006C000281\r\n'
Data from M3 soft at 14:30:49.878726 -> b':0103000066004056\r\n'
Data from PLC at 14:30:49.917554 -> b':0103404344313253303730323020203838393730383233010002080
40401001D07000032343632303031353036303730373039313902000200010C00020201020C03F2AD\r\n'
Data from M3 soft at 14:30:49.939575 -> b':0103000066404016\r\n'
Data from PLC at 14:30:49.997619 -> b':01034004D4470000000000002020202020202020202020200000
00000000000000000000000000000000000000202020202020202020202020000000000000000000009D\r\n'
Data from M3 soft at 14:30:50.008626 -> b':01030000668040D6\r\n'
Data from PLC at 14:30:50.058750 -> b':01034000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000BC\r\n'
Data from M3 soft at 14:30:50.070752 -> b':0103000066C004D2\r\n'
Data from PLC at 14:30:50.112383 -> b':01030400000000F8\r\n'
Data from M3 soft at 14:30:50.123392 -> b':011000006C00020F0072\r\n'
Data from PLC at 14:30:50.171425 -> b':011000006C000281\r\n'

The communication seemed to use the protocol Modbus Ascii, sort of. It used the same frame format but with custom instructions. To make sure I was right, I check if the checksum was calculated as expected with that protocol. The algorithm can be found in Wikipedia.

https://en.wikipedia.org/wiki/Modbus

The checksum calculation matched the one in the frames I was sniffing so everything looked good.

Analysing the data
Apart from the communication format, I could not see anything interesting, so I kept playing with the M3 soft. Things got exciting when I pressed the option "Controller Diagnostics" and received this message:



That meant that when I pressed that button, the PLC sent the model number to the PC anyhow. The log of my sniffer showed lots of data, it wasn't clear where the number was nor how it was encoded.

I discarded some frames that where duplicated and after messing around for some time I found the damn think. I discovered that the number was send in ascii hex, so the model 88970823 looked like 3838393730383233! Do you see it?


Data from PLC at 14:47:43.559812 -> b':01034043443132533037303230202038383937303832330100
0208040401001D07000032343632303031353036303730373039313902000200010C00020201020C03F2AD\r\n'


Faking the identity
That being so, I needed to inject the right model number in there, which was 88970042 or 3838393730303432 in hex. Also, I needed to regenerate the checksum once the data is modified so the M3 soft didn't complain (I tried it).

I modified my man-in-the-middle script to look for the request from the PC to send the PLC identity. Then it catches the frame from the PLC, injects the fake model number, regenerates the checksum and sends the info to the PC. The script doesn't messes with the rest of the data in the frame.

#Crouzet Hacker. Pedro Fernandez. Feb-2019

import serial, time
from datetime import datetime

#PARAMS--------------------------------------
#              port1    port2
portsAKAs = ["M3 soft", "PLC"]
enable_log = False

def log(data, source):
    if enable_log:
        timestamp = str(datetime.now())
        if data:
            print("Data from " + source + " at " + timestamp[11::] + " -> " + str(data), end='\n')

def modbusAsciiChecksum(frame):
    address = frame[1:3]
    function = frame[3:5]
    data = frame[5::]

    add = 0
    add += int(address, 16)
    add += int(function, 16)

    for i in range(0, len(data), 2):
        add += int(data[i:i+2], 16)

    add = -add
    add &= 0xFF
    result = str(hex(add).upper())
    return result[2::]

def generateInjection(frame):
    frame_decoded = frame.decode()      
    frame_decoded = frame_decoded.replace("3838393730383233", "3838393730303432") #replace 88970823 by 88970042

    #regenerate checksum
    frame_decoded = frame_decoded[0:-4] #crop old checksum and /r/n
    frame_decoded += modbusAsciiChecksum(frame_decoded)
    return bytes(frame_decoded, 'ascii') 



#MAIN-----------------------------------------
print("Crouzet Hacker. Pedro Fernandez. Feb-2019")
print("To stop press Ctrl + C.\n")

print("Enter M3 soft COM number: ", end='')
port1num = input()
print("Enter PLC COM number: ", end='')
port2num = input()

#Port inits
port1 = serial.Serial(port="COM" + str(port1num), baudrate=115200, bytesize=7, parity='E', stopbits=1)
port2 = serial.Serial(port="COM" + str(port2num), baudrate=115200, bytesize=7, parity='E', stopbits=1)

print("Waiting for data...")

IDrequested = False
try:
    while True:
        #M3 soft
        if port1.in_waiting > 0:
            time.sleep(0.01)
            data_from_1_to_2 = port1.readline()
            log(data_from_1_to_2, portsAKAs[0])
            port2.write(data_from_1_to_2)

            if data_from_1_to_2 == bytes(':0103000066004056\r\n', 'ascii'):
                IDrequested = True
                print("Identification requested by M3 soft")
        #PLC
        if port2.in_waiting > 0:
            time.sleep(0.01)
            data_from_2_to_1 = port2.readline()
            if IDrequested:
                data_from_2_to_1 = generateInjection(data_from_2_to_1)
                IDrequested = False
                print("Fake ID injected")
            log(data_from_2_to_1, portsAKAs[1])
            port1.write(data_from_2_to_1)

except KeyboardInterrupt:
    port1.close()
    port2.close()
    print("Ports closed.")
input()

Success
Fortunately, I got it to work flawlessly. I tried to read and write to the PLC without any problem. I even updated the PLC firmware (this didn't modified the model number though).





Tell me in the comments if it helped or whatever.