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,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);
}
}
}