From 5fb1695bcbfdfdd353a3f820a97bdb920a240432 Mon Sep 17 00:00:00 2001 From: Aaron Klotz Date: Fri, 18 Aug 2023 13:54:42 -0600 Subject: [PATCH] util/osdiag, util/osdiag/internal/wsc: add code to probe the Windows Security Center for installed software The Windows Security Center is a component that manages the registration of security products on a Windows system. Only products that have obtained a special cert from Microsoft may register themselves using the WSC API. Practically speaking, most vendors do in fact sign up for the program as it enhances their legitimacy. From our perspective, this is useful because it gives us a high-signal source of information to query for the security products installed on the system. I've tied this query into the osdiag package and is run during bugreports. It uses COM bindings that were automatically generated by my prototype metadata processor, however that program still has a few bugs, so I had to make a few manual tweaks. I dropped those binding into an internal package because (for the moment, at least) they are effectively purpose-built for the osdiag use case. We also update the wingoes dependency to pick up BSTR. Fixes #10646 Signed-off-by: Aaron Klotz --- cmd/tailscaled/depaware.txt | 4 +- util/osdiag/internal/wsc/wsc_windows.go | 313 ++++++++++++++++++++++++ util/osdiag/osdiag_windows.go | 107 ++++++++ 3 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 util/osdiag/internal/wsc/wsc_windows.go diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 87e0d8baa..9b99a11e9 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -78,7 +78,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+ - W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled + W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled+ + W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+ github.com/fxamacker/cbor/v2 from tailscale.com/tka @@ -335,6 +336,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/multierr from tailscale.com/control/controlclient+ tailscale.com/util/must from tailscale.com/logpolicy 💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+ + W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+ W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth tailscale.com/util/racebuild from tailscale.com/logpolicy diff --git a/util/osdiag/internal/wsc/wsc_windows.go b/util/osdiag/internal/wsc/wsc_windows.go new file mode 100644 index 000000000..6fb6e5400 --- /dev/null +++ b/util/osdiag/internal/wsc/wsc_windows.go @@ -0,0 +1,313 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Code generated by 'go generate'; DO NOT EDIT. + +package wsc + +import ( + "runtime" + "syscall" + "unsafe" + + "github.com/dblohm7/wingoes" + "github.com/dblohm7/wingoes/com" + "github.com/dblohm7/wingoes/com/automation" +) + +var ( + CLSID_WSCProductList = &com.CLSID{0x17072F7B, 0x9ABE, 0x4A74, [8]byte{0xA2, 0x61, 0x1E, 0xB7, 0x6B, 0x55, 0x10, 0x7A}} +) + +var ( + IID_IWSCProductList = &com.IID{0x722A338C, 0x6E8E, 0x4E72, [8]byte{0xAC, 0x27, 0x14, 0x17, 0xFB, 0x0C, 0x81, 0xC2}} + IID_IWscProduct = &com.IID{0x8C38232E, 0x3A45, 0x4A27, [8]byte{0x92, 0xB0, 0x1A, 0x16, 0xA9, 0x75, 0xF6, 0x69}} +) + +type WSC_SECURITY_PRODUCT_STATE int32 + +const ( + WSC_SECURITY_PRODUCT_STATE_ON = WSC_SECURITY_PRODUCT_STATE(0) + WSC_SECURITY_PRODUCT_STATE_OFF = WSC_SECURITY_PRODUCT_STATE(1) + WSC_SECURITY_PRODUCT_STATE_SNOOZED = WSC_SECURITY_PRODUCT_STATE(2) + WSC_SECURITY_PRODUCT_STATE_EXPIRED = WSC_SECURITY_PRODUCT_STATE(3) +) + +type WSC_SECURITY_SIGNATURE_STATUS int32 + +const ( + WSC_SECURITY_PRODUCT_OUT_OF_DATE = WSC_SECURITY_SIGNATURE_STATUS(0) + WSC_SECURITY_PRODUCT_UP_TO_DATE = WSC_SECURITY_SIGNATURE_STATUS(1) +) + +type WSC_SECURITY_PROVIDER int32 + +const ( + WSC_SECURITY_PROVIDER_FIREWALL = WSC_SECURITY_PROVIDER(1) + WSC_SECURITY_PROVIDER_AUTOUPDATE_SETTINGS = WSC_SECURITY_PROVIDER(2) + WSC_SECURITY_PROVIDER_ANTIVIRUS = WSC_SECURITY_PROVIDER(4) + WSC_SECURITY_PROVIDER_ANTISPYWARE = WSC_SECURITY_PROVIDER(8) + WSC_SECURITY_PROVIDER_INTERNET_SETTINGS = WSC_SECURITY_PROVIDER(16) + WSC_SECURITY_PROVIDER_USER_ACCOUNT_CONTROL = WSC_SECURITY_PROVIDER(32) + WSC_SECURITY_PROVIDER_SERVICE = WSC_SECURITY_PROVIDER(64) + WSC_SECURITY_PROVIDER_NONE = WSC_SECURITY_PROVIDER(0) + WSC_SECURITY_PROVIDER_ALL = WSC_SECURITY_PROVIDER(127) +) + +type SECURITY_PRODUCT_TYPE int32 + +const ( + SECURITY_PRODUCT_TYPE_ANTIVIRUS = SECURITY_PRODUCT_TYPE(0) + SECURITY_PRODUCT_TYPE_FIREWALL = SECURITY_PRODUCT_TYPE(1) + SECURITY_PRODUCT_TYPE_ANTISPYWARE = SECURITY_PRODUCT_TYPE(2) +) + +type IWscProductABI struct { + com.IUnknownABI // Technically IDispatch, but we're bypassing all of that atm +} + +func (abi *IWscProductABI) GetProductName() (pVal string, err error) { + var t0 automation.BSTR + + method := unsafe.Slice(abi.Vtbl, 14)[7] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(unsafe.Pointer(&t0))) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + if e.Failed() { + return + } + } + + pVal = t0.String() + t0.Close() + return +} + +func (abi *IWscProductABI) GetProductState() (val WSC_SECURITY_PRODUCT_STATE, err error) { + method := unsafe.Slice(abi.Vtbl, 14)[8] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(unsafe.Pointer(&val))) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + } + return +} + +func (abi *IWscProductABI) GetSignatureStatus() (val WSC_SECURITY_SIGNATURE_STATUS, err error) { + method := unsafe.Slice(abi.Vtbl, 14)[9] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(unsafe.Pointer(&val))) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + } + return +} + +func (abi *IWscProductABI) GetRemediationPath() (pVal string, err error) { + var t0 automation.BSTR + + method := unsafe.Slice(abi.Vtbl, 14)[10] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(unsafe.Pointer(&t0))) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + if e.Failed() { + return + } + } + + pVal = t0.String() + t0.Close() + return +} + +func (abi *IWscProductABI) GetProductStateTimestamp() (pVal string, err error) { + var t0 automation.BSTR + + method := unsafe.Slice(abi.Vtbl, 14)[11] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(unsafe.Pointer(&t0))) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + if e.Failed() { + return + } + } + + pVal = t0.String() + t0.Close() + return +} + +func (abi *IWscProductABI) GetProductGuid() (pVal string, err error) { + var t0 automation.BSTR + + method := unsafe.Slice(abi.Vtbl, 14)[12] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(unsafe.Pointer(&t0))) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + if e.Failed() { + return + } + } + + pVal = t0.String() + t0.Close() + return +} + +func (abi *IWscProductABI) GetProductIsDefault() (pVal bool, err error) { + var t0 int32 + + method := unsafe.Slice(abi.Vtbl, 14)[13] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(unsafe.Pointer(&t0))) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + if e.Failed() { + return + } + } + + pVal = t0 != 0 + return +} + +type WscProduct struct { + com.GenericObject[IWscProductABI] +} + +func (o WscProduct) GetProductName() (pVal string, err error) { + p := *(o.Pp) + return p.GetProductName() +} + +func (o WscProduct) GetProductState() (val WSC_SECURITY_PRODUCT_STATE, err error) { + p := *(o.Pp) + return p.GetProductState() +} + +func (o WscProduct) GetSignatureStatus() (val WSC_SECURITY_SIGNATURE_STATUS, err error) { + p := *(o.Pp) + return p.GetSignatureStatus() +} + +func (o WscProduct) GetRemediationPath() (pVal string, err error) { + p := *(o.Pp) + return p.GetRemediationPath() +} + +func (o WscProduct) GetProductStateTimestamp() (pVal string, err error) { + p := *(o.Pp) + return p.GetProductStateTimestamp() +} + +func (o WscProduct) GetProductGuid() (pVal string, err error) { + p := *(o.Pp) + return p.GetProductGuid() +} + +func (o WscProduct) GetProductIsDefault() (pVal bool, err error) { + p := *(o.Pp) + return p.GetProductIsDefault() +} + +func (o WscProduct) IID() *com.IID { + return IID_IWscProduct +} + +func (o WscProduct) Make(r com.ABIReceiver) any { + if r == nil { + return WscProduct{} + } + + runtime.SetFinalizer(r, com.ReleaseABI) + + pp := (**IWscProductABI)(unsafe.Pointer(r)) + return WscProduct{com.GenericObject[IWscProductABI]{Pp: pp}} +} + +func (o WscProduct) MakeFromKnownABI(r **IWscProductABI) WscProduct { + if r == nil { + return WscProduct{} + } + + runtime.SetFinalizer(r, func(r **IWscProductABI) { (*r).Release() }) + return WscProduct{com.GenericObject[IWscProductABI]{Pp: r}} +} + +func (o WscProduct) UnsafeUnwrap() *IWscProductABI { + return *(o.Pp) +} + +type IWSCProductListABI struct { + com.IUnknownABI // Technically IDispatch, but we're bypassing all of that atm +} + +func (abi *IWSCProductListABI) Initialize(provider WSC_SECURITY_PROVIDER) (err error) { + method := unsafe.Slice(abi.Vtbl, 10)[7] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(provider)) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + } + return +} + +func (abi *IWSCProductListABI) GetCount() (val int32, err error) { + method := unsafe.Slice(abi.Vtbl, 10)[8] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(unsafe.Pointer(&val))) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + } + return +} + +func (abi *IWSCProductListABI) GetItem(index uint32) (val WscProduct, err error) { + var t0 *IWscProductABI + + method := unsafe.Slice(abi.Vtbl, 10)[9] + hr, _, _ := syscall.SyscallN(method, uintptr(unsafe.Pointer(abi)), uintptr(index), uintptr(unsafe.Pointer(&t0))) + if e := wingoes.ErrorFromHRESULT(wingoes.HRESULT(hr)); !e.IsOK() { + err = e + if e.Failed() { + return + } + } + + var r0 WscProduct + val = r0.MakeFromKnownABI(&t0) + return +} + +type WSCProductList struct { + com.GenericObject[IWSCProductListABI] +} + +func (o WSCProductList) Initialize(provider WSC_SECURITY_PROVIDER) (err error) { + p := *(o.Pp) + return p.Initialize(provider) +} + +func (o WSCProductList) GetCount() (val int32, err error) { + p := *(o.Pp) + return p.GetCount() +} + +func (o WSCProductList) GetItem(index uint32) (val WscProduct, err error) { + p := *(o.Pp) + return p.GetItem(index) +} + +func (o WSCProductList) IID() *com.IID { + return IID_IWSCProductList +} + +func (o WSCProductList) Make(r com.ABIReceiver) any { + if r == nil { + return WSCProductList{} + } + + runtime.SetFinalizer(r, com.ReleaseABI) + + pp := (**IWSCProductListABI)(unsafe.Pointer(r)) + return WSCProductList{com.GenericObject[IWSCProductListABI]{Pp: pp}} +} + +func (o WSCProductList) UnsafeUnwrap() *IWSCProductListABI { + return *(o.Pp) +} diff --git a/util/osdiag/osdiag_windows.go b/util/osdiag/osdiag_windows.go index 12ea366b1..89ba49216 100644 --- a/util/osdiag/osdiag_windows.go +++ b/util/osdiag/osdiag_windows.go @@ -14,10 +14,12 @@ import ( "unicode/utf16" "unsafe" + "github.com/dblohm7/wingoes/com" "github.com/dblohm7/wingoes/pe" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" "tailscale.com/types/logger" + "tailscale.com/util/osdiag/internal/wsc" "tailscale.com/util/winutil" "tailscale.com/util/winutil/authenticode" ) @@ -44,6 +46,7 @@ func logSupportInfo(logf logger.Logf, reason LogSupportInfoReason) { const ( supportInfoKeyModules = "modules" supportInfoKeyRegistry = "registry" + supportInfoKeySecurity = "securitySoftware" supportInfoKeyWinsockLSP = "winsockLSP" ) @@ -65,6 +68,8 @@ func getSupportInfo(w io.Writer, reason LogSupportInfoReason) error { output[supportInfoKeyModules] = err } + output[supportInfoKeySecurity] = getSecurityInfo() + lspInfo, err := getWinsockLSPInfo() if err == nil { output[supportInfoKeyWinsockLSP] = lspInfo @@ -482,3 +487,105 @@ func enumWinsockProtocols() ([]wsaProtocolInfo, error) { return buf, nil } + +type providerKey struct { + provType wsc.WSC_SECURITY_PROVIDER + provKey string +} + +var providerKeys = []providerKey{ + providerKey{ + wsc.WSC_SECURITY_PROVIDER_ANTIVIRUS, + "av", + }, + providerKey{ + wsc.WSC_SECURITY_PROVIDER_ANTISPYWARE, + "antispy", + }, + providerKey{ + wsc.WSC_SECURITY_PROVIDER_FIREWALL, + "firewall", + }, +} + +const ( + maxProvCount = 100 +) + +type secProductInfo struct { + Name string `json:"name,omitempty"` + NameErr error `json:"nameErr,omitempty"` + State string `json:"state,omitempty"` + StateErr error `json:"stateErr,omitempty"` +} + +func getSecurityInfo() map[string]any { + result := make(map[string]any) + + for _, prov := range providerKeys { + // Note that we need to obtain a new product list for each provider type; + // the docs clearly state that we cannot reuse objects. + productList, err := com.CreateInstance[wsc.WSCProductList](wsc.CLSID_WSCProductList) + if err != nil { + result[prov.provKey] = err + continue + } + + err = productList.Initialize(prov.provType) + if err != nil { + result[prov.provKey] = err + continue + } + + n, err := productList.GetCount() + if err != nil { + result[prov.provKey] = err + continue + } + if n == 0 { + continue + } + + n = min(n, maxProvCount) + values := make([]any, 0, n) + + for i := int32(0); i < n; i++ { + product, err := productList.GetItem(uint32(i)) + if err != nil { + values = append(values, err) + continue + } + + var value secProductInfo + + value.Name, err = product.GetProductName() + if err != nil { + value.NameErr = err + } + + state, err := product.GetProductState() + if err == nil { + switch state { + case wsc.WSC_SECURITY_PRODUCT_STATE_ON: + value.State = "on" + case wsc.WSC_SECURITY_PRODUCT_STATE_OFF: + value.State = "off" + case wsc.WSC_SECURITY_PRODUCT_STATE_SNOOZED: + value.State = "snoozed" + case wsc.WSC_SECURITY_PRODUCT_STATE_EXPIRED: + value.State = "expired" + default: + value.State = fmt.Sprintf("", state) + } + } else { + value.StateErr = err + } + + values = append(values, value) + } + + result[prov.provKey] = values + } + + return result +}