Reverse of a schneider network protocol

biero llagas
9 min readDec 21, 2022

--

Reversing the MODBUS protocol of BMX P34 2020 PLC

INTRODUCTION

Hello, for this article I’m going to talk about a network protocol, more specifically the UMAS schneider compliant protocol.
it is recognizable by its byte 0x5a, but we will come back to this later.

The objective of this article is to agglomerate a maximum of relevant resources, to understand how the protocol works to do reverse engineering on it, then create and make available scripts allowing the sending and parsing of TCP/MODBUS packets.

What the hell is a UMAS ?

First we have to explain what MODBUS is.

Modbus is an industrial protocol developed in 1979 by Modicon, now Schneider Electric, for communication between automation devices. It is a data communication protocol used for establishing communication between multiple devices over various networks

Note that the UMAS protocol depend on the header of the MODBUS one.

Modbus UMAS is a protocol used for communication between industrial automation devices such as PLCs and other industrial control systems. It is based on the Modbus protocol, but adds some additional features such as a message authentication system, improved range checking, and additional error codes.

Here is a representation of the UMAS packet. We can see the modbus header and the UMAS packet that follows it

I’m going to address an important point here, that is the lack of official documentation. The resources I’m going to use come from communities and companies that are not directly linked to Schneider.

Ok, but how do we do it if we don’t have docs ?

If we don’t have a resource linked to the constructor we have to look for elements available in open source.

Here are the main documents used for my researches.

Kaspersky report on a vulnerability related to the implementation of the umas protocol.

I strongly advise you to read the following article, it explains extremely well how the protocol works, and he has done a reverse of the protocol on his side. So there is no need to reinvent the wheel.

Once we have read the article of lirasenlared we will understand how the protocol works, with the different functions and sendable requests.

However, what the article does not explain is how you can send custom UMAS requests, which we can use to modify the behavior of the PLC in depth.

Blacksmith time

To be able to achieve our goal of sending tcp packets containing umas instructions, we will use the python library scapy.

What is scapy ?

Scapy is a powerful interactive packet manipulation program. It is able to forge or decode packets of a wide number of protocols.

to make a long story short about the uses of scapy. it works like a sandwich.

The different layers like ethernet, TCP, IP are standard like bread ,butter and butter in a sandwich, and you just have to call them in scapy.

# exemple of a simple TCP/IP packet forging in scapy 
# sandwish=Bread(type of bread)/Butter(type of butter)/Ham(type of ham)
packet=IP(src='10.10.10.10', dst='192.168.0.254')/IP()/TCP()
send(packet)

On the other hand for the less standard protocols as MODBUS and especially UMAS. it is necessary to forge ourself the protocol.

this is done by defining a class that will contain the name and size of each fields. for example here is the structure of the modbus header, which precedes the UMAS frame

And here the representations in scapy

To confirm the elements found in the doc, we will compare them with the actual results we have in the pcap.
here is an example of the MODBUS header part (in orange), it represents 7 bytes in total, and seems coherent with the standar found in the doc.

what we can do next is to extract a packet we are interested in, and analyze it with scapy.

scapy has a build-in function for this, it is show()

Result of the scapy.show() function.

P.S : it’s not the same packet, but it’s the same concept except the destination port and the code of the UMAS functions and data, it’s the same packet

We know that the structure of a UMAS packet and a TCP packet which comports a UMAS header with a MODBUS function which takes the value 0x5a or 90 in decimal. the rest of the packet is sent in RawData, because it is not interpreted by wireshark.

Here is the scapy code to send a UMAS funtions with the number 6 (READ_CARD_INFO: Get internal PLC SD-Card Info) , according to the list made by lirasenlared (website cited above).

And here is the wireshark confirmation of the sending of the packet.

Parsing time

At this point we are able to send custom packets to the client. but it would be nice to be able to receive the answer.

For this we could use scapy again, because the lib has the ability to parse network captures. but i preferred to discover new things. so I decided to create a parsing module for wireshark.

Let me introduce you to wireshark dissector

A Wireshark dissector is simply a decoder, which is interesting in a specific type of traffic. Once it finds the traffic, it interprets the payload. In short, it is a protocol parser. Wireshark dissectors can be useful when you are working with a custom protocol that Wireshark doesn’t already have a dissector for.

The goal here is to make a dissector that fits the UMAS protocol.

The main lines of operation of the dissector are the following.

  • Select the protocol TCP or UDP and the source port.
local modbus = DissectorTable.get("tcp.port")
modbus:add(502, modbus1_protocol)
  • Define the name of the protocol we want to create.
modbus1_protocol = Proto("Modbus1", "Modbus .")
umas_protocol = Proto("UMAS", "UMAS .")
  • Make a function that concordate the UMAS functions number to their actual effect on the client.
function get_umas_function_name(code)
local code_name = "Unknow"
-- source: http://lirasenlared.blogspot.com/2017/08/the-unity-umas-protocol-part-i.html
if code == 1 then code_name = "0x01 - INIT_COMM: Initialize a UMAS communication"
elseif code == 2 then code_name = "0x02 - READ_ID: Request a PLC ID"
elseif code == 3 then code_name = "0x03 - READ_PROJECT_INFO: Read Project Information"
elseif code == 4 then code_name = "0x04 - READ_PLC_INFO: Get internal PLC Info"
elseif code == 6 then code_name = "0x06 - READ_CARD_INFO: Get internal PLC SD-Card Info"
elseif code == 10 then code_name = "0x0A - REPEAT: Sends back data sent to the PLC (used for synchronization)"
elseif code == 16 then code_name = "0x10 - TAKE_PLC_RESERVATION: Assign an owner to the PLC"
elseif code == 17 then code_name = "0x11 - RELEASE_PLC_RESERVATION: Release the reservation of a PLC"
elseif code == 18 then code_name = "0x12 - KEEP_ALIVE: Keep alive message (???)"
elseif code == 32 then code_name = "0x20 - READ_MEMORY_BLOCK: Read a memory block of the PLC"
elseif code == 34 then code_name = "0x22 - READ_VARIABLES: Read System bits, System Words and Strategy variables"
elseif code == 35 then code_name = "0x23 - WRITE_VARIABLES: Write System bits, System Words and Strategy variables"
elseif code == 36 then code_name = "0x24 - READ_COILS_REGISTERS: Read coils and holding registers from PLC"
elseif code == 37 then code_name = "0x25 - WRITE_COILS_REGISTERS: Write coils and holding registers into PLC"
elseif code == 48 then code_name = "0x30 - INITIALIZE_UPLOAD: Initialize Strategy upload (copy from engineering PC to PLC)"
elseif code == 49 then code_name = "0x31 - UPLOAD_BLOCK: Upload (copy from engineering PC to PLC) a strategy block to the PLC"
elseif code == 50 then code_name = "0x32 - END_STRATEGY_UPLOAD: Finish strategy Upload (copy from engineering PC to PLC)"
elseif code == 51 then code_name = "0x33 - INITIALIZE_UPLOAD: Initialize Strategy download (copy from PLC to engineering PC)"
elseif code == 52 then code_name = "0x34 - DOWNLOAD_BLOCK: Download (copy from PLC to engineering PC) a strategy block"
elseif code == 53 then code_name = "0x35 - END_STRATEGY_DOWNLOAD: Finish strategy Download (copy from PLC to engineering PC)"
elseif code == 57 then code_name = "0x39 - READ_ETH_MASTER_DATA: Read Ethernet Master Data"
elseif code == 58 then code_name = "0x40 - START_PLC: Starts the PLC"
elseif code == 59 then code_name = "0x41 - STOP_PLC: Stops the PLC"
elseif code == 80 then code_name = "0x50 - MONITOR_PLC: Monitors variables, Systems bits and words"
elseif code == 88 then code_name = "0x58 - CHECK_PLC: Check PLC Connection status"
elseif code == 112 then code_name = "0x70 - READ_IO_OBJECT: Read IO Object"
elseif code == 113 then code_name = "0x71 - WRITE_IO_OBJECT: WriteIO Object"
elseif code == 115 then code_name = "0x73 - GET_STATUS_MODULE: Get Status Module"
elseif code == 254 then code_name = "0xfe - Response Meaning OK"
elseif code == 253 then code_name = "0xfd - Response Meaning Error" end
return code_name
end
  • Define the size of each field
----------- part of the modbus protocol ---------------
Transaction_Identifier = ProtoField.uint16("Modbus1.Transaction_Identifier" , "Transaction_Identifier" , base.DEC)
Protocol_Identifier = ProtoField.uint16("Modbus1.Protocol_Identifier" , "Protocol_Identifier" , base.DEC)
Length = ProtoField.uint16("Modbus1.Length" , "Length" , base.DEC)
Unit_Identifier = ProtoField.int8("Modbus1.Unit_Identifier" , "Unit_Identifier" , base.DEC)
modbus1_protocol.fields = { Transaction_Identifier, Protocol_Identifier, Length, Unit_Identifier }
-------------------------------------------------------

----------- part of the UMAS protocol -----------------
Function_Code = ProtoField.uint8("UMAS.Function_Code" , "Function_Code" , base.HEX_DEC)
Pairing_Key = ProtoField.uint8("UMAS.Pairing_Key" , "Pairing_Key" , base.HEX)
Umas_Functions_Code = ProtoField.uint8("UMAS.Umas_Functions_Code" , "Umas_Functions_Code" , base.DEC)
Umas_Data = ProtoField.string("UMAS.Umas_Data" , "Umas_Data" , base.ASCII )
umas_protocol.fields = { Function_Code, Pairing_Key, Functions_Code , Umas_Functions_Code ,Umas_Data }
-------------------------------------------------------
  • Define subtree
    -- add the layer umas in the list of potential layer 
local subtree = tree:add(modbus1_protocol, buffer() , "Modbus Protocol Data")
local modbusSubtree = subtree:add(modbus1_protocol, buffer() ,"modbus header")
  • Apply fields to the subtree
    modbusSubtree:add(Transaction_Identifier   ,buffer(0,2))
modbusSubtree:add(Protocol_Identifier ,buffer(2,2))
modbusSubtree:add(Length ,buffer(4,2))
modbusSubtree:add(Unit_Identifier ,buffer(6,1))
  • Do the same but with the signature detection of the schneider UMAS protocol, so we can apply the modbus subtree on the capture.
    
------------------------------------------
-- BEGIN OF THE UMAS SECTIONS ------------
------------------------------------------
-- get the umas schneider signature
local umas_identifier = buffer(7,1):le_uint()
local umas_code = buffer(9,1):le_uint()
local umas_code_name = get_umas_function_name(umas_code)

local getData = buffer(10)
local data = getData:le_ustring()
-- verify the umas signature
if(umas_identifier == 90)
then
local umasSubtree = subtree:add(modbus1_protocol ,buffer() ,"umas")
umasSubtree:add(Function_Code, buffer(7,1))
umasSubtree:add(Pairing_Key, buffer(8,1))
umasSubtree:add(Umas_Functions_Code,buffer(9,1)):append_text(" (" .. umas_code_name .. ")")
umasSubtree:add(Umas_Data, 10 )
end
------------------------------------------
-- END OF THE UMAS SECTIONS ------------
------------------------------------------

the pieces of code shown above, are only examples taken out of their global context. to see the whole code of the wireshark dissector, the link is right here

we can see here that wireshark interprets well the MODBUS function (in red) and UMAS (in green).
Each field is correctly transcribed, only the data field of the UMAS function is misinterpreted, because of a lack of time I could not implement a correct interpetation of the elements of the data field.

Resources

Youtube stuff about basic botnet dissections:
https://www.youtube.com/watch?v=I4nf23HywmI&t=558s&ab_channel=dist67
http://didierstevens.com/files/workshops/botnet01-dissector.lua

Basic lua dissector:
https://mika-s.github.io/wireshark/lua/dissector/2017/11/04/creating-a-wireshark-dissector-in-lua-1.html

Strucutre of the UMAS protocol:
http://lirasenlared.blogspot.com/2017/08/the-unity-umas-protocol-part-i.html

Add a protocol to the dissector table:
https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Proto.html

The holy wireshark dissections tutorial (1–5):
https://mika-s.github.io/wireshark/lua/dissector/2017/11/04/creating-a-wireshark-dissector-in-lua-1.html

--

--