docs: add offline b-route reverse decision

This commit is contained in:
zhaoyilun
2026-07-12 02:36:59 +08:00
parent 485d5ea766
commit a4242c12be
11 changed files with 2160 additions and 13 deletions

View File

@@ -0,0 +1,168 @@
param(
[string]$OutputDir = "runs/offline-broute-reverse",
[string]$ReportPath = "docs/source-discovery/2026-07-12-offline-broute-reverse-index.md"
)
$ErrorActionPreference = "Stop"
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$outDir = Join-Path $repo $OutputDir
$reportFullPath = Join-Path $repo $ReportPath
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $reportFullPath) | Out-Null
$samples = @(
@{
path = "runs/offline-real-client-window/full/zyl/Impp/IMPlatformClient.exe"
role = "main managed client; text send, chat window, plugin/IPC candidates"
expected_sha256 = "E2966E58360DAEEBF178410A961E2DC52748C63E9CCBD94BD6EC8434A1A09CB7"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/IMPP.Service.dll"
role = "file upload service candidates"
expected_sha256 = "F859DE2F34E024B5F372D95F22CBF8A018C780F24203E7A76D392E52BD90CADD"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/IMPP.Interface.dll"
role = "service/interface contract candidates"
expected_sha256 = "DD093CF61FAE85C2D2581F289B01AA477546712E098FEF81272FDD989E50D12D"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/IMPP.Model.dll"
role = "model/parameter types"
expected_sha256 = "7CBABDD23A7234D15E294C549EEEC78EFBAD6E957CD45A6E8A85309349A624A5"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/IMPP.UI.dll"
role = "UI/plugin helpers and possible send controls"
expected_sha256 = "4C61212519C01D1B731ED4F20DF3411C491616F4ADA98A4529722A6AA3689E6D"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/IMPP.Common.dll"
role = "shared utility and message types"
expected_sha256 = "02A47C99C27C9BFE2BADC438389300C92220B07609BD87C48B4B5E1B4EE6AEB4"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/IMPP.Helper.dll"
role = "helper abstractions"
expected_sha256 = "30932D082B6678699C832C63F923E01EAD84674FAEDB613D631E034E03742FB9"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/smack.dll"
role = "XMPP chat/send layer"
expected_sha256 = "BAD0BB0591765DB153B17CECC1112B0BBDBF3B1D3370DDF973F91974E92CEC66"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/TcpFileTransfer.dll"
role = "native/transfer candidate"
expected_sha256 = "6C4874B46D1E7DC04C9B6784180603452400B881E04509D464A0D58814EFEDA7"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/HttpServerLib.dll"
role = "local API/IPC candidate"
expected_sha256 = "FFDB633216AE332B8EDFFC83E2AE546B7A80144C659FF4053FDFE4FE382A8D12"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/INetwork.dll"
role = "network abstraction candidate"
expected_sha256 = "E40BE4EE2439E4280461F4D2BADB318A5F4572428F069AB6D17B4C4F0E8CFAC9"
},
@{
path = "runs/offline-real-client-window/full/zyl/Impp/IOClientNetwork.dll"
role = "network implementation candidate"
expected_sha256 = "64CA231A614771325741A5F098E52EBF20F7190E339B64962DE2EDEDCFFFF904"
}
)
$entries = foreach ($sample in $samples) {
$fullPath = Join-Path $repo $sample.path
$exists = Test-Path -LiteralPath $fullPath -PathType Leaf
$item = $null
$sha256 = ""
$matches = $false
if ($exists) {
$item = Get-Item -LiteralPath $fullPath
$sha256 = (Get-FileHash -LiteralPath $fullPath -Algorithm SHA256).Hash.ToUpperInvariant()
$matches = ($sha256 -eq $sample.expected_sha256)
}
[pscustomobject]@{
path = $sample.path
file_name = Split-Path -Leaf $sample.path
role = $sample.role
exists = $exists
size_bytes = if ($item) { $item.Length } else { $null }
last_write_time = if ($item) { $item.LastWriteTime.ToString("o") } else { $null }
sha256 = $sha256
expected_sha256 = $sample.expected_sha256
sha256_matches_expected = $matches
}
}
$summary = [pscustomobject]@{
generated_at = (Get-Date).ToUniversalTime().ToString("o")
repository = $repo
output_dir = $OutputDir
samples_total = @($entries).Count
samples_present = @($entries | Where-Object { $_.exists }).Count
hash_matches = @($entries | Where-Object { $_.sha256_matches_expected }).Count
originals_copied = $false
originals_modified = $false
entries = $entries
}
$jsonPath = Join-Path $outDir "index.json"
$summary | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add("# Offline B-Route Reverse Index")
$lines.Add("")
$lines.Add("Date: 2026-07-12")
$lines.Add("")
$lines.Add("## Scope")
$lines.Add("")
$lines.Add("This index records the read-only offline samples for B-route reverse engineering. The script reads metadata and SHA256 hashes only; it does not copy or modify the original binaries.")
$lines.Add("")
$lines.Add("## Summary")
$lines.Add("")
$lines.Add("- Samples total: $($summary.samples_total)")
$lines.Add("- Samples present: $($summary.samples_present)")
$lines.Add("- Hash matches: $($summary.hash_matches)")
$lines.Add("- Originals copied: false")
$lines.Add("- Originals modified: false")
$lines.Add("- Machine JSON: ``$OutputDir/index.json``")
$lines.Add("")
$lines.Add("## Sample table")
$lines.Add("")
$lines.Add("| Sample | Role | Size | SHA256 match | SHA256 |")
$lines.Add("| --- | --- | ---: | --- | --- |")
foreach ($entry in $entries) {
$size = if ($null -ne $entry.size_bytes) { [string]$entry.size_bytes } else { "missing" }
$match = if ($entry.sha256_matches_expected) { "yes" } else { "no" }
$sha = if ($entry.sha256) { $entry.sha256 } else { "missing" }
$lines.Add("| ``$($entry.path)`` | $($entry.role) | $size | $match | ``$sha`` |")
}
$lines.Add("")
$lines.Add("## Verified facts")
$lines.Add("")
$lines.Add("- All metadata came from local filesystem reads under the repository workspace.")
$lines.Add("- The expected primary B-route samples are present if `Samples present` equals `Samples total`.")
$lines.Add("- Hash matching only proves local file identity against this plan's recorded sample list; it does not prove runtime invocability.")
$lines.Add("")
$lines.Add("## Next reverse targets")
$lines.Add("")
$lines.Add("1. Text-send static call graph: `SendTxtMessageByJid`, `MessageScheduling.SendChatMessage`, `Chat.SendMessage`, `XMPPConnection.send`.")
$lines.Add("2. File-send/upload static call graph: `OffLineFileSend.sendFile`, `FileTransfer.FileUpload`, `AsynFileUpload`, `sendFileMessage`.")
$lines.Add("3. Bridge/IPC/plugin map: `HttpServerLib`, plugin loaders, local HTTP, named pipes, command-line switches, services.")
($lines -join "`n") + "`n" | Set-Content -LiteralPath $reportFullPath -Encoding UTF8
[pscustomobject]@{
ok = $true
json_path = $jsonPath
report_path = $reportFullPath
samples_total = $summary.samples_total
samples_present = $summary.samples_present
hash_matches = $summary.hash_matches
originals_copied = $false
originals_modified = $false
} | ConvertTo-Json -Depth 4

View File

@@ -0,0 +1,537 @@
param(
[string]$IlDir = "runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il",
[string]$BinaryDir = "runs/offline-real-client-window/full/zyl/Impp",
[string]$OutputDir = "runs/offline-broute-reverse",
[string]$ReportPath = "docs/source-discovery/2026-07-12-broute-bridge-static-map.md",
[int]$WindowRadius = 4,
[int]$MaxEvidencePerCandidate = 18
)
$ErrorActionPreference = "Stop"
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$ilDirFull = Join-Path $repo $IlDir
$binaryDirFull = Join-Path $repo $BinaryDir
$outDir = Join-Path $repo $OutputDir
$reportFullPath = Join-Path $repo $ReportPath
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $reportFullPath) | Out-Null
$textSources = @(
@{ path = "IMPlatformClient.exe.il"; role = "main client IL; login, plugin manager, local HTTP starter, chat/runtime" },
@{ path = "IMPP.Common.dll.il"; role = "common helpers; WinCommand, ProcessComm, DTOs" },
@{ path = "IMPP.ISphere.exe.il"; role = "ISphere companion app IL; CEF and command helpers" },
@{ path = "IMPP.UI.dll.il"; role = "UI/browser support IL" },
@{ path = "IMPP.Service.dll.il"; role = "file service implementation IL" },
@{ path = "smack.dll.il"; role = "XMPP/socks5/network IL" }
)
$binarySources = @(
@{ path = "HttpServerLib.dll"; role = "local HTTP server framework binary" },
@{ path = "IMPlatformClient.Web.exe"; role = "CEF/web helper process binary" },
@{ path = "INetwork.dll"; role = "native network abstraction" },
@{ path = "IOClientNetwork.dll"; role = "native network implementation" },
@{ path = "IMPlatformClient.exe"; role = "main client binary" }
)
$liveProbeSources = @(
"runs/returned-live-probe/isphere-live-probe-20260710-121958/isphere-live-probe-20260710-121958/services_registry_shortcuts.json",
"runs/returned-live-probe/isphere-live-probe-20260710-121958/isphere-live-probe-20260710-121958/network_inventory.json"
)
$candidates = @(
@{
key = "local_http_win_server"
display = "frmLogin.StartWinServer / HttpServerLib.HttpService"
category = "localhost_http"
classification = "requires_logged_in_process"
usable_for_broute_send = $false
recommendation = "do_not_pursue_before_in_process_adapter"
reason = "Static IL builds a local HTTP service from logged-in user/auth state, but no send route or active live local listener was confirmed."
patterns = @("StartWinServer", "HttpServerLib", "HttpService::\.ctor", "HttpServer::Start", "GetFreePort", "0\.0\.0\.0", "messageAuth", "RepairDatabase")
},
@{
key = "http_route_framework"
display = "HttpServerLib route/module framework"
category = "localhost_http"
classification = "static_only_unknown"
usable_for_broute_send = $false
recommendation = "no_existing_ipc_first"
reason = "HttpServerLib has ServiceModule/RouteAttribute primitives, but client IL does not show RegisterModule or send-message routes."
patterns = @("RouteAttribute", "RouteMethod", "ServiceModule", "ServiceRoute", "RegisterModule", "OnGet\s*\(", "OnPost\s*\(", "ExecuteRoute", "SearchRoute")
},
@{
key = "plugin_named_pipe_stub"
display = "PluginLightAppMulProcessCommunication NamedPipeServerStream"
category = "named_pipe"
classification = "not_a_bridge"
usable_for_broute_send = $false
recommendation = "do_not_use"
reason = "A NamedPipeServerStream field exists, but StartLightAppMultProssCommunication has a one-instruction ret body."
patterns = @("NamedPipeServerStream", "PluginLightAppMulProcessCommunication", "StartLightAppMultProssCommunication", "MulProcess", "LightApp")
},
@{
key = "process_comm_wm_copydata"
display = "IMPP.Common.Helper.ProcessComm WM_COPYDATA helper"
category = "window_message"
classification = "static_only_unknown"
usable_for_broute_send = $false
recommendation = "not_a_send_bridge"
reason = "ProcessComm can send WM_COPYDATA to a foreground or titled window, but no receive handler or send-command contract is confirmed."
patterns = @("IMPP\.Common\.Helper\.ProcessComm", "\bProcessComm\b", "COPYDATASTRUCT", "WM_COPYDATA", "SendMessageToTargetWindow", "FindWindow\(string lpClassName", "GetForegroundWindow\(\)")
},
@{
key = "plugin_manager_command_surface"
display = "PluginManager / IMPlugin command surface"
category = "plugin"
classification = "requires_logged_in_process"
usable_for_broute_send = $false
recommendation = "plugin_extension_not_first"
reason = "Plugin evidence is UI/app-store/context-menu oriented and depends on logged-in PluginManager state; no Assembly.Load/LoadFrom or send API bridge was confirmed."
patterns = @("PluginManager", "IMPlugin::get_Command", "LoadPluginContextMenu", "FrmAppStore", "FrmCefHomePage", "ShowPlugins", "Assembly\.Load", "LoadFrom", "AddPluginInfo")
},
@{
key = "cef_web_helper"
display = "IMPlatformClient.Web / CEF helper"
category = "child_process"
classification = "requires_logged_in_process"
usable_for_broute_send = $false
recommendation = "preview_only_not_send"
reason = "The web helper/CEF surface exists, but static evidence points to UI/plugin browser support, not an external send/file API."
patterns = @("IMPlatformClient\.Web", "CefProcessName", "CefRuntime", "WinCefBrowser", "NavigateTo", "SendProcessMessage", "V8Context")
},
@{
key = "native_network_stack"
display = "INetwork / IOClientNetwork native stack"
category = "native_network"
classification = "not_a_bridge"
usable_for_broute_send = $false
recommendation = "do_not_pursue_as_ipc"
reason = "Native NETIO send/recv symbols are low-level transport, not a local command bridge into logged-in chat objects."
patterns = @("INetwork", "IOClientNetwork", "NETIO_", "NETIO_C_", "StreamClient", "RegistRecvCallback", "SendPacket", "BasePipe")
},
@{
key = "live_probe_named_pipe"
display = "returned live-probe named pipe inventory"
category = "named_pipe"
classification = "static_only_unknown"
usable_for_broute_send = $false
recommendation = "do_not_assume_isphere_api"
reason = "Live probe saw \\.\\pipe\\zfpinject-msg-server, but no matching iSphere static string or send contract was found in the configured samples."
patterns = @("zfpinject-msg-server", "named_pipes", "connections", "remote_port", "10088")
},
@{
key = "local_tcp_listener_candidates"
display = "local TcpListener candidates"
category = "tcp_listener"
classification = "not_a_bridge"
usable_for_broute_send = $false
recommendation = "not_existing_ipc"
reason = "TcpListener evidence is tied to TCP file manage or smack socks5, not a command API for text/file send. Returned live probe had only one remote XMPP connection."
patterns = @("TcpListener", "TCPFileManager::_listener", "TCPSocks5Server", "GetActiveTcpListeners", "BeginAcceptSocket", "remote_port")
}
)
function Get-RelativePath([string]$Path) {
if ($Path.StartsWith($repo, [System.StringComparison]::OrdinalIgnoreCase)) {
return $Path.Substring($repo.Length).TrimStart('\', '/')
}
return $Path
}
function Get-TextEvidenceKind([string]$Line) {
if ($Line -match "^\s*\.class\b") { return "type_definition" }
if ($Line -match "^\s*\.method\b") { return "method_header" }
if ($Line -match "\bend of method\b") { return "method_end" }
if ($Line -match "\bcall(?:virt)?\b") { return "callsite" }
if ($Line -match "\bnewobj\b") { return "constructor_call" }
if ($Line -match "\bldstr\b") { return "string_literal" }
if ($Line -match "^\s*(instance|static|StartWinServer|GetFreePort|SendMessageToTargetWindow)\b.*\(") { return "signature_continuation" }
return "reference"
}
function Get-MethodContext($Lines, [int]$Index) {
$start = [Math]::Max(0, $Index - 120)
$methodHeader = $null
$classHeader = $null
for ($i = $Index; $i -ge $start; $i--) {
$line = $Lines[$i]
if ($null -eq $methodHeader -and $line -match "^\s*\.method\b") {
$methodHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
}
if ($null -eq $classHeader -and $line -match "^\s*\.class\b") {
$classHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
}
if ($methodHeader -and $classHeader) { break }
}
[pscustomobject]@{ method_header = $methodHeader; class_header = $classHeader }
}
function New-TextEvidence($Lines, [int]$Index, [string]$SourceRel) {
$from = [Math]::Max(0, $Index - $WindowRadius)
$to = [Math]::Min($Lines.Count - 1, $Index + $WindowRadius)
$window = New-Object System.Collections.Generic.List[object]
for ($i = $from; $i -le $to; $i++) {
$window.Add([pscustomobject]@{ line_number = $i + 1; text = $Lines[$i].TrimEnd() })
}
$context = Get-MethodContext -Lines $Lines -Index $Index
[pscustomobject]@{
source = $SourceRel
source_type = "text_il"
line_number = $Index + 1
string_index = $null
kind = Get-TextEvidenceKind -Line $Lines[$Index]
text = $Lines[$Index].Trim()
method_header = $context.method_header
class_header = $context.class_header
window = $window
}
}
function Get-PrintableStrings([string]$Path, [int]$MinLength = 4) {
$bytes = [System.IO.File]::ReadAllBytes($Path)
$strings = New-Object System.Collections.Generic.List[string]
$sb = New-Object System.Text.StringBuilder
foreach ($b in $bytes) {
if ($b -ge 32 -and $b -le 126) {
[void]$sb.Append([char]$b)
} else {
if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) }
$sb.Clear() | Out-Null
}
}
if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) }
$sb = New-Object System.Text.StringBuilder
for ($i = 0; $i -lt ($bytes.Length - 1); $i += 2) {
$b = $bytes[$i]
$z = $bytes[$i + 1]
if ($z -eq 0 -and $b -ge 32 -and $b -le 126) {
[void]$sb.Append([char]$b)
} else {
if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) }
$sb.Clear() | Out-Null
}
}
if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) }
return $strings.ToArray()
}
function Get-HttpServerReflection([string]$Path) {
$result = [ordered]@{ path = Get-RelativePath -Path $Path; ok = $false; error = ""; types = @() }
try {
$asm = [System.Reflection.Assembly]::LoadFile($Path)
$types = New-Object System.Collections.Generic.List[object]
foreach ($type in $asm.GetTypes()) {
if ($type.FullName -notmatch "HTTPServerLib\.(HttpServer|HttpService|ServiceModule|ServiceRoute|RouteAttribute|IServer|ActionResult)") { continue }
$ctors = @($type.GetConstructors([System.Reflection.BindingFlags]'Public,NonPublic,Instance') | ForEach-Object {
$params = @($_.GetParameters() | ForEach-Object { $_.ParameterType.Name + " " + $_.Name })
[pscustomobject]@{ is_public = $_.IsPublic; signature = "$($type.Name)($([string]::Join(', ', $params)))" }
})
$methods = @($type.GetMethods([System.Reflection.BindingFlags]'Public,NonPublic,Instance,Static,DeclaredOnly') | ForEach-Object {
$params = @($_.GetParameters() | ForEach-Object { $_.ParameterType.Name + " " + $_.Name })
[pscustomobject]@{ name = $_.Name; signature = "$($_.ReturnType.Name) $($_.Name)($([string]::Join(', ', $params)))" }
})
$types.Add([pscustomobject]@{ full_name = $type.FullName; constructors = $ctors; methods = $methods })
}
$result.ok = $true
$result.types = $types.ToArray()
} catch {
$result.error = $_.Exception.Message
}
return [pscustomobject]$result
}
$textSourceResults = New-Object System.Collections.Generic.List[object]
$binarySourceResults = New-Object System.Collections.Generic.List[object]
$candidateBuckets = @{}
foreach ($candidate in $candidates) {
$candidateBuckets[[string]$candidate['key']] = New-Object System.Collections.Generic.List[object]
}
foreach ($source in $textSources) {
$sourceFullPath = Join-Path $ilDirFull ([string]$source['path'])
$sourceRel = Get-RelativePath -Path $sourceFullPath
if (-not (Test-Path -LiteralPath $sourceFullPath -PathType Leaf)) {
$textSourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $false; line_count = 0; size_bytes = $null })
continue
}
$item = Get-Item -LiteralPath $sourceFullPath
$lines = [System.IO.File]::ReadAllLines($sourceFullPath)
$textSourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $true; line_count = $lines.Count; size_bytes = $item.Length })
foreach ($candidate in $candidates) {
$key = [string]$candidate['key']
$compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
foreach ($rx in $compiled) {
if ($rx.IsMatch($line)) {
$candidateBuckets[$key].Add((New-TextEvidence -Lines $lines -Index $i -SourceRel $sourceRel))
break
}
}
}
}
}
foreach ($source in $binarySources) {
$sourceFullPath = Join-Path $binaryDirFull ([string]$source['path'])
$sourceRel = Get-RelativePath -Path $sourceFullPath
if (-not (Test-Path -LiteralPath $sourceFullPath -PathType Leaf)) {
$binarySourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $false; string_count = 0; size_bytes = $null })
continue
}
$item = Get-Item -LiteralPath $sourceFullPath
$strings = Get-PrintableStrings -Path $sourceFullPath -MinLength 4
$binarySourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $true; string_count = $strings.Count; size_bytes = $item.Length })
foreach ($candidate in $candidates) {
$key = [string]$candidate['key']
$compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
for ($i = 0; $i -lt $strings.Count; $i++) {
$s = $strings[$i]
foreach ($rx in $compiled) {
if ($rx.IsMatch($s)) {
$candidateBuckets[$key].Add([pscustomobject]@{
source = $sourceRel
source_type = "binary_string"
line_number = $null
string_index = $i
kind = "binary_string"
text = $s
method_header = $null
class_header = $null
window = @()
})
break
}
}
}
}
}
$liveProbe = New-Object System.Collections.Generic.List[object]
foreach ($relPath in $liveProbeSources) {
$fullPath = Join-Path $repo $relPath
if (Test-Path -LiteralPath $fullPath -PathType Leaf) {
$raw = Get-Content -LiteralPath $fullPath -Raw
$liveProbe.Add([pscustomobject]@{ path = $relPath; exists = $true; length = $raw.Length; data = ($raw | ConvertFrom-Json) })
foreach ($candidate in $candidates) {
$key = [string]$candidate['key']
$compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
foreach ($rx in $compiled) {
if ($rx.IsMatch($raw)) {
$candidateBuckets[$key].Add([pscustomobject]@{
source = $relPath
source_type = "live_probe_json"
line_number = $null
string_index = $null
kind = "live_probe_json"
text = ($raw -replace "\s+", " ").Trim()
method_header = $null
class_header = $null
window = @()
})
break
}
}
}
} else {
$liveProbe.Add([pscustomobject]@{ path = $relPath; exists = $false; length = 0; data = $null })
}
}
$httpServerPath = Join-Path $binaryDirFull "HttpServerLib.dll"
$httpReflection = if (Test-Path -LiteralPath $httpServerPath -PathType Leaf) { Get-HttpServerReflection -Path $httpServerPath } else { [pscustomobject]@{ path = Get-RelativePath -Path $httpServerPath; ok = $false; error = "missing"; types = @() } }
$httpReflectionPath = Join-Path $outDir "httpserverlib-reflection.json"
$httpReflection | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $httpReflectionPath -Encoding UTF8
if ($httpReflection.ok) {
$reflectionText = ($httpReflection.types | ConvertTo-Json -Depth 8 -Compress)
foreach ($candidate in $candidates) {
$key = [string]$candidate['key']
$compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
foreach ($rx in $compiled) {
if ($rx.IsMatch($reflectionText)) {
$candidateBuckets[$key].Add([pscustomobject]@{
source = Get-RelativePath -Path $httpReflectionPath
source_type = "reflection"
line_number = $null
string_index = $null
kind = "reflection_metadata"
text = "HttpServerLib reflection exposes matching types/methods; see httpserverlib-reflection.json"
method_header = $null
class_header = $null
window = @()
})
break
}
}
}
}
$candidateResults = New-Object System.Collections.Generic.List[object]
foreach ($candidate in $candidates) {
$key = [string]$candidate['key']
$allEvidence = $candidateBuckets[$key].ToArray()
$evidenceForReport = @(
$allEvidence |
Sort-Object @{ Expression = { if ($_.source_type -eq "text_il") { 0 } elseif ($_.source_type -eq "reflection") { 1 } elseif ($_.source_type -eq "live_probe_json") { 2 } else { 3 } } }, source, line_number, string_index |
Select-Object -First $MaxEvidencePerCandidate
)
$classification = if ($allEvidence.Count -eq 0) { "not_found" } else { [string]$candidate['classification'] }
$candidateResults.Add([pscustomobject]@{
key = $key
display = $candidate['display']
category = $candidate['category']
classification = $classification
usable_for_broute_send = [bool]$candidate['usable_for_broute_send']
recommendation = $candidate['recommendation']
reason = $candidate['reason']
match_count = $allEvidence.Count
evidence = $evidenceForReport
})
}
$recommendation = [pscustomobject]@{
pursue_existing_ipc_or_plugin_first = $false
decision = "in_process_adapter_research"
fallback_boundary = "external_sidecar_preview_only_until_bridge_exists"
rationale = "Offline static evidence found no existing local IPC/plugin route that exposes send-message or send-file. HttpServerLib and plugin surfaces either require logged-in runtime state or are UI/framework-only; the named-pipe stub is empty."
}
$summary = [pscustomobject]@{
generated_at = (Get-Date).ToUniversalTime().ToString("o")
repository = $repo
il_dir = $IlDir
binary_dir = $BinaryDir
text_sources = $textSourceResults
binary_sources = $binarySourceResults
live_probe = $liveProbe
httpserverlib_reflection = Get-RelativePath -Path $httpReflectionPath
candidates = $candidateResults
recommendation = $recommendation
originals_copied = $false
originals_modified = $false
}
$jsonPath = Join-Path $outDir "bridge-map.json"
$summary | ConvertTo-Json -Depth 14 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
$linesOut = New-Object System.Collections.Generic.List[string]
$linesOut.Add("# B-Route Bridge/IPC/Plugin Static Map")
$linesOut.Add("")
$linesOut.Add("Date: 2026-07-12")
$linesOut.Add("")
$linesOut.Add("## Scope")
$linesOut.Add("")
$linesOut.Add("This report uses offline IL, binary string metadata, reflection metadata for ``HttpServerLib.dll``, and returned live-probe JSON. It does not log in, open sockets, attach hooks, inject into a process, replay traffic, or mutate client binaries.")
$linesOut.Add("")
$linesOut.Add("Machine-readable evidence: ``$OutputDir/bridge-map.json``")
$linesOut.Add("Reflection metadata: ``$OutputDir/httpserverlib-reflection.json``")
$linesOut.Add("")
$linesOut.Add("## Recommendation")
$linesOut.Add("")
$linesOut.Add("| Question | Answer |")
$linesOut.Add("| --- | --- |")
$linesOut.Add("| Should B-route pursue existing IPC/plugin before an in-process adapter? | **No** |")
$linesOut.Add("| Offline decision | ``$($recommendation.decision)`` |")
$linesOut.Add("| External sidecar boundary | ``$($recommendation.fallback_boundary)`` |")
$linesOut.Add("| Reason | $($recommendation.rationale) |")
$linesOut.Add("")
$linesOut.Add("## Sources")
$linesOut.Add("")
$linesOut.Add("### Text/IL sources")
$linesOut.Add("")
$linesOut.Add("| Source | Role | Exists | Lines | Size bytes |")
$linesOut.Add("| --- | --- | --- | ---: | ---: |")
foreach ($sourceResult in $textSourceResults) {
$exists = if ($sourceResult.exists) { "yes" } else { "no" }
$sizeText = if ($null -ne $sourceResult.size_bytes) { [string]$sourceResult.size_bytes } else { "" }
$linesOut.Add("| ``$($sourceResult.path)`` | $($sourceResult.role) | $exists | $($sourceResult.line_count) | $sizeText |")
}
$linesOut.Add("")
$linesOut.Add("### Binary/string sources")
$linesOut.Add("")
$linesOut.Add("| Source | Role | Exists | Strings | Size bytes |")
$linesOut.Add("| --- | --- | --- | ---: | ---: |")
foreach ($sourceResult in $binarySourceResults) {
$exists = if ($sourceResult.exists) { "yes" } else { "no" }
$sizeText = if ($null -ne $sourceResult.size_bytes) { [string]$sourceResult.size_bytes } else { "" }
$linesOut.Add("| ``$($sourceResult.path)`` | $($sourceResult.role) | $exists | $($sourceResult.string_count) | $sizeText |")
}
$linesOut.Add("")
$linesOut.Add("## Candidate classification")
$linesOut.Add("")
$linesOut.Add("| Candidate | Category | Classification | Usable for B-route send | Recommendation | Matches |")
$linesOut.Add("| --- | --- | --- | --- | --- | ---: |")
foreach ($candidateResult in $candidateResults) {
$usable = if ($candidateResult.usable_for_broute_send) { "yes" } else { "no" }
$linesOut.Add("| ``$($candidateResult.display)`` | ``$($candidateResult.category)`` | ``$($candidateResult.classification)`` | $usable | ``$($candidateResult.recommendation)`` | $($candidateResult.match_count) |")
}
$linesOut.Add("")
$linesOut.Add("## Verified facts")
$linesOut.Add("")
$linesOut.Add("- ``frmLogin.StartWinServer`` constructs ``HTTPServerLib.HttpService`` on ``0.0.0.0`` and starts ``HttpServer.Start`` in a thread, but it depends on ``IMPPManager.UserInfo`` and ``ServiceManager.UnifiedAuthentication`` state.")
$linesOut.Add("- ``HttpServerLib.dll`` exposes route/module primitives such as ``HttpService.RegisterModule``, ``ServiceModule``, ``ServiceRoute``, ``RouteAttribute``, ``OnGet``, and ``OnPost``.")
$linesOut.Add("- Main client IL does not show a concrete ``RegisterModule`` call or send-message/send-file HTTP route registration.")
$linesOut.Add("- ``PluginLightAppMulProcessCommunication`` declares a ``NamedPipeServerStream`` field, but ``StartLightAppMultProssCommunication`` returns immediately.")
$linesOut.Add("- Returned live probe showed one remote TCP connection to port ``10088`` and a named pipe ``\\.\\pipe\\zfpinject-msg-server``; neither proves an iSphere send API.")
$linesOut.Add("- ``PluginManager``/``IMPlugin`` evidence is UI/app-store/context-menu oriented; no local assembly-load plugin bridge or send route was confirmed.")
$linesOut.Add("")
$linesOut.Add("## Candidate evidence")
foreach ($candidateResult in $candidateResults) {
$linesOut.Add("")
$linesOut.Add("### $($candidateResult.display)")
$linesOut.Add("")
$linesOut.Add("Classification: ``$($candidateResult.classification)``; usable for B-route send: ``$($candidateResult.usable_for_broute_send)``; matches: $($candidateResult.match_count).")
$linesOut.Add("")
$linesOut.Add("Reason: $($candidateResult.reason)")
if (@($candidateResult.evidence).Count -eq 0) {
$linesOut.Add("")
$linesOut.Add("No evidence found in configured offline sources.")
continue
}
$linesOut.Add("")
$linesOut.Add("| Source | Kind | Snippet | Context |")
$linesOut.Add("| --- | --- | --- | --- |")
foreach ($evidence in $candidateResult.evidence) {
$sourceLoc = if ($evidence.line_number) { "$($evidence.source):$($evidence.line_number)" } elseif ($null -ne $evidence.string_index) { "$($evidence.source)#str$($evidence.string_index)" } else { $evidence.source }
$method = if ($evidence.method_header) { "L$($evidence.method_header.line_number): $($evidence.method_header.text)" } else { "" }
$snippet = (($evidence.text -replace "\|", "\\|") -replace "`r?`n", " ")
if ($snippet.Length -gt 220) { $snippet = $snippet.Substring(0, 220) + "..." }
$method = ($method -replace "\|", "\\|")
$linesOut.Add("| ``$sourceLoc`` | ``$($evidence.kind)`` | ``$snippet`` | ``$method`` |")
}
}
$linesOut.Add("")
$linesOut.Add("## Decision")
$linesOut.Add("")
$linesOut.Add("No existing IPC/plugin/local HTTP bridge is confirmed usable for B-route text send or file send. The next B-route branch should be ``in_process_adapter_research``. Until that exists, an external sidecar can only do preview/probe work, while A-route/RPA remains backup-only.")
$linesOut.Add("")
$linesOut.Add("## Next work")
$linesOut.Add("")
$linesOut.Add("1. Update the B-route next plan and capability source matrix with ``in_process_adapter_research`` as the offline decision.")
$linesOut.Add("2. Create a separate plan before implementing any in-process adapter work.")
$linesOut.Add("3. Do not promote RPA beyond backup in this branch.")
($linesOut -join "`n") + "`n" | Set-Content -LiteralPath $reportFullPath -Encoding UTF8
$requiredNames = @("HttpServerLib", "NamedPipe", "Plugin", "IMPlatformClient.Web", "ProcessComm", "INetwork", "IOClientNetwork", "in_process_adapter_research")
$reportText = Get-Content -LiteralPath $reportFullPath -Raw
$missing = @($requiredNames | Where-Object { $reportText -notlike "*$_*" })
if ($missing.Count -gt 0) {
throw "Report missing required names: $($missing -join ', ')"
}
[pscustomobject]@{
ok = $true
json_path = $jsonPath
report_path = $reportFullPath
reflection_path = $httpReflectionPath
candidates = $candidateResults.Count
decision = $recommendation.decision
pursue_existing_ipc_or_plugin_first = $false
originals_copied = $false
originals_modified = $false
} | ConvertTo-Json -Depth 6

View File

@@ -0,0 +1,342 @@
param(
[string]$IlDir = "runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il",
[string]$OutputDir = "runs/offline-broute-reverse",
[string]$ReportPath = "docs/source-discovery/2026-07-12-broute-file-send-static-map.md",
[int]$WindowRadius = 5,
[int]$MaxEvidencePerTarget = 18
)
$ErrorActionPreference = "Stop"
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$ilDirFull = Join-Path $repo $IlDir
$outDir = Join-Path $repo $OutputDir
$reportFullPath = Join-Path $repo $ReportPath
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $reportFullPath) | Out-Null
$sources = @(
@{ path = "IMPlatformClient.exe.il"; role = "main client IL; UC_FileSend, OffLineFileSend, MessageScheduling" },
@{ path = "IMPP.Service.dll.il"; role = "file transfer service implementation" },
@{ path = "IMPP.ServiceBase.dll.il"; role = "file transfer contracts, parameters, and result DTOs" },
@{ path = "IMPP.Interface.dll.il"; role = "transport/event interface contracts" },
@{ path = "smack.dll.il"; role = "Chat.sendFileMessage and XMPP message construction" },
@{ path = "TcpFileTransfer.dll.il"; role = "native TCP transfer candidate; IL may be unavailable" },
@{ path = "IMPP.Model.dll.il"; role = "chat record models if extracted" }
)
$targets = @(
@{
key = "IMPP.Client.OffLineFileSend.sendFile"
display = "IMPP.Client.OffLineFileSend.sendFile"
phase = "upload"
intent = "offline file upload orchestration from UI file sender"
patterns = @("OffLineFileSend::sendFile", "^\s*sendFile\(\) cil managed")
},
@{
key = "IMPP.Client.OffLineFileSend.trans_UploadCompleted"
display = "IMPP.Client.OffLineFileSend.trans_UploadCompleted"
phase = "upload_to_message_boundary"
intent = "upload-completed handler that converts FileUploadResult.FileID into a chat file message"
patterns = @("OffLineFileSend::trans_UploadCompleted", "trans_UploadCompleted\(string fileId\)")
},
@{
key = "IMPP.Service.BLL.FileTransfer.FileUpload"
display = "IMPP.Service.BLL.FileTransfer.FileUpload"
phase = "upload"
intent = "synchronous service upload entry returning FileUploadResult"
patterns = @("FileTransfer::FileUpload", "FileUpload\(class \[IMPP\.ServiceBase\].*FileUploadPara")
},
@{
key = "IMPP.Service.BLL.FileTransfer.AsynFileUpload"
display = "IMPP.Service.BLL.FileTransfer.AsynFileUpload"
phase = "upload"
intent = "asynchronous upload queue entry"
patterns = @("FileTransfer::AsynFileUpload", "AsynFileUpload\(class \[IMPP\.ServiceBase\].*FileUploadPara")
},
@{
key = "FileUploadPara"
display = "FileUploadPara"
phase = "upload_parameters"
intent = "host/port/domain/local file path/cache/timeout/upload flags"
patterns = @("FileUploadPara", "FileUploadPara::\.ctor")
},
@{
key = "FileUploadResult.FileID"
display = "FileUploadResult.FileID"
phase = "upload_result"
intent = "server-side file id and URL returned by upload"
patterns = @("FileUploadResult::get_FileID", "FileUploadResult::set_FileID", "FileUploadResult", "get_FileID\(\) cil managed")
},
@{
key = "MessageScheduling.SendFileMessage"
display = "MessageScheduling.SendFileMessage"
phase = "message"
intent = "scheduled/file-message finalization path using RosterContactsArgs and FileTransferArgs"
patterns = @("MessageScheduling::SendFileMessage", "SendFileMessage\(class IMPP\.Client\.Business\.RosterContacts\.RosterContactsArgs")
},
@{
key = "Chat.sendFileMessage"
display = "Chat.sendFileMessage"
phase = "message"
intent = "smack API that sends the XMPP file message after upload metadata is known"
patterns = @("Chat::sendFileMessage", "P2PChat::sendFileMessage", "ChatRoom::sendFileMessage", "sendFileMessage\(string p_message")
},
@{
key = "Required values: path id size target jid"
display = "Required values: path id size target jid"
phase = "requirements"
intent = "static evidence for local file path, remote folder/file id, file size/name, target JID/chat, and server auth"
patterns = @("get_LocalFilePath", "get_RemoteFolder", "get__localFilePath", "get__fileSize", "FileInfo::get_Length", "FileSystemInfo::get_Name", "get_RosterJid", "GetChat\(", "HTTPFileServer", "UnifiedAuthentication", "Auth::get_token")
}
)
function Get-RelativePath([string]$Path) {
if ($Path.StartsWith($repo, [System.StringComparison]::OrdinalIgnoreCase)) {
return $Path.Substring($repo.Length).TrimStart('\', '/')
}
return $Path
}
function Get-EvidenceKind([string]$Line) {
if ($Line -match "^\s*\.class\b") { return "type_definition" }
if ($Line -match "^\s*\.method\b") { return "method_header" }
if ($Line -match "\bend of method\b") { return "method_end" }
if ($Line -match "\bcall(?:virt)?\b") { return "callsite" }
if ($Line -match "\bnewobj\b") { return "constructor_call" }
if ($Line -match "\bldstr\b") { return "string_literal" }
if ($Line -match "^\s*(instance|static|FileUpload|SendFileMessage|sendFileMessage|sendFile|trans_UploadCompleted)\b.*\(") { return "signature_continuation" }
return "reference"
}
function Get-MethodContext($Lines, [int]$Index) {
$start = [Math]::Max(0, $Index - 120)
$methodHeader = $null
$classHeader = $null
for ($i = $Index; $i -ge $start; $i--) {
$line = $Lines[$i]
if ($null -eq $methodHeader -and $line -match "^\s*\.method\b") {
$methodHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
}
if ($null -eq $classHeader -and $line -match "^\s*\.class\b") {
$classHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
}
if ($methodHeader -and $classHeader) { break }
}
[pscustomobject]@{ method_header = $methodHeader; class_header = $classHeader }
}
function New-Evidence($Lines, [int]$Index, [string]$SourceRel) {
$from = [Math]::Max(0, $Index - $WindowRadius)
$to = [Math]::Min($Lines.Count - 1, $Index + $WindowRadius)
$window = New-Object System.Collections.Generic.List[object]
for ($i = $from; $i -le $to; $i++) {
$window.Add([pscustomobject]@{ line_number = $i + 1; text = $Lines[$i].TrimEnd() })
}
$context = Get-MethodContext -Lines $Lines -Index $Index
[pscustomobject]@{
source = $SourceRel
line_number = $Index + 1
kind = Get-EvidenceKind -Line $Lines[$Index]
text = $Lines[$Index].Trim()
method_header = $context.method_header
class_header = $context.class_header
window = $window
}
}
function Get-Classification($Evidence) {
$kinds = @($Evidence | ForEach-Object { $_.kind })
if ($kinds -contains "type_definition" -or $kinds -contains "method_header" -or $kinds -contains "signature_continuation" -or $kinds -contains "method_end") { return "confirmed_signature" }
if ($kinds -contains "callsite" -or $kinds -contains "constructor_call") { return "confirmed_callsite" }
if ($kinds.Count -gt 0 -and @($kinds | Where-Object { $_ -ne "string_literal" }).Count -eq 0) { return "string_only" }
if ($kinds.Count -gt 0) { return "confirmed_callsite" }
return "not_found"
}
$sourceResults = New-Object System.Collections.Generic.List[object]
$targetBuckets = @{}
foreach ($target in $targets) {
$targetKey = [string]$target['key']
$targetBuckets[$targetKey] = New-Object System.Collections.Generic.List[object]
}
foreach ($source in $sources) {
$sourcePath = [string]$source['path']
$sourceFullPath = Join-Path $ilDirFull $sourcePath
$sourceRel = Get-RelativePath -Path $sourceFullPath
if (-not (Test-Path -LiteralPath $sourceFullPath -PathType Leaf)) {
$sourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $false; line_count = 0; size_bytes = $null })
continue
}
$item = Get-Item -LiteralPath $sourceFullPath
$lines = [System.IO.File]::ReadAllLines($sourceFullPath)
$sourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $true; line_count = $lines.Count; size_bytes = $item.Length })
foreach ($target in $targets) {
$targetKey = [string]$target['key']
$compiled = @($target['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
foreach ($rx in $compiled) {
if ($rx.IsMatch($line)) {
$targetBuckets[$targetKey].Add((New-Evidence -Lines $lines -Index $i -SourceRel $sourceRel))
break
}
}
}
}
}
$targetResults = New-Object System.Collections.Generic.List[object]
foreach ($target in $targets) {
$targetKey = [string]$target['key']
$allEvidence = $targetBuckets[$targetKey].ToArray()
$evidenceForReport = @(
$allEvidence |
Sort-Object @{ Expression = { if ($_.kind -in @("type_definition", "method_header", "signature_continuation", "method_end")) { 0 } elseif ($_.kind -in @("callsite", "constructor_call")) { 1 } elseif ($_.kind -eq "string_literal") { 2 } else { 3 } } }, source, line_number |
Select-Object -First $MaxEvidencePerTarget
)
$targetResults.Add([pscustomobject]@{
key = $targetKey
display = $target['display']
phase = $target['phase']
intent = $target['intent']
classification = Get-Classification -Evidence $allEvidence
match_count = $allEvidence.Count
evidence = $evidenceForReport
})
}
$requirements = @(
[pscustomobject]@{ value = "local file path"; evidence = "OffLineFileSend constructor receives localFilePath; sendFile uses get__localFilePath; MessageScheduling.SendFileMessage uses FileTransferArgs.get_LocalFilePath."; status = "required_confirmed" },
[pscustomobject]@{ value = "server file id / remote folder"; evidence = "Upload returns FileUploadResult.FileID; trans_UploadCompleted(fileId) passes it to Chat.sendFileMessage and Packet.set_id. MessageScheduling path uses FileTransferArgs.get_RemoteFolder."; status = "required_confirmed" },
[pscustomobject]@{ value = "file name and file size"; evidence = "SendFileMessage builds OFFLINE_FILE_TRANSFER payload from FileInfo/FileSystemInfo name and length; OffLineFileSend keeps _fileSize."; status = "required_confirmed" },
[pscustomobject]@{ value = "target JID / chat object"; evidence = "UC_FileSend and MessageScheduling both require a live smack Chat; scheduled path derives it from RosterContactsArgs.get_RosterJid + UtilsChat.GetChat."; status = "required_confirmed" },
[pscustomobject]@{ value = "HTTP file server and auth"; evidence = "FileUploadPara construction uses Config.ServerInfo.HTTPFileServer IP/Port/domain; service upload builds /FS/auth/resource/upload URL with UnifiedAuthentication token/app key/client secret/time/user id."; status = "required_confirmed" },
[pscustomobject]@{ value = "file hash"; evidence = "No file-transfer-path MD5/SHA/checksum requirement was confirmed in the configured IL. MD5 hits belong to image/RTF or auth digest areas, not the offline file-send path."; status = "not_confirmed_in_file_send_path" }
)
$summary = [pscustomobject]@{
generated_at = (Get-Date).ToUniversalTime().ToString("o")
repository = $repo
il_dir = $IlDir
sources = $sourceResults
targets = $targetResults
phase_decision = "Offline static evidence shows upload first, then chat file-message finalization. Direct Chat.sendFileMessage alone is not enough unless a valid uploaded file id/remote folder already exists."
requirements = $requirements
originals_copied = $false
originals_modified = $false
}
$jsonPath = Join-Path $outDir "file-send-map.json"
$summary | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
$linesOut = New-Object System.Collections.Generic.List[string]
$linesOut.Add("# B-Route File Send Static Map")
$linesOut.Add("")
$linesOut.Add("Date: 2026-07-12")
$linesOut.Add("")
$linesOut.Add("## Scope")
$linesOut.Add("")
$linesOut.Add("This report uses offline IL only. It does not upload files, send file messages, replay traffic, inject into a process, or mutate the original iSphere binaries.")
$linesOut.Add("")
$linesOut.Add("Machine-readable evidence: ``$OutputDir/file-send-map.json``")
$linesOut.Add("")
$linesOut.Add("## Sources")
$linesOut.Add("")
$linesOut.Add("| Source | Role | Exists | Lines | Size bytes |")
$linesOut.Add("| --- | --- | --- | ---: | ---: |")
foreach ($sourceResult in $sourceResults) {
$exists = if ($sourceResult.exists) { "yes" } else { "no" }
$sizeText = if ($null -ne $sourceResult.size_bytes) { [string]$sourceResult.size_bytes } else { "" }
$linesOut.Add("| ``$($sourceResult.path)`` | $($sourceResult.role) | $exists | $($sourceResult.line_count) | $sizeText |")
}
$linesOut.Add("")
$linesOut.Add("## Target classification")
$linesOut.Add("")
$linesOut.Add("| Target | Phase | Intent | Classification | Matches |")
$linesOut.Add("| --- | --- | --- | --- | ---: |")
foreach ($targetResult in $targetResults) {
$linesOut.Add("| ``$($targetResult.display)`` | ``$($targetResult.phase)`` | $($targetResult.intent) | ``$($targetResult.classification)`` | $($targetResult.match_count) |")
}
$linesOut.Add("")
$linesOut.Add("## Upload step vs chat-message step")
$linesOut.Add("")
$linesOut.Add("| Step | Static evidence | Business meaning |")
$linesOut.Add("| --- | --- | --- |")
$linesOut.Add("| 1. Upload | ``IMPP.Client.OffLineFileSend.sendFile`` constructs ``FileUploadPara`` and calls ``IFileTransfer.FileUpload``; ``IMPP.Service.BLL.FileTransfer.FileUpload`` delegates to ``SyncFileTransfer`` and private ``Upload``. | The local file must be uploaded to the HTTP file service first. |")
$linesOut.Add("| 2. Upload result | ``FileUploadResult.FileID`` is read; upload notification calls ``IMPP.Client.OffLineFileSend.trans_UploadCompleted(fileId)``. | The server-issued file id becomes the durable reference. |")
$linesOut.Add("| 3. Chat file message | ``trans_UploadCompleted`` and ``MessageScheduling.SendFileMessage`` build ``OFFLINE_FILE_TRANSFER;...`` payloads and call ``Chat.sendFileMessage``. | The chat send step publishes metadata/reference, not raw file bytes. |")
$linesOut.Add("")
$linesOut.Add("Conclusion: file send is upload-first, chat-message-second. A direct chat message is insufficient without a valid uploaded file id or remote folder reference.")
$linesOut.Add("")
$linesOut.Add("## Required values")
$linesOut.Add("")
$linesOut.Add("| Value | Status | Evidence |")
$linesOut.Add("| --- | --- | --- |")
foreach ($req in $requirements) {
$linesOut.Add("| $($req.value) | ``$($req.status)`` | $($req.evidence) |")
}
$linesOut.Add("")
$linesOut.Add("## Evidence excerpts")
foreach ($targetResult in $targetResults) {
$linesOut.Add("")
$linesOut.Add("### $($targetResult.display)")
$linesOut.Add("")
$linesOut.Add("Classification: ``$($targetResult.classification)``; phase: ``$($targetResult.phase)``; matches: $($targetResult.match_count).")
if (@($targetResult.evidence).Count -eq 0) {
$linesOut.Add("")
$linesOut.Add("No evidence found in configured offline IL sources.")
continue
}
$linesOut.Add("")
$linesOut.Add("| Source line | Kind | Snippet | Method context |")
$linesOut.Add("| --- | --- | --- | --- |")
foreach ($evidence in $targetResult.evidence) {
$method = if ($evidence.method_header) { "L$($evidence.method_header.line_number): $($evidence.method_header.text)" } else { "" }
$snippet = ($evidence.text -replace "\|", "\\|")
$method = ($method -replace "\|", "\\|")
$linesOut.Add("| ``$($evidence.source):$($evidence.line_number)`` | ``$($evidence.kind)`` | ``$snippet`` | ``$method`` |")
}
}
$linesOut.Add("")
$linesOut.Add("## Inference")
$linesOut.Add("")
$linesOut.Add("- High confidence: upload and file-message finalization are separate steps.")
$linesOut.Add("- High confidence: ``FileUploadPara`` needs HTTP file server host/port/domain plus local file path and upload options.")
$linesOut.Add("- High confidence: ``Chat.sendFileMessage`` produces the XMPP file-message envelope after file metadata is available.")
$linesOut.Add("- Medium confidence: B-route file send must either call the same logged-in service/runtime or reproduce the authenticated HTTP upload plus XMPP file-message sequence.")
$linesOut.Add("- Low confidence until bridge evidence: an external sidecar can obtain the same service/auth/chat objects without being inside the logged-in client process.")
$linesOut.Add("")
$linesOut.Add("## Decision")
$linesOut.Add("")
$linesOut.Add("Offline static evidence supports B-route research, but file sending needs more than one method call: upload first, then chat file-message finalization. The next blocker is reachability: whether an IPC/plugin/in-process bridge can access the logged-in ``IFileTransfer`` service and smack ``Chat`` object.")
$linesOut.Add("")
$linesOut.Add("## Next work")
$linesOut.Add("")
$linesOut.Add("1. Extract bridge/IPC/plugin candidates before coding any file-send adapter.")
$linesOut.Add("2. If a bridge exists, prototype non-mutating discovery first: service availability, chat object lookup, and upload endpoint metadata only.")
$linesOut.Add("3. Keep A-route/RPA backup-only.")
($linesOut -join "`n") + "`n" | Set-Content -LiteralPath $reportFullPath -Encoding UTF8
$requiredNames = @("IMPP.Client.OffLineFileSend.sendFile", "IMPP.Client.OffLineFileSend.trans_UploadCompleted", "IMPP.Service.BLL.FileTransfer.FileUpload", "IMPP.Service.BLL.FileTransfer.AsynFileUpload", "FileUploadPara", "sendFileMessage", "Upload step vs chat-message step")
$reportText = Get-Content -LiteralPath $reportFullPath -Raw
$missing = @($requiredNames | Where-Object { $reportText -notlike "*$_*" })
if ($missing.Count -gt 0) {
throw "Report missing required names: $($missing -join ', ')"
}
[pscustomobject]@{
ok = $true
json_path = $jsonPath
report_path = $reportFullPath
sources = $sourceResults.Count
targets = $targetResults.Count
classifications = @($targetResults | ForEach-Object { "$($_.display)=$($_.classification)" })
phase_decision = $summary.phase_decision
originals_copied = $false
originals_modified = $false
} | ConvertTo-Json -Depth 6

View File

@@ -0,0 +1,285 @@
param(
[string]$IlDir = "runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il",
[string]$OutputDir = "runs/offline-broute-reverse",
[string]$ReportPath = "docs/source-discovery/2026-07-12-broute-text-send-static-map.md",
[int]$WindowRadius = 4,
[int]$MaxEvidencePerTarget = 18
)
$ErrorActionPreference = "Stop"
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$ilDirFull = Join-Path $repo $IlDir
$outDir = Join-Path $repo $OutputDir
$reportFullPath = Join-Path $repo $ReportPath
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $reportFullPath) | Out-Null
$sources = @(
@{
path = "IMPlatformClient.exe.il"
role = "main managed client IL; chat UI, AppContextManager, scheduling call sites"
},
@{
path = "smack.dll.il"
role = "XMPP/smack managed IL; Chat and XMPPConnection implementation"
}
)
$targets = @(
@{
key = "AppContextManager.SendTxtMessageByJid"
display = "AppContextManager.SendTxtMessageByJid"
intent = "high-level text send entry exposed by AppContextManager"
patterns = @("AppContextManager::SendTxtMessageByJid", "\bSendTxtMessageByJid\b")
},
@{
key = "MessageScheduling.SendChatMessage"
display = "MessageScheduling.SendChatMessage"
intent = "message scheduling text/chat send helper"
patterns = @("MessageScheduling::SendChatMessage", "\bSendChatMessage\s*\(")
},
@{
key = "Chat.SendMessage"
display = "Chat.SendMessage"
intent = "smack Chat send API used by chat/UI code"
patterns = @("com\.vision\.smack\.Chat::SendMessage", "com\.vision\.smack\.Chat::sendMessage", "\bChat::SendMessage\b", "\bChat::sendMessage\b")
},
@{
key = "XMPPConnection.send"
display = "XMPPConnection.send"
intent = "lowest-level XMPP connection send primitive"
patterns = @("XMPPConnection::send\b", "\bXMPPConnection\.send\b")
},
@{
key = "MessageCenter send-related callbacks"
display = "MessageCenter send-related callbacks"
intent = "MessageCenter or IMPPManager message-center callbacks touching send/message flow"
patterns = @("GetMessageCenter\s*\(\)", "MessageCenter::.*Send", "MessageCenter.*Send", "class IMPP\.Client\.control\.MessageCenter", "IMPP\.Client\.control\.MessageCenter")
}
)
function Get-RelativePath([string]$Path) {
if ($Path.StartsWith($repo, [System.StringComparison]::OrdinalIgnoreCase)) {
return $Path.Substring($repo.Length).TrimStart('\', '/')
}
return $Path
}
function Get-EvidenceKind([string]$Line) {
if ($Line -match "^\s*\.class\b") { return "type_definition" }
if ($Line -match "^\s*\.method\b") { return "method_header" }
if ($Line -match "\bend of method\b") { return "method_end" }
if ($Line -match "\bcall(?:virt)?\b") { return "callsite" }
if ($Line -match "\bldstr\b") { return "string_literal" }
if ($Line -match "^\s*(instance|static)\b.*\(") { return "signature_continuation" }
return "reference"
}
function Get-MethodContext($Lines, [int]$Index) {
$start = [Math]::Max(0, $Index - 90)
$methodHeader = $null
$classHeader = $null
for ($i = $Index; $i -ge $start; $i--) {
$line = $Lines[$i]
if ($null -eq $methodHeader -and $line -match "^\s*\.method\b") {
$methodHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
}
if ($null -eq $classHeader -and $line -match "^\s*\.class\b") {
$classHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
}
if ($methodHeader -and $classHeader) { break }
}
[pscustomobject]@{ method_header = $methodHeader; class_header = $classHeader }
}
function New-Evidence($Lines, [int]$Index, [string]$SourceRel) {
$from = [Math]::Max(0, $Index - $WindowRadius)
$to = [Math]::Min($Lines.Count - 1, $Index + $WindowRadius)
$window = New-Object System.Collections.Generic.List[object]
for ($i = $from; $i -le $to; $i++) {
$window.Add([pscustomobject]@{ line_number = $i + 1; text = $Lines[$i].TrimEnd() })
}
$context = Get-MethodContext -Lines $Lines -Index $Index
[pscustomobject]@{
source = $SourceRel
line_number = $Index + 1
kind = Get-EvidenceKind -Line $Lines[$Index]
text = $Lines[$Index].Trim()
method_header = $context.method_header
class_header = $context.class_header
window = $window
}
}
function Get-Classification($Evidence) {
$kinds = @($Evidence | ForEach-Object { $_.kind })
if ($kinds -contains "method_header" -or $kinds -contains "signature_continuation" -or $kinds -contains "method_end") { return "confirmed_signature" }
if ($kinds -contains "callsite") { return "confirmed_callsite" }
if ($kinds.Count -gt 0 -and @($kinds | Where-Object { $_ -ne "string_literal" }).Count -eq 0) { return "string_only" }
if ($kinds.Count -gt 0) { return "confirmed_callsite" }
return "not_found"
}
$sourceResults = New-Object System.Collections.Generic.List[object]
$targetBuckets = @{}
foreach ($target in $targets) {
$targetKey = [string]$target['key']
$targetBuckets[$targetKey] = New-Object System.Collections.Generic.List[object]
}
foreach ($source in $sources) {
$sourcePath = [string]$source['path']
$sourceFullPath = Join-Path $ilDirFull $sourcePath
$sourceRel = Get-RelativePath -Path $sourceFullPath
if (-not (Test-Path -LiteralPath $sourceFullPath -PathType Leaf)) {
$sourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $false; line_count = 0; size_bytes = $null })
continue
}
$item = Get-Item -LiteralPath $sourceFullPath
$lines = [System.IO.File]::ReadAllLines($sourceFullPath)
$sourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $true; line_count = $lines.Count; size_bytes = $item.Length })
foreach ($target in $targets) {
$targetKey = [string]$target['key']
$compiled = @($target['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
foreach ($rx in $compiled) {
if ($rx.IsMatch($line)) {
$targetBuckets[$targetKey].Add((New-Evidence -Lines $lines -Index $i -SourceRel $sourceRel))
break
}
}
}
}
}
$targetResults = New-Object System.Collections.Generic.List[object]
foreach ($target in $targets) {
$targetKey = [string]$target['key']
$allEvidence = $targetBuckets[$targetKey].ToArray()
$evidenceForReport = @(
$allEvidence |
Sort-Object @{ Expression = { if ($_.kind -in @("method_header", "signature_continuation", "method_end")) { 0 } elseif ($_.kind -eq "callsite") { 1 } elseif ($_.kind -eq "string_literal") { 2 } else { 3 } } }, line_number |
Select-Object -First $MaxEvidencePerTarget
)
$targetResults.Add([pscustomobject]@{
key = $targetKey
display = $target['display']
intent = $target['intent']
classification = Get-Classification -Evidence $allEvidence
match_count = $allEvidence.Count
evidence = $evidenceForReport
})
}
$summary = [pscustomobject]@{
generated_at = (Get-Date).ToUniversalTime().ToString("o")
repository = $repo
il_dir = $IlDir
sources = $sourceResults
targets = $targetResults
decision = "B-route text send remains plausible only if a bridge can enter the logged-in runtime or an existing IPC/API exposes the same call. Offline IL confirms call shape, but not runtime invocability."
originals_copied = $false
originals_modified = $false
}
$jsonPath = Join-Path $outDir "text-send-map.json"
$summary | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
$linesOut = New-Object System.Collections.Generic.List[string]
$linesOut.Add("# B-Route Text Send Static Map")
$linesOut.Add("")
$linesOut.Add("Date: 2026-07-12")
$linesOut.Add("")
$linesOut.Add("## Scope")
$linesOut.Add("")
$linesOut.Add("This report uses offline IL only. It does not log in, send messages, replay traffic, inject into a process, or mutate the original iSphere binaries.")
$linesOut.Add("")
$linesOut.Add("Machine-readable evidence: ``$OutputDir/text-send-map.json``")
$linesOut.Add("")
$linesOut.Add("## Sources")
$linesOut.Add("")
$linesOut.Add("| Source | Role | Exists | Lines | Size bytes |")
$linesOut.Add("| --- | --- | --- | ---: | ---: |")
foreach ($sourceResult in $sourceResults) {
$exists = if ($sourceResult.exists) { "yes" } else { "no" }
$sizeText = if ($null -ne $sourceResult.size_bytes) { [string]$sourceResult.size_bytes } else { "" }
$linesOut.Add("| ``$($sourceResult.path)`` | $($sourceResult.role) | $exists | $($sourceResult.line_count) | $sizeText |")
}
$linesOut.Add("")
$linesOut.Add("## Target classification")
$linesOut.Add("")
$linesOut.Add("| Target | Intent | Classification | Matches |")
$linesOut.Add("| --- | --- | --- | ---: |")
foreach ($targetResult in $targetResults) {
$linesOut.Add("| ``$($targetResult.display)`` | $($targetResult.intent) | ``$($targetResult.classification)`` | $($targetResult.match_count) |")
}
$linesOut.Add("")
$linesOut.Add("## Verified facts")
$linesOut.Add("")
$linesOut.Add("- ``AppContextManager.SendTxtMessageByJid`` appears in the offline client IL and is not just a UIA/RPA artifact.")
$linesOut.Add("- ``MessageScheduling.SendChatMessage`` appears in the offline client IL and has call-site evidence around chat-message flow.")
$linesOut.Add("- ``Chat.SendMessage`` / ``Chat.sendMessage`` appears as a smack chat API used by client code.")
$linesOut.Add("- ``XMPPConnection.send`` appears as a lower-level connection send primitive in offline IL.")
$linesOut.Add("- ``MessageCenter send-related callbacks`` appear as MessageCenter/GetMessageCenter references, but offline static evidence alone does not prove an external bridge can call them.")
$linesOut.Add("")
$linesOut.Add("## Evidence excerpts")
foreach ($targetResult in $targetResults) {
$linesOut.Add("")
$linesOut.Add("### $($targetResult.display)")
$linesOut.Add("")
$linesOut.Add("Classification: ``$($targetResult.classification)``; matches: $($targetResult.match_count).")
if (@($targetResult.evidence).Count -eq 0) {
$linesOut.Add("")
$linesOut.Add("No evidence found in configured offline IL sources.")
continue
}
$linesOut.Add("")
$linesOut.Add("| Source line | Kind | Snippet | Method context |")
$linesOut.Add("| --- | --- | --- | --- |")
foreach ($evidence in $targetResult.evidence) {
$method = if ($evidence.method_header) { "L$($evidence.method_header.line_number): $($evidence.method_header.text)" } else { "" }
$snippet = ($evidence.text -replace "\|", "\\|")
$method = ($method -replace "\|", "\\|")
$linesOut.Add("| ``$($evidence.source):$($evidence.line_number)`` | ``$($evidence.kind)`` | ``$snippet`` | ``$method`` |")
}
}
$linesOut.Add("")
$linesOut.Add("## Inference")
$linesOut.Add("")
$linesOut.Add("- High confidence: text sending has an internal managed call path that reaches smack chat/connection send APIs.")
$linesOut.Add("- Medium confidence: the likely chain is high-level AppContextManager or UI chat code -> MessageScheduling/frmP2PChat -> smack Chat.SendMessage/sendMessage -> XMPPConnection.send.")
$linesOut.Add("- Low confidence until live/runtime evidence: an external sidecar can invoke this path without being inside the logged-in process.")
$linesOut.Add("")
$linesOut.Add("## Decision")
$linesOut.Add("")
$linesOut.Add("B-route text send remains plausible only if a bridge can enter the logged-in runtime or an existing IPC/API exposes the same call. Offline IL confirms call shape, but not runtime invocability.")
$linesOut.Add("")
$linesOut.Add("## Next work")
$linesOut.Add("")
$linesOut.Add("1. Extract file-send/upload call graph and separate upload from chat-message finalization.")
$linesOut.Add("2. Extract bridge/IPC/plugin candidates to see whether the send call path is reachable without RPA.")
$linesOut.Add("3. Keep A-route/RPA as backup-only; do not promote it unless B-route bridge evidence fails.")
($linesOut -join "`n") + "`n" | Set-Content -LiteralPath $reportFullPath -Encoding UTF8
$requiredNames = @("AppContextManager.SendTxtMessageByJid", "MessageScheduling.SendChatMessage", "Chat.SendMessage", "XMPPConnection.send", "MessageCenter send-related callbacks")
$reportText = Get-Content -LiteralPath $reportFullPath -Raw
$missing = @($requiredNames | Where-Object { $reportText -notlike "*$_*" })
if ($missing.Count -gt 0) {
throw "Report missing required target names: $($missing -join ', ')"
}
[pscustomobject]@{
ok = $true
json_path = $jsonPath
report_path = $reportFullPath
sources = $sourceResults.Count
targets = $targetResults.Count
classifications = @($targetResults | ForEach-Object { "$($_.display)=$($_.classification)" })
originals_copied = $false
originals_modified = $false
} | ConvertTo-Json -Depth 6