feat: add iSphere live probe recorder

This commit is contained in:
zhaoyilun
2026-07-10 10:59:48 +08:00
parent 349fbe4a44
commit f7914a63dc
13 changed files with 2209 additions and 16 deletions

View File

@@ -0,0 +1,802 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Management;
using System.Net;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace ISphereWinHelper
{
internal static class LiveProbeInventory
{
private static readonly string[] InterestingTerms = new[]
{
"isphere", "implatform", "impp", "importal", "smark", "smack", "xmpp", "msg", "loginlog", "packetreader", "sendreceive"
};
private static readonly string[] InterestingExtensions = new[]
{
".exe", ".dll", ".config", ".xml", ".ini", ".json", ".log", ".db", ".sqlite", ".dat", ".idx", ".ldb", ".txt"
};
public static Dictionary<string, object> CollectProcessDetails(Dictionary<string, object> runtime)
{
var pids = TargetPids(runtime);
var details = new List<Dictionary<string, object>>();
foreach (int pid in pids)
{
string query = "SELECT ProcessId,ParentProcessId,Name,ExecutablePath,CommandLine,CreationDate FROM Win32_Process WHERE ProcessId=" + pid.ToString(CultureInfo.InvariantCulture);
foreach (ManagementObject obj in QueryWmi(query))
{
using (obj)
{
details.Add(new Dictionary<string, object>
{
{ "pid", SafeWmi(obj, "ProcessId") },
{ "parent_pid", SafeWmi(obj, "ParentProcessId") },
{ "name", SafeWmi(obj, "Name") },
{ "executable_path", SafeWmi(obj, "ExecutablePath") },
{ "command_line_redacted", RedactSecrets(SafeWmi(obj, "CommandLine")) },
{ "creation_date", SafeWmi(obj, "CreationDate") }
});
}
}
}
return new Dictionary<string, object>
{
{ "target_pid_count", pids.Count },
{ "details", details }
};
}
public static Dictionary<string, object> CollectNetwork(Dictionary<string, object> runtime)
{
var targetPids = TargetPids(runtime);
return new Dictionary<string, object>
{
{ "tcp", CaptureTargetTcpConnections(targetPids) },
{ "named_pipes", CaptureNamedPipes() }
};
}
public static Dictionary<string, object> CollectServicesRegistryShortcuts()
{
return new Dictionary<string, object>
{
{ "services", CaptureServices() },
{ "registry_uninstall", CaptureUninstallEntries() },
{ "shortcuts", CaptureShortcuts() }
};
}
public static Dictionary<string, object> CollectFilesystem(Dictionary<string, object> runtime)
{
var roots = CandidateRoots(runtime);
var entries = new List<Dictionary<string, object>>();
foreach (string root in roots)
{
entries.Add(InventoryRoot(root));
}
return new Dictionary<string, object>
{
{ "root_count", entries.Count },
{ "roots", entries }
};
}
public static Dictionary<string, object> SummarizeFeasibility(Dictionary<string, object> runtime, Dictionary<string, object> filesystem, Dictionary<string, object> network)
{
var targetPids = TargetPids(runtime);
bool hasTarget = targetPids.Count > 0;
bool hasSmack = AnyAssemblyPresent(runtime, "smack.dll");
bool hasInterface = AnyAssemblyPresent(runtime, "IMPP.Interface.dll");
bool hasService = AnyAssemblyPresent(runtime, "IMPP.Service.dll") || AnyAssemblyPresent(runtime, "IMPP.ServiceBase.dll");
bool hasTcpFileTransfer = AnyAssemblyPresent(runtime, "TcpFileTransfer.dll");
string route = hasTarget && hasSmack && hasInterface ? "B_ROUTE_PROMISING" : "NEEDS_REVIEW_OR_A_ROUTE";
return new Dictionary<string, object>
{
{ "target_process_seen", hasTarget },
{ "target_pid_count", targetPids.Count },
{ "has_smack_loaded", hasSmack },
{ "has_impp_interface_loaded", hasInterface },
{ "has_impp_service_loaded", hasService },
{ "has_tcp_file_transfer_loaded", hasTcpFileTransfer },
{ "initial_route_hint", route },
{ "next_decision", hasTarget ? "review loaded modules, bitness, install files, and UIA target windows" : "run again after manual login in the working environment" }
};
}
private static Dictionary<string, object> InventoryRoot(string root)
{
var files = new List<Dictionary<string, object>>();
var directories = new List<string>();
if (!Directory.Exists(root))
{
return new Dictionary<string, object>
{
{ "root", root },
{ "exists", false },
{ "directories_sample", directories },
{ "files", files }
};
}
foreach (string dir in SafeDirectories(root, 3, 120))
{
directories.Add(dir);
}
foreach (string file in SafeFiles(root, 4, 1000))
{
if (IsInterestingFile(file))
{
files.Add(DescribeFile(file));
}
}
files.Sort(delegate(Dictionary<string, object> left, Dictionary<string, object> right)
{
return string.Compare(Convert.ToString(left["path"]), Convert.ToString(right["path"]), StringComparison.OrdinalIgnoreCase);
});
return new Dictionary<string, object>
{
{ "root", root },
{ "exists", true },
{ "directories_sample", directories },
{ "interesting_file_count", files.Count },
{ "files", files }
};
}
private static Dictionary<string, object> DescribeFile(string path)
{
var info = new FileInfo(path);
string version = "";
string productVersion = "";
try
{
FileVersionInfo vi = FileVersionInfo.GetVersionInfo(path);
version = vi.FileVersion ?? "";
productVersion = vi.ProductVersion ?? "";
}
catch
{
}
bool keyBinary = IsKeyBinary(info.Name);
bool smallText = IsSmallTextFile(info);
return new Dictionary<string, object>
{
{ "path", info.FullName },
{ "name", info.Name },
{ "extension", info.Extension },
{ "length", info.Length },
{ "creation_time_utc", info.CreationTimeUtc.ToString("o") },
{ "last_write_time_utc", info.LastWriteTimeUtc.ToString("o") },
{ "file_version", version },
{ "product_version", productVersion },
{ "sha256", keyBinary || info.Length <= 8 * 1024 * 1024 ? Sha256(info.FullName) : "" },
{ "text_sample_redacted", smallText ? RedactedTextSample(info.FullName, 4096) : "" }
};
}
private static SortedSet<string> CandidateRoots(Dictionary<string, object> runtime)
{
var roots = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Dictionary<string, object> target in GetDictionaryList(runtime, "targets"))
{
string exe = GetString(target, "executable_path");
if (!string.IsNullOrWhiteSpace(exe))
{
AddRoot(roots, Path.GetDirectoryName(exe));
}
}
AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Temp", "importal"));
AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "importal"));
AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "importal"));
AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "importal"));
AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "iSphere"));
AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "IMPP"));
return roots;
}
private static void AddRoot(SortedSet<string> roots, string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return;
}
try
{
if (Directory.Exists(path))
{
roots.Add(Path.GetFullPath(path));
}
}
catch
{
}
}
private static List<Dictionary<string, object>> CaptureServices()
{
var services = new List<Dictionary<string, object>>();
foreach (ManagementObject obj in QueryWmi("SELECT Name,DisplayName,State,StartMode,PathName,ProcessId FROM Win32_Service"))
{
using (obj)
{
string combined = (SafeWmi(obj, "Name") + " " + SafeWmi(obj, "DisplayName") + " " + SafeWmi(obj, "PathName")).ToLowerInvariant();
if (!ContainsInterestingTerm(combined))
{
continue;
}
services.Add(new Dictionary<string, object>
{
{ "name", SafeWmi(obj, "Name") },
{ "display_name", SafeWmi(obj, "DisplayName") },
{ "state", SafeWmi(obj, "State") },
{ "start_mode", SafeWmi(obj, "StartMode") },
{ "path_name_redacted", RedactSecrets(SafeWmi(obj, "PathName")) },
{ "process_id", SafeWmi(obj, "ProcessId") }
});
}
}
return services;
}
private static List<Dictionary<string, object>> CaptureUninstallEntries()
{
var entries = new List<Dictionary<string, object>>();
CaptureUninstallHive(entries, RegistryHive.LocalMachine, RegistryView.Registry64);
CaptureUninstallHive(entries, RegistryHive.LocalMachine, RegistryView.Registry32);
CaptureUninstallHive(entries, RegistryHive.CurrentUser, RegistryView.Registry64);
CaptureUninstallHive(entries, RegistryHive.CurrentUser, RegistryView.Registry32);
return entries;
}
private static void CaptureUninstallHive(List<Dictionary<string, object>> entries, RegistryHive hive, RegistryView view)
{
try
{
using (RegistryKey baseKey = RegistryKey.OpenBaseKey(hive, view))
using (RegistryKey uninstall = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"))
{
if (uninstall == null)
{
return;
}
foreach (string subName in uninstall.GetSubKeyNames())
{
using (RegistryKey sub = uninstall.OpenSubKey(subName))
{
if (sub == null)
{
continue;
}
string displayName = Convert.ToString(sub.GetValue("DisplayName", ""));
string installLocation = Convert.ToString(sub.GetValue("InstallLocation", ""));
string publisher = Convert.ToString(sub.GetValue("Publisher", ""));
string combined = (subName + " " + displayName + " " + installLocation + " " + publisher).ToLowerInvariant();
if (!ContainsInterestingTerm(combined))
{
continue;
}
entries.Add(new Dictionary<string, object>
{
{ "hive", hive.ToString() },
{ "view", view.ToString() },
{ "key", subName },
{ "display_name", displayName },
{ "display_version", Convert.ToString(sub.GetValue("DisplayVersion", "")) },
{ "publisher", publisher },
{ "install_location", installLocation },
{ "uninstall_string_redacted", RedactSecrets(Convert.ToString(sub.GetValue("UninstallString", ""))) }
});
}
}
}
}
catch
{
}
}
private static List<Dictionary<string, object>> CaptureShortcuts()
{
var shortcuts = new List<Dictionary<string, object>>();
string[] roots = new[]
{
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory),
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu)
};
foreach (string root in roots)
{
if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
{
continue;
}
foreach (string lnk in SafeFiles(root, 5, 500))
{
if (!lnk.EndsWith(".lnk", StringComparison.OrdinalIgnoreCase))
{
continue;
}
string lower = lnk.ToLowerInvariant();
if (!ContainsInterestingTerm(lower))
{
continue;
}
FileInfo info = new FileInfo(lnk);
shortcuts.Add(new Dictionary<string, object>
{
{ "path", info.FullName },
{ "length", info.Length },
{ "last_write_time_utc", info.LastWriteTimeUtc.ToString("o") },
{ "sha256", info.Length <= 1024 * 1024 ? Sha256(info.FullName) : "" }
});
}
}
return shortcuts;
}
private static List<Dictionary<string, object>> CaptureNamedPipes()
{
var pipes = new List<Dictionary<string, object>>();
try
{
foreach (string pipe in Directory.GetFiles(@"\\.\pipe\"))
{
string lower = pipe.ToLowerInvariant();
if (!ContainsInterestingTerm(lower))
{
continue;
}
pipes.Add(new Dictionary<string, object> { { "path", pipe } });
}
}
catch (Exception ex)
{
pipes.Add(new Dictionary<string, object> { { "error", ex.GetType().Name + ": " + ex.Message } });
}
return pipes;
}
private const int AF_INET = 2;
private const int TCP_TABLE_OWNER_PID_ALL = 5;
[DllImport("iphlpapi.dll", SetLastError = true)]
private static extern uint GetExtendedTcpTable(IntPtr pTcpTable, ref int dwOutBufLen, bool sort, int ipVersion, int tblClass, uint reserved);
[StructLayout(LayoutKind.Sequential)]
private struct MIB_TCPROW_OWNER_PID
{
public uint state;
public uint localAddr;
public uint localPort;
public uint remoteAddr;
public uint remotePort;
public uint owningPid;
}
private static Dictionary<string, object> CaptureTargetTcpConnections(HashSet<int> targetPids)
{
var rows = new List<Dictionary<string, object>>();
string error = "";
try
{
int bufferSize = 0;
GetExtendedTcpTable(IntPtr.Zero, ref bufferSize, true, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
IntPtr buffer = Marshal.AllocHGlobal(bufferSize);
try
{
uint result = GetExtendedTcpTable(buffer, ref bufferSize, true, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
if (result != 0)
{
error = "GetExtendedTcpTable result=" + result.ToString(CultureInfo.InvariantCulture);
}
else
{
int count = Marshal.ReadInt32(buffer);
IntPtr rowPtr = new IntPtr(buffer.ToInt64() + 4);
int rowSize = Marshal.SizeOf(typeof(MIB_TCPROW_OWNER_PID));
for (int i = 0; i < count; i++)
{
var row = (MIB_TCPROW_OWNER_PID)Marshal.PtrToStructure(rowPtr, typeof(MIB_TCPROW_OWNER_PID));
int pid = unchecked((int)row.owningPid);
if (targetPids.Contains(pid))
{
rows.Add(new Dictionary<string, object>
{
{ "pid", pid },
{ "state", TcpStateName(row.state) },
{ "local_address", UIntToIp(row.localAddr) },
{ "local_port", NetworkToHostPort(row.localPort) },
{ "remote_address", UIntToIp(row.remoteAddr) },
{ "remote_port", NetworkToHostPort(row.remotePort) }
});
}
rowPtr = new IntPtr(rowPtr.ToInt64() + rowSize);
}
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
catch (Exception ex)
{
error = ex.GetType().Name + ": " + ex.Message;
}
return new Dictionary<string, object>
{
{ "target_pid_count", targetPids.Count },
{ "connection_count", rows.Count },
{ "connections", rows },
{ "error", error }
};
}
private static string UIntToIp(uint value)
{
try
{
return new IPAddress(value).ToString();
}
catch
{
return value.ToString(CultureInfo.InvariantCulture);
}
}
private static int NetworkToHostPort(uint port)
{
return (int)(((port & 0xFF) << 8) | ((port >> 8) & 0xFF));
}
private static string TcpStateName(uint state)
{
switch (state)
{
case 1: return "CLOSED";
case 2: return "LISTEN";
case 3: return "SYN_SENT";
case 4: return "SYN_RCVD";
case 5: return "ESTABLISHED";
case 6: return "FIN_WAIT1";
case 7: return "FIN_WAIT2";
case 8: return "CLOSE_WAIT";
case 9: return "CLOSING";
case 10: return "LAST_ACK";
case 11: return "TIME_WAIT";
case 12: return "DELETE_TCB";
default: return state.ToString(CultureInfo.InvariantCulture);
}
}
private static IEnumerable<ManagementObject> QueryWmi(string query)
{
var list = new List<ManagementObject>();
try
{
using (var searcher = new ManagementObjectSearcher(query))
using (ManagementObjectCollection results = searcher.Get())
{
foreach (ManagementObject obj in results)
{
list.Add(obj);
}
}
}
catch
{
}
return list;
}
private static string SafeWmi(ManagementObject obj, string property)
{
try
{
object value = obj[property];
return value == null ? "" : Convert.ToString(value);
}
catch
{
return "";
}
}
private static bool AnyAssemblyPresent(Dictionary<string, object> runtime, string name)
{
foreach (Dictionary<string, object> target in GetDictionaryList(runtime, "targets"))
{
object rawPresence;
if (!target.TryGetValue("assembly_presence", out rawPresence))
{
continue;
}
var presence = rawPresence as Dictionary<string, object>;
if (presence == null)
{
continue;
}
object value;
if (presence.TryGetValue(name, out value) && value is bool && (bool)value)
{
return true;
}
}
return false;
}
private static HashSet<int> TargetPids(Dictionary<string, object> runtime)
{
var pids = new HashSet<int>();
foreach (Dictionary<string, object> target in GetDictionaryList(runtime, "targets"))
{
int pid = GetInt(target, "pid");
if (pid > 0)
{
pids.Add(pid);
}
}
return pids;
}
private static List<Dictionary<string, object>> GetDictionaryList(Dictionary<string, object> source, string key)
{
var list = new List<Dictionary<string, object>>();
object value;
if (source == null || !source.TryGetValue(key, out value) || value == null)
{
return list;
}
var direct = value as List<Dictionary<string, object>>;
if (direct != null)
{
return direct;
}
return list;
}
private static string GetString(Dictionary<string, object> source, string key)
{
object value;
if (source != null && source.TryGetValue(key, out value) && value != null)
{
return Convert.ToString(value);
}
return "";
}
private static int GetInt(Dictionary<string, object> source, string key)
{
object value;
if (source != null && source.TryGetValue(key, out value) && value != null)
{
try
{
return Convert.ToInt32(value);
}
catch
{
}
}
return 0;
}
private static List<string> SafeDirectories(string root, int maxDepth, int maxItems)
{
var result = new List<string>();
SafeDirectories(root, 0, maxDepth, maxItems, result);
return result;
}
private static void SafeDirectories(string root, int depth, int maxDepth, int maxItems, List<string> result)
{
if (depth > maxDepth || result.Count >= maxItems || !Directory.Exists(root))
{
return;
}
string[] dirs;
try
{
dirs = Directory.GetDirectories(root);
}
catch
{
return;
}
foreach (string dir in dirs)
{
if (result.Count >= maxItems)
{
return;
}
result.Add(dir);
SafeDirectories(dir, depth + 1, maxDepth, maxItems, result);
}
}
private static List<string> SafeFiles(string root, int maxDepth, int maxItems)
{
var result = new List<string>();
SafeFiles(root, 0, maxDepth, maxItems, result);
return result;
}
private static void SafeFiles(string root, int depth, int maxDepth, int maxItems, List<string> result)
{
if (depth > maxDepth || result.Count >= maxItems || !Directory.Exists(root))
{
return;
}
string[] files;
try
{
files = Directory.GetFiles(root);
}
catch
{
return;
}
foreach (string file in files)
{
if (result.Count >= maxItems)
{
return;
}
result.Add(file);
}
string[] dirs;
try
{
dirs = Directory.GetDirectories(root);
}
catch
{
return;
}
foreach (string dir in dirs)
{
if (result.Count >= maxItems)
{
return;
}
SafeFiles(dir, depth + 1, maxDepth, maxItems, result);
}
}
private static bool IsInterestingFile(string path)
{
string name = Path.GetFileName(path).ToLowerInvariant();
string ext = Path.GetExtension(path).ToLowerInvariant();
if (ContainsInterestingTerm(name))
{
return true;
}
foreach (string interestingExt in InterestingExtensions)
{
if (ext == interestingExt)
{
return true;
}
}
return false;
}
private static bool IsKeyBinary(string name)
{
string lower = name.ToLowerInvariant();
return lower == "implatformclient.exe" ||
lower == "smack.dll" ||
lower == "impp.interface.dll" ||
lower == "impp.service.dll" ||
lower == "impp.servicebase.dll" ||
lower == "tcpfiletransfer.dll" ||
lower == "hyhc.impp.dal.dll" ||
lower == "inetwork.dll" ||
lower == "ioclientnetwork.dll";
}
private static bool IsSmallTextFile(FileInfo info)
{
if (info.Length > 256 * 1024)
{
return false;
}
string ext = info.Extension.ToLowerInvariant();
return ext == ".config" || ext == ".xml" || ext == ".ini" || ext == ".json" || ext == ".log" || ext == ".txt";
}
private static string RedactedTextSample(string path, int maxChars)
{
try
{
string text = File.ReadAllText(path, Encoding.UTF8);
if (text.Length > maxChars)
{
text = text.Substring(0, maxChars);
}
return RedactSecrets(text);
}
catch
{
try
{
byte[] bytes = File.ReadAllBytes(path);
int len = Math.Min(bytes.Length, maxChars);
string text = Encoding.Default.GetString(bytes, 0, len);
return RedactSecrets(text);
}
catch
{
return "";
}
}
}
private static string RedactSecrets(string text)
{
if (string.IsNullOrEmpty(text))
{
return "";
}
string redacted = Regex.Replace(text, "(password|passwd|pwd|token|secret|session|cookie|auth|authorization)\\s*[:=]\\s*[^\\s;&<>\\\"]+", "$1=<redacted>", RegexOptions.IgnoreCase);
redacted = Regex.Replace(redacted, "(UserID|UserName|LoginName)\\s*[:=]\\s*[^\\s;&<>\\\"]+", "$1=<redacted>", RegexOptions.IgnoreCase);
return redacted;
}
private static bool ContainsInterestingTerm(string text)
{
if (string.IsNullOrEmpty(text))
{
return false;
}
string lower = text.ToLowerInvariant();
foreach (string term in InterestingTerms)
{
if (lower.Contains(term))
{
return true;
}
}
return false;
}
private static string Sha256(string path)
{
try
{
using (SHA256 sha = SHA256.Create())
using (FileStream stream = File.OpenRead(path))
{
byte[] hash = sha.ComputeHash(stream);
var sb = new StringBuilder(hash.Length * 2);
foreach (byte b in hash)
{
sb.Append(b.ToString("x2", CultureInfo.InvariantCulture));
}
return sb.ToString();
}
}
catch
{
return "";
}
}
}
}

View File

@@ -0,0 +1,259 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace ISphereWinHelper
{
internal static class LiveProbeRecorder
{
public static int Run(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
string outputRoot = ResolveOutputRoot(args);
string runId = DateTime.Now.ToString("yyyyMMdd-HHmmss");
string outputDir = Path.Combine(outputRoot, "isphere-live-probe-" + runId);
Directory.CreateDirectory(outputDir);
try
{
WriteText(Path.Combine(outputDir, "README.txt"), Readme());
WriteJson(Path.Combine(outputDir, "manifest.json"), Manifest(runId, outputDir));
var probeArgs = new Dictionary<string, object>
{
{ "process_names", new object[] { "IMPlatformClient", "IMPP.ISphere", "iSphere", "importal" } },
{ "include_modules", true },
{ "max_modules", 180 }
};
Dictionary<string, object> runtime = RuntimeProbe.ProbeClientRuntime(probeArgs);
WriteJson(Path.Combine(outputDir, "probe_client_runtime.json"), runtime);
Dictionary<string, object> processDetails = LiveProbeInventory.CollectProcessDetails(runtime);
WriteJson(Path.Combine(outputDir, "process_details.json"), processDetails);
Dictionary<string, object> windows = WindowScanner.Scan(new Dictionary<string, object>
{
{ "include_all_visible", true }
});
WriteJson(Path.Combine(outputDir, "scan_windows.json"), windows);
Dictionary<string, object> network = LiveProbeInventory.CollectNetwork(runtime);
WriteJson(Path.Combine(outputDir, "network_inventory.json"), network);
Dictionary<string, object> servicesRegistryShortcuts = LiveProbeInventory.CollectServicesRegistryShortcuts();
WriteJson(Path.Combine(outputDir, "services_registry_shortcuts.json"), servicesRegistryShortcuts);
Dictionary<string, object> filesystem = LiveProbeInventory.CollectFilesystem(runtime);
WriteJson(Path.Combine(outputDir, "filesystem_inventory.json"), filesystem);
string uiaDir = Path.Combine(outputDir, "uia");
Directory.CreateDirectory(uiaDir);
List<Dictionary<string, object>> uiaSummary = DumpTargetWindowUia(runtime, windows, uiaDir);
WriteJson(Path.Combine(outputDir, "uia_summary.json"), uiaSummary);
Dictionary<string, object> feasibility = LiveProbeInventory.SummarizeFeasibility(runtime, filesystem, network);
WriteJson(Path.Combine(outputDir, "feasibility_summary.json"), feasibility);
WriteText(Path.Combine(outputDir, "NEXT_STEP.txt"),
"把整个目录压缩后发回即可。\r\n" +
"重点文件probe_client_runtime.json、process_details.json、network_inventory.json、filesystem_inventory.json、services_registry_shortcuts.json、scan_windows.json、uia_summary.json、uia/*.json、feasibility_summary.json\r\n");
Console.WriteLine("OK");
Console.WriteLine("已生成采集目录:");
Console.WriteLine(outputDir);
Console.WriteLine();
Console.WriteLine("请把这个目录压缩后发回。");
Pause(args);
return 0;
}
catch (Exception ex)
{
WriteText(Path.Combine(outputDir, "RECORDER_ERROR.txt"), ex.ToString());
Console.WriteLine("FAILED");
Console.WriteLine(ex.ToString());
Pause(args);
return 1;
}
}
private static List<Dictionary<string, object>> DumpTargetWindowUia(Dictionary<string, object> runtime, Dictionary<string, object> windows, string uiaDir)
{
var targetPids = TargetPids(runtime);
var summary = new List<Dictionary<string, object>>();
object rawWindows;
if (!windows.TryGetValue("windows", out rawWindows))
{
return summary;
}
var windowList = rawWindows as List<Dictionary<string, object>>;
if (windowList == null)
{
return summary;
}
foreach (Dictionary<string, object> window in windowList)
{
int pid = GetInt(window, "pid");
if (!targetPids.Contains(pid))
{
continue;
}
string hwnd = GetString(window, "hwnd");
string title = GetString(window, "title");
string fileName = "uia-pid" + pid + "-" + hwnd.Replace("0x", "") + ".json";
string outPath = Path.Combine(uiaDir, fileName);
Dictionary<string, object> dump = UiaDumper.Dump("live-probe-uia", "dump_uia", new Dictionary<string, object>
{
{ "hwnd", hwnd },
{ "max_depth", 8 },
{ "include_text", true },
{ "max_children", 180 }
});
WriteJson(outPath, dump);
summary.Add(new Dictionary<string, object>
{
{ "pid", pid },
{ "hwnd", hwnd },
{ "title", title },
{ "path", outPath },
{ "ok", GetBool(dump, "ok") }
});
}
return summary;
}
private static HashSet<int> TargetPids(Dictionary<string, object> runtime)
{
var pids = new HashSet<int>();
object rawTargets;
if (!runtime.TryGetValue("targets", out rawTargets))
{
return pids;
}
var targets = rawTargets as List<Dictionary<string, object>>;
if (targets == null)
{
return pids;
}
foreach (Dictionary<string, object> target in targets)
{
int pid = GetInt(target, "pid");
if (pid > 0)
{
pids.Add(pid);
}
}
return pids;
}
private static Dictionary<string, object> Manifest(string runId, string outputDir)
{
return new Dictionary<string, object>
{
{ "recorder_name", "ISphereLiveProbeRecorder" },
{ "recorder_version", "0.2.0" },
{ "helper_protocol", HelperProtocol.Protocol },
{ "run_id", runId },
{ "timestamp_local", DateTime.Now.ToString("o") },
{ "timestamp_utc", DateTime.UtcNow.ToString("o") },
{ "output_dir", outputDir },
{ "scope", "read_only_full_inventory_for_B_route_sidecar_and_A_route_rpa_fallback" },
{ "does_not", new object[] { "send_message", "send_file", "upload_file", "click_ui", "type_text", "inject_hook", "modify_client_data" } },
{ "machine_name", Environment.MachineName },
{ "user_domain", Environment.UserDomainName },
{ "user_name", Environment.UserName },
{ "os_version", Environment.OSVersion.ToString() },
{ "is_64_bit_os", Environment.Is64BitOperatingSystem },
{ "is_64_bit_process", Environment.Is64BitProcess },
{ "clr_version", Environment.Version.ToString() }
};
}
private static string Readme()
{
return "ISphereLiveProbeRecorder\r\n\r\n" +
"使用方式:\r\n" +
"1. 到可以正常连接服务端的环境。\r\n" +
"2. 手工打开并登录 iSphere / IMPlatformClient。\r\n" +
"3. 尽量手工打开一个联系人聊天窗口、一个群聊窗口,并让输入框/发送按钮/附件按钮所在区域可见;不用发送任何内容。\r\n" +
"4. 保持客户端窗口打开。\r\n" +
"5. 双击 ISphereLiveProbeRecorder.exe。\r\n" +
"6. 程序会在 probe-output 下生成 isphere-live-probe-时间戳 目录。\r\n" +
"7. 把整个目录压缩后发回。\r\n\r\n" +
"采集内容:运行进程、模块、命令行摘要、窗口、目标窗口 UIA 控件树、安装目录关键文件、配置/日志/数据库候选、注册表卸载项、快捷方式、服务、本地 TCP 连接、命名管道。\r\n" +
"不会执行发送消息、发送文件、上传文件、点击、输入、注入、hook、修改客户端数据。\r\n";
}
private static string ResolveOutputRoot(string[] args)
{
for (int i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], "--out", StringComparison.OrdinalIgnoreCase))
{
return Path.GetFullPath(args[i + 1]);
}
}
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "probe-output");
}
private static void Pause(string[] args)
{
foreach (string arg in args)
{
if (string.Equals(arg, "--no-pause", StringComparison.OrdinalIgnoreCase))
{
return;
}
}
if (Environment.UserInteractive)
{
Console.WriteLine();
Console.WriteLine("按回车退出...");
try { Console.ReadLine(); } catch { }
}
}
private static string GetString(Dictionary<string, object> source, string key)
{
object value;
if (source.TryGetValue(key, out value) && value != null)
{
return Convert.ToString(value);
}
return "";
}
private static int GetInt(Dictionary<string, object> source, string key)
{
object value;
if (source.TryGetValue(key, out value) && value != null)
{
try { return Convert.ToInt32(value); } catch { }
}
return 0;
}
private static bool GetBool(Dictionary<string, object> source, string key)
{
object value;
if (source.TryGetValue(key, out value) && value != null)
{
try { return Convert.ToBoolean(value); } catch { }
}
return false;
}
private static void WriteJson(string path, object value)
{
File.WriteAllText(path, HelperProtocol.ToJson(value), Encoding.UTF8);
}
private static void WriteText(string path, string value)
{
File.WriteAllText(path, value, Encoding.UTF8);
}
}
}

View File

@@ -9,6 +9,11 @@ namespace ISphereWinHelper
[STAThread]
private static int Main(string[] args)
{
if (ShouldRunLiveProbeRecorder(args))
{
return LiveProbeRecorder.Run(args);
}
string requestText = Console.In.ReadToEnd();
Dictionary<string, object> request;
string requestId = "";
@@ -39,6 +44,9 @@ namespace ISphereWinHelper
case "dump_uia":
response = UiaDumper.Dump(requestId, op, opArgs);
break;
case "probe_client_runtime":
response = HelperProtocol.Success(requestId, op, RuntimeProbe.ProbeClientRuntime(opArgs));
break;
default:
response = HelperProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op);
break;
@@ -55,12 +63,26 @@ namespace ISphereWinHelper
}
}
private static bool ShouldRunLiveProbeRecorder(string[] args)
{
foreach (string arg in args)
{
if (string.Equals(arg, "--record-live-probe", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
string exeName = Path.GetFileNameWithoutExtension(AppDomain.CurrentDomain.FriendlyName);
return exeName != null && exeName.IndexOf("LiveProbeRecorder", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static Dictionary<string, object> VersionData()
{
return new Dictionary<string, object>
{
{ "helper_name", "ISphereWinHelper" },
{ "helper_version", "0.1.0" },
{ "helper_version", "0.3.0" },
{ "protocol", HelperProtocol.Protocol },
{ "runtime", ".NET Framework " + Environment.Version }
};

View File

@@ -0,0 +1,332 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ISphereWinHelper
{
internal static class RuntimeProbe
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool IsWow64Process(IntPtr hProcess, out bool wow64Process);
public static Dictionary<string, object> ProbeClientRuntime(Dictionary<string, object> args)
{
var processNames = GetStringList(args, "process_names", new[] { "IMPlatformClient", "IMPP.ISphere", "iSphere", "importal" });
var moduleHints = GetStringList(args, "module_hints", new[]
{
"IMPlatformClient.exe",
"smack.dll",
"IMPP.Interface.dll",
"IMPP.ServiceBase.dll",
"IMPP.Service.dll",
"IMPP.Common.dll",
"IMPP.Helper.dll",
"IMPP.Model.dll",
"IMPP.Util.dll",
"IMPP.UI.dll",
"TcpFileTransfer.dll",
"HYHC.IMPP.DAL.dll",
"INetwork.dll",
"IOClientNetwork.dll"
});
bool includeModules = HelperProtocol.GetBool(args, "include_modules", true);
int maxModules = HelperProtocol.GetInt(args, "max_modules", 120);
if (maxModules < 0)
{
maxModules = 0;
}
var targets = new List<Dictionary<string, object>>();
foreach (Process process in Process.GetProcesses())
{
using (process)
{
string processName = SafeProcessName(process);
if (!MatchesAny(processName, processNames))
{
continue;
}
targets.Add(DescribeProcess(process, moduleHints, includeModules, maxModules));
}
}
targets.Sort(delegate(Dictionary<string, object> left, Dictionary<string, object> right)
{
int leftScore = Convert.ToInt32(left["score"]);
int rightScore = Convert.ToInt32(right["score"]);
int scoreCompare = rightScore.CompareTo(leftScore);
if (scoreCompare != 0)
{
return scoreCompare;
}
return Convert.ToInt32(left["pid"]).CompareTo(Convert.ToInt32(right["pid"]));
});
return new Dictionary<string, object>
{
{ "probe_mode", "read_only_process_module_inventory" },
{ "host", new Dictionary<string, object>
{
{ "is_64_bit_os", Environment.Is64BitOperatingSystem },
{ "is_64_bit_process", Environment.Is64BitProcess },
{ "machine_name", Environment.MachineName }
}
},
{ "process_names", processNames },
{ "module_hints", moduleHints },
{ "target_count", targets.Count },
{ "targets", targets }
};
}
private static Dictionary<string, object> DescribeProcess(Process process, List<string> moduleHints, bool includeModules, int maxModules)
{
string executablePath = "";
string mainModuleName = "";
string mainModuleVersion = "";
string mainModuleError = "";
try
{
ProcessModule mainModule = process.MainModule;
if (mainModule != null)
{
executablePath = mainModule.FileName ?? "";
mainModuleName = mainModule.ModuleName ?? "";
FileVersionInfo versionInfo = mainModule.FileVersionInfo;
if (versionInfo != null)
{
mainModuleVersion = versionInfo.FileVersion ?? "";
}
}
}
catch (Exception ex)
{
mainModuleError = ex.GetType().Name + ": " + ex.Message;
}
bool? isWow64 = GetIsWow64(process);
string bitness = "unknown";
if (Environment.Is64BitOperatingSystem && isWow64.HasValue)
{
bitness = isWow64.Value ? "x86" : "x64";
}
else if (!Environment.Is64BitOperatingSystem)
{
bitness = "x86";
}
string moduleProbeError = "";
var modules = new List<Dictionary<string, object>>();
var assemblyPresence = NewPresenceMap(moduleHints);
if (includeModules)
{
try
{
int count = 0;
foreach (ProcessModule module in process.Modules)
{
if (count >= maxModules)
{
break;
}
string moduleName = module.ModuleName ?? "";
string modulePath = module.FileName ?? "";
string matchedHint = FindMatchedHint(moduleName, modulePath, moduleHints);
if (!string.IsNullOrEmpty(matchedHint))
{
assemblyPresence[matchedHint] = true;
}
modules.Add(new Dictionary<string, object>
{
{ "name", moduleName },
{ "path", modulePath },
{ "matched_hint", matchedHint }
});
count++;
}
}
catch (Exception ex)
{
moduleProbeError = ex.GetType().Name + ": " + ex.Message;
}
}
if (!string.IsNullOrEmpty(mainModuleName))
{
string matchedMain = FindMatchedHint(mainModuleName, executablePath, moduleHints);
if (!string.IsNullOrEmpty(matchedMain))
{
assemblyPresence[matchedMain] = true;
}
}
int score = Score(process.ProcessName, executablePath, assemblyPresence);
return new Dictionary<string, object>
{
{ "pid", process.Id },
{ "process_name", process.ProcessName },
{ "main_window_title", SafeMainWindowTitle(process) },
{ "main_window_handle", WindowScanner.FormatHwnd(process.MainWindowHandle) },
{ "has_main_window", process.MainWindowHandle != IntPtr.Zero },
{ "executable_path", executablePath },
{ "main_module_name", mainModuleName },
{ "main_module_version", mainModuleVersion },
{ "main_module_error", mainModuleError },
{ "bitness", bitness },
{ "is_wow64", isWow64.HasValue ? (object)isWow64.Value : null },
{ "module_probe_ok", string.IsNullOrEmpty(moduleProbeError) },
{ "module_probe_error", moduleProbeError },
{ "assembly_presence", assemblyPresence },
{ "modules", modules },
{ "score", score }
};
}
private static Dictionary<string, object> NewPresenceMap(List<string> moduleHints)
{
var presence = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
foreach (string hint in moduleHints)
{
if (!presence.ContainsKey(hint))
{
presence.Add(hint, false);
}
}
return presence;
}
private static int Score(string processName, string executablePath, Dictionary<string, object> assemblyPresence)
{
int score = 0;
string combined = ((processName ?? "") + " " + (executablePath ?? "")).ToLowerInvariant();
if (combined.Contains("implatformclient")) score += 80;
if (combined.Contains("isphere")) score += 40;
if (combined.Contains("impp")) score += 20;
if (combined.Contains("importal")) score += 10;
foreach (object value in assemblyPresence.Values)
{
if (value is bool && (bool)value)
{
score += 5;
}
}
return score;
}
private static bool? GetIsWow64(Process process)
{
try
{
bool isWow64;
if (IsWow64Process(process.Handle, out isWow64))
{
return isWow64;
}
}
catch
{
}
return null;
}
private static string SafeProcessName(Process process)
{
try
{
return process.ProcessName ?? "";
}
catch
{
return "";
}
}
private static string SafeMainWindowTitle(Process process)
{
try
{
return process.MainWindowTitle ?? "";
}
catch
{
return "";
}
}
private static bool MatchesAny(string processName, List<string> candidates)
{
foreach (string candidate in candidates)
{
if (string.Equals(processName, candidate, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static string FindMatchedHint(string moduleName, string modulePath, List<string> hints)
{
foreach (string hint in hints)
{
if (string.Equals(moduleName, hint, StringComparison.OrdinalIgnoreCase))
{
return hint;
}
if (!string.IsNullOrEmpty(modulePath) &&
modulePath.EndsWith("\\" + hint, StringComparison.OrdinalIgnoreCase))
{
return hint;
}
}
return "";
}
private static List<string> GetStringList(Dictionary<string, object> source, string key, string[] defaults)
{
var values = new List<string>();
object raw;
if (source != null && source.TryGetValue(key, out raw) && raw != null)
{
object[] rawArray = raw as object[];
if (rawArray != null)
{
foreach (object item in rawArray)
{
string value = Convert.ToString(item);
if (!string.IsNullOrWhiteSpace(value))
{
values.Add(value.Trim());
}
}
}
else
{
string single = Convert.ToString(raw);
if (!string.IsNullOrWhiteSpace(single))
{
foreach (string part in single.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
string value = part.Trim();
if (!string.IsNullOrWhiteSpace(value))
{
values.Add(value);
}
}
}
}
}
if (values.Count == 0)
{
values.AddRange(defaults);
}
return values;
}
}
}