From 07e0ecfe0e0a80a1b4ed4046d9d0f61e50d0b5ed Mon Sep 17 00:00:00 2001 From: TheSulak3 <00253220@uca.edu.sv> Date: Sun, 23 Aug 2026 20:29:21 -0600 Subject: [PATCH 1/8] Fix false-positive successes when admin privileges are missing Get-Tpm and other elevated commands print a localized "requires administrator" message to stdout while still exiting 0, so the tool logged them as SUCCESS with error text as data. Now detects these privilege-denied phrases and reports FAILED correctly, and surfaces admin status in the console and report header. --- main.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/main.go b/main.go index 21eb327..e616eef 100644 --- a/main.go +++ b/main.go @@ -26,9 +26,53 @@ type HWIDData struct { value string } +// isRunningAsAdmin reports whether the process has administrator privileges. +// Opening a physical drive handle requires elevation on Windows, so a +// successful open is a reliable signal without extra dependencies. +func isRunningAsAdmin() bool { + f, err := os.Open(`\\.\PHYSICALDRIVE0`) + if err != nil { + return false + } + _ = f.Close() + return true +} + +// isPrivilegeError detects command output that reports missing admin rights +// even though the process itself exited successfully (e.g. Get-Tpm prints a +// localized "requires administrator privileges" message to stdout and still +// returns exit code 0). Without this check that output gets recorded as a +// SUCCESS with garbage content instead of a clear FAILED result. +func isPrivilegeError(output string) bool { + lower := strings.ToLower(output) + phrases := []string{ + "se requiere privilegios de administrador", + "acceso denegado", + "privilegios adecuados", + "access is denied", + "access denied", + "administrator privileges are required", + "run as administrator", + "requires elevation", + "you must run this cmdlet from an elevated", + } + for _, phrase := range phrases { + if strings.Contains(lower, phrase) { + return true + } + } + return false +} + func main() { reader := bufio.NewReader(os.Stdin) + if !isRunningAsAdmin() { + fmt.Println("\n[Warning] Not running as Administrator — TPM, Secure Boot, and some") + fmt.Println(" other checks will fail or return incomplete data.") + fmt.Println(" Re-launch this program as Administrator for full results.") + } + for { fmt.Println("\n========================================") fmt.Println(" HWID Checker") @@ -333,6 +377,14 @@ func executeCommandWithResult(args []string) CommandResult { } } + if isPrivilegeError(outputStr) { + return CommandResult{ + success: false, + error: "Administrator privileges required", + output: outputStr, + } + } + return CommandResult{ success: true, output: outputStr, @@ -373,6 +425,14 @@ func executePipedCommandWithResult(args []string) CommandResult { } } + if isPrivilegeError(outputStr) { + return CommandResult{ + success: false, + error: "Administrator privileges required", + output: outputStr, + } + } + return CommandResult{ success: true, output: outputStr, @@ -852,6 +912,11 @@ func writeFileHeader(file *os.File, cleanList bool) error { return fmt.Errorf("file is nil") } + adminStatus := "No (run as Administrator for TPM/Secure Boot/full results)" + if isRunningAsAdmin() { + adminStatus = "Yes" + } + var header string if cleanList { header = fmt.Sprintf( @@ -860,8 +925,10 @@ func writeFileHeader(file *os.File, cleanList bool) error { "========================================\n"+ "Generated: %s\n"+ "System: Windows\n"+ + "Administrator: %s\n"+ "========================================\n\n", time.Now().Format("2006-01-02 15:04:05"), + adminStatus, ) } else { header = fmt.Sprintf( @@ -870,8 +937,10 @@ func writeFileHeader(file *os.File, cleanList bool) error { "========================================\n"+ "Generated: %s\n"+ "System: Windows\n"+ + "Administrator: %s\n"+ "========================================\n\n", time.Now().Format("2006-01-02 15:04:05"), + adminStatus, ) } From 4c60434692d50dbe7877563825d3c80b53b7e8ba Mon Sep 17 00:00:00 2001 From: TheSulak3 <00253220@uca.edu.sv> Date: Sun, 23 Aug 2026 20:40:24 -0600 Subject: [PATCH 2/8] Fix piped-command quoting and drop locale-dependent findstr checks CmdLine now bypasses Go's default Windows arg re-escaping so quoted findstr patterns (e.g. /C:"OS Serial Number") reach cmd.exe intact instead of being split into bogus file-open attempts. The Windows Product ID (Alternative) and MAC Addresses (IPConfig) checks also relied on English-only systeminfo/ipconfig labels that don't exist on non-English Windows installs; replaced both with locale-independent CIM/PowerShell equivalents. --- main.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index e616eef..c5b889a 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "os/exec" "path/filepath" "strings" + "syscall" "time" ) @@ -206,9 +207,9 @@ func main() { }) fmt.Println("\n[Starting] Windows Product ID Check (Alternative)...") runCommandWithFallbacks("Windows Product ID (Alternative)", Command{ - primary: []string{"systeminfo", "|", "findstr", "/B", "/C:\"OS Serial Number\""}, + primary: []string{"powershell", "-Command", "(Get-CimInstance -ClassName Win32_OperatingSystem).SerialNumber"}, fallbacks: [][]string{ - {"powershell", "-Command", "systeminfo | Select-String 'OS Serial Number'"}, + {"powershell", "-Command", "Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion' -Name ProductId | Select-Object -ExpandProperty ProductId"}, }, }) fmt.Println("[Complete] Windows Product ID Check finished") @@ -237,9 +238,9 @@ func main() { }) fmt.Println("\n[Starting] MAC Addresses Check (4/4)...") runCommandWithFallbacks("MAC Addresses (IPConfig)", Command{ - primary: []string{"ipconfig", "/all", "|", "findstr", `"Physical Address"`}, + primary: []string{"powershell", "-Command", "Get-NetAdapter | Select-Object Name, MacAddress, Status"}, fallbacks: [][]string{ - {"powershell", "-Command", "ipconfig /all | Select-String 'Physical Address'"}, + {"powershell", "-Command", "Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object { $_.MACAddress -ne $null } | Select-Object Description, MACAddress"}, }, }) fmt.Println("[Complete] MAC Addresses Check finished") @@ -400,7 +401,13 @@ func executePipedCommandWithResult(args []string) CommandResult { } fullCommand := strings.Join(args, " ") - cmd := exec.Command("cmd.exe", "/C", fullCommand) + cmd := exec.Command("cmd.exe") + // fullCommand can contain embedded double quotes (e.g. findstr /C:"OS Serial Number"). + // exec.Command's default Windows argument escaping re-escapes those quotes for CRT-style + // parsing, but cmd.exe parses its command line differently, which splits the quoted + // phrase into separate tokens. Setting CmdLine directly bypasses that re-escaping and + // hands cmd.exe the literal command line it expects. + cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "cmd.exe /C " + fullCommand} cmd.Env = os.Environ() output, err := cmd.CombinedOutput() @@ -1062,9 +1069,9 @@ func buildCommandList() []FileCommandEntry { }, }}, {"Windows Product ID (Alternative)", Command{ - primary: []string{"systeminfo", "|", "findstr", "/B", "/C:\"OS Serial Number\""}, + primary: []string{"powershell", "-Command", "(Get-CimInstance -ClassName Win32_OperatingSystem).SerialNumber"}, fallbacks: [][]string{ - {"powershell", "-Command", "systeminfo | Select-String 'OS Serial Number'"}, + {"powershell", "-Command", "Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion' -Name ProductId | Select-Object -ExpandProperty ProductId"}, }, }}, {"MAC Addresses (GetMac)", Command{ @@ -1087,9 +1094,9 @@ func buildCommandList() []FileCommandEntry { }, }}, {"MAC Addresses (IPConfig)", Command{ - primary: []string{"ipconfig", "/all", "|", "findstr", `"Physical Address"`}, + primary: []string{"powershell", "-Command", "Get-NetAdapter | Select-Object Name, MacAddress, Status"}, fallbacks: [][]string{ - {"powershell", "-Command", "ipconfig /all | Select-String 'Physical Address'"}, + {"powershell", "-Command", "Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object { $_.MACAddress -ne $null } | Select-Object Description, MACAddress"}, }, }}, {"TPM Status", Command{ From c7dc9b475ac2b3af17ffba546b3aa205759d9bfc Mon Sep 17 00:00:00 2001 From: TheSulak3 <00253220@uca.edu.sv> Date: Sun, 23 Aug 2026 21:00:16 -0600 Subject: [PATCH 3/8] Fix garbled Clean HWID List output for table-shaped commands extractCleanValue used a generic header-stripping heuristic that worked for single-value wmic output but glued every row of MAC adapter tables, volume tables, and TPM property lists into one unreadable line. Added dedicated extractors: regex-based MAC address pulling (dedup, locale-independent), TPM property summary, and volume drive/size parsing, dispatched by check description. --- main.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index c5b889a..d01d755 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "syscall" "time" @@ -570,7 +571,7 @@ func processCommandForCleanList(cmdEntry FileCommandEntry) (bool, string) { result := executeCommandWithResult(cmdEntry.command.primary) if result.success { - return true, extractCleanValue(result.output) + return true, extractCleanValue(cmdEntry.description, result.output) } for _, fallback := range cmdEntry.command.fallbacks { @@ -580,14 +581,109 @@ func processCommandForCleanList(cmdEntry FileCommandEntry) (bool, string) { result = executeCommandWithResult(fallback) if result.success { - return true, extractCleanValue(result.output) + return true, extractCleanValue(cmdEntry.description, result.output) } } return false, "" } -func extractCleanValue(output string) string { +// extractCleanValue turns raw command output into a single readable line for +// the clean HWID list. Table-shaped output (MAC adapters, volumes, TPM +// properties) needs dedicated parsing — the old generic line filter just +// stripped known header words and glued every remaining row together, +// producing garbled multi-column dumps for anything wider than one value. +func extractCleanValue(description, output string) string { + lower := strings.ToLower(description) + switch { + case strings.Contains(lower, "mac address"): + return extractMACAddresses(output) + case strings.Contains(lower, "tpm status"): + return extractTPMSummary(output) + case strings.Contains(lower, "volume information"): + return extractVolumeSummary(output) + default: + return extractGenericValue(output) + } +} + +var macAddressPattern = regexp.MustCompile(`(?i)\b([0-9A-F]{2}[:-]){5}[0-9A-F]{2}\b`) + +func extractMACAddresses(output string) string { + matches := macAddressPattern.FindAllString(output, -1) + + seen := make(map[string]bool) + var macs []string + for _, mac := range matches { + normalized := strings.ToUpper(strings.ReplaceAll(mac, "-", ":")) + if normalized == "00:00:00:00:00:00" || seen[normalized] { + continue + } + seen[normalized] = true + macs = append(macs, normalized) + } + + if len(macs) == 0 { + return "Not Available" + } + + return strings.Join(macs, ", ") +} + +var tpmPropertyPattern = regexp.MustCompile(`^(\w+)\s*:\s*(.+)$`) + +func extractTPMSummary(output string) string { + labels := map[string]string{ + "IsActivated_InitialValue": "Activated", + "IsEnabled_InitialValue": "Enabled", + "IsOwned_InitialValue": "Owned", + "SpecVersion": "SpecVersion", + "TpmPresent": "Present", + "TpmReady": "Ready", + } + + var parts []string + for _, line := range strings.Split(output, "\n") { + match := tpmPropertyPattern.FindStringSubmatch(strings.TrimSpace(line)) + if match == nil { + continue + } + key := strings.TrimSpace(match[1]) + label, ok := labels[key] + if !ok { + continue + } + parts = append(parts, fmt.Sprintf("%s=%s", label, strings.TrimSpace(match[2]))) + } + + if len(parts) == 0 { + return extractGenericValue(output) + } + + return strings.Join(parts, ", ") +} + +var volumeRowPattern = regexp.MustCompile(`^([A-Z])\s{2,}\S.*?(\d[\d.]*\s*(?:KB|MB|GB|TB))\s+(\d[\d.]*\s*(?:KB|MB|GB|TB))\s*$`) + +func extractVolumeSummary(output string) string { + var drives []string + for _, line := range strings.Split(output, "\n") { + match := volumeRowPattern.FindStringSubmatch(strings.TrimRight(line, " \t\r")) + if match == nil { + continue + } + letter, free, total := match[1], match[2], match[3] + drives = append(drives, fmt.Sprintf("%s: %s free of %s", letter, free, total)) + } + + if len(drives) == 0 { + return "Not Available" + } + + return strings.Join(drives, ", ") +} + +func extractGenericValue(output string) string { lines := strings.Split(output, "\n") var values []string From e98838e96604e2669909308032bbc235ab8752f3 Mon Sep 17 00:00:00 2001 From: TheSulak3 <00253220@uca.edu.sv> Date: Sun, 23 Aug 2026 21:06:42 -0600 Subject: [PATCH 4/8] Add TPM Endorsement Key check and fix false-success on empty tables Win32_Tpm has no SerialNumber property; the EK public key hash is the closest thing to a real per-chip identifier, retrieved via Get-TpmEndorsementKeyInfo. That cmdlet returns an empty formatted table (header + dashes, no data, exit 0) instead of an error when not elevated, which slipped past the existing success checks as a false SUCCESS with no actual data. Added isEmptyTableOutput to catch that pattern generally. Also dropped the EK fallback command, which used an invalid -HashAlgorithm value and always errored. --- main.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index d01d755..20e1e28 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,23 @@ func isRunningAsAdmin() bool { // localized "requires administrator privileges" message to stdout and still // returns exit code 0). Without this check that output gets recorded as a // SUCCESS with garbage content instead of a clear FAILED result. +var emptyTableSeparatorPattern = regexp.MustCompile(`^-+(\s+-+)*$`) + +// isEmptyTableOutput detects a PowerShell formatted table with a header row +// and dashed separator but zero data rows — e.g. Get-TpmEndorsementKeyInfo +// silently returns nothing (no error, exit 0) when not run elevated. That +// text is non-empty, so without this check it slips past as a false SUCCESS. +func isEmptyTableOutput(output string) bool { + var nonEmpty []string + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if line != "" { + nonEmpty = append(nonEmpty, line) + } + } + return len(nonEmpty) == 2 && emptyTableSeparatorPattern.MatchString(nonEmpty[1]) +} + func isPrivilegeError(output string) bool { lower := strings.ToLower(output) phrases := []string{ @@ -254,6 +271,11 @@ func main() { {"powershell", "-Command", "Get-CimInstance -Namespace ROOT\\CIMV2\\Security\\MicrosoftTpm -ClassName Win32_Tpm"}, }, }) + fmt.Println("\n[Checking] TPM Endorsement Key...") + runCommandWithFallbacks("TPM Endorsement Key", Command{ + primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo | Select-Object PublicKeyHash, ManufacturerId, ManufacturerVersion"}, + fallbacks: [][]string{}, + }) fmt.Println("\n[Checking] Secure Boot Status...") runCommandWithFallbacks("Secure Boot", Command{ primary: []string{"powershell", "-Command", "Confirm-SecureBootUEFI"}, @@ -387,6 +409,14 @@ func executeCommandWithResult(args []string) CommandResult { } } + if isEmptyTableOutput(outputStr) { + return CommandResult{ + success: false, + error: "Command returned no data (possibly requires administrator privileges)", + output: outputStr, + } + } + return CommandResult{ success: true, output: outputStr, @@ -441,6 +471,14 @@ func executePipedCommandWithResult(args []string) CommandResult { } } + if isEmptyTableOutput(outputStr) { + return CommandResult{ + success: false, + error: "Command returned no data (possibly requires administrator privileges)", + output: outputStr, + } + } + return CommandResult{ success: true, output: outputStr, @@ -598,7 +636,7 @@ func extractCleanValue(description, output string) string { switch { case strings.Contains(lower, "mac address"): return extractMACAddresses(output) - case strings.Contains(lower, "tpm status"): + case strings.Contains(lower, "tpm status"), strings.Contains(lower, "tpm endorsement key"): return extractTPMSummary(output) case strings.Contains(lower, "volume information"): return extractVolumeSummary(output) @@ -634,12 +672,16 @@ var tpmPropertyPattern = regexp.MustCompile(`^(\w+)\s*:\s*(.+)$`) func extractTPMSummary(output string) string { labels := map[string]string{ - "IsActivated_InitialValue": "Activated", - "IsEnabled_InitialValue": "Enabled", - "IsOwned_InitialValue": "Owned", - "SpecVersion": "SpecVersion", - "TpmPresent": "Present", - "TpmReady": "Ready", + "IsActivated_InitialValue": "Activated", + "IsEnabled_InitialValue": "Enabled", + "IsOwned_InitialValue": "Owned", + "SpecVersion": "SpecVersion", + "TpmPresent": "Present", + "TpmReady": "Ready", + "ManufacturerVersion": "FirmwareVersion", + "PhysicalPresenceVersionInfo": "PPIVersion", + "PublicKeyHash": "EKPublicKeyHash", + "ManufacturerId": "ManufacturerId", } var parts []string @@ -1202,6 +1244,10 @@ func buildCommandList() []FileCommandEntry { {"powershell", "-Command", "Get-CimInstance -Namespace ROOT\\CIMV2\\Security\\MicrosoftTpm -ClassName Win32_Tpm"}, }, }}, + {"TPM Endorsement Key", Command{ + primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo | Select-Object PublicKeyHash, ManufacturerId, ManufacturerVersion"}, + fallbacks: [][]string{}, + }}, {"Secure Boot", Command{ primary: []string{"powershell", "-Command", "Confirm-SecureBootUEFI"}, fallbacks: [][]string{ From 0077570ddabdcdf7e4215c10ffee36746921f1c7 Mon Sep 17 00:00:00 2001 From: TheSulak3 <00253220@uca.edu.sv> Date: Sun, 23 Aug 2026 21:12:28 -0600 Subject: [PATCH 5/8] Stop piping TPM Endorsement Key output through Select-Object Get-TpmEndorsementKeyInfo writes its "administrator privileges required" message through a channel that gets silently dropped once a downstream Select-Object stage exists in the pipeline, so the check always looked like an empty result with no explanation regardless of the real cause. Running it unpiped lets the actual message reach stdout/stderr, where isPrivilegeError now catches it correctly. --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 20e1e28..5dc203a 100644 --- a/main.go +++ b/main.go @@ -273,7 +273,7 @@ func main() { }) fmt.Println("\n[Checking] TPM Endorsement Key...") runCommandWithFallbacks("TPM Endorsement Key", Command{ - primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo | Select-Object PublicKeyHash, ManufacturerId, ManufacturerVersion"}, + primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo"}, fallbacks: [][]string{}, }) fmt.Println("\n[Checking] Secure Boot Status...") @@ -1245,7 +1245,7 @@ func buildCommandList() []FileCommandEntry { }, }}, {"TPM Endorsement Key", Command{ - primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo | Select-Object PublicKeyHash, ManufacturerId, ManufacturerVersion"}, + primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo"}, fallbacks: [][]string{}, }}, {"Secure Boot", Command{ From 376a9017f70a4de29666f08d79e062f24505de48 Mon Sep 17 00:00:00 2001 From: TheSulak3 <00253220@uca.edu.sv> Date: Sun, 23 Aug 2026 21:20:37 -0600 Subject: [PATCH 6/8] Update README to match current menu, checks, and behavior Menu, examples, and output samples were still describing the old 12-option/14-check version. Documents the current 15-option menu, 17-check scan, Clean HWID List, Compare Scans, Administrator detection, and the recent locale/false-success fixes. --- README.md | 95 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index aef40f2..dfc6bb5 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,18 @@ HWID Checker is a Go application that allows you to easily gather various hardwa - Display RAM serial number - Display Windows product ID - Display MAC addresses +- Display TPM status and TPM Endorsement Key +- Display Secure Boot status ### Advanced Features - **Timestamped File Output:** Save all information to uniquely timestamped text files (format: `hwid_info_YYYY-MM-DD_HH-MM-SS.txt`) to prevent overwriting previous scans +- **Clean HWID List:** Generate a compact, human-readable summary (`hwid_clean_YYYY-MM-DD_HH-MM-SS.txt`) with one line per identifier instead of raw command output +- **Scan Comparison:** Compare two previous scan files to see what changed between runs +- **Administrator Detection:** Reports whether the process is elevated, both on-screen and in the file header, since TPM/Secure Boot/Endorsement Key checks require Administrator privileges to return real data - **Real-time Progress Tracking:** Visual progress indicators showing completion percentage and status during full system scans -- **Comprehensive Error Handling:** Robust error handling with detailed logging and graceful recovery from failures +- **Comprehensive Error Handling:** Robust error handling with detailed logging and graceful recovery from failures, including detection of commands that report a privilege error or return an empty result while still exiting successfully - **Multiple Fallback Commands:** Automatic command fallback when primary commands fail, ensuring maximum compatibility +- **Locale-Independent:** Checks use CIM/PowerShell property access instead of parsing localized command-line text, so results are consistent regardless of the system's display language - **PowerShell Integration:** Support for both WMI (legacy) and CIM (modern) PowerShell cmdlets - **Detailed Status Reporting:** Success/failure tracking with execution time and success rate statistics - **Enhanced User Experience:** Clear visual feedback with [Starting], [Complete], [Success], and [Failed] status tags @@ -34,7 +40,7 @@ You can download the pre-compiled executable for Windows from the [Releases](htt - Windows operating system - PowerShell (any version, improved functionality with PowerShell 3.0+) -- Administrator privileges recommended for complete hardware information access +- Administrator privileges required for complete results — TPM Status, TPM Endorsement Key, and Secure Boot all fail or return incomplete data without elevation. The application warns on startup and records `Administrator: Yes/No` in every report if not run elevated. **Note on OS Support:** While HWID Checker is primarily developed and optimized for Windows, the Go programming language allows for cross-platform compilation. However, this tool relies heavily on Windows-specific commands (WMIC, PowerShell, cmd.exe) and will not function correctly on other operating systems without significant modifications. If you're interested in using HWID Checker on another operating system, you would need to modify the source code to use platform-appropriate system commands. @@ -88,8 +94,11 @@ Select an option: 8. RAM (Serial Number) 9. Windows Product ID 10. MAC Addresses -11. Print All to File and Save -12. Exit +11. TPM and Secure Boot Status +12. Print All to File (Detailed) +13. Print Clean HWID List +14. Compare with Previous Scan +15. Exit ======================================== ``` @@ -112,51 +121,72 @@ XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX Press Enter to continue... ``` -#### Save All Information to File +#### Save All Information to File (Detailed) ``` -Enter your choice: 11 +Enter your choice: 12 ======================================== Starting full system scan... -Output file: hwid_info_2024-10-14_15-30-45.txt +Output file: hwid_info_2026-08-23_21-08-45.txt ======================================== -[1/14] (0.0%) Processing: SMBIOS (UUID) -[1/14] SUCCESS - SMBIOS (UUID) -[2/14] (7.1%) Processing: BIOS (Serial Number) -[2/14] SUCCESS - BIOS (Serial Number) -[3/14] (14.3%) Processing: Motherboard (Serial Number) -[3/14] SUCCESS - Motherboard (Serial Number) +[1/17] (0.0%) Processing: SMBIOS (UUID) +[1/17] SUCCESS - SMBIOS (UUID) +[2/17] (5.9%) Processing: BIOS (Serial Number) +[2/17] SUCCESS - BIOS (Serial Number) +[3/17] (11.8%) Processing: Motherboard (Serial Number) +[3/17] SUCCESS - Motherboard (Serial Number) ... -[14/14] (92.9%) Processing: MAC Addresses (IPConfig) -[14/14] SUCCESS - MAC Addresses (IPConfig) +[17/17] (94.1%) Processing: Secure Boot +[17/17] SUCCESS - Secure Boot ======================================== Scan Complete ======================================== -Total Commands: 14 -Successful: 14 +Total Commands: 17 +Successful: 17 Failed: 0 Success Rate: 100.0% -Execution Time: 5.234s -Output saved to: hwid_info_2024-10-14_15-30-45.txt +Execution Time: 6.419s +Output saved to: hwid_info_2026-08-23_21-08-45.txt ======================================== Press Enter to continue... ``` +#### Print Clean HWID List +``` +Enter your choice: 13 + +======================================== +Generating clean HWID list... +Output file: hwid_clean_2026-08-23_21-00-53.txt +======================================== +... +``` +Produces a compact summary file, one line per identifier: +``` +SMBIOS (UUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +Motherboard (Serial Number): XXXXXXXXXXXXXXXX +Volume Information: C: 470.88 GB free of 952.98 GB, G: 14.83 GB free of 953.85 GB, E: 205.34 GB free of 447.11 GB +MAC Addresses (GetMac): XX:XX:XX:XX:XX:XX, XX:XX:XX:XX:XX:XX, ... +TPM Status: Activated=True, Enabled=True, Owned=True, SpecVersion=2.0, 0, 1.38 +Secure Boot: True +``` + ### Output File Format -When you select option 11, a timestamped file is created with the following structure: +When you select option 12, a timestamped file is created with the following structure: ``` ======================================== Hardware ID Information Report ======================================== -Generated: 2024-10-14 15:30:45 +Generated: 2026-08-23 21:08:45 System: Windows +Administrator: Yes ======================================== -[1/14] SMBIOS (UUID) +[1/17] SMBIOS (UUID) ======================================== Primary Command: wmic csproduct get uuid Status: SUCCESS @@ -169,12 +199,12 @@ XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX ======================================== Report Summary ======================================== -Total Commands Executed: 14 -Successful: 14 +Total Commands Executed: 17 +Successful: 17 Failed: 0 Success Rate: 100.0% -Execution Time: 5.234s -Completion Time: 2024-10-14 15:30:50 +Execution Time: 6.419s +Completion Time: 2026-08-23 21:08:52 ======================================== ``` @@ -229,11 +259,24 @@ Real-time progress indication includes: - Check available disk space - Verify antivirus isn't blocking file creation +### TPM Status, TPM Endorsement Key, or Secure Boot report FAILED +- These checks require Administrator privileges; the report header's `Administrator:` line confirms whether the process was elevated +- Even when elevated, some hardware/firmware combinations don't expose a TPM Endorsement Key — the report will show the actual PowerShell error message rather than a blank result + ## License This project is licensed under the AGPL-3.0 License. See the [LICENSE](LICENSE) file for more details. ## Changelog +- Added TPM Endorsement Key check as a real per-chip TPM identifier +- Added Administrator-privilege detection, with a startup warning and an `Administrator:` field in every report +- Fixed commands (e.g. `Get-Tpm`, `Confirm-SecureBootUEFI`, `Get-TpmEndorsementKeyInfo`) being logged as SUCCESS when they actually printed a privilege error or returned an empty result +- Fixed a Windows command-line quoting bug that broke `findstr`-based piped commands +- Replaced locale-dependent `findstr` label parsing (English-only `systeminfo`/`ipconfig` labels) with locale-independent CIM/PowerShell equivalents +- Fixed the Clean HWID List producing garbled, unreadable output for MAC address, volume, and TPM checks +- Added TPM and Secure Boot status checks +- Implemented Clean HWID List generation (option 13) +- Added scan comparison functionality (option 14) - Added timestamped file output to prevent overwriting previous scans - Implemented real-time progress tracking with percentage indicators - Improved visual feedback with status tags From 8eea3e02fd283965f73b790a7f238884f4a05397 Mon Sep 17 00:00:00 2001 From: TheSulak3 <00253220@uca.edu.sv> Date: Mon, 24 Aug 2026 20:46:29 -0600 Subject: [PATCH 7/8] Add TPM Endorsement Key check using the tpm-info.exe (Tulach) method Adds a native check that replicates how Samuel Tulach's tpm-info.exe reads the EK: NCryptOpenStorageProvider + NCryptGetProperty against the Microsoft Platform Crypto Provider directly (ncrypt.dll), rather than going through PowerShell's Get-TpmEndorsementKeyInfo. The raw BCRYPT_RSAPUBLIC_BLOB is DER-encoded as a PKCS#1 RSAPublicKey and hashed with MD5/SHA1/SHA256, matching tpm-info.exe's output exactly (verified byte-for-byte against the real tool). Notably this route works without Administrator privileges, unlike the PowerShell cmdlet. --- main.go | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/main.go b/main.go index 5dc203a..a0da0e4 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,13 @@ package main import ( "bufio" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding/asn1" + "encoding/binary" "fmt" + "math/big" "os" "os/exec" "path/filepath" @@ -10,6 +16,7 @@ import ( "strings" "syscall" "time" + "unsafe" ) type Command struct { @@ -83,6 +90,128 @@ func isPrivilegeError(output string) bool { return false } +// The following replicates how Samuel Tulach's tpm-info.exe reads the TPM +// Endorsement Key: it bypasses PowerShell/WMI entirely and calls the CNG +// Platform Crypto Provider directly (NCryptOpenStorageProvider on "Microsoft +// Platform Crypto Provider", then NCryptGetProperty for "PCP_EKPUB", which +// returns a raw BCRYPT_RSAPUBLIC_BLOB). That's why Get-TpmEndorsementKeyInfo's +// PublicKeyHash never matches its output — PowerShell hashes its own internal +// byte layout, while tpm-info.exe DER-encodes the raw key as a PKCS#1 +// RSAPublicKey and hashes that instead, then prints MD5/SHA1/SHA256 of it. +var ( + ncryptDLL = syscall.NewLazyDLL("ncrypt.dll") + procNCryptOpenStorageProvider = ncryptDLL.NewProc("NCryptOpenStorageProvider") + procNCryptGetProperty = ncryptDLL.NewProc("NCryptGetProperty") + procNCryptFreeObject = ncryptDLL.NewProc("NCryptFreeObject") +) + +const bcryptRSAPublicMagic = 0x31415352 // "RSA1" little-endian, per bcrypt.h + +func utf16Ptr(s string) *uint16 { + p, err := syscall.UTF16PtrFromString(s) + if err != nil { + return nil + } + return p +} + +// tulachEKHash pulls the raw TPM Endorsement Key public key via the CNG +// Platform Crypto Provider (the same low-level API tpm-info.exe uses), +// DER-encodes it as a PKCS#1 RSAPublicKey, and returns its MD5/SHA1/SHA256 +// hashes in the same labeled format tpm-info.exe prints. +func tulachEKHash() (string, error) { + providerName := utf16Ptr("Microsoft Platform Crypto Provider") + if providerName == nil { + return "", fmt.Errorf("failed to encode provider name") + } + + var hProvider uintptr + ret, _, _ := procNCryptOpenStorageProvider.Call( + uintptr(unsafe.Pointer(&hProvider)), + uintptr(unsafe.Pointer(providerName)), + 0, + ) + if ret != 0 { + return "", fmt.Errorf("NCryptOpenStorageProvider failed: 0x%08X", uint32(ret)) + } + defer procNCryptFreeObject.Call(hProvider) + + propName := utf16Ptr("PCP_EKPUB") + if propName == nil { + return "", fmt.Errorf("failed to encode property name") + } + + var cbResult uint32 + ret, _, _ = procNCryptGetProperty.Call( + hProvider, + uintptr(unsafe.Pointer(propName)), + 0, + 0, + uintptr(unsafe.Pointer(&cbResult)), + 0, + ) + if ret != 0 || cbResult == 0 { + return "", fmt.Errorf("NCryptGetProperty (size query) failed: 0x%08X", uint32(ret)) + } + + buf := make([]byte, cbResult) + ret, _, _ = procNCryptGetProperty.Call( + hProvider, + uintptr(unsafe.Pointer(propName)), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(cbResult), + uintptr(unsafe.Pointer(&cbResult)), + 0, + ) + if ret != 0 { + return "", fmt.Errorf("NCryptGetProperty failed: 0x%08X", uint32(ret)) + } + buf = buf[:cbResult] + + // BCRYPT_RSAKEY_BLOB header: Magic, BitLength, cbPublicExp, cbModulus, + // cbPrime1, cbPrime2 (6 x uint32), followed by PublicExponent then Modulus. + if len(buf) < 24 { + return "", fmt.Errorf("EK public key blob too short (%d bytes)", len(buf)) + } + + magic := binary.LittleEndian.Uint32(buf[0:4]) + if magic != bcryptRSAPublicMagic { + return "", fmt.Errorf("unsupported EK key type (magic 0x%08X, expected RSA)", magic) + } + + cbPublicExp := int(binary.LittleEndian.Uint32(buf[8:12])) + cbModulus := int(binary.LittleEndian.Uint32(buf[12:16])) + + offset := 24 + if offset+cbPublicExp+cbModulus > len(buf) { + return "", fmt.Errorf("EK public key blob truncated") + } + + exponent := new(big.Int).SetBytes(buf[offset : offset+cbPublicExp]) + offset += cbPublicExp + modulus := new(big.Int).SetBytes(buf[offset : offset+cbModulus]) + + der, err := asn1.Marshal(struct { + Modulus *big.Int + Exponent *big.Int + }{modulus, exponent}) + if err != nil { + return "", fmt.Errorf("DER encoding failed: %v", err) + } + + md5Sum := md5.Sum(der) + sha1Sum := sha1.Sum(der) + sha256Sum := sha256.Sum256(der) + + return fmt.Sprintf("MD5: %x\nSHA1: %x\nSHA256: %x", md5Sum, sha1Sum, sha256Sum), nil +} + +const nativeTulachEKMarker = "__native_tulach_ek__" + +var nativeChecks = map[string]func() (string, error){ + nativeTulachEKMarker: tulachEKHash, +} + func main() { reader := bufio.NewReader(os.Stdin) @@ -276,6 +405,11 @@ func main() { primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo"}, fallbacks: [][]string{}, }) + fmt.Println("\n[Checking] TPM Endorsement Key (Tulach Method)...") + runCommandWithFallbacks("TPM Endorsement Key (Tulach Method)", Command{ + primary: []string{nativeTulachEKMarker}, + fallbacks: [][]string{}, + }) fmt.Println("\n[Checking] Secure Boot Status...") runCommandWithFallbacks("Secure Boot", Command{ primary: []string{"powershell", "-Command", "Confirm-SecureBootUEFI"}, @@ -364,6 +498,14 @@ func executeCommandWithResult(args []string) CommandResult { fmt.Printf("Command: %s\n", strings.Join(args, " ")) + if fn, ok := nativeChecks[args[0]]; ok { + output, err := fn() + if err != nil { + return CommandResult{success: false, error: err.Error()} + } + return CommandResult{success: true, output: output} + } + if containsPipe(args) { return executePipedCommandWithResult(args) } @@ -1248,6 +1390,10 @@ func buildCommandList() []FileCommandEntry { primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo"}, fallbacks: [][]string{}, }}, + {"TPM Endorsement Key (Tulach Method)", Command{ + primary: []string{nativeTulachEKMarker}, + fallbacks: [][]string{}, + }}, {"Secure Boot", Command{ primary: []string{"powershell", "-Command", "Confirm-SecureBootUEFI"}, fallbacks: [][]string{ From 672240aa4e7c33148d5f36baf2193efe6c44bd2e Mon Sep 17 00:00:00 2001 From: TheSulak3 <00253220@uca.edu.sv> Date: Mon, 24 Aug 2026 20:49:12 -0600 Subject: [PATCH 8/8] Explain why the two TPM Endorsement Key checks print different values Adds a note (console + written into the detailed report file) right after both EK checks run, explaining that they read the same physical key but hash different byte encodings of it: Get-TpmEndorsementKeyInfo hashes Microsoft's internal PublicKeyHash property, while the Tulach Method hashes a PKCS#1 DER encoding of the raw key pulled via ncrypt.dll directly. Otherwise the differing strings look like a bug or a sign the two methods disagree about the actual key. --- main.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/main.go b/main.go index a0da0e4..21f6283 100644 --- a/main.go +++ b/main.go @@ -212,6 +212,23 @@ var nativeChecks = map[string]func() (string, error){ nativeTulachEKMarker: tulachEKHash, } +const tpmEKMethodExplanation = `[Note] Why "TPM Endorsement Key" and "TPM Endorsement Key (Tulach Method)" print different values: + Both read the exact same physical Endorsement Key burned into the TPM chip - + the strings differ because each method hashes a different byte encoding of + that key, not because the underlying key data is different. + + - "TPM Endorsement Key" calls PowerShell's Get-TpmEndorsementKeyInfo, which + returns Microsoft's own internally-computed PublicKeyHash property. This + requires Administrator privileges. + - "TPM Endorsement Key (Tulach Method)" reads the raw EK public key + directly from the CNG Platform Crypto Provider (ncrypt.dll) via + NCryptOpenStorageProvider/NCryptGetProperty("PCP_EKPUB") - the same + low-level API Samuel Tulach's tpm-info.exe uses - then DER-encodes it as + a PKCS#1 RSAPublicKey and hashes that with MD5/SHA1/SHA256. This does + not require elevation, and its output has been verified to match the + real tpm-info.exe tool byte-for-byte. +` + func main() { reader := bufio.NewReader(os.Stdin) @@ -410,6 +427,7 @@ func main() { primary: []string{nativeTulachEKMarker}, fallbacks: [][]string{}, }) + fmt.Println(tpmEKMethodExplanation) fmt.Println("\n[Checking] Secure Boot Status...") runCommandWithFallbacks("Secure Boot", Command{ primary: []string{"powershell", "-Command", "Confirm-SecureBootUEFI"}, @@ -715,6 +733,11 @@ func saveAllToFile(cleanList bool) { } } else { success = processCommandForFile(file, cmdEntry, progress) + if cmdEntry.description == "TPM Endorsement Key (Tulach Method)" { + if _, err := fmt.Fprintln(file, tpmEKMethodExplanation); err != nil { + logError(fmt.Sprintf("[Warning] Failed to write TPM EK explanation: %s", err)) + } + } } if success {