Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scapy.contrib.modbus cannot handle packets with multiple ADU/PDU sets #4096

Open
Insanitree opened this issue Aug 15, 2023 · 4 comments
Open

Comments

@Insanitree
Copy link

Insanitree commented Aug 15, 2023

Brief description

In non-spec packets where multiple ADU/PDU layers exist in parallel the first Modbus ADU Response layer consumes the entire payload. This causes registerVal fields in ModbusPDU03 and 04 responses (at least), to end up significantly larger than expected and missing other ModbusTCPResponse packets.

Scapy version

2.5.0

Python version

3.11.3

Operating system

Windows 10

Additional environment information

No response

How to reproduce

from scapy.all import *
import scapy.contrib.modbus as mb
packets = PcapReader("FailedPackets.pcap")
for pkt in packets:
    if (mb.ModbusPDU04ReadInputRegistersResponse in pkt):
                #print (str(inc) +":" + str(pkt[mb.ModbusPDU04ReadInputRegistersResponse].fields))
        pdureglen = len(pkt[mb.ModbusPDU04ReadInputRegistersResponse].fields["registerVal"])
        pduregval = (pkt[mb.ModbusPDU04ReadInputRegistersResponse].fields["byteCount"])
        #if (pdureglen != (pduregval/2)):
        print (pdureglen,int(pduregval/2),sep=":")

Actual result

132:99
Observe that we have 132 registerVal words, when we expected 99 based on bytecount

Expected result

99:99

Related resources

SamplePackets.zip

@Insanitree
Copy link
Author

Insanitree commented Aug 15, 2023

I'm working on fixing this myself, but I'm not familiar with Scapy. It appears we need two steps to a solution.
First step is properly calculating the length of each packet. in the PDU, that's as easy as assigning the length from the packet data itself to the length_from.

                   FieldListField("registerVal", [0x0000],
                                  ShortField("", 0x0000),
                                  length_from=lambda pkt: pkt.byteCount,
                                  count_from=lambda pkt: pkt.byteCount)]

In the ADU, well, I haven't figured out how to interpret the len variable from the packet during dissection without breaking the build function.

The second step is to implement extract_padding. I'll know if I get there.

@Insanitree
Copy link
Author

Step 1: Properly calculate the length of the PDU using: length_from=lambda pkt: pkt.byteCount

Step 2: Extract PDU padding with

def extract_padding(self, s):
        pay = s[:self.byteCount]
        pad = s[self.byteCount:]
        return pay, pad

Step 3: Extract ADU Padding with

def extract_padding(self, s):
        pay = s[:self.len-1]
        pad = s[self.len-1:]
        return pay, pad

Step 4: Add a layer that contains a PacketListField

    name = "ModbusTCPResponse"
    fields_desc = [
        PacketListField("ADU_List",None,ModbusADUResponse)
        ]

Step 5: Bind the new layer like: bind_layers(TCP, ModbusTCPResponse, sport=502)
Result:

           \ADU_List  \
            |###[ ModbusADU ]### 
            |  transId   = 0x7cfe
            |  protoId   = 0x0
            |  len       = 201
            |  unitId    = 0xff
            |###[ Read Input Registers Response ]### 
            |     funcCode  = 0x4
            |     byteCount = 198
            |     registerVal= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 475, 0, 470, 0, 19000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25697, 26989, 110, 0, 0, 0, 0, 0, 0, 0, 12337, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
            |###[ ModbusADU ]### 
            |  transId   = 0x7cff
            |  protoId   = 0x0
            |  len       = 7
            |  unitId    = 0xff
            |###[ Read Input Registers Response ]### 
            |     funcCode  = 0x4
            |     byteCount = 4
            |     registerVal= [4, 0]
            |###[ ModbusADU ]### 
            |  transId   = 0x7d00
            |  protoId   = 0x0
            |  len       = 47
            |  unitId    = 0xff
            |###[ Read Input Registers Response ]### 
            |     funcCode  = 0x4
            |     byteCount = 44
            |     registerVal= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

This works, but I'm not convinced it's still the best way. I suspect there is a way to not use the PacketListField and still have each ADU be a sibling not a child.

Are there unit tests associated with this module?

@Insanitree
Copy link
Author

I have run and passed the unit tests for this module. I intend to expand them in lieu of further guidance.
I have not contributed to an open source project. What is my next step here, now that I have a potential solution to present?

@gpotter2
Copy link
Member

Sorry for the delay. You may provide a Pull Request :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants