Инструменты и информация были следующие:
- программируемый логический контроллер Овен ПЛК110-32;
- среда разработки для контроллеров Codesys 2.3;
- Wireshark;
- Eltima Serial Port Monitor (пробная версия);
- plc_io.exe — утилита для копирования файлов с/на ПЛК, можно найти на закоулках owen.ru;
- отчёт о дырках в безопасности Codesys компании DigitalBond с описанием протокола.
К контроллеру можно подключиться и по TCP/IP, и через COM-порт. Быстро выяснилось, что формат посылок там и там одинаковый. Вот пример обмена по команде PLCInfo из Кодесиса:
[21/10/2014 11:39:39] Written data aa aa 0d 00 01 00 60 01 92 00 00 00 00 50 4c 43 ЄЄ....`.’....PLC 49 6e 66 6f 00 Info. [21/10/2014 11:39:39] Read data 55 55 0a 00 01 00 UU.... [21/10/2014 11:39:39] Written data 55 55 0a 00 00 00 UU.... [21/10/2014 11:39:39] Read data aa aa 80 00 01 00 34 00 ЄЄЂ...4. [21/10/2014 11:39:39] Read data 00 00 00 04 00 01 00 50 4c 43 20 6d 6f 64 65 6c .......PLC model 20 4d 4f 44 45 4c 20 50 4c 43 20 31 31 30 2d 33 MODEL PLC 110-3 32 20 0d 0a 42 69 6e 61 72 79 20 20 56 45 52 53 2 ..Binary VERS 49 4f 4e 20 32 2e 31 34 2e 30 20 0d 0a 4e 65 65 ION 2.14.0 ..Nee 64 20 54 61 72 67 65 74 20 76 65 72 73 69 6f 6e d Target version 20 32 2e 31 30 20 0d 0a 43 6f 6d 70 69 6c 65 64 2.10 ..Compiled 3a 20 31 34 3a 33 37 3a 34 31 20 41 70 72 20 32 : 14:37:41 Apr 2 38 20 32 30 31 31 20 0d 0a 4d 41 43 20 36 41 3a 8 2011 ..MAC 6A: [21/10/2014 11:39:39] Written data 55 55 0a 00 01 00 UU.... [21/10/2014 11:39:39] Read data aa aa 7d 00 02 00 13 01 ЄЄ}..... [21/10/2014 11:39:39] Read data 37 37 3a 30 30 3a 32 31 3a 34 34 3a 46 38 20 0d 77:00:21:44:F8 . 0a 49 50 20 31 30 2e 30 2e 36 2e 31 30 0d 0a 47 .IP 10.0.6.10..G 41 54 45 20 31 30 2e 30 2e 36 2e 31 0d 0a 4d 41 ATE 10.0.6.1..MA 53 4b 20 32 35 35 2e 32 35 35 2e 32 35 35 2e 30 SK 255.255.255.0 0d 0a 50 49 43 20 75 70 70 65 72 20 76 65 72 73 ..PIC upper vers 69 6f 6e 20 69 73 20 31 35 0d 0a 4c 69 63 65 6e ion is 15..Licen 63 65 20 6c 69 6d 69 74 65 64 20 74 6f 20 33 36 ce limited to 36 30 20 62 79 74 65 73 0d 0a 00 00 00 00 0 bytes...... [21/10/2014 11:39:39] Written data 55 55 0a 00 02 00 UU.... [21/10/2014 11:39:39] Read data 55 55 0a 00 00 00 UU....
Основные расхождения со скриптами DigitalBond — в заголовках. В примере видно, что пакеты начинаются байтами 0xAA или 0x55 (в скриптах — 0xCC и 0x66). То есть, скорее всего, мы имеем дело с другой версией протокола. Тем не менее, порядок следования полей тот же.
Возьмём первую посылку: aa aa 0d 00 01 ... Если поиграться разными командами, то можно увидеть, что меняются те байты, которые здесь равны 0d, 60 и, само собой, текст команды. Очевидно, 0d — размер пакета начиная со следующего байта. Что такое 60 становится понятно, если поиграться символами в команде. Отправим, например, вместо PLCInfo команду QLCInfo, и получим 61. Стало быть, это контрольная сумма. Так как контрольная сумма не может содержаться в сообщении, суммой которого она является, то пробуем просуммировать все байты без неё, и — ура! — всё совпадает. В первом скрипте (который shell) контрольную сумму почему-то не считают. Алгоритм есть во втором, но туда я догадался посмотреть уже потом. Байты 0x92 и единицы не меняются, и поэтому (пока) не особо интересны.
Дальше обратим внимание на пакеты 55 55. Сначала казалось, что нужно просто нулём отвечать на единицу, единицей на ноль, а двойку отправлять когда обмен закончен. Всё это никак не сходилось с практикой. ПЛК либо выдавал ответы повторно, либо завершал обмен странным пакетом. Путаницы добавлял ответ ПЛК на команду «?»:
[21/10/2014 13:16:34] Written data aa aa 07 00 01 00 2e 01 92 00 00 00 00 3f 00 ЄЄ......’....?. [21/10/2014 13:16:34] Read data 55 55 0a 00 01 00 UU.... [21/10/2014 13:16:34] Written data 55 55 0a 00 00 00 UU.... [21/10/2014 13:16:34] Read data aa aa 35 00 01 00 c2 01 ЄЄ5...В. [21/10/2014 13:16:34] Read data 00 00 00 03 00 01 00 3f 20 20 20 20 20 20 20 20 .......? 20 20 20 20 20 20 2d 20 73 68 6f 77 20 69 6d 70 - show imp 6c 65 6d 65 6e 74 65 64 20 63 6f 6d 6d 61 6e 64 lemented command 73 00 00 00 00 s.... [21/10/2014 13:16:34] Written data 55 55 0a 00 01 00 UU.... [21/10/2014 13:16:34] Read data 55 55 0a 00 00 00 UU.... [21/10/2014 13:16:34] Written data aa aa 06 00 01 00 f0 01 92 01 00 01 00 00 ЄЄ....р.’..... [21/10/2014 13:16:34] Read data 55 55 0a 00 01 00 UU.... [21/10/2014 13:16:34] Written data 55 55 0a 00 00 00 UU.... [21/10/2014 13:16:34] Read data aa aa 26 00 01 00 03 01 ЄЄ&..... [21/10/2014 13:16:34] Read data 00 00 00 03 00 02 00 6d 65 6d 20 20 20 20 20 20 .......mem 20 20 20 20 20 20 2d 20 4d 65 6d 6f 72 79 64 75 - Memorydu 6d 70 00 65 6e 74 mp.ent [21/10/2014 13:16:34] Written data 55 55 0a 00 01 00 UU.... [21/10/2014 13:16:34] Read data 55 55 0a 00 00 00 UU.... [21/10/2014 13:16:34] Written data aa aa 06 00 01 00 f1 01 92 01 00 02 00 00 ЄЄ....с.’..... [21/10/2014 13:16:34] Read data 55 55 0a 00 01 00 UU.... [21/10/2014 13:16:34] Written data 55 55 0a 00 00 00 UU.... [21/10/2014 13:16:34] Read data aa aa 44 00 01 00 99 01 ЄЄD...™. [21/10/2014 13:16:34] Read data 00 00 00 03 00 03 00 6d 65 6d 63 20 20 20 20 20 .......memc 20 20 20 20 20 20 2d 20 4d 65 6d 6f 72 79 64 75 - Memorydu <...> 00 00 00 03 00 21 00 53 65 74 4d 6f 64 65 6d 50 .....!.SetModemP 6f 72 74 5b 20 53 65 74 4d 6f 64 65 6d 50 6f 72 ort[ SetModemPor 74 20 58 5d 00 61 72 79 t X].ary [21/10/2014 13:16:34] Written data 55 55 0a 00 01 00 UU.... [21/10/2014 13:16:34] Read data 55 55 0a 00 00 00 UU.... [21/10/2014 13:16:34] Written data aa aa 06 00 01 00 10 01 92 01 00 21 00 00 ЄЄ......’..!.. [21/10/2014 13:16:34] Read data 55 55 0a 00 01 00 UU.... [21/10/2014 13:16:34] Written data 55 55 0a 00 00 00 UU.... [21/10/2014 13:16:34] Read data aa aa 26 00 01 00 0c 01 ЄЄ&..... [21/10/2014 13:16:34] Read data 00 00 00 04 00 22 00 53 65 74 4d 6f 64 65 6d 43 .....".SetModemC 66 67 5b 20 53 65 74 4d 6f 64 65 6d 43 66 67 20 fg[ SetModemCfg 58 5d 00 5d 00 61 X].].a [21/10/2014 13:16:34] Written data 55 55 0a 00 01 00 UU.... [21/10/2014 13:16:34] Read data 55 55 0a 00 00 00 UU....
Как видно, ответ здесь разбивается совершенно иначе. Скрипт хоть и работает, но комментарии в нём туманные. Якобы на каждое сообщение надо давать два подтверждения. На самом же деле в протоколе просто-напросто предусматривается два уровня фрагментации: ответ может состоять из нескольких сообщений, каждое из которых в свою очередь может состоять из нескольких фрагментов.
Например, ответ ПЛК на первую посылку: 55 55 0a 00 01 00. Единица означает готовность принять второй (считая с нулевого) фрагмент. Если второго и/или последующего фрагмента нет, то мы отвечаем 55 55 0a 00 00 00. ПЛК делает то же самое. Номер фрагмента идёт вслед за байтом размера - та самая единица, которая ранее была нам мало интересна. Он становится двойкой во втором фрагменте (см. первый листинг).
Аналогично с сообщениями. Упомянутая команда «?» выдаёт по сообщению на каждую строку, каждое из которых подтверждается пакетами 55 55. Приняв сообщение (как и фрагмент) мы отправляем ответ вида aa aa 06 00 01 00 f0 01 92 01 00 01 00 00. Здесь тоже присутствуют байты размера и контрольной суммы, а также номер сообщения (0x21 в последнем ответе: aa aa 06 00 01 00 10 01 92 01 00 21 00 00). Разница с фрагментами в том, что последним является сообщение типа 04, а не то, на которое даётся нулевой ответ (сравните aa aa 26 00 01 00 0c 01 00 00 00 04 ... и aa aa 44 00 01 00 99 01 00 00 00 03).
Не буду томить вас оставшейся интерпретацией размера сообщений. Скажу лишь, что размер 0x80 означает «до самого конца», а не «128 байт» (видимо, знаковый байт используется). Напоследок привожу код получившейся консольной программы:
private static void PLCCmd(byte[] buf, Socket client, string cmd) { int len = 0; client.Send(MakeCommandPacket(cmd)); client.Receive(buf); client.Send(MakeSubPacketRequest(0)); bool finished = false; while (!finished) { len = client.Receive(buf); //HexOut(buf, len); byte packetType = buf[11]; byte packetNum = buf[13]; Console.Write(Encoding.ASCII.GetString(buf, 15, buf[2] < 0x80 ? buf[2] - 11 : len - 15)); finished = packetType == 4; // Get subpackets while (true) { client.Send(MakeSubPacketRequest(buf[4])); len = client.Receive(buf); //HexOut(buf, len); if (buf[0] == 0xAA) { Console.Write(Encoding.ASCII.GetString(buf, 8, buf[2] < 0x80 ? buf[2] - 4 : len - 8)); } else { Console.WriteLine(); break; } } if (!finished) { client.Send(MakePacketRequest(packetNum)); client.Receive(buf); client.Send(MakeSubPacketRequest(0)); } } } static byte[] MakeCommandPacket(string command) { var prefix = new byte[] {0x92, 0, 0, 0, 0 }; var packetInfo = new byte[] { 0, 1, 0, 0, 1 }; var cmd = prefix.Concat(Encoding.ASCII.GetBytes(command)).Concat(new byte[] {0}); var packet = aa.Concat(new byte[] { (byte)cmd.Count() }).Concat(packetInfo).Concat(cmd).ToArray(); packet[6] = GetChecksum(packet); return packet; } static byte[] MakePacketRequest(byte packetNumber) { var packet = new byte[] { 0xAA, 0xAA, 6, 0, 1, 0, 0, 1, 0x92, 1, 0, packetNumber, 0, 0 }; packet[6] = GetChecksum(packet); return packet; } static byte[] MakeSubPacketRequest(byte subPacketNumber) { return new byte[] { 0x55, 0x55, 0x0A, 0, subPacketNumber, 0 }; } static byte GetChecksum(byte[] packet) { int checksum = 0; foreach (byte b in packet) { checksum += b; } return (byte)checksum; }
