Files
Radicade/2023Patch/RadicadeEacBypass/ImageSignatureRequest.cs
T
2026-10-16 23:33:13 -07:00

188 lines
7.0 KiB
C#

#nullable enable
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public static class ImageSignatureRequest
{
const int HeaderScanLimit = 0x100;
const int MinimumRecords = 32;
const int PrefixProbe = 8;
const int PackingProbe = 1024;
const int MaxLiteralLength = 1 << 20;
static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true);
public static string? DetectKeyId(byte[]? metadata)
{
if (metadata == null || metadata.Length < 24 ||
BinaryPrimitives.ReadUInt32LittleEndian(metadata) != 0xFAB11BAF ||
BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(4)) != 27)
return null;
int table = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(8));
int size = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(12));
int data = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(16));
int dataSize = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(20));
if (table < 24 || size < 0 || size % 8 != 0 || (long)table + size > metadata.Length ||
data < 24 || dataSize < 0 || (long)data + dataSize > metadata.Length)
return null;
return ScanKeyTails(metadata, table, size / 8, data, dataSize);
}
public static string? DetectKeyIdFromGameMetadata(byte[]? metadata)
{
if (metadata == null || metadata.Length < 24) return null;
if (BinaryPrimitives.ReadUInt32LittleEndian(metadata) == 0xFAB11BAF &&
BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(4)) == 27)
return DetectKeyId(metadata);
return DetectKeyIdByHeaderScan(metadata);
}
static string? ScanKeyTails(byte[] metadata, int tableOffset, int recordCount, int dataOffset, int dataSize)
{
string? key = null;
for (int record = 0; record < recordCount; record++)
{
var (length, index) = Record(metadata, tableOffset, record);
if (length < 0 || index < 0 || (long)index + length > dataSize) return null;
if (length != 8 && length != 18) continue;
var literal = Encoding.UTF8.GetString(metadata, dataOffset + index, length);
if (literal != ".rec.net" && literal != ".radicade.internal") continue;
var candidate = "KEY:RSA:p1" + literal;
if (key != null && key != candidate) return null;
key = candidate;
}
return key;
}
static (int Length, int Index) Record(byte[] metadata, int tableOffset, int record)
{
int offset = tableOffset + 8 * record;
return (BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset)),
BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset + 4)));
}
public static string? DetectKeyIdByHeaderScan(byte[]? metadata)
{
if (metadata == null || metadata.Length < HeaderScanLimit + 8) return null;
var fields = new List<int>();
for (int offset = 0; offset + 4 <= HeaderScanLimit; offset += 4)
{
int value = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset));
if (value > 0 && value <= metadata.Length && !fields.Contains(value)) fields.Add(value);
}
var tables = new List<(int Offset, int Count)>();
foreach (var tableOffset in fields)
foreach (var tableSize in fields)
{
if (tableSize % 8 != 0 || tableSize / 8 < MinimumRecords) continue;
if (tableOffset < HeaderScanLimit || (long)tableOffset + tableSize > metadata.Length) continue;
tables.Add((tableOffset, tableSize / 8));
}
var slices = new List<(int Offset, int Size)>();
foreach (var dataOffset in fields)
foreach (var dataSize in fields)
{
if (dataSize <= 0 || dataOffset < HeaderScanLimit) continue;
if ((long)dataOffset + dataSize > metadata.Length) continue;
slices.Add((dataOffset, dataSize));
}
tables.Sort((a, b) => b.Count.CompareTo(a.Count));
foreach (var (tableOffset, count) in tables)
foreach (var (dataOffset, dataSize) in slices)
{
if (!LooksLikeLiteralTable(metadata, tableOffset, count, dataOffset, dataSize, PrefixProbe)) continue;
if (!SampleDecodesAsText(metadata, tableOffset, count, dataOffset, dataSize)) continue;
if (!LooksLikeLiteralTable(metadata, tableOffset, count, dataOffset, dataSize, PackingProbe)) continue;
var key = ScanKeyTails(metadata, tableOffset, count, dataOffset, dataSize);
if (key != null) return key;
}
return null;
}
static bool LooksLikeLiteralTable(byte[] metadata, int tableOffset, int count, int dataOffset, int dataSize, int probe)
{
int examined = Math.Min(count, probe);
int contiguous = 0;
long running = 0;
for (int record = 0; record < examined; record++)
{
var (length, index) = Record(metadata, tableOffset, record);
if (length < 0 || length > MaxLiteralLength || index < 0 || (long)index + length > dataSize) return false;
if (index == running) contiguous++;
running = index + length;
}
return contiguous >= examined - Math.Max(1, examined / 100);
}
static bool SampleDecodesAsText(byte[] metadata, int tableOffset, int count, int dataOffset, int dataSize)
{
int step = Math.Max(1, count / 48);
for (int record = 0; record < count; record += step)
{
var (length, index) = Record(metadata, tableOffset, record);
if (length < 0 || index < 0 || (long)index + length > dataSize) return false;
if (length == 0) continue;
try
{
StrictUtf8.GetCharCount(metadata, dataOffset + index, length);
}
catch (ArgumentException)
{
return false;
}
}
return true;
}
public static string AddKeyId(string url, string serverUrl, string? keyId)
{
if (string.IsNullOrEmpty(keyId) ||
!Uri.TryCreate(url, UriKind.Absolute, out var request) ||
!Uri.TryCreate(serverUrl, UriKind.Absolute, out var server) ||
(server.Scheme != "http" && server.Scheme != "https") ||
request.Scheme != server.Scheme || request.Authority != server.Authority ||
!request.AbsolutePath.StartsWith(server.AbsolutePath.TrimEnd('/') + "/images/", StringComparison.OrdinalIgnoreCase))
return url;
var parameters = request.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)
.Where(p => !Uri.UnescapeDataString(p.Split('=')[0]).Equals("rcsig", StringComparison.OrdinalIgnoreCase))
.Append("rcsig=" + Uri.EscapeDataString(keyId));
return request.GetLeftPart(UriPartial.Path) + "?" + string.Join("&", parameters) + request.Fragment;
}
}