DEV Community

Cover image for Inside S7comm: From ISO-on-TCP to PLC Memory
404Saint
404Saint

Posted on Originally published at rugerotesla.tech

Inside S7comm: From ISO-on-TCP to PLC Memory

By RUGERO Tesla (@404Saint).

S7comm is one of those protocols that looks simple until you start following the conversation all the way down. At first, there is just a TCP connection to port 102, then TPKT appears, then COTP, then Siemens-specific connection parameters, then the S7comm session has its own negotiation, then a memory address becomes meaningful, then a single function code can turn a packet from a read request into a write operation. That was exactly what I wanted to investigate.

After working through Modbus TCP, EtherNet/IP, DNP3, BACnet, OPC UA, IEC 104, IEC 61850, and PROFINET, S7comm felt like the right protocol to finish this part of the research with. The earlier projects taught me to establish the architecture before touching the wire, but this time, I wanted to stay on the wire.

The laboratory target was a Snap7 server exposing a Siemens S7-style communication interface on TCP/102. I built the research client myself, constructed the protocol messages, captured the exchanges, and then started deliberately deviating from the expected conversation, and that ended up being the most interesting.


The Stack Before the First S7 Packet

Before looking at an S7comm packet, there is a small stack that needs to be understood.

┌──────────────────────────────┐
│          S7comm              │
├──────────────────────────────┤
│           COTP               │
├──────────────────────────────┤
│        TPKT / RFC 1006       │
├──────────────────────────────┤
│            TCP               │
├──────────────────────────────┤
│            IP                │
├──────────────────────────────┤
│         Ethernet             │
└──────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The TCP endpoint is normally port 102.

TPKT provides the ISO-on-TCP framing. COTP provides the transport session semantics above it. S7comm then uses that established channel for Siemens-specific application services. That matters during research because an apparently simple S7comm exchange can actually contain several different state machines. I started with the COTP connection because it was the lowest S7-specific piece I could directly control.


Establishing the Session

The first experiment constructed a COTP Connection Request manually.

The target was:

127.0.0.1:102
Enter fullscreen mode Exit fullscreen mode

The destination TSAP represented the lab configuration's Rack 0, Slot 2:

Dst-TSAP: 0x0102
Src-TSAP: 0x0100
Enter fullscreen mode Exit fullscreen mode

The client transmitted:

03 00 00 16
11 E0
00 00 00 01
00 C2 02 01 02
C1 02 01 00
C0 01 09
Enter fullscreen mode Exit fullscreen mode

The response came back as a COTP Connection Confirm. The important part was seeing the layers line up in an actual capture rather than simply accepting what a high-level S7 library told me was happening.

Figure 1

Figure 1. Captured S7comm session establishment showing the TPKT/COTP connection sequence between the research client and Snap7 target.

The TPKT header reported a 22-byte frame.

The COTP TPDU changed from: 0xE0 for the Connection Request to: 0xD0 for the Connection Confirm. The reference fields were also visible directly in the exchange.

One detail that caught my attention was the TPDU size parameter: C0 01 09. This indicates a COTP TPDU size of 512 bytes. That number is easy to confuse with the S7 PDU size negotiated later. They are different parts of the communication model.

I also tested unusual destination TSAP values. A destination TSAP of 0x01FF was accepted by the Snap7 target. In the lab's Siemens-style interpretation, that corresponds to Rack 7, Slot 31.

Another unusual value, 0x0F02, was also accepted. That result is useful, but only within the boundaries of the experiment. The Snap7 server accepting those values does not establish that a physical Siemens PLC would accept the same combinations. At this point, the transport session existed and the actual S7 conversation had barely started.


Teaching the PLC How We Want to Talk

Once the COTP session was established, the next step was S7comm PDU negotiation. This was where the protocol started becoming much more interesting. The Setup Communication exchange negotiated a maximum S7 PDU size of: 480 bytes. The session also reported one parallel calling job and one parallel called job.

The negotiated value was visible directly in the response rather than being inferred from the later packets.

Figure 2.

Figure 2. S7comm Setup Communication followed by a ReadVar request targeting DB3.

The next request used the ReadVar service:

ROSCTR: 0x01
Function: 0x04
Enter fullscreen mode Exit fullscreen mode

The variable specification identified an S7-Any pointer.

The target was:

Area:      DB
Area ID:   0x84
DB number: 3
Offset:    0
Count:     16
Enter fullscreen mode Exit fullscreen mode

The server returned an Ack-Data PDU with: Return code: 0xFF and 16 bytes of data.

The lab DB3 contents were:

01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01
Enter fullscreen mode Exit fullscreen mode

This was the first point where the protocol stopped looking like an abstract collection of headers, because now I had a memory model. I could specify an area, a DB number, an offset, and a length. The PLC-style target would interpret those fields and return the corresponding data and that made the next question fairly obvious.

What happens when the operation changes from reading memory to modifying it?


Reading Was Only the Beginning

The S7comm WriteVar service uses function: 0x05

For the first write experiment, I targeted DB3 at offset zero and supplied 16 bytes of 0xFF, and the request was accepted. The response returned: 0xFF for the write item.

The Snap7 server confirmed the operation:

Write request, Area : DB3, Start : 0, Size : 16 --> OK
Enter fullscreen mode Exit fullscreen mode

In this laboratory configuration, there was no additional authentication step protecting that memory write. That is an important observation, but it needs to be scoped correctly. This demonstrates unauthenticated memory modification against the Snap7 target used in the lab. It does not establish that a particular physical Siemens PLC would accept the same request under the same conditions.

Figure 3.

Figure 3. Wire-level validation of the WriteVar experiments, including the successful DB3 write and subsequent boundary-testing request.

I then moved the write far outside the DB boundary. DB3 was 1024 bytes. The next request targeted offset: 65000 and the server rejected it.

The response returned: 0x05 and the server reported:

Write request, Area : DB3, Start : 65000, Size : 4 --> Out of range
Enter fullscreen mode Exit fullscreen mode

This was a useful contrast. The same service that allowed a valid write also enforced the memory boundary when the requested address was outside the configured DB. So the experiment wasn't simply “send WriteVar and everything works.” There was an actual implementation boundary and that becomes important later.


Trying the Control Plane

The next experiment moved away from ordinary memory access.

S7comm UserData uses: ROSCTR: 0x07 among other functions. I constructed a CPU control request intended to exercise the PLC STOP control path and the request was transmitted to the Snap7 server. There was no successful response. Instead, the connection was dropped. The important result here is what the experiment did not prove. The packet was successfully constructed and transmitted but the Snap7 demo server did not implement the requested CPU control function.

That means the experiment demonstrated the structure and transmission of the control-plane request, along with the emulator's implementation boundary. It did not demonstrate that a physical Siemens PLC would enter STOP as a result. That is easy to lose when packet-level research gets compressed into a headline.


Asking the PLC About Itself

After reading and writing memory, I moved into S7 diagnostics. S7comm UserData provides access to System Status List information, commonly referred to as SZL data.

The first query used:

SZL ID: 0x0011
Index: 0x0000
Enter fullscreen mode Exit fullscreen mode

The Snap7 target returned the request successfully.

The response exposed module identification information:

6ES7 315-2EH14-0AB0
6ES7 315-2EH14-0AB0
Boot Loader
Enter fullscreen mode Exit fullscreen mode

Figure 4.

Figure 4. SZL UserData exchanges used to enumerate module-identification information from the simulated S7 target.

This is where diagnostic enumeration becomes interesting from a security perspective. A protocol does not need to provide an obvious “enumerate device” command to expose useful information. If diagnostic services can return module identifiers, firmware information, hardware details, or other system metadata, those services become part of the information-disclosure surface.

I tested another SZL identifier: 0x0111 but this time the target did not provide the requested object. The response indicated that the object was unavailable, matching the Snap7 server's own NOT AVAILABLE result.

Again, the implementation boundary matters. The absence of a response for this identifier does not tell us what every Siemens CPU would do. It tells us what this implementation did when presented with the request.


What Happens When We Ignore the Conversation's Rules?

This was the point where the research became much more interesting.

By now, the expected session flow was familiar:

TCP
  ↓
COTP CR / CC
  ↓
Setup Communication
  ↓
S7comm services
Enter fullscreen mode Exit fullscreen mode

So I decided to remove one of those steps. Instead of establishing COTP, negotiating the S7 PDU parameters, and then issuing a ReadVar request, I established the COTP session and immediately sent the S7comm read. No Setup Communication, and the Snap7 server responded. The ReadVar operation returned live DB3 data.

Figure 5.

Figure 5. Phase 5 capture showing the state-manipulation and held-open session experiments against the Snap7 target.

That was one of the most interesting findings in the entire project. The expected protocol sequence existed conceptually, but the implementation did not enforce it for this read operation.

The packet capture made the sequence very clear:

COTP connection
      ↓
ReadVar
      ↓
Setup Communication
Enter fullscreen mode Exit fullscreen mode

The S7comm service arrived before the negotiation that was supposed to precede it. At this point, I stopped asking what the protocol was supposed to do and started asking what the implementation would actually tolerate.

The write experiment was different

I repeated the same idea with WriteVar. This time the server processed the request but returned: 0x05 for the write item. The subsequent fully negotiated read showed that DB3.0 remained unchanged. So there was no pre-Setup write bypass demonstrated here.

The results were asymmetric:

Operation Before Setup Communication
ReadVar Accepted
WriteVar Reached service, rejected with 0x05
CPU control Not implemented by Snap7 target

That is useful than simply calling the implementation “vulnerable.” The state enforcement was incomplete for the tested read path, while the write path did not produce a successful modification.


Holding the Connections Open

The last experiment looked at resource handling. I created 50 sessions, completed the expected negotiation, then sent a ReadVar request with a TPKT length deliberately larger than the actual payload. Each socket was kept open for approximately 15 seconds. All 50 connection attempts were established and held. The server remained available after the sockets were released.

Connections attempted: 50
Successfully held open: 50
Failed before hold: 0

Total wall time: 15.01s
Server liveness: UP
Enter fullscreen mode Exit fullscreen mode

That result does not demonstrate a denial of service. It demonstrates that the tested server tolerated the controlled connection load and malformed-length condition for the duration of the experiment without becoming unavailable. But that is still useful. Connection handling, timeout behavior, parser robustness, and state management are all part of the attack surface of an industrial protocol implementation.

And real Siemens products have historically had security advisories involving port 102, including connection exhaustion and other protocol-handling issues. Siemens has also published advisories for vulnerabilities affecting S7 communication over TCP/102. The lesson from the lab is therefore: Protocol state and resource handling deserve to be tested independently of application-level functionality.


The Emulator Boundary

This was probably the most important discipline in the whole project. A protocol emulator is incredibly useful. It gives me something I can repeatedly connect to, instrument, reset, and abuse without worrying about damaging production equipment. But it also creates a boundary around the evidence.

The experiments demonstrated that the Snap7 implementation:

  • accepted the tested unauthenticated WriteVar request against DB3
  • enforced the configured DB boundary for an out-of-range write
  • returned SZL 0x0011 identification data
  • processed a ReadVar request before Setup Communication
  • rejected the tested pre-Setup WriteVar operation
  • did not implement the tested CPU control request
  • remained available after the controlled 50-session experiment

But they did not demonstrate:

  • that a physical Siemens PLC would accept the same unauthenticated write
  • that a physical PLC would enter STOP from the constructed control request
  • that 50 held-open sessions constitute a denial-of-service condition
  • that every S7-300, S7-400, S7-1200, or S7-1500 behaves this way
  • that the tested Snap7 state-machine behavior represents Siemens' implementation

Keeping that boundary intact makes the research stronger. It is very easy to turn a packet capture into a claim about an entire product family, but I would rather have a smaller claim that I can actually defend.


From S7comm to S7CommPlus

Classic S7comm also provides a useful reference point for understanding why Siemens introduced additional security mechanisms into newer generations.

Modern S7-1200 and S7-1500 systems include configurable access protection and newer secure communication mechanisms. Siemens documentation describes protection levels that can restrict reading, writing, and CPU state changes, depending on the configured access level.

Siemens has also documented TLS-based secure PG/HMI communication in newer firmware and TIA Portal environments. For example, its S7-1500 communication documentation lists TLS 1.2 and TLS 1.3 support for newer firmware versions. The security model has therefore moved considerably beyond the classic S7comm environment. The protocol stack itself is still relevant, though.

TCP/102, TPKT, and COTP remain important pieces of the communication path. What changes is what happens above that foundation: session establishment, authentication, integrity protection, confidentiality, authorization, and the handling of protected operations. That atters because securing the application layer does not make the lower layers irrelevant. An attacker still needs to understand how the device establishes communication before they can meaningfully investigate what is protected above it.

There is also no single timeless “S7CommPlus” behavior that can be applied to every modern Siemens CPU. Protocol generations, firmware versions, configuration, and enabled communication paths matter. Siemens itself has documented the transition toward TLS-protected communication and has issued security advisories involving legacy communication mechanisms. One advisory, for example, describes weaknesses in legacy key protection and notes the introduction of individual device passwords and TLS-protected PG/PC and HMI communication in newer TIA Portal and CPU versions.

That makes the contrast more useful than simply saying that S7comm is “old” and S7CommPlus is “secure.” The interesting question is what security properties were added, where they were added, and which communication paths still depend on legacy mechanisms.


Security Characteristics

Area Observation
Transport S7comm laboratory communication used TCP/102 with TPKT and COTP
Session establishment COTP CR/CC exchange exposed TSAP and reference parameters
PDU negotiation Setup Communication negotiated a 480-byte S7 PDU
Memory access ReadVar successfully accessed DB3 in the Snap7 lab
Memory modification WriteVar successfully modified DB3 without authentication in the tested configuration
Bounds enforcement Out-of-range DB3 write was rejected with 0x05
Diagnostics SZL 0x0011 returned module identification data
State enforcement Snap7 accepted ReadVar before Setup Communication
Write state handling Pre-Setup WriteVar did not successfully modify memory
CPU control Tested UserData control request was not implemented by Snap7
Resource handling 50 held-open test sessions did not make the Snap7 target unavailable
Modern security contrast Newer Siemens platforms provide configurable access protection and secure communication mechanisms

This result is a much clearer picture than I had when I started. S7comm is not simply “TCP port 102.” It is a layered conversation with its own transport setup, session negotiation, memory model, diagnostic services, and state assumptions. And once those assumptions become visible on the wire, they become testable.


Closing the Loop

The most satisfying part of this research was not finding one magic packet. It was watching the protocol reveal itself one layer at a time. First, a COTP connection, then a negotiated PDU, then a memory read, then a write, then diagnostics, then deliberately breaking the expected order and watching the implementation decide what it would tolerate.

After IEC 104, IEC 61850, and PROFINET, S7comm felt like the natural place to finally cross the boundary I had been preparing for throughout the series.

Research Repository

The complete laboratory, research notes, scripts, packet captures, and screenshots are available in the industrial-protocol-labs repository:

https://github.com/404saint/industrial-protocol-labs/tree/main/s7comm-research

The experiments were performed against a local Snap7-based laboratory target. They should not be interpreted as evidence of identical behavior on physical Siemens PLCs.

Top comments (0)