S7comm Anatomy: What Travels To A Siemens PLC On Port 102
The stack, the TSAP, the function codes and the S7ANY addressing scheme — plus why classic S7comm is still very much alive on S7-1500.
Siemens S7 is probably the most deployed PLC family on the planet, and S7comm is how you talk to it.
Most engineers who work next to these controllers every day have never looked at what actually moves on the wire.
This is the anatomy — the stack, the function codes, the addressing scheme, and the part that matters for security.
S7comm Sits On TCP 102
S7comm does not run directly on TCP. It rides on two layers below it.
TPKT is a thin framing layer that gives the ISO transport a length field it can trust.
COTP handles the connection itself — a Connection Request, a Connection Confirm, then data.
One thing to know before going further. Classic S7comm starts with 0x32. S7CommPlus, which S7-1200 and S7-1500 use for TIA Portal and HMI traffic, starts with 0x72 and rides the exact same TCP 102 stack. Same port, different protocol. Everything below describes the classic 0x32 variant.
The TSAP Decides Whether You Get A Session
Inside the COTP Connection Request sit two TSAPs, source and destination.
For the classic PG or OP connection the destination TSAP is two bytes:
- first byte is connection type — 0x01 PG, 0x02 OP or HMI, 0x03 and up for S7 basic communication
- second byte is rack multiplied by 32, plus slot
For a classic S7-300 the CPU normally sits at rack 0, slot 2. For S7-1200 and S7-1500 the documented value is rack 0, slot 1.
Point at the wrong slot and you do not get silence. The CPU answers the Connection Request with a COTP Disconnect Request instead of a Connect Confirm.
That difference is not a detail — it is exactly why rack and slot enumeration is scriptable, and why a scan that only checks whether port 102 is open tells you far less than a scan that sweeps TSAPs.
The two-byte form is also not universal. The COTP called and calling TSAP are variable-length parameters, and named TSAPs are common — a TIA Portal engineering connection to an S7-1500 uses the ASCII TSAP SIMATIC-ROOT-ES. If your monitoring assumes two bytes, it will not see those sessions.
Setup Communication Comes First
Before any read or write, both sides negotiate with function code 0xF0.
This exchange settles how many requests can be in flight and how large one PDU may be — commonly 240, 480 or 960 bytes.
Note that the negotiated number is the whole S7 PDU including header and parameter block, not usable payload. At 240 the actual data you get back from a read is closer to 222 bytes.
That is the reason a large data block read gets split into several requests.
Every Request Is A Function Code
Once the session is up, each S7comm job carries one function code.
- 0xF0 setup communication
- 0x04 read variable
- 0x05 write variable
- 0x1A request download
- 0x1B download block
- 0x1C download ended
- 0x1D start upload
- 0x1E upload
- 0x28 PI-Service, program invocation
- 0x29 PLC stop
The header is small but it is not fixed length.
Job and userdata headers are 10 bytes. Ack and ack_data headers are 12, because responses carry two extra bytes of error class and error code. If you are writing byte-offset rules against this header, that difference is the thing that breaks them.
Two groups deserve attention. 0x1A and 0x1B are how logic gets downloaded into a controller, and 0x1D and 0x1E are how it gets read back out. 0x28 is worth understanding properly — it is labelled PI-Service and carries a service name string, so it covers not only starting the CPU but also memory reset and block delete. The function code alone does not tell you which.
Addressing: Usually No Tag Names On The Wire
This is the part that surprises people coming from IT.
In the common case a read request does not ask for a tag called MotorSetpoint. It asks for a memory area, a byte offset and a length.
That form uses syntax id 0x10, called S7ANY, and names a memory area:
- 0x81 process inputs
- 0x82 process outputs
- 0x83 flags, the M area
- 0x84 data block
- 0x85 instance data block
- 0x1C counters
- 0x1D timers
Then a transport size — 0x01 bit, 0x02 byte, 0x04 word, 0x06 double word, 0x08 real — and a count.
For the data areas the address field is a bit address, so a byte offset is multiplied by 8. This does not apply everywhere — for timers and counters the field carries the timer or counter number directly, and for BIT transport size the caller already supplies a bit address.
A single read of four bytes starting at byte 20 of DB1 looks like this on the wire:
That is DB1.DBB20 through DBB23.
S7ANY is one of roughly fifteen syntax ids, not the only one. Classic S7comm from TIA Portal to an S7-1200 often uses syntax id 0xB2, a symbolic addressing mode that carries a CRC of the symbol and a chain of LID values rather than an area and an offset. There is still no readable tag name in the packet, but the area-plus-offset model is not universal.
Responses carry a return code per item.
- 0xFF success
- 0x0A object does not exist
- 0x05 address out of range
- 0x03 access denied
- 0x06 data type not supported
The Security Part
Classic S7comm carries no user identity. Nothing in the session says who is asking.
It is not true that there is no password mechanism at all. There is one — a legitimation exchange in the userdata security function group — and it is how a client unlocks a CPU that has been set to a restricted protection level.
The accurate statement is narrower and more useful: that exchange authenticates a shared secret, not a person, and published research going back to Black Hat 2011 has shown it to be replayable. Authorisation is a property of how the CPU was configured, not of anything the requester proved in that session.
S7-300 and S7-400 have three protection levels. S7-1200 V4 and S7-1500 have four access levels, where the highest also blocks HMI access.
The commonly repeated line that S7-1200 and S7-1500 simply moved to S7CommPlus is worth correcting, because it leads people to assume classic S7comm is a legacy concern.
Those controllers still speak classic S7comm whenever PUT/GET communication is enabled — which is how snap7, python-snap7, many historians and a lot of third-party SCADA drivers actually talk to them. Siemens ships PUT/GET disabled by default, and in the field it is frequently switched on because something needs it. Where it is on, everything in this article applies to an S7-1500.
S7CommPlus itself did add session integrity and made naive replay harder. It has also been publicly defeated more than once — the Black Hat Europe 2017 work on early S7CommPlus and the Rogue7 research at Black Hat USA 2019 against S7-1500 are the two to read.
None of this is a criticism of Siemens engineering. These protocols were designed for a network that was physically separated from everything else. The network changed. The protocol is still doing its original job.
What This Means For Monitoring
If you are watching S7 traffic, the useful questions are not about ports.
- which hosts open S7 sessions at all, and which TSAP do they target
- does anything other than the engineering workstation send download functions 0x1A and 0x1B
- does anything read logic back out with 0x1D and 0x1E
- does 0x28 or 0x29 appear outside a change window, and which PI service string did it carry
- which data blocks get written, by whom, and how often
Add one more that is easy to miss. Userdata messages, ROSCTR 0x07, are the reconnaissance step — reading SZL system status lists is how a client fingerprints firmware, hardware and protection settings. It is not a job function code, so a rule set built only from the list above will not see it.
On tooling: Zeek has no S7comm analyzer in its base scripts. The maintained option is the CISA package, github.com/cisagov/icsnpp-s7comm, which also parses S7CommPlus and COTP. Suricata has no s7comm keyword in a default build, so byte-offset rules on the 0x32 header are the practical route there — keeping the 10 versus 12 byte header difference in mind.
Knowing the function code list is what turns a wall of port 102 traffic into something you can actually write a rule against.
Try It Yourself
Reading this is one thing. Opening a session against a running CPU and watching the process answer is another.
- The open-source part of Labshock — github.com/zakharb/labshock
- Start free in Loginward, the starter zone, and work up to the PLC zones
- Full platform and zone list — labshocksecurity.com
- Ask questions in the community — discord.gg/bpmaQFfW76
OT security must be testable. Not documented.
More on education
