From 47e3470fe96ad056ecb22387426ac763d94df8d4 Mon Sep 17 00:00:00 2001 From: Sean Smith Date: Mon, 6 Apr 2026 12:28:53 -0500 Subject: [PATCH 1/4] Add omronfins support --- modules/omronfins.go | 10 + modules/omronfins/omronfins.go | 289 ++++++++++++++++++++++++++ modules/omronfins/omronfins_test.go | 312 ++++++++++++++++++++++++++++ modules/omronfins/scanner.go | 81 ++++++++ zgrab2_schemas/zgrab2/__init__.py | 1 + zgrab2_schemas/zgrab2/omronfins.py | 35 ++++ 6 files changed, 728 insertions(+) create mode 100644 modules/omronfins.go create mode 100644 modules/omronfins/omronfins.go create mode 100644 modules/omronfins/omronfins_test.go create mode 100644 modules/omronfins/scanner.go create mode 100644 zgrab2_schemas/zgrab2/omronfins.py diff --git a/modules/omronfins.go b/modules/omronfins.go new file mode 100644 index 00000000..63a88321 --- /dev/null +++ b/modules/omronfins.go @@ -0,0 +1,10 @@ +package modules + +import ( + "github.com/zmap/zgrab2" + "github.com/zmap/zgrab2/modules/omronfins" +) + +func init() { + zgrab2.RegisterModule(omronfins.NewModule()) +} diff --git a/modules/omronfins/omronfins.go b/modules/omronfins/omronfins.go new file mode 100644 index 00000000..1677dc6a --- /dev/null +++ b/modules/omronfins/omronfins.go @@ -0,0 +1,289 @@ +package omronfins + +import ( + "encoding/binary" + "encoding/hex" + "errors" + "net" + "strings" +) + +const ( + QUERY_UDP_PACKET = "800002000000006300ef050100" + REQ_ADDR_PACKET = "46494e530000000c000000000000000000000000" + QUERY_TCP_PACKET_PART1 = "46494e5300000015000000020000000080000200" + QUERY_TCP_PACKET_PART2 = "000000ef05050100" + +) + +var NOT_ENOUGH_DATA = errors.New("not enough data to Omron Fins Header") +var NOT_OMRON_FINS = errors.New("not Omron-Fins response") + +var queryUDPBytes []byte +var queryReqAddrBytes []byte +var queryTcpPart1Bytes []byte +var queryTcpPart2Bytes []byte + +func init() { + var err error + queryUDPBytes, err = hex.DecodeString(QUERY_UDP_PACKET) + if err != nil { + panic("Could not decode Omron-Fins query") + } + + queryReqAddrBytes, err = hex.DecodeString(REQ_ADDR_PACKET) + if err != nil { + panic("Could not decode Omron-Fins query") + } + + queryTcpPart1Bytes, err = hex.DecodeString(QUERY_TCP_PACKET_PART1) + if err != nil { + panic("Could not decode Omron-Fins query") + } + + queryTcpPart2Bytes, err = hex.DecodeString(QUERY_TCP_PACKET_PART2) + if err != nil { + panic("Could not decode Omron-Fins query") + } +} + +var MemoryCardTypeMap = map[int]string{ + 0: "No memory card", + 1: "SPARM", + 2: "EPROM", + 3: "EEPROM", +} +var ResponseCodeMap = map[int]string{ + 0x0000: "Normal completion", + 0x0001: "Service was interrupted", + 0x0101: "Local node not part of Network", + 0x0102: "Token time-out, destination node busy", + 0x0103: "Number of transmit retries exceeded", + 0x0104: "Maximum number of frames exceeded", + 0x0105: "Node number setting error (range)", + 0x0106: "Node number duplication error", + 0x0201: "Destination node not part of Network", + 0x0202: "No node with the specified node number", + 0x0203: "Third node not part of Network : Broadcasting was specified", + 0x0204: "Busy error, error still exists", + 0x0205: "Response time-out", + 0x0301: "Error occurred : ERC indicator is lit", + 0x0302: "CPU error occurred in the PC at the destination node", + 0x0303: "A controller error has prevented a normal response", + 0x0304: "Node number setting error", + 0x0401: "An undefined command has been used", + 0x0402: "Cannot process command because the specified unit model or version is wrong", + 0x0501: "Destination node number is not set in the routing table", + 0x0502: "Routing table isn't registered", + 0x0503: "Routing table error", + 0x0504: "Max relay nodes (2) was exceeded", + 0x1001: "The command is longer than the max permissible length", + 0x1002: "The command is shorter than the min permissible length", + 0x1003: "The designated number of data items differs from the actual number", + 0x1004: "An incorrect command format has been used", + 0x1005: "An incorrect header has been used", + 0x1101: "Memory area code invalid or DM is not available", + 0x1102: "Access size is wrong in command", + 0x1103: "First address in inaccessible area", + 0x1104: "The end of specified word range exceeds acceptable range", + 0x1106: "A non-existent program number", + 0x1109: "The size of data items in command block are wrong", + 0x110A: "The IOM break function cannot be executed", + 0x110B: "The response block is longer than the max length", + 0x110C: "An incorrect parameter code has been specified", + 0x2002: "The data is protected", + 0x2003: "Registered table does not exist", + 0x2004: "Search data does not exist", + 0x2005: "Non-existent program number", + 0x2006: "Non-existent file", + 0x2007: "Verification error", + 0x2101: "Specified area is read-only", + 0x2102: "The data is protected", + 0x2103: "Too many files open", + 0x2105: "Non-existent program number", + 0x2106: "Non-existent file", + 0x2107: "File already exists", + 0x2108: "Data cannot be changed", + 0x2201: "The mode is wrong (executing)", + 0x2202: "The mode is wrong (stopped)", + 0x2203: "The PC is in the PROGRAM mode", + 0x2204: "The PC is in the DEBUG mode", + 0x2205: "The PC is in the MONITOR mode", + 0x2206: "The PC is in the RUN mode", + 0x2207: "The specified node is not the control node", + 0x2208: "The mode is wrong and the step cannot be executed", + 0x2301: "The file device does not exist where specified", + 0x2302: "The specified memory does not exist", + 0x2303: "No clock exists", + 0x2401: "Data link table is incorrect", + 0x2502: "Parity / checksum error occurred", + 0x2503: "I/O setting error", + 0x2504: "Too many I/O points", + 0x2505: "CPU bus error", + 0x2506: "I/O duplication error", + 0x2507: "I/O bus error", + 0x2509: "SYSMAC BUS/2 error", + 0x250A: "Special I/O Unit error", + 0x250D: "Duplication in SYSMAC BUS word allocation", + 0x250F: "A memory error has occurred", + 0x2510: "Terminator not connected in SYSMAC BUS system", + 0x2601: "The specified area is not protected", + 0x2602: "An incorrect password has been specified", + 0x2604: "The specified area is protected", + 0x2605: "The service is being executed", + 0x2606: "The service is not being executed", + 0x2607: "Service cannot be execute from local node", + 0x2608: "Service cannot be executed settings are incorrect", + 0x2609: "Service cannot be executed incorrect settings in command data", + 0x260A: "The specified action has already been registered", + 0x260B: "Cannot clear error, error still exists", + 0x3001: "The access right is held by another device", + 0x4001: "Command aborted with ABORT command", +} + +type DeviceInfo struct { + // Response code name of the query request of the device: taken from the map:ResponseCodeMap + ResponseCodeVal string `json:"response_code_val"` + + // The value of the response code of the query request + ResponseCode int `json:"response_code"` + + // The controller model + ControllerModel string `json:"controller_model"` + + // The controller firmware version + ControllerVersion string `json:"controller_version"` + + // Description string of the system + ForSystemUse string `json:"for_system_use"` + + // The ladder logic program size that runs over the controller + ProgramAreaSize int `json:"program_area_size"` + + // The input/ouput memory size of the controller + IOMsize int `json:"io_msize"` + + //No. DM Words of the controller + NoDMSize int `json:"no_dm_size"` + + // Timer/Counter value of the controller + Timer int `json:"time_counter"` + + // Expansion DM Size of the controller + ExpansionDMSize int `json:"expansion_dm_size"` + + // Number of transitions that controller did + NoOfTransitions int `json:"no_of_transitions"` + + // The type of the memory card that is connected to the controller: taken from the map MemoryCardTypeMap + MemoryCardType int `json:"memory_card_type"` + + // the memory card type value + MemoryCardTypeVal string `json:"memory_card_type_val"` + + // The size of the memory card + MemoryCardSize int `json:"memory_card_size"` +} + +const HeaderSize = 12 +const TCPHEADERSIZE = 16 + +func GetDeviceInfo(response []byte, length int, offset int, result *DeviceInfo) { + result.ResponseCode = int(binary.BigEndian.Uint16(response[offset : offset+2])) + val, ok := ResponseCodeMap[result.ResponseCode] + if !ok { + result.ResponseCodeVal = "Unknown response code" + } else { + result.ResponseCodeVal = val + } + offset = offset + 2 + if result.ResponseCode == 0 && length >= offset+92 { + result.ControllerModel = strings.TrimSpace(strings.Split(string(response[offset:offset+0x14]), "\x00")[0]) + offset += 0x14 + result.ControllerVersion = strings.TrimSpace(strings.Split(string(response[offset:offset+0x14]), "\x00")[0]) + offset += 0x14 + result.ForSystemUse = strings.TrimSpace(strings.Split(string(response[offset:offset+40]), "\x00")[0]) + offset += 40 + result.ProgramAreaSize = int(binary.BigEndian.Uint16(response[offset : offset+2])) + offset += 2 + result.IOMsize = int(response[offset]) + offset += 1 + result.NoDMSize = int(binary.BigEndian.Uint16(response[offset : offset+2])) + offset += 2 + result.Timer = int(response[offset]) + offset += 1 + result.ExpansionDMSize = int(response[offset]) + offset += 1 + result.NoOfTransitions = int(binary.BigEndian.Uint16(response[offset : offset+2])) + offset += 2 + result.MemoryCardType = int(response[offset]) + offset += 1 + val, ok := MemoryCardTypeMap[result.MemoryCardType] + if !ok { + result.MemoryCardTypeVal = "Unknown memory card type" + } else { + result.MemoryCardTypeVal = val + } + result.MemoryCardSize = int(binary.BigEndian.Uint16(response[offset : offset+2])) + } +} + +func QueryDeviceTCP(Con net.Conn) (DeviceInfo, error) { + result := DeviceInfo{} + _, err := Con.Write(queryReqAddrBytes) + if err != nil { + return result, err + } + response := make([]byte, 256, 1024) + read, err := Con.Read(response) + if err != nil { + return result, err + } + if read >= 24 && string(response[0:4]) == "FINS" { + clientAddress := response[23:24] + queryPacket := make([]byte, 0, 128) + queryPacket = append(queryPacket, queryTcpPart1Bytes...) + queryPacket = append(queryPacket, clientAddress...) + queryPacket = append(queryPacket, queryTcpPart2Bytes...) + _, err := Con.Write(queryPacket) + if err != nil { + return result, err + } + read, err := Con.Read(response) + if err != nil { + return result, err + } + if read < HeaderSize+TCPHEADERSIZE { + return result, NOT_ENOUGH_DATA + } else if (response[TCPHEADERSIZE] == 0xc0 || response[TCPHEADERSIZE] == 0xc1) && + (binary.BigEndian.Uint16(response[TCPHEADERSIZE+10:TCPHEADERSIZE+12]) == 0x501) && read >= 14 { + GetDeviceInfo(response, read, TCPHEADERSIZE+12, &result) + return result, nil + } + + } + + return result, NOT_OMRON_FINS +} + +func QueryDeviceUDP(Con net.Conn) (DeviceInfo, error) { + result := DeviceInfo{} + _, err := Con.Write(queryUDPBytes) + if err != nil { + return result, err + } + response := make([]byte, 106, 512) + read, err := Con.Read(response) + if err != nil { + return result, err + } + + if read < HeaderSize { + return result, NOT_ENOUGH_DATA + } else if (response[0] == 0xc0 || response[0] == 0xc1) && (binary.BigEndian.Uint16(response[10:12]) == 0x501) && read >= 14 { + GetDeviceInfo(response, read, 12, &result) + return result, nil + } + + return result, NOT_OMRON_FINS +} diff --git a/modules/omronfins/omronfins_test.go b/modules/omronfins/omronfins_test.go new file mode 100644 index 00000000..4374cfbb --- /dev/null +++ b/modules/omronfins/omronfins_test.go @@ -0,0 +1,312 @@ +package omronfins + +import ( + "context" + "encoding/hex" + "fmt" + "io" + "log" + "math/rand" + "net" + "strings" + "testing" + "time" + + "github.com/zmap/zgrab2" +) + +func (cfg *OmronFinsTestConfig) getScanner(t *testing.T) *Scanner { + var module Module + scanner := module.NewScanner() + flags := module.NewFlags().(*Flags) + flags.Port = uint(cfg.port) + flags.TCP = cfg.TCP + flags.TargetTimeout = 5 * time.Second + scanner.Init(flags) + return scanner.(*Scanner) +} + +func _write(writer io.Writer, data []byte) error { + n, err := writer.Write(data) + if err == nil && len(data) != n { + err = io.ErrShortWrite + } + return err +} + +// Configuration for a single test run +type OmronFinsTestConfig struct { + // port where the server listens. + port int + + // Is it TCP or UDP version + TCP bool + // The bytes the server should return. + response1 []byte + + // The bytes the server should return. + response2 []byte + + expectedResult DeviceInfo + + // 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 OmronsConfigs = map[string]OmronFinsTestConfig{ + "Omron Fins CP1L UDP": { + TCP: false, + response1: hexDecode("c00002006300000300ef050100004350314c2d454d343044522d440000002020202030312e3030000000000030312e3037000000000008000000000000000000000000000000000000010000000000000000000000000000000000010004001417800008000000000000"), + + response2: hexDecode("00"), + expectedResult: DeviceInfo{ + ResponseCodeVal: "Normal completion", + ResponseCode: 0, + ControllerModel: "CP1L-EM40DR-D", + ControllerVersion: "01.00", + ForSystemUse: "\b", + ProgramAreaSize: 20, + IOMsize: 23, + NoDMSize: 32768, + Timer: 8, + ExpansionDMSize: 0, + NoOfTransitions: 0, + MemoryCardType: 0, + MemoryCardTypeVal: "No memory card", + MemoryCardSize: 0, + }, + expectedStatus: zgrab2.SCAN_SUCCESS, + expectedError: "", + }, + "Omron Fins CP1L TCP": { + TCP: true, + response1: hexDecode("46494e53000000100000000100000000000000fb00000021"), + response2: hexDecode("46494e53000000720000000200000000c0000200fbef00210005050100004350314c2d454d343044522d440000002020202030312e3030000000000030312e3038000000000000000000000000000000000000000000000000010000000000000000000000000000000000010100001417800008000000000000"), + + expectedResult: DeviceInfo{ + ResponseCodeVal: "Normal completion", + ResponseCode: 0, + ControllerModel: "CP1L-EM40DR-D", + ControllerVersion: "01.00", + ForSystemUse: "", + ProgramAreaSize: 20, + IOMsize: 23, + NoDMSize: 32768, + Timer: 8, + ExpansionDMSize: 0, + NoOfTransitions: 0, + MemoryCardType: 0, + MemoryCardTypeVal: "No memory card", + MemoryCardSize: 0, + }, + expectedStatus: zgrab2.SCAN_SUCCESS, + expectedError: "", + }, +} + +func getResult(result any) *DeviceInfo { + finsResult, _ := result.(*DeviceInfo) + + return finsResult +} + +// Start a local server that sends responds after two following packets +func (cfg *OmronFinsTestConfig) runFakeServerTCP(t *testing.T, port uint) net.Listener { + endpoint := fmt.Sprintf("127.0.0.1:%d", port) + listener, err := net.Listen("tcp", endpoint) + if err != nil { + t.Fatal(err) + } + go func() { + for { + sock, err := listener.Accept() + if err != nil { + return + } + 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.response1); err != nil { + log.Printf("Failed writing body to client: %v", err) + return + } + + 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) + } + + if err := _write(sock, cfg.response2); err != nil { + log.Printf("Failed writing body to client: %v", err) + return + } + } + + }() + return listener +} + +// Start a local server that sends responds after two following packets +func (cfg *OmronFinsTestConfig) runFakeServerUDP(t *testing.T, port uint) *net.UDPConn { + endpoint := fmt.Sprintf("127.0.0.1:%d", port) + udpAddr, err := net.ResolveUDPAddr("udp", endpoint) + if err != nil { + t.Fatal(err) + } + sock, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatal(err) + } + go func() { + buf := make([]byte, 1024) + r1, addr, err := sock.ReadFromUDP(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) + } + w, err := sock.WriteToUDP(cfg.response1, addr) + + if w < len(cfg.response1) { + log.Printf("Failed writing body to client: write less fromt he response") + } else if err != nil { + log.Printf("Failed writing body to client: %v", err) + + } + }() + return sock +} + +func (cfg *OmronFinsTestConfig) runTest(t *testing.T, testName string) { + scanner := cfg.getScanner(t) + port := uint(rand.Intn(10000) + 10000) + if cfg.TCP { + server := cfg.runFakeServerTCP(t, port) + defer server.Close() + } else { + server := cfg.runFakeServerUDP(t, port) + defer server.Close() + } + target := zgrab2.ScanTarget{ + IP: net.ParseIP("127.0.0.1"), + Port: port, + } + + 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("%s Wrong status: expected %s, got %s", testName, cfg.expectedStatus, status) + return + } + if err != nil { + if !strings.Contains(err.Error(), cfg.expectedError) { + t.Errorf("%s Wrong error: expected %s, got %s", testName, 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.ResponseCode != cfg.expectedResult.ResponseCode { + t.Errorf("Received different scan results, actual ResponseCode %d, expected ResponseCode %d", + result.ResponseCode, + cfg.expectedResult.ResponseCode, + ) + } else if result.ResponseCodeVal != cfg.expectedResult.ResponseCodeVal { + t.Errorf("Received different scan results, actual ResponseCodeVal %s, expected ResponseCodeVal %s", + result.ResponseCodeVal, + cfg.expectedResult.ResponseCodeVal, + ) + } else if result.ControllerModel != cfg.expectedResult.ControllerModel { + t.Errorf("Received different scan results, actual ControllerModel %s, expected ControllerModel %s", + result.ControllerModel, + cfg.expectedResult.ControllerModel, + ) + } else if result.ControllerVersion != cfg.expectedResult.ControllerVersion { + t.Errorf("Received different scan results, actual ControllerVersion %s, expected ControllerVersion %s", + result.ControllerVersion, + cfg.expectedResult.ControllerVersion, + ) + } else if result.ForSystemUse != cfg.expectedResult.ForSystemUse { + t.Errorf("Received different scan results, actual ForSystemUse %s, expected ForSystemUse %s", + result.ForSystemUse, + cfg.expectedResult.ForSystemUse, + ) + } else if result.ProgramAreaSize != cfg.expectedResult.ProgramAreaSize { + t.Errorf("Received different scan results, actual ProgramAreaSize %d, expected ProgramAreaSize %d", + result.ProgramAreaSize, + cfg.expectedResult.ProgramAreaSize, + ) + } else if result.IOMsize != cfg.expectedResult.IOMsize { + t.Errorf("Received different scan results, actual IOMsize %d, expected IOMsize %d", + result.IOMsize, + cfg.expectedResult.IOMsize, + ) + } else if result.NoDMSize != cfg.expectedResult.NoDMSize { + t.Errorf("Received different scan results, actual NoDMSize %d, expected NoDMSize %d", + result.NoDMSize, + cfg.expectedResult.NoDMSize, + ) + } else if result.Timer != cfg.expectedResult.Timer { + t.Errorf("Received different scan results, actual Timer %d, expected Timer %d", + result.Timer, + cfg.expectedResult.Timer, + ) + } else if result.ExpansionDMSize != cfg.expectedResult.ExpansionDMSize { + t.Errorf("Received different scan results, actual ExpansionDMSize %d, expected ExpansionDMSize %d", + result.ExpansionDMSize, + cfg.expectedResult.ExpansionDMSize, + ) + } else if result.NoOfTransitions != cfg.expectedResult.NoOfTransitions { + t.Errorf("Received different scan results, actual NoOfTransitions %d, expected NoOfTransitions %d", + result.NoOfTransitions, + cfg.expectedResult.NoOfTransitions, + ) + } else if result.MemoryCardType != cfg.expectedResult.MemoryCardType { + t.Errorf("Received different scan results, actual MemoryCardType %d, expected MemoryCardType %d", + result.MemoryCardType, + cfg.expectedResult.MemoryCardType, + ) + } else if result.MemoryCardTypeVal != cfg.expectedResult.MemoryCardTypeVal { + t.Errorf("Received different scan results, actual MemoryCardTypeVal %s, expected MemoryCardTypeVal %s", + result.MemoryCardTypeVal, + cfg.expectedResult.MemoryCardTypeVal, + ) + } else if result.MemoryCardSize != cfg.expectedResult.MemoryCardSize { + t.Errorf("Received different scan results, actual MemoryCardSize %d, expected MemoryCardSize %d", + result.MemoryCardSize, + cfg.expectedResult.MemoryCardSize, + ) + } + } +} + +func TestOmronFis(t *testing.T) { + for testName, cfg := range OmronsConfigs { + cfg.runTest(t, testName) + } +} diff --git a/modules/omronfins/scanner.go b/modules/omronfins/scanner.go new file mode 100644 index 00000000..e30df9a9 --- /dev/null +++ b/modules/omronfins/scanner.go @@ -0,0 +1,81 @@ +package omronfins + +import ( + "context" + "errors" + + "github.com/zmap/zgrab2" +) + +// Based on nmap omron fins scan script:https://github.com/nmap/nmap/blob/master/scripts/omron-info.nse +// Protocol was two version TCP and UDP both works on port 9600 +// This protocol has also a Wireshark Dissector: https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-omron-fins.c + +// Flags holds the command-line configuration for this scan module, +// you can add custom command-line flags you would want to pass in +// in this module. +type Flags struct { + zgrab2.BaseFlags + + TCP bool `long:"tcp" description:"runs this module in TCP mode"` +} + +// Scanner implements the `zgrab2.Scanner` interface, can be used to +// store scanner's state. +type Scanner struct { + zgrab2.BaseScanner + config *Flags +} + +// Init implements zgrab2.Scanner +func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { + f, _ := flags.(*Flags) + scanner.config = f + scanner.SetBaseFlags(&f.BaseFlags) + + if scanner.config.TCP { + scanner.DialerGroupConfig = &zgrab2.DialerGroupConfig{ + TransportAgnosticDialerProtocol: zgrab2.TransportTCP, + BaseFlags: &f.BaseFlags, + } + } else { + scanner.DialerGroupConfig = &zgrab2.DialerGroupConfig{ + TransportAgnosticDialerProtocol: zgrab2.TransportUDP, + BaseFlags: &f.BaseFlags, + } + } + return nil +} + +// Scan implements zgrab2.Scanner +func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, target *zgrab2.ScanTarget) (zgrab2.ScanStatus, any, error) { + // Open a connection to the target + scantarget, err := dialGroup.Dial(ctx, target) + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } + var result DeviceInfo + if scanner.config.TCP { + result, err = QueryDeviceTCP(scantarget) + } else { + result, err = QueryDeviceUDP(scantarget) + } + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } else if result.ResponseCode != 0 { + return zgrab2.SCAN_PROTOCOL_ERROR, &result, errors.New("got error response code from the device") + } + + return zgrab2.SCAN_SUCCESS, &result, err +} + + +// Module is the implementation of the zgrab2.Module interface. +func NewModule() *zgrab2.TypedModule[Flags, Scanner, *Scanner] { + return zgrab2.NewTypedModule[Flags, Scanner, *Scanner]( + "omronfins", + "Module for the omron fins protocol", + "Module for the omron fins protocol", + 9600, + ) +} diff --git a/zgrab2_schemas/zgrab2/__init__.py b/zgrab2_schemas/zgrab2/__init__.py index 5b0e9ab2..f7fed9db 100644 --- a/zgrab2_schemas/zgrab2/__init__.py +++ b/zgrab2_schemas/zgrab2/__init__.py @@ -12,6 +12,7 @@ from . import mysql from . import mysql_errors from . import ntp +from . import omronfins from . import oracle from . import pop3 from . import postgres diff --git a/zgrab2_schemas/zgrab2/omronfins.py b/zgrab2_schemas/zgrab2/omronfins.py new file mode 100644 index 00000000..67dc555a --- /dev/null +++ b/zgrab2_schemas/zgrab2/omronfins.py @@ -0,0 +1,35 @@ +# zschema sub-schema for zgrab2's omronfins module +# Registers zgrab2-omronfins globally, and omronfins with the main zgrab2 schema. +from zschema.leaves import * +from zschema.compounds import * +import zschema.registry + +from . import zgrab2 + +omronfins_scan_response = SubRecord( + { + "result": SubRecord( + { + "response_code_val": String(), + "response_code": Unsigned16BitInteger(), + "controller_model": String(), + "controller_version": String(), + "for_system_use": String(), + "program_area_size": Unsigned16BitInteger(), + "io_msize": Unsigned8BitInteger(), + "no_dm_size": Unsigned16BitInteger(), + "time_counter": Unsigned8BitInteger(), + "expansion_dm_size": Unsigned8BitInteger(), + "no_of_transitions": Unsigned16BitInteger(), + "memory_card_type": Unsigned8BitInteger(), + "memory_card_type_val": String(), + "memory_card_size": Unsigned16BitInteger(), + } + ) + }, + extends=zgrab2.base_scan_response, +) + +zschema.registry.register_schema("zgrab2-omronfins", omronfins_scan_response) + +zgrab2.register_scan_response_type("omronfins", omronfins_scan_response) From 731cf334da150c380fb8eafd2e823cb0c247429d Mon Sep 17 00:00:00 2001 From: Sean Smith Date: Mon, 8 Jun 2026 15:12:24 -0500 Subject: [PATCH 2/4] Lint/format conformance --- modules/omronfins/omronfins.go | 13 ++++++------- modules/omronfins/omronfins_test.go | 6 +++--- modules/omronfins/scanner.go | 3 +-- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/modules/omronfins/omronfins.go b/modules/omronfins/omronfins.go index 1677dc6a..a04842b8 100644 --- a/modules/omronfins/omronfins.go +++ b/modules/omronfins/omronfins.go @@ -13,11 +13,10 @@ const ( REQ_ADDR_PACKET = "46494e530000000c000000000000000000000000" QUERY_TCP_PACKET_PART1 = "46494e5300000015000000020000000080000200" QUERY_TCP_PACKET_PART2 = "000000ef05050100" - ) -var NOT_ENOUGH_DATA = errors.New("not enough data to Omron Fins Header") -var NOT_OMRON_FINS = errors.New("not Omron-Fins response") +var ErrNotEnoughData = errors.New("not enough data to Omron Fins Header") +var ErrNotOmronFins = errors.New("not Omron-Fins response") var queryUDPBytes []byte var queryReqAddrBytes []byte @@ -254,7 +253,7 @@ func QueryDeviceTCP(Con net.Conn) (DeviceInfo, error) { return result, err } if read < HeaderSize+TCPHEADERSIZE { - return result, NOT_ENOUGH_DATA + return result, ErrNotEnoughData } else if (response[TCPHEADERSIZE] == 0xc0 || response[TCPHEADERSIZE] == 0xc1) && (binary.BigEndian.Uint16(response[TCPHEADERSIZE+10:TCPHEADERSIZE+12]) == 0x501) && read >= 14 { GetDeviceInfo(response, read, TCPHEADERSIZE+12, &result) @@ -263,7 +262,7 @@ func QueryDeviceTCP(Con net.Conn) (DeviceInfo, error) { } - return result, NOT_OMRON_FINS + return result, ErrNotOmronFins } func QueryDeviceUDP(Con net.Conn) (DeviceInfo, error) { @@ -279,11 +278,11 @@ func QueryDeviceUDP(Con net.Conn) (DeviceInfo, error) { } if read < HeaderSize { - return result, NOT_ENOUGH_DATA + return result, ErrNotEnoughData } else if (response[0] == 0xc0 || response[0] == 0xc1) && (binary.BigEndian.Uint16(response[10:12]) == 0x501) && read >= 14 { GetDeviceInfo(response, read, 12, &result) return result, nil } - return result, NOT_OMRON_FINS + return result, ErrNotOmronFins } diff --git a/modules/omronfins/omronfins_test.go b/modules/omronfins/omronfins_test.go index 4374cfbb..08fd7807 100644 --- a/modules/omronfins/omronfins_test.go +++ b/modules/omronfins/omronfins_test.go @@ -16,9 +16,9 @@ import ( ) func (cfg *OmronFinsTestConfig) getScanner(t *testing.T) *Scanner { - var module Module - scanner := module.NewScanner() - flags := module.NewFlags().(*Flags) + m := NewModule() + scanner := m.NewScanner() + flags := m.NewFlags().(*Flags) flags.Port = uint(cfg.port) flags.TCP = cfg.TCP flags.TargetTimeout = 5 * time.Second diff --git a/modules/omronfins/scanner.go b/modules/omronfins/scanner.go index e30df9a9..399ee586 100644 --- a/modules/omronfins/scanner.go +++ b/modules/omronfins/scanner.go @@ -24,7 +24,7 @@ type Flags struct { // store scanner's state. type Scanner struct { zgrab2.BaseScanner - config *Flags + config *Flags } // Init implements zgrab2.Scanner @@ -69,7 +69,6 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup, return zgrab2.SCAN_SUCCESS, &result, err } - // Module is the implementation of the zgrab2.Module interface. func NewModule() *zgrab2.TypedModule[Flags, Scanner, *Scanner] { return zgrab2.NewTypedModule[Flags, Scanner, *Scanner]( From b052ed73a8646da2c17152ac91c485a46005d939 Mon Sep 17 00:00:00 2001 From: Sean Smith Date: Mon, 8 Jun 2026 22:51:00 -0500 Subject: [PATCH 3/4] Include fuzz tests --- modules/omronfins/omronfins_fuzz_test.go | 115 +++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 modules/omronfins/omronfins_fuzz_test.go diff --git a/modules/omronfins/omronfins_fuzz_test.go b/modules/omronfins/omronfins_fuzz_test.go new file mode 100644 index 00000000..a7848cb0 --- /dev/null +++ b/modules/omronfins/omronfins_fuzz_test.go @@ -0,0 +1,115 @@ +package omronfins + +import ( + "encoding/hex" + "net" + "testing" +) + +// FuzzGetDeviceInfo fuzzes the GetDeviceInfo parsing function directly. +func FuzzGetDeviceInfo(f *testing.F) { + // Seed: valid UDP response payload starting at offset 12 (response code + device fields) + // This is the tail of the CP1L UDP response from the unit tests. + f.Add( + hexMustDecode("00004350314c2d454d343044522d440000002020202030312e3030000000000030312e3037000000000008000000000000000000000000000000000000010000000000000000000000000000000000010004001417800008000000000000"), + 0, + ) + // Seed: non-zero response code (service interrupted) + f.Add([]byte{0x00, 0x01}, 0) + // Seed: zero response code but truncated (no device fields) + f.Add([]byte{0x00, 0x00}, 0) + // Seed: minimal 2-byte with non-zero offset + f.Add([]byte{0x00, 0x00, 0x00, 0x00}, 2) + + f.Fuzz(func(t *testing.T, data []byte, offset int) { + if offset < 0 || offset >= len(data) { + return + } + if offset+2 > len(data) { + return + } + result := &DeviceInfo{} + GetDeviceInfo(data, len(data), offset, result) + }) +} + +// FuzzQueryDeviceUDP fuzzes the UDP query path with arbitrary server responses. +func FuzzQueryDeviceUDP(f *testing.F) { + // Seed: valid CP1L UDP response + f.Add(hexMustDecode("c00002006300000300ef050100004350314c2d454d343044522d440000002020202030312e3030000000000030312e3037000000000008000000000000000000000000000000000000010000000000000000000000000000000000010004001417800008000000000000")) + // Seed: too short + f.Add([]byte{0xc0, 0x00}) + // Seed: wrong magic byte + f.Add([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + // Seed: valid header prefix but truncated payload + f.Add([]byte{0xc0, 0x00, 0x02, 0x00, 0x63, 0x00, 0x00, 0x03, 0x00, 0xef, 0x05, 0x01, 0x00, 0x00}) + + f.Fuzz(func(t *testing.T, data []byte) { + clientConn, serverConn := net.Pipe() + done := make(chan struct{}) + + go func() { + defer close(done) + defer serverConn.Close() + buf := make([]byte, 1024) + serverConn.Read(buf) + serverConn.Write(data) + }() + + _, _ = QueryDeviceUDP(clientConn) + clientConn.Close() + <-done + }) +} + +// FuzzQueryDeviceTCP fuzzes the TCP query path with arbitrary server responses. +func FuzzQueryDeviceTCP(f *testing.F) { + // Seed: valid CP1L TCP address response + query response pair + f.Add( + hexMustDecode("46494e53000000100000000100000000000000fb00000021"), + hexMustDecode("46494e53000000720000000200000000c0000200fbef00210005050100004350314c2d454d343044522d440000002020202030312e3030000000000030312e3038000000000000000000000000000000000000000000000000010000000000000000000000000000000000010100001417800008000000000000"), + ) + // Seed: valid FINS header but short + f.Add( + hexMustDecode("46494e530000000800000001000000000000000000000000"), + []byte{0x00}, + ) + // Seed: non-FINS response + f.Add( + []byte{0x00, 0x00, 0x00, 0x00}, + []byte{0x00}, + ) + // Seed: empty responses + f.Add([]byte{}, []byte{}) + + f.Fuzz(func(t *testing.T, response1 []byte, response2 []byte) { + clientConn, serverConn := net.Pipe() + done := make(chan struct{}) + + go func() { + defer close(done) + defer serverConn.Close() + buf := make([]byte, 1024) + // Read the address request + serverConn.Read(buf) + // Send first response (address assignment) + serverConn.Write(response1) + // Read the query request + serverConn.Read(buf) + // Send second response (device info) + serverConn.Write(response2) + }() + + _, _ = QueryDeviceTCP(clientConn) + clientConn.Close() + <-done + }) +} + +func hexMustDecode(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic("bad hex in test seed: " + err.Error()) + } + return b +} From 3b0a27607c43cb28cb4a1f5e3a501a795c9ff434 Mon Sep 17 00:00:00 2001 From: Sean Smith Date: Mon, 8 Jun 2026 22:57:10 -0500 Subject: [PATCH 4/4] Fix omronfins review findings: bounds check, TCP length guard, fuzz deadlock - Add early bounds check in GetDeviceInfo before reading response code - Fix TCP read >= 14 to read >= TCPHEADERSIZE+14 for correct minimum length - Add deadlines to fuzz test net.Pipe connections to prevent hangs - Remove empty-bytes seed from TCP fuzz that could deadlock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/omronfins/omronfins.go | 5 ++++- modules/omronfins/omronfins_fuzz_test.go | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/modules/omronfins/omronfins.go b/modules/omronfins/omronfins.go index a04842b8..29798e06 100644 --- a/modules/omronfins/omronfins.go +++ b/modules/omronfins/omronfins.go @@ -188,6 +188,9 @@ const HeaderSize = 12 const TCPHEADERSIZE = 16 func GetDeviceInfo(response []byte, length int, offset int, result *DeviceInfo) { + if offset+2 > len(response) || offset+2 > length { + return + } result.ResponseCode = int(binary.BigEndian.Uint16(response[offset : offset+2])) val, ok := ResponseCodeMap[result.ResponseCode] if !ok { @@ -255,7 +258,7 @@ func QueryDeviceTCP(Con net.Conn) (DeviceInfo, error) { if read < HeaderSize+TCPHEADERSIZE { return result, ErrNotEnoughData } else if (response[TCPHEADERSIZE] == 0xc0 || response[TCPHEADERSIZE] == 0xc1) && - (binary.BigEndian.Uint16(response[TCPHEADERSIZE+10:TCPHEADERSIZE+12]) == 0x501) && read >= 14 { + (binary.BigEndian.Uint16(response[TCPHEADERSIZE+10:TCPHEADERSIZE+12]) == 0x501) && read >= TCPHEADERSIZE+14 { GetDeviceInfo(response, read, TCPHEADERSIZE+12, &result) return result, nil } diff --git a/modules/omronfins/omronfins_fuzz_test.go b/modules/omronfins/omronfins_fuzz_test.go index a7848cb0..388c7ada 100644 --- a/modules/omronfins/omronfins_fuzz_test.go +++ b/modules/omronfins/omronfins_fuzz_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "net" "testing" + "time" ) // FuzzGetDeviceInfo fuzzes the GetDeviceInfo parsing function directly. @@ -46,6 +47,9 @@ func FuzzQueryDeviceUDP(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { clientConn, serverConn := net.Pipe() + deadline := time.Now().Add(5 * time.Second) + clientConn.SetDeadline(deadline) + serverConn.SetDeadline(deadline) done := make(chan struct{}) go func() { @@ -79,11 +83,11 @@ func FuzzQueryDeviceTCP(f *testing.F) { []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00}, ) - // Seed: empty responses - f.Add([]byte{}, []byte{}) - f.Fuzz(func(t *testing.T, response1 []byte, response2 []byte) { clientConn, serverConn := net.Pipe() + deadline := time.Now().Add(5 * time.Second) + clientConn.SetDeadline(deadline) + serverConn.SetDeadline(deadline) done := make(chan struct{}) go func() {