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:

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

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
Data from M3 soft at 14:30:49.572890 -> b':0103000066404016\r\n'
Data from PLC at 14:30:49.617998 -> b':01034004D44700000000000020202020202020202020202
Data from M3 soft at 14:30:49.629034 -> b':01030000668040D6\r\n'
Data from PLC at 14:30:49.678148 -> b':010340000000000000000000000000000000000000000000
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
Data from M3 soft at 14:30:49.939575 -> b':0103000066404016\r\n'
Data from PLC at 14:30:49.997619 -> b':01034004D4470000000000002020202020202020202020200000
Data from M3 soft at 14:30:50.008626 -> b':01030000668040D6\r\n'
Data from PLC at 14:30:50.058750 -> b':01034000000000000000000000000000000000000000000000000000000
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.

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

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

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

def log(data, source):
    if enable_log:
        timestamp = str(
        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') 

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
    while True:
        #M3 soft
        if port1.in_waiting > 0:
            data_from_1_to_2 = port1.readline()
            log(data_from_1_to_2, portsAKAs[0])

            if data_from_1_to_2 == bytes(':0103000066004056\r\n', 'ascii'):
                IDrequested = True
                print("Identification requested by M3 soft")
        if port2.in_waiting > 0:
            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])

except KeyboardInterrupt:
    print("Ports closed.")

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.


  1. This comment has been removed by a blog administrator.

  2. Great work and thank you for sharing! Did you investigate the data further? My dream is to use an off-the shelf PLC and programm the microcontroller in C (without modifying the bootloader). I already tried to read out the firmware of the Atmega128 via SPI, but the interface seems to be disabled. I did not yet try parallel programming (but I assume that the lock bits are set).
    I also captured the serial data transferred when uploading the firmware. My initial guess was that a standard intel hex file would be transferred, but until now I do not understand the data.
    Do you have any idea/suggestion for me?

    1. example data:
      PC: :01108000C1004000F5CA0AF5CA0AF5CA0CF5CA0AF5CA16F7CA1602CB06F5CA180DCB081CCB1633CB03F5CA04F5CA03F5CA03F5CA05F5CA06F5CA05F5CA05F5CA04F5CA06F5CA0A6B
      PLC: :01108000C100406E
      PC: :01108000C14040F5CA0AF5CA04F5CA123ECB0AF5CA08F5CA0AF5CA08F5CA044CCB14F5CA06F5CA0CF5CA04F5CA06F5CA04F5CA06F5CA04F5CA04F5CA02F5CA02F5CA03F5CA0EF54D
      PLC: :01108000C140402E

      address: 01
      function: 10
      data (intel hex?): 8000C1004000F5CA0AF5CA0AF5CA0CF5CA0AF5CA16F7CA1602CB06F5CA180DCB081CCB1633CB03F5CA04F5CA03F5CA03F5CA05F5CA06F5CA05F5CA05F5CA04F5CA06F5CA
      checksum: 0A6B (or 6B only?)

      from hex file documentation: [Colon] [Data Size] [Start Address] [Record Type] [Data] [Checksum]
      data: 8000C1004000F5CA0AF5CA0AF5CA0CF5CA0AF5CA16F7CA1602CB06F5CA180DCB081CCB1633CB03F5CA04F5CA03F5CA03F5CA05F5CA06F5CA05F5CA05F5CA04F5CA06F5CA
      command/data size? 800
      start adress? 0C10(0? here or below)
      data+checksum: (0?)4000F5CA0AF5CA0AF5CA0CF5CA0AF5CA16F7CA1602CB06F5CA180DCB081CCB1633CB03F5CA04F5CA03F5CA03F5CA05F5CA06F5CA05F5CA05F5CA04F5CA06F5CA

    2. Hi mate. I didn't explore that path because I saw no sense in it. The best of this PLC is the firmware in it because programs can be designed easily with M3 soft. The hardware itself is very basic and not interesting for repurposing with a custom firmware. Anyway, if by any chance I'd like to reprogram the uC with my custom code then I'd see no sense in keeping the original bootloader.

    3. Hi, your are misinterpreting the comm. It's not a hex file send over serial comm, it's MODBUS ASCII communication protocol. The way of decoding the frames has nothing to be with the Intel hex format, check the wikipedia link I posted to understand the correct enconding.

    4. My only suggestion is that you reverse engineer the hardware and flash the uC with your code deleting the bootloader.

    5. Thank you for your suggestions. I agree that the original firmware is great, but sometimes I am/was limited by the cycle time. On my device, JTAG was enabled, so after soldering some wires I was able to extract the firmware and place my own. (Just a note: when comparing the extracted hex with the data sent over the serial it seems that they perfectly match. So the hex-file is really sent over the serial, with a little MODBUS overhead).

  3. Awesome, thanks for the details.. just acquired a crouzet m3 embedded in a bio research instrument .. and hope to make a cable to re program it for my hobbies.

    1. Question? If I'm using a cut off usb cable.. can I simply wire it to the appropriate Tx Rx Ground pins of the Mellenium3, or do I need some kind of conversion circuitry..UART? Any advice would be great..

    2. Hello Davis. No, you can't do it with a bare USB cable (by the way, USB has no Tx and Rx but D+ and D-). You need a USB to serial TTL cable, like the ones used to communicate with a microcontroller through UART. Regards.

    3. Fantastic. I ordered one today.. I kinda thought that was the case, as Arduino and such all have a UART between the usb and the uC. Really excited to play with this thing.

      Have played sucessfully with Modbus registers/RS485 on my Midnite Classic 150 Solar charge controllers for volts/kw/amps/kwh, and hope to perhaps use this PLC as a "smart" way to sense > trigger transfer switch(s) for several loads between on-grid / off-grid depending on battery state / solar available / time of day.. using Node MCU to put status up on cloud/tablet/phone.. and chicken coop doors/lights.. Thank you so much for your work here!

    4. You're welcome mate. I hope you achieve your goal since it looks like a cool and USEFUL application. I also hope your PLC works straight on with M3 soft so you can skip all this bullshit. My PLC was quite particular so I had to develop this stuff.

    5. hell yeah Pedro! Got my little $3. usb<>ch340<>ttl dongle in the mail 20minutes ago, and wired it up, success!! and unit was unlocked from the previous author, so easy wipe > update firmware & language using the software suite from crouzet.. YOU ARE THE MAN! Thank you for posting your project and guidance!

  4. Hi Pedro - Great work and thanks for sharing. I actually have Zelio PLC's, so didn't actually need to hack around the unit identity, however...

    I found bits of your program (especially the checksum parts) really helpful to migrate from a old C++ utility I used, to a Python based one. This program is used to publish values from the FBD function SLOUT (which I assume Crouzet has the same function) to MQTT.
    Would you be happy for me to publish this resulting program (with Acknowledgement to your works) on a open-source forum such as the community?

    Cheers - Glen

    1. Hello Glen. Glad to hear it was useful for you. You can publish this stuff wherever you like. Cheers.

  5. Hi Pedro - I have got myself a headace and you might just be the one who could solve it. Glad to see a post on serial communication to a Zelio that is not from 2015 :) So the thing is I have just put up solar panels on the roof and to control it I used an old zelio that I bought years ago. I had to build a tool PC with Win7 to get my USB to RS232 convert to work. Everything is basically working fine.

    But it would be cool to setup a datalogger to se how the controller performs. It is always a different picture when you see graphs of the performance. So in the drawer I found the Raspberry 3 I also bought years ago. Got it up and running Raspbian connected the USB to serial, found a small Python scrip that should read the port and nothing. I have to say I do not know that much of programming and basically nothing when it comes to serial communication coding. Tried Putty, nothing.

    So I have realized the zelio is slave and I have to send the right command to get the readings and this is where I get lost :)

    The communication spec is Baud 115200, DB 7, P Even, SB 1 (you know this)
    I want to read 8 integers in address 25-32. (and eventually log it but that is later)

    The following describe how to request the data:
    The Read Frame and the Response
    The read frame to be sent to the smart relay is as follows:
     Beginning delimiter: " : "
     Slave address: 0x01
     Read command: 0x03
     Data address: 0x00 00 FF xx
    xx is a number between 0x00 and 0x2F inclusively, corresponding to the address of the first data
    to read less 1.
     Number of bytes: 0xnn
    This is the number of data to read. Each value is made up of two bytes.
     Checksum: 0xcc
    This is the completed sum increased by 2, of the bytes between the slave address and the
    number of bytes.
     End delimiter: " CR " " LF "
    FBD Language Elements
    336 EIO0000002612 10/2017
    The smart relay response is structured as follows:
     Beginning delimiter: " : "
     Slave address: 0x01
     Read command: 0x03
     Number of bytes: 0xnn
     Data read: 0xd1H d1L d2H ... dnnL
    These are the 0xnn bytes read.
     Checksum: 0xcc
    This is the completed sum increased by 2, of the bytes between the slave address and the last
    of the data to read.
     End delimiter: " CR " " LF "
    Read 5 16-bit data from address 17:
     Hexadecimal frame before ASCII coding:
    " : " 01 03 00 00 FF 10 0A E4 " CR " " LF "
     Hexadecimal frame after ASCII coding:
    3A 30 31 30 33 30 30 30 30 46 46 31 30 30 41 45 34 0D 0A
     The response will be as follows if the five values equal 0:
    3A 30 31 30 33 30 41 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 46 33 0D 0A

    I think I have understood how to construct the command based on the description except the checksum.

    So question is how do I test this in a simple way (which too) to see if I can get the relay to answer?

    Help is really needed thanks!

    1. Hello Einar,

      first, thanks for posting all that info about the protocol. It may be useful for someone else.

      So, if understand correctly, you have 2 problems: How to calculate the checksum and how to create a python script that allows you to perform this communication, right?

      - Checksum:
      An explanation can be found in the chapter "Modbus Ascii" in Wikipedia.
      Also, in my script, the function modbusAsciiChecksum calculates it.

      You can do tests with this and check with many of the frames in my post if the calculus is performed ok.

      - Python serial comm:

      Python is a very easy language, but you still need some programming bases. For basic sending and receiving data there is a lot of info out there. Check this link, this is as simple as it can get, and I think it's almost spot on what you need.

      Take a look and tell me if you need help with some more specific question about python or checksum and I'll see if I can help.
      Good luck, cheers.

  6. Einar Ritterbusch is the name :)