diff --git a/print-com-interfaces/Cargo.lock b/print-com-interfaces/Cargo.lock index bfe3bbb..8fa2840 100644 --- a/print-com-interfaces/Cargo.lock +++ b/print-com-interfaces/Cargo.lock @@ -17,6 +17,55 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "async-compression" version = "0.4.13" @@ -109,6 +158,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "cpp_demangle" version = "0.4.4" @@ -305,6 +400,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -429,6 +530,12 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -615,6 +722,7 @@ dependencies = [ name = "print-com-interfaces" version = "0.1.0" dependencies = [ + "clap", "debugid", "eyre", "object", @@ -624,6 +732,7 @@ dependencies = [ "symsrv", "tokio", "uuid", + "windows", ] [[package]] @@ -977,6 +1086,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1230,6 +1345,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.10.0" @@ -1350,6 +1471,51 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-registry" version = "0.2.0" diff --git a/print-com-interfaces/Cargo.toml b/print-com-interfaces/Cargo.toml index df74e1b..258c512 100644 --- a/print-com-interfaces/Cargo.toml +++ b/print-com-interfaces/Cargo.toml @@ -8,6 +8,8 @@ description = "Find information about Virtual Desktop COM interfaces from debug [dependencies] eyre = "0.6.12" # Nicer Rust errors tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros"] } # Async runtime +clap = { version = "4.5.19", features = ["derive"] } # CLI argument parsing + object = "0.36.4" # Parse DLL to get unique breakpad debug id associated with PDB file debugid = "0.8.0" # Format breakpad id as string symsrv = "0.5.3" # Download PDB file from Microsoft Symbol Server @@ -15,6 +17,11 @@ pdb = { package = "pdb", version = "0.8" } # Parse PDB file symbolic-demangle = "12.12.0" # Interpret symbol names symbolic-common = "12.12.0" # Exposes types used by symbolic-demangle uuid = "1.10.0" # Parse and print GUID interface ids +windows = { version = "0.58.0", features = [ + "Win32_System_SystemInformation", + "Wdk_System_SystemServices", + "Win32_System_Registry" +] } # Get Windows version + read registry to find IID # symbolic-debuginfo = "12.12.0" # Alternative to object and pdb crates (too simple API, only exposes functions) # pdb-addr2line = "0.10.4" # Requires type information that has been removed from the PDB files we are reading. diff --git a/print-com-interfaces/src/main.rs b/print-com-interfaces/src/main.rs index e826a9e..e95c7cc 100644 --- a/print-com-interfaces/src/main.rs +++ b/print-com-interfaces/src/main.rs @@ -1,12 +1,4 @@ -//! This program uses [Microsoft Symbol Server] to get debug symbols for -//! `twinui.pcshell.dll` and then searches those symbols for information related -//! to the Virtual Desktop COM interfaces. -//! -//! Code was inspired by the python script at [GetVirtualDesktopAPI_DIA] -//! -//! [GetVirtualDesktopAPI_DIA]: https://github.com/mzomparelli/GetVirtualDesktopAPI_DIA -//! [Microsoft Symbol Server]: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/microsoft-public-symbols - +use clap::Parser; use debugid::DebugId; use eyre::{Context, OptionExt}; use object::{ @@ -14,23 +6,79 @@ use object::{ Object as _, }; use pdb::{FallibleIterator, Rva, PDB}; -use std::{ - collections::HashMap, - fs::File, - path::{Path, PathBuf}, - str::FromStr, -}; +use std::{collections::HashMap, fmt, fs::File, path::PathBuf, str::FromStr}; use symbolic_demangle::Demangle as _; use symsrv::SymsrvDownloader; +/// This program uses [Microsoft Symbol Server] to get debug symbols for +/// `twinui.pcshell.dll` and then searches those symbols for information related +/// to the Virtual Desktop COM interfaces. +/// +/// Code was inspired by the python script at [GetVirtualDesktopAPI_DIA] +/// +/// [GetVirtualDesktopAPI_DIA]: https://github.com/mzomparelli/GetVirtualDesktopAPI_DIA +/// +/// [Microsoft Symbol Server]: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/microsoft-public-symbols +#[derive(Debug, clap::Parser)] +struct Args { + /// Show all interface ids and show info about all virtual function tables. + /// If this is not specified then only info about COM interfaces that seem + /// relevant will be shown. + #[clap(long = "all", visible_alias = "unfiltered")] + unfiltered: bool, + + /// Specify a PeCodeId for the `twinui.pcshell.dll` file in order to + /// download a specific version of the dll file from a Microsoft Symbol + /// Server. + /// + /// Note: if the specified version already exists then nothing will be + /// downloaded. + #[clap(long)] + twinui_dll_id: Option, + + /// Specify a PeCodeId for the `actxprxy.dll` file in order to download a + /// specific version of the dll file from a Microsoft Symbol Server. + /// + /// Note: if the specified version already exists then nothing will be + /// downloaded. + #[clap(long)] + actxprxy_dll_id: Option, + + /// Don't use any information from `twinui.pcshell.dll`. + #[clap(long, conflicts_with = "twinui_dll_id")] + skip_twinui: bool, + + /// Don't use any information from `actxprxy.dll`. + #[clap(long, conflicts_with = "actxprxy_dll_id")] + skip_actxprxy: bool, +} + +fn system32() -> eyre::Result { + // https://learn.microsoft.com/en-us/windows/deployment/usmt/usmt-recognized-environment-variables + if let Some(found) = std::env::var_os("CSIDL_SYSTEM") { + Ok(found.into()) + } else if let Some(windows) = + std::env::var_os("WINDIR").or_else(|| std::env::var_os("SYSTEMROOT")) + { + Ok(PathBuf::from(windows).join("System32")) + } else { + // Assume it on the C drive: + Ok(PathBuf::from(r"C:\Windows\System32")) + } +} + /// Contains virtual function tables (vftables). -const TWINUI_PCSHELL_PATH: &str = r"C:\Windows\System32\twinui.pcshell.dll"; +fn twinui_pcshell_path() -> eyre::Result { + Ok(system32()?.join("twinui.pcshell.dll")) +} /// Contains IID values for private virtual desktop interfaces. /// /// Note that we can read interface ids from the Windows registry as well if we /// can't find them here. -const ACTXPRXY_PATH: &str = r"C:\Windows\System32\actxprxy.dll"; +fn actxprxy_path() -> eyre::Result { + Ok(system32()?.join("actxprxy.dll")) +} /// Parts of known mangled names for vtables const VIRTUAL_DESKTOP_V_TABLE_NAMES: &[&str] = &[ @@ -44,6 +92,125 @@ const VIRTUAL_DESKTOP_V_TABLE_NAMES: &[&str] = &[ "??_7ApplicationViewCollectionBase@@6B@", ]; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct WindowsVersion { + pub major_version: u32, + pub minor_version: u32, + pub build_number: u32, + pub patch_version: Option, +} +impl WindowsVersion { + /// Get the Windows patch version (the last number in the full version). + /// + /// # References + /// + /// - This is how the C# VirtualDesktop library does it: [VirtualDesktop/src/VirtualDesktop/Utils/OS.cs at 7e37b9848aef681713224dae558d2e51960cf41e ยท mzomparelli/VirtualDesktop](https://github.com/mzomparelli/VirtualDesktop/blob/7e37b9848aef681713224dae558d2e51960cf41e/src/VirtualDesktop/Utils/OS.cs#L21) + /// - We use this function: [RegGetValueW in windows::Win32::System::Registry - Rust](https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Registry/fn.RegGetValueW.html) + /// - Function docs: [RegGetValueW function (winreg.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-reggetvaluew) + /// - StackOverflow usage example: [windows - RegGetValueW(), how to do it right - Stack Overflow](https://stackoverflow.com/questions/78224404/reggetvaluew-how-to-do-it-right) + /// - Info about the registry key: [.net - C# - How to show the full Windows 10 build number? - Stack Overflow](https://stackoverflow.com/questions/52041735/c-sharp-how-to-show-the-full-windows-10-build-number) + fn read_patch_version_from_registry() -> Option { + use windows::{ + core::w, + Win32::System::Registry::{RegGetValueW, HKEY_LOCAL_MACHINE, RRF_RT_REG_DWORD}, + }; + + let mut buffer: [u8; 4] = [0; 4]; + let mut cb_data = buffer.len() as u32; + let res = unsafe { + RegGetValueW( + HKEY_LOCAL_MACHINE, + w!(r#"SOFTWARE\Microsoft\Windows NT\CurrentVersion"#), + w!("UBR"), + RRF_RT_REG_DWORD, + Some(std::ptr::null_mut()), + Some(buffer.as_mut_ptr() as _), + Some(&mut cb_data as *mut u32), + ) + }; + if res.is_err() { + eprintln!( + "Failed to read Windows patch version from the registry: {:?}", + windows::core::Error::from(res.to_hresult()) + ); + return None; + } + + // REG_DWORD is signed 32-bit, using little endian + let patch_version = i32::from_le_bytes(buffer); + if patch_version < 0 { + eprintln!( + "Windows patch version read from the registry was negative \ + ({patch_version}), ignoring read value" + ); + } + u32::try_from(patch_version).ok() + } + /// Get info about the current Windows version. Only differentiates between + /// Windows versions that have different virtual desktop interfaces. + /// + /// # Determining Windows Version + /// + /// We could use the [`GetVersionExW` function + /// (sysinfoapi.h)](https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getversionexw), + /// but it is deprecated after Windows 8.1. It also changes behavior depending + /// on what manifest is embedded in the executable. + /// + /// That pages links to [Version Helper functions - Win32 + /// apps](https://learn.microsoft.com/en-us/windows/win32/sysinfo/version-helper-apis) + /// where we are linked to the [`IsWindowsVersionOrGreater` function + /// (versionhelpers.h)](https://learn.microsoft.com/en-us/windows/win32/api/VersionHelpers/nf-versionhelpers-iswindowsversionorgreater) + /// and the [`VerifyVersionInfoA` function + /// (winbase.h)](https://learn.microsoft.com/en-us/windows/win32/api/Winbase/nf-winbase-verifyversioninfoa) + /// that it uses internally (though the later function is deprecated in Windows + /// 10). + /// + /// We can use `RtlGetVersion` [RtlGetVersion function (wdm.h) - Windows + /// drivers](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-rtlgetversion?redirectedfrom=MSDN) + /// as mentioned at [c++ - Detecting Windows 10 version - Stack + /// Overflow](https://stackoverflow.com/questions/36543301/detecting-windows-10-version/36545162#36545162). + /// + /// # `windows` API References + /// + /// - [GetVersionExW in windows::Win32::System::SystemInformation - + /// Rust](https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/SystemInformation/fn.GetVersionExW.html) + /// - Affected by manifest. + /// - [RtlGetVersion in windows::Wdk::System::SystemServices - + /// Rust](https://microsoft.github.io/windows-docs-rs/doc/windows/Wdk/System/SystemServices/fn.RtlGetVersion.html) + /// - Always returns the correct version. + pub fn get() -> eyre::Result { + let mut version: windows::Win32::System::SystemInformation::OSVERSIONINFOW = + Default::default(); + version.dwOSVersionInfoSize = core::mem::size_of_val(&version) as u32; + unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) } + .ok() + .context("Failed to get Windows version from RtlGetVersion")?; + + let patch_version = Self::read_patch_version_from_registry(); + Ok(Self { + major_version: version.dwMajorVersion, + minor_version: version.dwMinorVersion, + build_number: version.dwBuildNumber, + patch_version, + }) + } +} +impl fmt::Display for WindowsVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}.{}.{}.{}", + self.major_version, + self.minor_version, + self.build_number, + match self.patch_version { + Some(v) => v.to_string(), + None => "N/A".to_owned(), + } + ) + } +} + /// The code ID for a Windows PE file. /// /// When combined with the binary name, the `PeCodeId` lets you obtain binaries from @@ -57,7 +224,7 @@ const VIRTUAL_DESKTOP_V_TABLE_NAMES: &[&str] = &[ /// Note: copied from the [`wholesym`] crate. /// /// [`wholesym`]: https://docs.rs/samply-symbols/0.23.0/src/samply_symbols/shared.rs.html#227 -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct PeCodeId { pub timestamp: u32, pub image_size: u32, @@ -91,14 +258,14 @@ impl PeCodeId { } } impl FromStr for PeCodeId { - type Err = (); + type Err = &'static str; fn from_str(s: &str) -> Result { if s.len() < 9 || s.len() > 16 { - return Err(()); + return Err("invalid length"); } - let timestamp = u32::from_str_radix(&s[..8], 16).map_err(|_| ())?; - let image_size = u32::from_str_radix(&s[8..], 16).map_err(|_| ())?; + let timestamp = u32::from_str_radix(&s[..8], 16).map_err(|_| "invalid timestamp")?; + let image_size = u32::from_str_radix(&s[8..], 16).map_err(|_| "invalid image size")?; Ok(Self { timestamp, image_size, @@ -126,6 +293,14 @@ impl PeFile { pdb_path: None, } } + /// Dll file name without file extension. + pub fn file_stem(&self) -> eyre::Result<&str> { + self.dll_path + .file_stem() + .ok_or_eyre("dll paths have file names")? + .to_str() + .ok_or_eyre("dll files have UTF8 file names") + } /// Get a debug id that can be used to download a `.pdb` file. Use the /// [`DebugId::breakpad`] method and then [`ToString::to_string`] that. pub fn debug_id(&self) -> eyre::Result { @@ -143,19 +318,50 @@ impl PeFile { } /// This id can be used to download the `.dll` file. pub fn pe_code_id(&self) -> eyre::Result { - let data = std::fs::read(&self.dll_path) - .with_context(|| format!("Failed to read {}", self.dll_path.display()))?; - PeCodeId::for_file_data(data.as_slice()) + PeCodeId::for_file_data(self.read_dll()?.as_slice()) + } + /// Ensures `dll_path` points to a DLL with the specified code id. If the + /// current DLL doesn't match the id then a new DLL will be downloaded. + /// + /// Returns `true` if a new DLL had to be downloaded. + pub async fn maybe_download_dll( + &mut self, + downloader: &SymsrvDownloader, + wanted_pe_code_id: PeCodeId, + ) -> eyre::Result { + if self.pe_code_id()? == wanted_pe_code_id { + return Ok(false); + } + let dll_name = self + .dll_path + .file_name() + .ok_or_eyre("dll paths have file names")? + .to_str() + .ok_or_eyre("dll files have UTF8 file names")?; + assert!(dll_name.to_ascii_lowercase().ends_with(".dll")); + + // Get hash: + let hash = self.pe_code_id()?.to_string(); + + // Download and cache a DLL file. + let local_path = downloader.get_file(dll_name, &hash).await?; + + // At this point we don't want to use the DLL inside + // C:/Windows/System32, instead we want to use the newly downloaded DLL + // next to the executable: + self.dll_path = local_path; + + Ok(true) } /// Download and cache `.pdb` debug symbol file. pub async fn download_pdb(&mut self, downloader: &SymsrvDownloader) -> eyre::Result<()> { - let pdb_name = Path::new(&self.dll_path); - let pdb_name = pdb_name.with_extension("pdb"); + let pdb_name = self.dll_path.with_extension("pdb"); let pdb_name = pdb_name .file_name() .ok_or_eyre("dll paths have file names")? .to_str() .ok_or_eyre("dll files have UTF8 file names")?; + assert!(pdb_name.to_ascii_lowercase().ends_with(".pdb")); // Get hash: let hash = self.debug_id()?.breakpad().to_string(); @@ -174,7 +380,8 @@ impl PeFile { Ok(pdb::PDB::open(file)?) } pub fn read_dll(&self) -> eyre::Result> { - Ok(std::fs::read(&self.dll_path)?) + std::fs::read(&self.dll_path) + .with_context(|| format!("Failed to read DLL file at: {}", self.dll_path.display())) } } @@ -204,10 +411,12 @@ struct AddressInfo { rva: Rva, } +type SymbolWithSize<'sym> = (Option, pdb::Symbol<'sym>); + /// Inspired by [`symbolic_debuginfo::SymbolMap::from_iter`], assumes that a /// symbol occupies all space until the next symbol. fn calculate_size_for_symbols( - symbols: &mut [(Option, pdb::Symbol<'_>)], + symbols: &mut [SymbolWithSize<'_>], address_map: &pdb::AddressMap<'_>, ) { let mut symbols = symbols @@ -241,82 +450,174 @@ fn calculate_size_for_symbols( } } +struct DllRelated { + symbols: pdb::SymbolTable<'static>, + address_map: pdb::AddressMap<'static>, + /// All data from the DLL file. + dll_data: Vec, +} +impl DllRelated { + fn collect(dll_info: &PeFile) -> eyre::Result { + let mut pdb = dll_info.open_pdb()?; + + if !pdb.type_information()?.is_empty() { + eprintln!( + "Info: Type info isn't empty for {} as was expected, perhaps it could be useful", + dll_info.file_stem()? + ); + } + if !pdb.frame_table()?.is_empty() { + eprintln!( + "Info: Frame table isn't empty for {} as was expected, perhaps it could be useful", + dll_info.file_stem()? + ); + } + if !pdb.id_information()?.is_empty() { + eprintln!( + "Info: Id information isn't empty for {} as was expected, perhaps it could be useful", + dll_info.file_stem()? + ); + } + + let symbols = pdb.global_symbols()?; + let address_map = pdb.address_map()?; + + let dll_data = dll_info.read_dll()?; + + Ok(Self { + symbols, + address_map, + dll_data, + }) + } + /// Symbol together with its estimated size (from the + /// [`calculate_size_for_symbols`]). + fn estimate_symbol_sizes(&self) -> eyre::Result>> { + let mut all_symbols = self + .symbols + .iter() + .map(|sym| Ok((None, sym))) + .collect::>()?; + calculate_size_for_symbols(all_symbols.as_mut_slice(), &self.address_map); + Ok(all_symbols) + } +} + #[tokio::main] async fn main() -> eyre::Result<()> { - let mut unfiltered = false; - for arg in std::env::args().skip(1) { - if arg.eq_ignore_ascii_case("--all") { - unfiltered = true; - } else { - eyre::bail!("Unknown cli argument: {arg:?}"); - } + let Args { + unfiltered, + twinui_dll_id, + actxprxy_dll_id, + skip_twinui, + skip_actxprxy, + } = Args::parse(); + + if twinui_dll_id.is_none() && actxprxy_dll_id.is_none() { + println!("\nAnalyzing COM interfaces for local Windows installation.\n"); + println!("Windows Version: {}\n\n", WindowsVersion::get()?); + + // TODO: print IIDs from Windows registry + // HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Interface + + // https://stackoverflow.com/questions/17386755/get-keys-in-registry + // https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Registry/index.html + } else { + println!("\nAnalyzing COM interfaces for specific DLL files using PE code ids.\n") } let downloader = setup_download_next_to_exe(); - let mut twinui = PeFile::new(TWINUI_PCSHELL_PATH); - let mut actxprxy = PeFile::new(ACTXPRXY_PATH); - let mut pe_files = [&mut twinui, &mut actxprxy]; + let mut twinui = PeFile::new(twinui_pcshell_path()?); + let mut actxprxy = PeFile::new(actxprxy_path()?); + let mut pe_files = [ + (&mut twinui, twinui_dll_id, skip_twinui), + (&mut actxprxy, actxprxy_dll_id, skip_actxprxy), + ]; + + eprintln!( + "\nFetching PDB (and DLLs if PE code ids were specified) from Microsoft Symbol Server" + ); + for (pe_file, dll_id, skip) in &mut pe_files { + eprintln!(); + if *skip { + eprintln!("Ignoring information from: {}", pe_file.file_stem()?); + continue; + } + + if let Some(dll_id) = dll_id { + let did_download = pe_file + .maybe_download_dll(&downloader, *dll_id) + .await + .with_context(|| { + format!( + "Failed to download DLL for {}", + pe_file.file_stem().unwrap() + ) + })?; + if did_download { + eprintln!( + "PeCodeId for {}.dll differed from the dll in local Windows installation, \ + so it was downloaded from a Microsoft Symbol Server", + pe_file.file_stem()? + ); + } else { + eprintln!( + "PeCodeId for {}.dll matched with the dll in local Windows installation, \ + so nothing was downloaded", + pe_file.file_stem()? + ); + } + } + eprintln!("Using dll file at: {}", pe_file.dll_path.display()); - for pe_file in &mut pe_files { pe_file.download_pdb(&downloader).await?; - println!( - "Find pdb debug file at: {}", + eprintln!( + "Using pdb debug file at: {}", pe_file.pdb_path.as_ref().unwrap().display() ); - } - - let mut twinui_pdb = twinui.open_pdb()?; - let mut actxprxy_pdb = actxprxy.open_pdb()?; - if !twinui_pdb.type_information()?.is_empty() { - eprintln!("Info: Type info isn't empty as was expected, perhaps it could be useful"); - } - if !twinui_pdb.frame_table()?.is_empty() { - eprintln!("Info: Frame table isn't empty as was expected, perhaps it could be useful"); - } - if !twinui_pdb.id_information()?.is_empty() { - eprintln!("Info: Id information isn't empty as was expected, perhaps it could be useful"); + println!( + "\n{}.dll with PeCodeId: {}", + pe_file.file_stem()?, + pe_file.pe_code_id()? + ); + println!( + "{}.pdb with breakpad id: {}", + pe_file.file_stem()?, + pe_file.debug_id()?.breakpad() + ); } + println!("\n"); + eprintln!("\nFinding interface ids (IID) in the DLL files using PDB debug info:\n"); // actxprxy related: - let actxprxy_symbols = actxprxy_pdb.global_symbols()?; - let actxprxy_address_map = actxprxy_pdb.address_map()?; - - let mut actxprxy_all_symbols = actxprxy_symbols - .iter() - .map(|sym| Ok((None, sym))) - .collect::>()?; - calculate_size_for_symbols(actxprxy_all_symbols.as_mut_slice(), &actxprxy_address_map); - - let actxprxy_data = actxprxy.read_dll()?; + let actxprxy_info = (!skip_actxprxy) + .then(|| DllRelated::collect(&actxprxy)) + .transpose()?; + let actxprxy_symbols = actxprxy_info + .as_ref() + .map(|info| info.estimate_symbol_sizes()) + .transpose()?; // twinui realted: - let mut symbol_lookup = HashMap::new(); - - let twinui_symbols = twinui_pdb.global_symbols()?; - let twinui_address_map = twinui_pdb.address_map()?; - - let mut twinui_all_symbols = twinui_symbols - .iter() - .map(|sym| Ok((None, sym))) - .collect::>()?; - calculate_size_for_symbols(twinui_all_symbols.as_mut_slice(), &twinui_address_map); - for (info, sym) in &twinui_all_symbols { - let Some(info) = info else { continue }; - symbol_lookup.insert(info.rva, (info, sym)); - } - - let twinui_data = twinui.read_dll()?; - let twinui_image_base = object::File::parse(twinui_data.as_slice())?.relative_address_base(); + let twinui_info = (!skip_twinui) + .then(|| DllRelated::collect(&twinui)) + .transpose()?; + let twinui_symbols = twinui_info + .as_ref() + .map(|info| info.estimate_symbol_sizes()) + .transpose()?; // Search both dll files even though we are likely only interested in IID from actxprxy.dll: let pdb_related = [ - (&actxprxy_address_map, &actxprxy_all_symbols, &actxprxy_data), - (&twinui_address_map, &twinui_all_symbols, &twinui_data), + (&actxprxy_info, &actxprxy_symbols), + (&twinui_info, &twinui_symbols), ]; for related in pdb_related { - let (address_map, all_symbols, dll_data) = related; + let (Some(info), Some(all_symbols)) = related else { + continue; + }; for (size, symbol) in all_symbols { let Ok(pdb::SymbolData::Public(data)) = symbol.parse() else { @@ -340,16 +641,33 @@ async fn main() -> eyre::Result<()> { size.unwrap_or_default().size ); } - let rva = data.offset.to_rva(address_map).unwrap_or_default(); - let iid = &dll_data[rva.0 as usize..][..16]; + let rva = data.offset.to_rva(&info.address_map).unwrap_or_default(); + let iid = &info.dll_data[rva.0 as usize..][..16]; let iid = uuid::Uuid::from_slice_le(iid).context("Failed to parse IID as GUID")?; - println!("\n"); - println!("{}", data.name); - println!("\tIID: {iid:X}"); - println!(); + println!("{iid:X} for {}", data.name); } } + println!(); + + let (Some(twinui_info), Some(twinui_all_symbols)) = (&twinui_info, twinui_symbols) else { + eprintln!("Skipping virtual function tables because of --skip-twinui flag"); + return Ok(()); + }; + + eprintln!( + "\n\n\nFinding virtual function tables for COM interfaces \ + in the DLL files using PDB debug info:\n" + ); + + let mut symbol_lookup = HashMap::new(); + for (info, sym) in &twinui_all_symbols { + let Some(info) = info else { continue }; + symbol_lookup.insert(info.rva, (info, sym)); + } + + let twinui_image_base = + object::File::parse(twinui_info.dll_data.as_slice())?.relative_address_base(); for (size, symbol) in &twinui_all_symbols { // Will be either SymbolData::ProcedureReference or @@ -358,7 +676,10 @@ async fn main() -> eyre::Result<()> { let Ok(pdb::SymbolData::Public(data)) = symbol.parse() else { continue; }; - let rva = data.offset.to_rva(&twinui_address_map).unwrap_or_default(); + let rva = data + .offset + .to_rva(&twinui_info.address_map) + .unwrap_or_default(); let name = data.name.to_string(); // These filtering rules were ported from the Python script: @@ -380,25 +701,24 @@ async fn main() -> eyre::Result<()> { symbolic_common::NameMangling::Unknown, symbolic_common::Language::Unknown, ); - let lang = name_info.detect_language(); + let _lang = name_info.detect_language(); let demangled = name_info.demangle(symbolic_demangle::DemangleOptions::complete()); if !matches!(&demangled, Some(demangled) if demangled.contains("vftable")) { // Not a vtable definition! continue; } - println!("\n"); - println!("{} is {}", rva.0, data.name); - if let Some(name) = &demangled { - println!("\tDemangeled name ({lang:?}): {name}"); + if let Some(demangled) = &demangled { + println!("\n\nDumping vftable: {} ({})", demangled, data.name); + } else { + println!("\n\nDumping vftable: ({})", data.name); } if let Some(size) = size { - println!("\tEstimated Size: {}", size.size); + println!("\tVftable estimated size: {} bytes", size.size); } - println!("\t{:?}", data); - println!(); - let vft_data = &twinui_data[rva.0 as usize..][..size.unwrap_or_default().size as usize]; + let vft_data = + &twinui_info.dll_data[rva.0 as usize..][..size.unwrap_or_default().size as usize]; let vft_ptrs = vft_data .chunks_exact(8) .map(|bytes| { @@ -421,7 +741,7 @@ async fn main() -> eyre::Result<()> { }; let Ok(pdb::SymbolData::Public(sym)) = sym.parse() else { - unreachable!("previously parsed symbol when gather address info"); + unreachable!("previously parsed symbol when gathering address info"); }; let name_info = symbolic_common::Name::new(