// Package verify connects to a live, already-loaded driver and exercises the // IOCTL codes and shared-memory sections extracted statically. package verify import ( "fmt" "strings" "time" "golang.org/x/sys/windows" "ringer/extract" ) // IoctlResult is the outcome of a single DeviceIoControl call. type IoctlResult struct { Code uint32 Device string Success bool ErrorCode uint32 Error string BytesReturned uint32 Output []byte TimedOut bool } // DeviceCandidates derives candidate user-mode device paths from extracted // strings. Kernel device names (\Device\X) and symbolic links (\DosDevices\X, // \??\X) are both tried, in the local and global object namespaces. func DeviceCandidates(strs []extract.StringInfo) []string { seen := map[string]bool{} var out []string add := func(p string) { if p == "" || seen[p] { return } seen[p] = true out = append(out, p) } for _, s := range strs { switch s.Category { case "device": name := strings.TrimPrefix(s.Value, `\Device\`) add(`\\.\` + name) add(`\\.\Global\` + name) case "symlink": name := strings.TrimPrefix(s.Value, `\DosDevices\`) name = strings.TrimPrefix(name, `\??\`) add(`\\.\` + name) } } return out } // VerifyIoctls opens each candidate device and sends every IOCTL code, reading // back the output buffer. It returns one result per (device, code) pair. func VerifyIoctls(devices []string, codes []extract.IoctlCode, bufferSize int, timeout time.Duration) []IoctlResult { if bufferSize <= 0 { bufferSize = 4096 } if timeout <= 0 { timeout = 2 * time.Second } var results []IoctlResult for _, dev := range devices { h, err := openDevice(dev) if err != nil { // Record a single failure for the device so the caller knows it // could not be opened, then skip its IOCTLs. results = append(results, IoctlResult{ Device: dev, ErrorCode: uint32(err.(windows.Errno)), Error: err.Error(), }) continue } for _, c := range codes { res := probeIoctl(h, dev, c.Code, bufferSize, timeout) results = append(results, res) } windows.CloseHandle(h) } return results } // openDevice opens a device path with overlapped I/O so probes can be timed out. func openDevice(path string) (windows.Handle, error) { p, err := windows.UTF16PtrFromString(path) if err != nil { return 0, err } return windows.CreateFile( p, windows.GENERIC_READ|windows.GENERIC_WRITE, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OVERLAPPED, 0, ) } // probeIoctl sends one IOCTL with a zeroed input buffer and reads the output. // On ERROR_INSUFFICIENT_BUFFER it retries with a larger output buffer. func probeIoctl(h windows.Handle, dev string, code uint32, bufferSize int, timeout time.Duration) IoctlResult { res := IoctlResult{Code: code, Device: dev} // METHOD_NEITHER dereferences user pointers directly in the kernel; a bad // pointer can bugcheck. We still send it (per "always attempt live") but // with real, valid buffers. in := make([]byte, bufferSize) out := make([]byte, bufferSize) returned, err, timedOut := deviceIoControl(h, code, in, out, timeout) res.TimedOut = timedOut if timedOut { res.Error = "timed out" return res } // Retry with a larger output buffer on insufficient-buffer errors. if err == windows.ERROR_INSUFFICIENT_BUFFER || err == windows.ERROR_MORE_DATA { big := make([]byte, bufferSize*16) returned, err, timedOut = deviceIoControl(h, code, in, big, timeout) if timedOut { res.TimedOut = true res.Error = "timed out" return res } out = big } res.BytesReturned = returned if err != nil { res.ErrorCode = uint32(err.(windows.Errno)) res.Error = win32ErrorName(res.ErrorCode) return res } res.Success = true res.Output = out[:returned] return res } // deviceIoControl performs an overlapped DeviceIoControl with a timeout. func deviceIoControl(h windows.Handle, code uint32, in, out []byte, timeout time.Duration) (returned uint32, err error, timedOut bool) { ev, eerr := windows.CreateEvent(nil, 1, 0, nil) if eerr != nil { return 0, eerr, false } defer windows.CloseHandle(ev) ov := &windows.Overlapped{HEvent: ev} var inPtr, outPtr *byte if len(in) > 0 { inPtr = &in[0] } if len(out) > 0 { outPtr = &out[0] } err = windows.DeviceIoControl(h, code, inPtr, uint32(len(in)), outPtr, uint32(len(out)), &returned, ov) if err == windows.ERROR_IO_PENDING { wait, _ := windows.WaitForSingleObject(ev, uint32(timeout.Milliseconds())) if wait == uint32(windows.WAIT_TIMEOUT) { windows.CancelIoEx(h, ov) return 0, nil, true } err = windows.GetOverlappedResult(h, ov, &returned, false) } return returned, err, false } // win32ErrorName maps common Win32 error codes to friendly names. func win32ErrorName(code uint32) string { names := map[uint32]string{ 0: "ERROR_SUCCESS", 1: "ERROR_INVALID_FUNCTION", 2: "ERROR_FILE_NOT_FOUND", 3: "ERROR_PATH_NOT_FOUND", 5: "ERROR_ACCESS_DENIED", 6: "ERROR_INVALID_HANDLE", 8: "ERROR_NOT_ENOUGH_MEMORY", 50: "ERROR_NOT_SUPPORTED", 87: "ERROR_INVALID_PARAMETER", 122: "ERROR_INSUFFICIENT_BUFFER", 234: "ERROR_MORE_DATA", 997: "ERROR_IO_PENDING", 1117: "ERROR_IO_DEVICE", } if n, ok := names[code]; ok { return n } return fmt.Sprintf("Win32 error %d", code) }