Browse Source

add SniTamperProxy

HOME 9 months ago
parent
commit
bb88f5d01b

+ 162 - 0
SniTamperPoc/Program.cs

@@ -0,0 +1,162 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Runtime.ConstrainedExecution;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading;
+using DnsClient;
+
+
+//////////////////////////////////
+
+#region __________ INIT __________
+
+using Microsoft.Extensions.Logging.Console;
+
+var builder = WebApplication.CreateBuilder(args);
+
+//控制台日志格式
+builder.Services.AddLogging(opt =>
+{
+    opt.AddSimpleConsole(p =>
+    {
+        p.TimestampFormat = "[dd HH:mm:ss] ";
+        p.SingleLine = true;
+        p.ColorBehavior = LoggerColorBehavior.Enabled;
+    });
+});
+
+using var host = builder.Build();
+builder.WebHost.UseUrls("http://*:0");
+
+await host.StartAsync();
+var isRunning = true;
+var cts = new CancellationTokenSource();
+Console.CancelKeyPress += (_, _) =>
+{
+    isRunning = false;
+    cts.Cancel(false);
+};
+
+var logger = host.Services.GetRequiredService<ILogger<Program>>();
+logger.LogInformation("Hello, World!");
+
+//Main
+try
+{
+    await RealMain();
+}
+catch (Exception ex)
+{
+    logger.LogError(ex, "Main");
+}
+finally
+{
+    logger.LogInformation("Bye!");
+
+    await host.StopAsync();
+
+    Console.WriteLine();
+    Console.Write("Press ENTER to exit...");
+    Console.ReadLine();
+}
+
+#endregion __________ INIT __________
+
+//////////////////////////////////
+async Task RealMain()
+{
+    const string url = "http://www.example.org/index.html";
+    const string host = "www.example.org";
+    const string path = "/index.html";
+
+    const string dnsServerName = "reliable-dns-server-in-hosts";
+    var dnsServerIp = Dns.GetHostEntry(dnsServerName).AddressList.FirstOrDefault();
+    var lookup = new LookupClient(dnsServerIp);
+
+    var result = await lookup.QueryAsync(host, QueryType.A);
+
+    var record = result.Answers.ARecords().FirstOrDefault();
+    var ip = record?.Address;
+    var tcpClient = new TcpClient();
+    tcpClient.Connect(new IPEndPoint(ip, 443)); // stuck if was ip gfw-ed
+
+    var ssl = new SslStream(tcpClient.GetStream());
+    var sslOptions = new SslClientAuthenticationOptions
+    {
+        TargetHost = string.Empty, // Leave this empty to avoid sending SNI
+        RemoteCertificateValidationCallback = (_, certificate, chain, errs) =>
+        {
+            if (errs == SslPolicyErrors.None) return true;
+
+            if (errs != SslPolicyErrors.RemoteCertificateNameMismatch) return false;
+
+            if (certificate is not X509Certificate2 cert2) return false;
+
+            // 比较证书名称和主机名称
+            var certName = cert2.GetNameInfo(X509NameType.DnsName, false);
+            if (certName.StartsWith("*."))
+            {
+                if (!host.EndsWith(certName[2..], StringComparison.OrdinalIgnoreCase)) return false;
+            }
+            else if (!certName.Equals(host, StringComparison.OrdinalIgnoreCase)) return false;
+
+
+            // 验证证书的有效期
+            if (DateTime.Now < cert2.NotBefore || DateTime.Now > cert2.NotAfter) return false;
+
+            // 构建证书链
+            if (chain == null) return false;
+
+            chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; //检测吊销耗时太长,忽略
+            chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
+            chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 10);
+            chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
+
+            var isValidChain = chain.Build(cert2);
+            if (isValidChain) return true;
+
+            foreach (X509ChainStatus chainStatus in chain.ChainStatus)
+            {
+                // 仅处理会影响安全性的错误状态
+                if (chainStatus.Status == X509ChainStatusFlags.RevocationStatusUnknown ||
+                    chainStatus.Status == X509ChainStatusFlags.OfflineRevocation ||
+                    chainStatus.Status == X509ChainStatusFlags.NoError)
+                {
+                    continue;
+                }
+
+                // 其他任何错误状态都认为证书无效
+                return false;
+            }
+
+            return true;
+
+        }
+    };
+
+    await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
+
+    ssl.Write(Encoding.ASCII.GetBytes($"GET {path} HTTP/1.1\r\n"));
+    ssl.Write(Encoding.ASCII.GetBytes($"Host: {host}\r\n"));
+    ssl.Write(Encoding.ASCII.GetBytes($"\r\n"));
+
+    var reader = new StreamReader(ssl);
+
+    var lines = new List<string>(); // <- 200 OK , PoC SUCC!
+    do
+    {
+        var line = reader.ReadLine();
+        if (line == "") break;
+        lines.Add(line);
+
+    } while (true);
+
+
+    int bp = 0;
+}

+ 12 - 0
SniTamperPoc/Properties/launchSettings.json

@@ -0,0 +1,12 @@
+{
+  "profiles": {
+    "SniTamperPoc": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "applicationUrl": "https://localhost:12816;http://localhost:12817"
+    }
+  }
+}

+ 14 - 0
SniTamperPoc/SniTamperPoc.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="DnsClient" Version="1.7.0" />
+  </ItemGroup>
+
+</Project>

+ 171 - 0
SniTamperProxy/Program.cs

@@ -0,0 +1,171 @@
+using System.Buffers;
+using System.IO.Pipelines;
+using Microsoft.AspNetCore.Connections;
+using System.Net;
+using System.Security.Cryptography.X509Certificates;
+using DnsClient;
+using System.Text;
+
+#region INIT
+
+using Microsoft.Extensions.Logging.Console;
+using System.Net.Security;
+using System.Net.Sockets;
+
+var builder = WebApplication.CreateBuilder(args);
+
+//控制台日志格式
+builder.Services.AddLogging(opt =>
+{
+    opt.AddSimpleConsole(p =>
+    {
+        p.TimestampFormat = "[dd HH:mm:ss] ";
+        p.SingleLine = true;
+        p.ColorBehavior = LoggerColorBehavior.Enabled;
+    });
+});
+
+builder.WebHost.UseKestrel(opt =>
+{
+    opt.Listen(IPAddress.Any, 0, lisOpt =>
+    {
+        lisOpt.UseConnectionHandler<NoSniProxyHandler>();
+    });
+});
+
+await using var host = builder.Build();
+
+var logger = host.Services.GetRequiredService<ILogger<Program>>();
+logger.LogInformation("Hello, World!");
+await host.RunAsync();
+
+#endregion INIT
+
+public class NoSniProxyHandler : ConnectionHandler
+{
+    private const string dnsServerName = "reliable-dns-server-in-hosts";
+    private static readonly IPAddress? dnsServerIp = Dns.GetHostEntry(dnsServerName).AddressList.FirstOrDefault();
+    private static readonly LookupClient lookup = new(dnsServerIp);
+
+    public override async Task OnConnectedAsync(ConnectionContext connection)
+    {
+        var requestStream = connection.Transport.Input;
+        var responseStream = connection.Transport.Output;
+
+        var firstLine = await ReadLineAsync(requestStream);
+        if (firstLine == null)
+        {
+            connection.Abort(new ConnectionAbortedException("Canceled: First line rad fail"));
+            return;
+        }
+
+        var firstLineParts = firstLine.Split(' ', 3);
+        if (firstLineParts.Length < 3)
+        {
+            connection.Abort(new ConnectionAbortedException("Canceled: First line bad"));
+            return;
+        }
+
+        var method = firstLineParts[0];
+        var url = firstLineParts[1];
+        var ver = firstLineParts[2];
+
+        var uri = new Uri(url);
+        var targetHost = uri.Host;
+
+        var result = await lookup.QueryAsync(targetHost, QueryType.A);
+        var record = result.Answers.ARecords().FirstOrDefault();
+        var ip = record?.Address;
+
+        var tcpClient = new TcpClient();
+        await tcpClient.ConnectAsync(new IPEndPoint(ip, 443));
+        var ssl = new SslStream(tcpClient.GetStream());
+        var sslOptions = new SslClientAuthenticationOptions
+        {
+            TargetHost = string.Empty, // Leave this empty to avoid sending SNI
+            RemoteCertificateValidationCallback = (o, certificate, chain, errors) => VerifyServerCert(targetHost, certificate, chain, errors),
+        };
+
+        await ssl.AuthenticateAsClientAsync(sslOptions, new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token);
+        ssl.Write(Encoding.ASCII.GetBytes($"{method} {uri.PathAndQuery} {ver}\r\n"));
+
+        var outgoing = requestStream.CopyToAsync(ssl);
+        var inbound = ssl.CopyToAsync(responseStream);
+
+        await Task.WhenAll(outgoing, inbound);
+    }
+
+    private async Task<string?> ReadLineAsync(PipeReader requestStream)
+    {
+        while (true)
+        {
+            var readResult = await requestStream.ReadAsync();
+            if (readResult.IsCanceled) return null;
+
+            var (seq, exm) = ExtractLine(readResult.Buffer, out var line);
+            requestStream.AdvanceTo(seq, exm);
+            if (line != null) return line;
+        }
+    }
+
+    private (SequencePosition pos, SequencePosition exm) ExtractLine(ReadOnlySequence<byte> buffer, out string? line)
+    {
+        var reader = new SequenceReader<byte>(buffer);
+        if (reader.TryReadTo(out ReadOnlySpan<byte> span, "\r\n"u8))
+        {
+            if (span.Length > 4096) throw new InvalidDataException("Too long for line");
+            line = Encoding.ASCII.GetString(span);
+            return (reader.Position, reader.Position);
+        }
+
+        line = null;
+        return (buffer.Start, buffer.End);
+    }
+
+    private bool VerifyServerCert(string targetHost, X509Certificate certificate, X509Chain? chain, SslPolicyErrors errs)
+    {
+        if (errs == SslPolicyErrors.None) return true;
+
+        if (errs != SslPolicyErrors.RemoteCertificateNameMismatch) return false;
+
+        if (certificate is not X509Certificate2 cert2) return false;
+
+        // 比较证书名称和主机名称
+        var certName = cert2.GetNameInfo(X509NameType.DnsName, false);
+        if (certName.StartsWith("*."))
+        {
+            if (!targetHost.EndsWith(certName[2..], StringComparison.OrdinalIgnoreCase)) return false;
+        }
+        else if (!certName.Equals(targetHost, StringComparison.OrdinalIgnoreCase)) return false;
+
+        // 验证证书的有效期
+        if (DateTime.Now < cert2.NotBefore || DateTime.Now > cert2.NotAfter) return false;
+
+        // 构建证书链
+        if (chain == null) return false;
+
+        chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // 不检查吊销,太耗时了
+        chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
+        chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 10);
+        chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
+
+        var isValidChain = chain.Build(cert2);
+        if (isValidChain) return true;
+
+        foreach (X509ChainStatus chainStatus in chain.ChainStatus)
+        {
+            // 仅处理会影响安全性的错误状态
+            if (chainStatus.Status == X509ChainStatusFlags.RevocationStatusUnknown ||
+                chainStatus.Status == X509ChainStatusFlags.OfflineRevocation ||
+                chainStatus.Status == X509ChainStatusFlags.NoError)
+            {
+                continue;
+            }
+
+            // 其他任何错误状态都认为证书无效
+            return false;
+        }
+
+        return true;
+    }
+}

+ 11 - 0
SniTamperProxy/Properties/launchSettings.json

@@ -0,0 +1,11 @@
+{
+  "profiles": {
+    "SniTamperProxy": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+    }
+  }
+}

+ 14 - 0
SniTamperProxy/SniTamperProxy.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="DnsClient" Version="1.7.0" />
+  </ItemGroup>
+
+</Project>

+ 18 - 1
StrangeTools.sln

@@ -74,7 +74,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DummyCursor", "DummyCursor\
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShowUserComment", "ShowUserComment\ShowUserComment.csproj", "{D47A9506-9522-4365-8FC8-56FF6DA50E1E}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompressWorker", "CompressWorker\CompressWorker.csproj", "{AB8E4594-A331-47BA-B806-3CF8B554A0CF}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompressWorker", "CompressWorker\CompressWorker.csproj", "{AB8E4594-A331-47BA-B806-3CF8B554A0CF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SniTamper", "SniTamper", "{0CAFC020-EA0B-4DB1-9782-645970A53BA5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SniTamperPoc", "SniTamperPoc\SniTamperPoc.csproj", "{BAEB09B6-59C9-479C-AB35-12BC2C10F5AE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SniTamperProxy", "SniTamperProxy\SniTamperProxy.csproj", "{8E775D0B-CB5E-4921-81F0-D236F60072FC}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -206,6 +212,14 @@ Global
 		{AB8E4594-A331-47BA-B806-3CF8B554A0CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{AB8E4594-A331-47BA-B806-3CF8B554A0CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{AB8E4594-A331-47BA-B806-3CF8B554A0CF}.Release|Any CPU.Build.0 = Release|Any CPU
+		{BAEB09B6-59C9-479C-AB35-12BC2C10F5AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{BAEB09B6-59C9-479C-AB35-12BC2C10F5AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{BAEB09B6-59C9-479C-AB35-12BC2C10F5AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{BAEB09B6-59C9-479C-AB35-12BC2C10F5AE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8E775D0B-CB5E-4921-81F0-D236F60072FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8E775D0B-CB5E-4921-81F0-D236F60072FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8E775D0B-CB5E-4921-81F0-D236F60072FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{8E775D0B-CB5E-4921-81F0-D236F60072FC}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -243,6 +257,9 @@ Global
 		{88F5DDCC-2674-4586-9FA9-7880EC712668} = {3120ADE6-C606-42F1-9AA8-B7F1A8933CD7}
 		{D47A9506-9522-4365-8FC8-56FF6DA50E1E} = {3120ADE6-C606-42F1-9AA8-B7F1A8933CD7}
 		{AB8E4594-A331-47BA-B806-3CF8B554A0CF} = {3120ADE6-C606-42F1-9AA8-B7F1A8933CD7}
+		{0CAFC020-EA0B-4DB1-9782-645970A53BA5} = {3120ADE6-C606-42F1-9AA8-B7F1A8933CD7}
+		{BAEB09B6-59C9-479C-AB35-12BC2C10F5AE} = {0CAFC020-EA0B-4DB1-9782-645970A53BA5}
+		{8E775D0B-CB5E-4921-81F0-D236F60072FC} = {0CAFC020-EA0B-4DB1-9782-645970A53BA5}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {017A8C58-F476-47E7-9CBE-077A98A76AB4}