diff --git a/modules/codesys2.go b/modules/codesys2.go new file mode 100644 index 00000000..5676280b --- /dev/null +++ b/modules/codesys2.go @@ -0,0 +1,7 @@ +package modules + +import codesys2 "github.com/zmap/zgrab2/modules/codesys2" + +func init() { + codesys2.RegisterModule() +} diff --git a/modules/codesys2/codesysv2.go b/modules/codesys2/codesysv2.go new file mode 100755 index 00000000..0561efdc --- /dev/null +++ b/modules/codesys2/codesysv2.go @@ -0,0 +1,261 @@ +package codesys2 + +import ( + "bytes" + "encoding/binary" +) + +const CodeSysV2Magic = 0xbbbb +const HeaderSize = 6 + +type CodeSysV2Header struct { + Magic uint16 + Length uint32 +} + +func (header *CodeSysV2Header) New() { + header.Magic = CodeSysV2Magic +} + +func (header *CodeSysV2Header) SetHeaderSize(payload any) { + data, err := Marshal(payload, binary.BigEndian) + if err == nil { + header.Length = uint32(len(data) - HeaderSize) + } +} + +type CodeSysV2Request struct { + CodeSysV2Header + Cmd byte +} + +func (request *CodeSysV2Request) New(cmd byte) { + request.CodeSysV2Header.New() + request.Cmd = cmd +} + +const ( + Login = 0x1 + + Logout = 0x2 + + Start = 0x3 + + Stop = 0x4 + + Readvariablelist = 0x5 + + Writevariablelist = 0x6 + + Enable = 0x7 + + Disable = 0x8 + + Force = 0x9 + + Stepin = 0xa + + Stepover = 0xb + + Setbreakpoint = 0xc + + Deletebreakpoint = 0xd + + Deleteallbreakpoints = 0xe + + Go = 0xf + + Readstatus = 0x10 + + Readidentity = 0x11 + + Readbreakpointlist = 0x12 + + Reset = 0x13 + + Definevariablelist = 0x14 + + Deletevariablelist = 0x15 + + Callstack = 0x17 + + Cycle = 0x18 + + Defineflowcontrol = 0x19 + + Readflowcontrol = 0x1a + + Stopflowcontrol = 0x1b + + Definetrace = 0x1c + + Starttrace = 0x1d + + Readtrace = 0x1e + + Stoptrace = 0x1f + + Forcevariables = 0x20 + + Releasevariables = 0x21 + + Onlinechange = 0x22 + + Startstep = 0x23 + + Cyclestep = 0x24 + + Defineaccuflow = 0x28 + + Definesnapshot = 0x29 + + Cancelsnapshot = 0x2a + + Exit = 0x2b + + ReadWritevariable = 0x2c + + Defineconfig = 0x2d + + Readvariablesdirect = 0x2e + + Filewritestart = 0x2f + + Filewritecontinue = 0x30 + + Filereadstart = 0x31 + + Filereadcontinue = 0x32 + + Filereadlist = 0x33 + + Filereadinfo = 0x34 + + Filerename = 0x35 + + Filedelete = 0x36 + + Downloadtaskconfig = 0x37 + + Definedebugtask = 0x38 + + Createbootproject = 0x39 + + Downloadsymbols = 0x3a + + Readtaskruntimeinfo = 0x3b + + Writevariablesdirect = 0x3c + + Seteventcycletime = 0x3d + + DownloadIODescription = 0x3e + + Visualizationready = 0x3f + + Downloadprojectinfo = 0x40 + + Checkbootproject = 0x41 + + Checktargetid = 0x42 + + Filetransferdone = 0x43 + + Readvariablesex = 0x44 + + Writevariablesex = 0x45 + + Readvariablesdirectex = 0x46 + + Writevariablesdirectex = 0x47 + + FileDir = 0x48 + + ForceIntracycle = 0x48 + + ForceIntracyclePRE = 0x49 + + Extendedvariableservice = 0x50 + + Extendeddebugservice = 0x51 + + GLdownload = 0x64 + + GLobserve = 0x65 + + GLdownloadblock = 0x66 + + Download = 0x80 + + Downloadsource = 0x81 + + Uploadsource = 0x82 + + Flash = 0x83 + + Downloadready = 0x8f + + Getlasterror = 0x90 + + Setpassword = 0x91 + + Browsercommand = 0x92 + + ODservice = 0x93 +) + +type CodeSysV2LoginRequest struct { + CodeSysV2Request + Unknown1 uint32 + Unknown2 uint32 + PasswordLength uint32 +} + +// Login request as anonymous user to get information from the device, even if this is not allowed the device will response with information +func (request *CodeSysV2LoginRequest) New() { + request.CodeSysV2Request.New(Login) + request.Unknown1 = 4 + request.Unknown2 = 6 + request.PasswordLength = 0 + request.SetHeaderSize(request) +} + +type CodeSysV2LoginResponse struct { + CodeSysV2Header + LoginResult uint16 + Unknown1 [56]byte + OsType [28]byte + Unknown2 uint32 + OsVersion [32]byte + Vendor [28]byte + Unknown3 [56]byte +} + +func Marshal(packet any, byteOrder binary.ByteOrder) ([]byte, error) { + data := make([]byte, 0, 1024) + buffer := bytes.NewBuffer(data) + buffer.Reset() + err := binary.Write(buffer, byteOrder, packet) + if err != nil { + return nil, err + } + + return buffer.Bytes(), err +} + +func UnMarshal(packet []byte, byteOrder binary.ByteOrder, packet_struct any) error { + buffer := bytes.NewBuffer(packet) + err := binary.Read(buffer, byteOrder, packet_struct) + return err +} + +type CodeSysV2DeviceInfo struct { + // The operation system that runs on the device + OsType string `json:"os_type"` + + // The operation system version that runs on the device + OsVersion string `json:"os_version"` + + // The vendor of the device + Vendor string `json:"vendor"` +} diff --git a/modules/codesys2/codesysv2_test.go b/modules/codesys2/codesysv2_test.go new file mode 100644 index 00000000..17339063 --- /dev/null +++ b/modules/codesys2/codesysv2_test.go @@ -0,0 +1,187 @@ +package codesys2 + +import ( + "context" + "encoding/hex" + "fmt" + "io" + "log" + "net" + "strings" + "testing" + "time" + + "github.com/zmap/zgrab2" +) + +// Helper function to write and check for short writes +func _write(writer io.Writer, data []byte) error { + n, err := writer.Write(data) + if err == nil && len(data) != n { + err = io.ErrShortWrite + } + return err +} + +func (cfg *CodeSysV2TestConfig) getScanner(t *testing.T) *Scanner { + var module Module + scanner := module.NewScanner() + flags := module.NewFlags().(*Flags) + flags.Port = uint(cfg.port) + flags.TargetTimeout = 2 * time.Second + scanner.Init(flags) + return scanner.(*Scanner) +} + +// Configuration for a single test run +type CodeSysV2TestConfig struct { + // port where the server listens. + port int + + // number of loop the fake server need to serve the scanner + numberOfLoop int + // The bytes the server should return. + response []byte + + expectedResult CodeSysV2DeviceInfo + + // The status that should be returned by the scan. + expectedStatus zgrab2.ScanStatus + + // If set, the error returned by the scan must contain this. + expectedError string +} + +func hexDecode(s string) []byte { + decodeString, err := hex.DecodeString(s) + if err != nil { + return nil + } + + return decodeString +} + +var CodeSysV2Configs = map[string]CodeSysV2TestConfig{ + "Virtual": { + port: 1200, + numberOfLoop: 1, + response: hexDecode("bbbbce000000e8030100000000000000b00d0000000002007e13000000000200f401000000000000fa" + + "000000000000000000000000000000c80600000100010057696e646f77730000000000000000000000000000000000000000000000" + + "00004e542f323030302f5850205b72756e74696d6520706f72742076332028322e0033532d536d61727420536f66747761726520536f" + + "6c7574696f6e730000000000f4ff000000000000010000000000ffff000000000000c80000000300000100100000001000000010000000002000003030000100"), + expectedResult: CodeSysV2DeviceInfo{ + OsType: "Windows", + Vendor: "3S-Smart Software Solutions", + OsVersion: "NT/2000/XP [runtime port v3 (2.", + }, + expectedStatus: zgrab2.SCAN_SUCCESS, + expectedError: "", + }, + "ABB": { + port: 1200, + numberOfLoop: 2, + response: hexDecode("bbbb000000ce03e8000000010000000000020cb0000013880000137e00001388000001f400000800000000fa" + + "000000000000000000000000000006d400030001534d580000000000000000000000000000000000000000000000000000000000736d7" + + "850504320332e352e32000000000000000000" + + "0000000000000000000000414242000000000000000000000000000000000000000000000000000000000000000400000000000008000000" + + "000000139400000000000000c80003000100008000000080000001000000040000000240000000"), + expectedResult: CodeSysV2DeviceInfo{ + OsType: "SMX", + Vendor: "ABB", + OsVersion: "smxPPC 3.5.2", + }, + expectedStatus: zgrab2.SCAN_SUCCESS, + expectedError: "", + }, +} + +func getResult(result any) CodeSysV2DeviceInfo { + codesysV2Result, _ := result.(CodeSysV2DeviceInfo) + + return codesysV2Result +} + +// Start a local server that sends responds after two following packets +func (cfg *CodeSysV2TestConfig) runFakeCodeSysV2Server(t *testing.T, numberofLoop int) net.Listener { + endpoint := fmt.Sprintf("127.0.0.1:%d", cfg.port) + listener, err := net.Listen("tcp", endpoint) + if err != nil { + t.Fatal(err) + } + go func() { + for i := 0; i < numberofLoop; i++ { + sock, err := listener.Accept() + if err != nil { + log.Fatal(err) + } + defer sock.Close() + + buf := make([]byte, 1024) + r1, err := sock.Read(buf) + if err != nil && err != io.EOF && r1 > 0 { + // Read will return an EOF when it's done reading + log.Fatalf("1 Unexpected error reading from client: %v", err) + } + // The client should ignore this packet but it will wait for it + if err := _write(sock, cfg.response); err != nil { + log.Printf("Failed writing body to client: %v", err) + return + } + } + + }() + return listener +} + +func (cfg *CodeSysV2TestConfig) runTest(t *testing.T, testName string) { + scanner := cfg.getScanner(t) + server := cfg.runFakeCodeSysV2Server(t, cfg.numberOfLoop) + target := zgrab2.ScanTarget{ + IP: net.ParseIP("127.0.0.1"), + Port: 1200, + } + dialerGroup, err := scanner.GetDialerGroupConfig().GetDefaultDialerGroupFromConfig() + if err != nil { + t.Errorf("Unexpected error got %s", err.Error()) + return + } + status, ret, err := scanner.Scan(context.Background(), dialerGroup, &target) + + if status != cfg.expectedStatus { + t.Errorf("Wrong status: expected %s, got %s", cfg.expectedStatus, status) + return + } + if err != nil { + if !strings.Contains(err.Error(), cfg.expectedError) { + t.Errorf("Wrong error: expected %s, got %s", err.Error(), cfg.expectedError) + } + } else if len(cfg.expectedError) > 0 { + t.Errorf("Expected error '%s' but got none", cfg.expectedError) + } + if cfg.expectedStatus == zgrab2.SCAN_SUCCESS { + result := getResult(ret) + if result.OsVersion != cfg.expectedResult.OsVersion { + t.Errorf("Received different scan results, actual OsVersion %s, expected OsVersion %s", + result.OsVersion, + cfg.expectedResult.OsVersion, + ) + } else if result.Vendor != cfg.expectedResult.Vendor { + t.Errorf("Received different scan results, actual Vendor %s, expected Vendor %s", + result.Vendor, + cfg.expectedResult.Vendor, + ) + } else if result.OsType != cfg.expectedResult.OsType { + t.Errorf("Received different scan results, actual OsType %s, expected OsType %s", + result.OsType, + cfg.expectedResult.OsType, + ) + } + server.Close() + } +} + +func TestCodeSysV2(t *testing.T) { + for testName, cfg := range CodeSysV2Configs { + cfg.runTest(t, testName) + } +} diff --git a/modules/codesys2/scanner.go b/modules/codesys2/scanner.go new file mode 100755 index 00000000..7c0893a4 --- /dev/null +++ b/modules/codesys2/scanner.go @@ -0,0 +1,190 @@ +package codesys2 + +import ( + "context" + "encoding/binary" + "errors" + "io" + "net" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/zmap/zgrab2" +) + +// Based on internal research of the protocol strucuture: https://microsoft.sharepoint.com/:w:/t/section52/ET2CESsVyoJCpgUbxaJMay8B8zTu_SdFnd2a41Xd_7X-RQ?e=HoOV4I +// Using the following ports: 1200, 1201, 2455 over TCP +// The protocol has two version little and big endiness +type Flags struct { + zgrab2.BaseFlags +} + +// Module implements the zgrab2.Module interface. +type Module struct { +} + +// Scanner implements the zgrab2.Scanner interface. +type Scanner struct { + config *Flags + dialerGroupConfig *zgrab2.DialerGroupConfig +} + +// RegisterModule registers the zgrab2 module. +func RegisterModule() { + var module Module + _, err := zgrab2.AddCommand("codesys2", "codesys2", module.Description(), 1200, &module) + if err != nil { + log.Fatal(err) + } +} + +// NewFlags returns a default Flags object. +func (module *Module) NewFlags() any { + return new(Flags) +} + +// NewScanner returns a new Scanner instance. +func (module *Module) NewScanner() zgrab2.Scanner { + return new(Scanner) +} + +// Description returns an overview of this module. +func (module *Module) Description() string { + return "Probe for CodeSysV2 devices, usually PLCs as part of a SCADA system" +} + +// Validate checks that the flags are valid. +// On success, returns nil. +// On failure, returns an error instance describing the error. +func (flags *Flags) Validate(_ []string) error { + return nil +} + +// Help returns the module's help string. +func (flags *Flags) Help() string { + return "" +} + +// Init initializes the Scanner. +func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { + f, _ := flags.(*Flags) + scanner.config = f + scanner.dialerGroupConfig = &zgrab2.DialerGroupConfig{ + TransportAgnosticDialerProtocol: zgrab2.TransportTCP, + BaseFlags: &f.BaseFlags, + } + return nil +} + +// InitPerSender initializes the scanner for a given sender. +func (scanner *Scanner) InitPerSender(senderID int) error { + return nil +} + +// GetName returns the Scanner name defined in the Flags. +func (scanner *Scanner) GetName() string { + return scanner.config.Name +} + +// GetTrigger returns the Trigger defined in the Flags. +func (scanner *Scanner) GetTrigger() string { + return scanner.config.Trigger +} + +// Protocol returns the protocol identifier of the scan. +func (scanner *Scanner) Protocol() string { + return "codesys2" +} + +func (scanner *Scanner) GetDialerGroupConfig() *zgrab2.DialerGroupConfig { + return scanner.dialerGroupConfig +} + +// GetScanMetadata returns any metadata on the scan itself from this module. +func (scanner *Scanner) GetScanMetadata() any { + return nil +} + +// Conn wraps the connection state (more importantly, it provides the interface used by the old zgrab code, so that it +// could be taken over as-is). +type Conn struct { + Conn net.Conn + scanner *Scanner +} + +func (c *Conn) getUnderlyingConn() net.Conn { + return c.Conn +} + +func (scanner *Scanner) ScanWithByteOrder(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget, order binary.ByteOrder) (zgrab2.ScanStatus, any, error) { + conn, err := dialGroup.Dial(ctx, target) + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } + defer conn.Close() + + c := Conn{Conn: conn, scanner: scanner} + req := CodeSysV2LoginRequest{} + req.New() + + data, err := Marshal(req, order) + if err != nil { + log.Fatalf("Unexpected error marshaling CodesysV2 packet: %v", err) + } + w := 0 + for w < len(data) { + written, writeerr := c.getUnderlyingConn().Write(data[w:]) + w += written + if writeerr != nil { + log.Fatalf("Unexpected error sending CodesysV2 Login Request: %v", err) + return zgrab2.SCAN_PROTOCOL_ERROR, nil, err + } + } + + headerbytes := make([]byte, HeaderSize) + var header CodeSysV2Header + _, err = io.ReadFull(c.getUnderlyingConn(), headerbytes) + if err != nil { + //log.Fatalf("Unexpected error reading CodesysV2 header: %v", err) + return zgrab2.SCAN_PROTOCOL_ERROR, nil, err + } + err = UnMarshal(headerbytes, order, &header) + if err != nil { + //log.Fatalf("Unexpected error unmarshaling CodesysV2 packet: %v", err) + return zgrab2.SCAN_PROTOCOL_ERROR, nil, errors.New("failed to read CodeSysV2 Header") + } else if header.Magic != CodeSysV2Magic { + return zgrab2.SCAN_PROTOCOL_ERROR, nil, errors.New("didn't receive CodesysV2 packet magic") + } else if (header.Length & 0xff000000) != 0 { + return zgrab2.SCAN_PROTOCOL_ERROR, nil, errors.New("seems like the wrong byte order of the protocol") + } + payloadbytes := make([]byte, header.Length) + _, err = io.ReadFull(c.getUnderlyingConn(), payloadbytes) + if err != nil { + //log.Fatalf("Unexpected error reading CodesysV2 payload: %v", err) + return zgrab2.SCAN_PROTOCOL_ERROR, nil, err + } + fullpacketbytes := append(headerbytes, payloadbytes...) + var res CodeSysV2LoginResponse + err = UnMarshal(fullpacketbytes, order, &res) + if err != nil { + //log.Fatalf("Unexpected error unmarshaling CodesysV2 packet: %v", err) + return zgrab2.SCAN_PROTOCOL_ERROR, nil, err + } + + DeviceInfo := CodeSysV2DeviceInfo{OsType: strings.ReplaceAll(string(res.OsType[:]), "\000", ""), + OsVersion: strings.ReplaceAll(string(res.OsVersion[:]), "\000", ""), + Vendor: strings.ReplaceAll(string(res.Vendor[:]), "\000", "")} + return zgrab2.SCAN_SUCCESS, DeviceInfo, nil +} + +// Scanner needs to scan the ports 1200, 1201, 2455 +func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget) (zgrab2.ScanStatus, any, error) { + log.Debugf("Trying to connect to the target with Little Endian version of the protocol...") + scanResult, event, err := scanner.ScanWithByteOrder(ctx, dialGroup, target, binary.LittleEndian) + if scanResult == zgrab2.SCAN_PROTOCOL_ERROR { + log.Debugf("Trying to connect to the target with Big Endian version of the protocol...") + scanResult, event, err = scanner.ScanWithByteOrder(ctx, dialGroup, target, binary.BigEndian) + } + return scanResult, event, err +} diff --git a/zgrab2_schemas/zgrab2/codesys2.py b/zgrab2_schemas/zgrab2/codesys2.py new file mode 100644 index 00000000..784ce0ab --- /dev/null +++ b/zgrab2_schemas/zgrab2/codesys2.py @@ -0,0 +1,26 @@ +# zschema sub-schema for zgrab2's banner module +# Registers zgrab2-banner globally, and banner with the main zgrab2 schema. +from zschema.leaves import * +from zschema.compounds import * +import zschema.registry + +import zcrypto_schemas.zcrypto as zcrypto +from . import zgrab2 + +# modules/banner/scanner.go - Results +codesys2_scan_response = SubRecord( + { + "result": SubRecord( + { + "os_type": String(), + "os_version": String(), + "vendor": String(), + } + ) + }, + extends=zgrab2.base_scan_response, +) + +zschema.registry.register_schema("zgrab2-codesys2", codesys2_scan_response) + +zgrab2.register_scan_response_type("codesys2", codesys2_scan_response)