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.

Beware this script needs module "pyserial" to run (not module "serial").

#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.

*Update for clarification

- COM ports configuration

If, for example, your physical USB-TTL adapter is COM1 and you have the following bridge in com0com:


Then, you'll need to set M3 Soft connection to COM4, then in the script set COM5 for M3 Soft and COM1 for PLC.



33 comments:

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

    ReplyDelete
  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?

    ReplyDelete
    Replies
    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

      Delete
    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.

      Delete
    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.

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

      Delete
    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).

      Delete
  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.

    ReplyDelete
    Replies
    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..

      Delete
    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.

      Delete
    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!

      Delete
    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.

      Delete
    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!

      Delete
  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 Openhab.org community?

    Cheers - Glen

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

      Delete
  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 "
    Example
    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!

    ReplyDelete
    Replies
    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. https://en.wikipedia.org/wiki/Modbus
      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.
      https://stackoverflow.com/questions/16701401/python-and-serial-how-to-send-a-message-and-receive-an-answer

      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.

      Delete
  6. Einar Ritterbusch is the name :)

    ReplyDelete
  7. Hi Pedro, Thanks for writing this nice Python program. With small modifications, I was able to interface M3 Soft with a XD26S 24VDC which also had a custom model number. M3 Soft is able to read every bit of information from the PLC including program data. I was able to replace the model name and model number on PLC itself by overwriting 20 bytes at address 0x6600 so that it permanently identifies as an 88970162 model without the need for injection. I made a full binary dump beforehand so that I could rollback. :-) However writing a program to the PLC is not possible. For some reason M3 soft is telling me: "Type of controller is not compatible with the controller defined in the program". Do you have any ideas on how to fix this?

    ReplyDelete
    Replies
    1. Hello Benno. I'm very happy to hear it was useful to you to that extend. I think not many people have custom PLCs. Thanks for sharing the permanent solution, I had no idea about this. Regarding your issue... I have no clue, hopefully some of these hypotheses help:

      1. This was very long ago and I cannot remember well how M3 worked but the first thing I feel about that message is that you may be trying to load a program that is configured for another PLC. I mean, maybe there are some kind of 'project properties' where you set the desired PLC for which you create the program and you have a model configured that is different than your actual PLC. Then when it comes to flashing the program, the expected PLC doesn't match with the actual connected one. Can it be the case? I don't even know if this is possible in M3, just guessing but I really think it may be something related to this.

      2. Something got screwed up when you permanently wrote the ID. Maybe overwriting more than necessary? Maybe some internal checksum now it doesn't match? Don't know but should be fairly easy to test since you say you keep a dump. Try to revert it and load the program using the script.

      3. The PLC model that your are using isn't correct for the actual hardware of your PLC. I remember there were very similar variants and you must be careful by choosing the right one for your HW.

      Let me know if you make it work! Good luck!

      Delete
    2. Hi Pedro,

      Thanks for replying and your suggestions! I carefully considered them all, but I'm afraid it did not make a difference in the end.

      However I discovered something: After I power cycled the controller, the "permanent" modifications were purged and reverted to their original values. This could mean that the values at 0x6600 are set at boot and generated or read from the EEPROM, but this is hypothetical of course. So I still have unmodified software.

      I checked what would happen if I injected the entire identification response from your communication log in your post. Doing this allowed me to write a blank program to the controller successfully. And I didn't get a "Type of controller is not compatible with the controller defined in the program" message! However running the program on the controller would create a fault state with code 59 (The application is incompatible with the controller).

      Right now I am stuck at this point. I would be helped if someone had a communication dump of writing a blank program to a controller. Preferably of an official XD26S. With this I would be able to compare outputs and maybe figure out what is causing the incompatibility error.

      Did you encounter the 59 fault code as well? Or did the controller run immediately after only using the injection method? It could be a difference between the model you have and the model I got, but I'm not sure.

      Thanks again!

      Delete
    3. Hi Benno,

      Great effort you are putting into this. I'm just guessing... but I think the same as I commented before. In this video, min 2:42 https://www.youtube.com/watch?v=LS7rzXuHGZE
      I can see there are several types of XD26S. Are you sure you are choosing the right one?

      Injecting the entire comm response I think doesn't make too much sense and I'd have expected undefined behaviour. Sorry I'm not of much help but I'm no expert on this.

      BTW, yes I remember my program worked straight, didn't get any error code.

      Good luck, keep going, you're almost there, an PLEASE let me know if you finally get it to run!

      Delete
    4. Hi Pedro,

      I am afraid I have to conclude that the downloadable version of M3 soft is not compatible with my particular model. The bundled controller software that is uploaded to the PLC is too new. So I don't think spoofing communication is a workable solution in my case.

      I guess asking the company for the original software that the controller was made for is the only reasonable option. But I don't expect them to honor that request.

      I'm going to throw in the towel. Thanks again!

      Delete
  8. Hallo mensen,
    Zit met hetzelfde probleem als Benno van de Bogaard. Heb een Millennium 3 XD26S 88 970 807. Waarbij ik de melding krijg van " onbekend model" graag kom ik in contact als hier een oplossing voor is

    ReplyDelete
  9. Hi
    have you discovered a way how to convert Zelio program to Millenium *.mp3
    If I would patch in pasted program from Zelio to .mp3?

    ReplyDelete
  10. Hello Pedro,
    I want to thank you very much for your work.
    I had a problem similar to yours. Indeed, I recovered a PLC "Crouzet CD20" used for a high-speed door of the brand "Nergéco" with a special reference (88970875). I initially found on the web the way to make the USB->Plc interface. But I couldn't read the program. I contacted "Crouzet", but they replied that they needed a specific program reserved for their client "Nergéco". After some research on the web, I found your page which exactly answered my problem. I applied your program and everything works perfectly. Thank you again for sharing your experience as it was very helpful to me. Best regards.

    ReplyDelete
  11. Hi, I've tested to change my zelio SR2A101FU to SR2B121FU, and ZelioSoft recognized my A101FU as a B121FU perfectly, but in my naive approach I tried to override the firmware of my original Zelio, because I see is the same board and same components between SR2A101FU and SR2B121FU, the only notorious difference is the RT-clock module, the CR2032 battery and some electronic componets like resistors, capacitors and diodes.
    Maybe do you have any idea how to change the firmware from any model to a different model?
    maybe how to upload a new .hex or something like that

    ReplyDelete
    Replies
    1. Hello. Should be possible I guess but sorry, didn't investigated that path.

      Delete
    2. Hello, Pedro!Thanks for interesting article, do you know is that possible to "read" or find out password on Zelio SR3 controller. I tried to use Serial Port monitor to look at the traffic through com port, removed old password and changed them so many times. But I couldn't find a string responsible for the holding password! What would you recommend me to do? Thanks in advance!

      Delete
    3. Hello mate. Most probably the password is never sent nor stored in the PLC but its hash. Check 'hashed password' in Google. If you want to know the original password, I guess it won't be possible. Sniffing the hash so you can fake it shouldn't be too difficult.

      Delete