- Add Windows 4.1.x Config.Cipher pointer-chain key extraction with cross-chunk
MemoryPatternScanner and bounded blob XOR decoding; keys still require per-DB
page-1 HMAC verification before caching.
- Cache keys by account fingerprint + database relative path; db scan --save
skips memory scanning entirely once all discovered databases are cached.
- Add DbMessage model (localId, serverId, type, timestamp, sender, display name,
avatar, isSelf) read read-only from Msg_{md5(chat)} via SQLCipher with hex/zstd
content decoding and contact.db name resolution.
- CLI: db messages / db contacts / db schema with masked defaults; include the
earlier chat send --session wiring. 97 Core tests pass; real-machine validated.
111 lines
4.4 KiB
C#
111 lines
4.4 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using WxAgent.Core;
|
|
using Xunit;
|
|
|
|
namespace WxAgent.Core.Tests;
|
|
|
|
public sealed class MemoryScanningTests
|
|
{
|
|
private static byte[] EncodedLiteral(string literal)
|
|
{
|
|
var bytes = Encoding.ASCII.GetBytes(literal);
|
|
var mask = Convert.FromHexString("d2c7442458020000004889442450488b450048844c2448488944254048584c24");
|
|
for (var i = 0; i < bytes.Length; i++) bytes[i] ^= mask[i % mask.Length];
|
|
return bytes;
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfigCipherDecodeFindsKeyAndEmbeddedSalt()
|
|
{
|
|
var key = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f";
|
|
var salt = "101112131415161718191a1b1c1d1e1f";
|
|
var decoded = WcdbConfigCipher.Decode(EncodedLiteral($"x'{key}{salt}'"));
|
|
Assert.Equal(new WcdbKeyCandidate(key, salt), Assert.Single(decoded));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfigCipherDecodeWindowsOnLongRuns()
|
|
{
|
|
var key = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f";
|
|
var salt = "101112131415161718191a1b1c1d1e1f";
|
|
var run = key + salt + key + salt;
|
|
var decoded = WcdbConfigCipher.Decode(EncodedLiteral($"x'{run}'"));
|
|
Assert.Contains(decoded, item => item.EncKey == key && item.SaltHint == salt);
|
|
Assert.Equal(4, decoded.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfigCipherDecodeRejectsBoundsAndUniformKeys()
|
|
{
|
|
Assert.Empty(WcdbConfigCipher.Decode([]));
|
|
Assert.Empty(WcdbConfigCipher.Decode(EncodedLiteral("x'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'" + "00000000000000000000000000000000")));
|
|
Assert.Empty(WcdbConfigCipher.Decode(EncodedLiteral("plain")));
|
|
Assert.Empty(WcdbConfigCipher.Decode(EncodedLiteral("x''")));
|
|
var tooLong = EncodedLiteral($"x'{new string('e', 200)}'");
|
|
Assert.Empty(WcdbConfigCipher.Decode(tooLong));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfigCipherDecodeRejectsBlobsOverLimit()
|
|
{
|
|
var literal = $"x'{new string('f', 64)}{new string('1', 32)}'";
|
|
var blob = new List<byte>(EncodedLiteral(literal));
|
|
while (blob.Count <= WcdbConfigCipher.MaximumBlobLength) blob.Insert(0, 0x00);
|
|
Assert.Empty(WcdbConfigCipher.Decode(blob.ToArray()));
|
|
}
|
|
|
|
[Fact]
|
|
public void PatternScannerMatchesAcrossChunkBoundaries()
|
|
{
|
|
var needle = Encoding.ASCII.GetBytes("com.Tencent.WCDB.Config.Cipher");
|
|
var content = Encoding.ASCII.GetBytes("prefix " + Encoding.ASCII.GetString(needle) + " suffix");
|
|
var split = 10;
|
|
var scanner = new MemoryPatternScanner(needle);
|
|
var matches = scanner.Feed(content.AsSpan(0, split), 0x1000).ToList();
|
|
matches.AddRange(scanner.Feed(content.AsSpan(split), 0x1000 + (ulong)split));
|
|
Assert.Equal([0x1000 + 7UL], matches);
|
|
}
|
|
|
|
[Fact]
|
|
public void PatternScannerReportsMatchesInSecondChunk()
|
|
{
|
|
var needle = Encoding.ASCII.GetBytes("abc");
|
|
var content = Encoding.ASCII.GetBytes("xxabc");
|
|
var scanner = new MemoryPatternScanner(needle);
|
|
var matches = scanner.Feed(content.AsSpan(0, 3), 0x2000).ToList();
|
|
matches.AddRange(scanner.Feed(content.AsSpan(3), 0x2003));
|
|
Assert.Equal([0x2002UL], matches);
|
|
}
|
|
|
|
[Fact]
|
|
public void PatternScannerDoesNotJoinAcrossGaps()
|
|
{
|
|
var needle = Encoding.ASCII.GetBytes("abc");
|
|
var scanner = new MemoryPatternScanner(needle);
|
|
var matches = scanner.Feed("a"u8[..1], 0x3000).ToList();
|
|
matches.AddRange(scanner.Feed("b"u8[..1], 0x4000)); // Gap: end was 0x3001, next feed at 0x4000.
|
|
matches.AddRange(scanner.Feed("c"u8[..1], 0x4001));
|
|
Assert.Empty(matches);
|
|
}
|
|
|
|
[Fact]
|
|
public void PatternScannerFindsMatchSpanningThreeChunks()
|
|
{
|
|
var needle = Encoding.ASCII.GetBytes("abcde");
|
|
var content = Encoding.ASCII.GetBytes("xxabcde");
|
|
var scanner = new MemoryPatternScanner(needle);
|
|
var matches = scanner.Feed(content.AsSpan(0, 3), 0x5000).ToList();
|
|
matches.AddRange(scanner.Feed(content.AsSpan(3, 3), 0x5003));
|
|
matches.AddRange(scanner.Feed(content.AsSpan(6), 0x5006));
|
|
Assert.Equal([0x5002UL], matches);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeProducesNoMatchAfterPatternError()
|
|
{
|
|
// Simulates the reference's own fail-open behavior after a failed read.
|
|
var decoded = WcdbConfigCipher.Decode(Encoding.ASCII.GetBytes("x'abc'"));
|
|
Assert.Empty(decoded);
|
|
}
|
|
} |