Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Confuser.Core/Helpers/MutationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,37 @@ public static void InjectKeys(MethodDef method, int[] keyIds, int[] keys) {
}
}

/// <summary>
/// Replaces the mutation key placeholders like <see cref="InjectKeys" />, but first
/// verifies that every requested key id actually has a placeholder in the method. Throws
/// if one is missing, so a runtime-source change that drops or renames a placeholder
/// fails loudly instead of silently emitting a stub whose injected values no longer
/// match the obfuscator side.
/// </summary>
/// <param name="method">The method to process.</param>
/// <param name="keyIds">The mutation key IDs.</param>
/// <param name="keys">The actual keys.</param>
public static void InjectKeysEnsured(MethodDef method, int[] keyIds, int[] keys) {
var found = new bool[keyIds.Length];
foreach (Instruction instr in method.Body.Instructions) {
if (instr.OpCode != OpCodes.Ldsfld || !(instr.Operand is IField field))
continue;
if (field.DeclaringType.FullName == mutationType &&
field2index.TryGetValue(field.Name, out int keyIndex)) {
int pos = Array.IndexOf(keyIds, keyIndex);
if (pos != -1)
found[pos] = true;
}
}
for (int i = 0; i < found.Length; i++)
if (!found[i])
throw new InvalidOperationException(
$"Mutation placeholder KeyI{keyIds[i]} not found in method '{method.Name}'; " +
"the injected runtime source and the injector are out of sync.");

InjectKeys(method, keyIds, keys);
}

/// <summary>
/// Replaces the placeholder call in method with actual instruction sequence.
/// </summary>
Expand Down
48 changes: 48 additions & 0 deletions Confuser.Protections/Constants/ConstantXorshift.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace Confuser.Protections.Constants {
/// <summary>
/// Curated full-period xorshift32 triples (a, b, c) for the constant key generator
/// n ^= n >> a; n ^= n << b; n ^= n >> c. One triple is chosen per module and injected
/// into the runtime Constant.Initialize (Mutation.KeyI2/KeyI3/KeyI4), replacing the fixed
/// 12/25/27 that de4dot/AV pattern-match. Both the obfuscator and the runtime use the same
/// triple, so the round-trip holds for any shifts; each listed triple is additionally a
/// full-period generator (period 2^32-1, verified by GF(2) matrix order) with non-extreme
/// shifts, so the expanded key stream is well mixed -- unlike the historic 12/25/27, which
/// is not full period. Generated by scripts/curate_xorshift.py.
/// </summary>
internal static class ConstantXorshift {
internal static readonly byte[][] Triples = {
new byte[] { 2, 5, 15 }, new byte[] { 2, 5, 21 }, new byte[] { 2, 7, 7 }, new byte[] { 2, 7, 9 },
new byte[] { 2, 7, 25 }, new byte[] { 2, 9, 15 }, new byte[] { 2, 15, 17 }, new byte[] { 2, 15, 25 },
new byte[] { 2, 21, 9 }, new byte[] { 3, 3, 26 }, new byte[] { 3, 3, 28 }, new byte[] { 3, 3, 29 },
new byte[] { 3, 5, 20 }, new byte[] { 3, 5, 22 }, new byte[] { 3, 5, 25 }, new byte[] { 3, 7, 29 },
new byte[] { 3, 13, 7 }, new byte[] { 3, 23, 25 }, new byte[] { 3, 25, 24 }, new byte[] { 3, 27, 11 },
new byte[] { 4, 3, 17 }, new byte[] { 4, 3, 27 }, new byte[] { 4, 5, 15 }, new byte[] { 5, 3, 21 },
new byte[] { 5, 7, 22 }, new byte[] { 5, 9, 7 }, new byte[] { 5, 9, 28 }, new byte[] { 5, 13, 6 },
new byte[] { 5, 15, 17 }, new byte[] { 5, 17, 13 }, new byte[] { 5, 21, 12 }, new byte[] { 5, 27, 8 },
new byte[] { 5, 27, 21 }, new byte[] { 5, 27, 25 }, new byte[] { 5, 27, 28 }, new byte[] { 6, 3, 17 },
new byte[] { 6, 13, 5 }, new byte[] { 6, 17, 9 }, new byte[] { 6, 21, 7 }, new byte[] { 6, 21, 13 },
new byte[] { 7, 7, 2 }, new byte[] { 7, 9, 5 }, new byte[] { 7, 13, 3 }, new byte[] { 7, 13, 25 },
new byte[] { 7, 17, 21 }, new byte[] { 7, 21, 6 }, new byte[] { 7, 25, 12 }, new byte[] { 7, 25, 20 },
new byte[] { 8, 7, 23 }, new byte[] { 8, 9, 23 }, new byte[] { 8, 27, 5 }, new byte[] { 9, 5, 14 },
new byte[] { 9, 5, 25 }, new byte[] { 9, 7, 2 }, new byte[] { 9, 11, 19 }, new byte[] { 9, 17, 6 },
new byte[] { 9, 21, 2 }, new byte[] { 9, 21, 16 }, new byte[] { 10, 9, 21 }, new byte[] { 10, 9, 25 },
new byte[] { 11, 7, 12 }, new byte[] { 11, 7, 16 }, new byte[] { 11, 17, 13 }, new byte[] { 11, 21, 13 },
new byte[] { 11, 27, 3 }, new byte[] { 12, 7, 11 }, new byte[] { 12, 9, 23 }, new byte[] { 12, 21, 5 },
new byte[] { 12, 25, 7 }, new byte[] { 13, 3, 17 }, new byte[] { 13, 3, 27 }, new byte[] { 13, 5, 19 },
new byte[] { 13, 17, 5 }, new byte[] { 13, 17, 11 }, new byte[] { 13, 17, 15 }, new byte[] { 13, 21, 6 },
new byte[] { 13, 21, 11 }, new byte[] { 14, 5, 9 }, new byte[] { 14, 13, 15 }, new byte[] { 15, 5, 2 },
new byte[] { 15, 5, 4 }, new byte[] { 15, 9, 2 }, new byte[] { 15, 13, 14 }, new byte[] { 15, 17, 13 },
new byte[] { 16, 7, 11 }, new byte[] { 16, 21, 9 }, new byte[] { 17, 3, 4 }, new byte[] { 17, 3, 6 },
new byte[] { 17, 3, 13 }, new byte[] { 17, 15, 2 }, new byte[] { 17, 15, 5 }, new byte[] { 17, 15, 20 },
new byte[] { 17, 15, 23 }, new byte[] { 17, 15, 26 }, new byte[] { 19, 5, 13 }, new byte[] { 19, 11, 9 },
new byte[] { 20, 5, 3 }, new byte[] { 20, 15, 17 }, new byte[] { 20, 25, 7 }, new byte[] { 21, 3, 5 },
new byte[] { 21, 5, 2 }, new byte[] { 21, 9, 10 }, new byte[] { 21, 17, 7 }, new byte[] { 21, 27, 5 },
new byte[] { 22, 5, 3 }, new byte[] { 22, 7, 5 }, new byte[] { 23, 7, 8 }, new byte[] { 23, 9, 8 },
new byte[] { 23, 9, 12 }, new byte[] { 23, 15, 17 }, new byte[] { 24, 25, 3 }, new byte[] { 25, 5, 3 },
new byte[] { 25, 5, 9 }, new byte[] { 25, 7, 2 }, new byte[] { 25, 9, 10 }, new byte[] { 25, 13, 7 },
new byte[] { 25, 15, 2 }, new byte[] { 25, 23, 3 }, new byte[] { 25, 27, 5 }, new byte[] { 26, 3, 3 },
new byte[] { 26, 15, 17 }, new byte[] { 27, 3, 4 }, new byte[] { 27, 3, 13 }, new byte[] { 28, 3, 3 },
new byte[] { 28, 9, 5 }, new byte[] { 28, 27, 5 }, new byte[] { 29, 3, 3 }, new byte[] { 29, 7, 3 }
};
}
}
17 changes: 11 additions & 6 deletions Confuser.Protections/Constants/EncodePhase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,17 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa

// encrypt
uint keySeed = moduleCtx.Random.NextUInt32();
// Randomize the xorshift triple (was the fixed 12/25/27, a de4dot/AV fingerprint) from a
// curated set of full-period triples; the same shifts are injected into the runtime
// Initialize below (Mutation.KeyI2/KeyI3/KeyI4) so both sides expand the same key stream.
byte[] shifts = ConstantXorshift.Triples[moduleCtx.Random.NextInt32(ConstantXorshift.Triples.Length)];
int shiftA = shifts[0], shiftB = shifts[1], shiftC = shifts[2];
var key = new uint[0x10];
uint state = keySeed;
for (int i = 0; i < 0x10; i++) {
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
state ^= state >> shiftA;
state ^= state << shiftB;
state ^= state >> shiftC;
key[i] = state;
}

Expand All @@ -113,9 +118,9 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa
moduleCtx.DataField.InitialValue = encryptedBuffer;
moduleCtx.DataField.HasFieldRVA = true;
moduleCtx.DataType.ClassLayout = new ClassLayoutUser(0, (uint)encryptedBuffer.Length);
MutationHelper.InjectKeys(moduleCtx.InitMethod,
new[] { 0, 1 },
new[] { encryptedBuffer.Length / 4, (int)keySeed });
MutationHelper.InjectKeysEnsured(moduleCtx.InitMethod,
new[] { 0, 1, 2, 3, 4 },
new[] { encryptedBuffer.Length / 4, (int)keySeed, shiftA, shiftB, shiftC });
MutationHelper.ReplacePlaceholder(moduleCtx.InitMethod, arg => {
var repl = new List<Instruction>();
repl.AddRange(arg);
Expand Down
6 changes: 3 additions & 3 deletions Confuser.Runtime/Constant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ static void Initialize() {
var k = new uint[0x10];
var n = (uint)Mutation.KeyI1;
for (int i = 0; i < 0x10; i++) {
n ^= n >> 12;
n ^= n << 25;
n ^= n >> 27;
n ^= n >> Mutation.KeyI2;
n ^= n << Mutation.KeyI3;
n ^= n >> Mutation.KeyI4;
k[i] = n;
}

Expand Down
112 changes: 112 additions & 0 deletions scripts/curate_xorshift.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Curate full-period xorshift32 triples for the Constants protection key generator.

Constant.Initialize (runtime) and EncodePhase (obfuscator) both seed a 32-bit xorshift
of the exact form

n ^= n >> a; n ^= n << b; n ^= n >> c;

to expand a seed into the key stream. Both sides must use the same (a, b, c); the historic
fixed 12/25/27 fingerprints the stub. A replacement triple is only valid if the generator
still has full period 2**32 - 1 (otherwise the key stream degenerates).

The step map is linear over GF(2), so the whole round is a 32x32 bit matrix M. The period
is the multiplicative order of M and equals 2**32 - 1 iff

M**(2**32-1) == I and M**((2**32-1)/p) != I for every prime p | (2**32-1).

2**32-1 = 3 * 5 * 17 * 257 * 65537. This is the standard primitivity test and is exact.
Emits Confuser.Protections/Constants/ConstantXorshift.cs. Deterministic; run from repo root:
python scripts/curate_xorshift.py
"""
import math
import os

MASK = 0xFFFFFFFF
N = 32
PERIOD = (1 << 32) - 1
FACTORS = (3, 5, 17, 257, 65537)
IDENT = [1 << j for j in range(N)]
OUT = os.path.join(os.path.dirname(__file__), "..", "Confuser.Protections", "Constants", "ConstantXorshift.cs")


def step(n, a, b, c):
n &= MASK
n ^= n >> a
n ^= (n << b) & MASK
n ^= n >> c
return n & MASK


def apply(M, v): # matrix (columns) times bit-vector, over GF(2)
r = j = 0
while v:
if v & 1:
r ^= M[j]
v >>= 1
j += 1
return r & MASK


def matmul(A, B): # apply B then A
return [apply(A, B[j]) for j in range(N)]


def matpow(M, e):
r = IDENT
while e:
if e & 1:
r = matmul(r, M)
M = matmul(M, M)
e >>= 1
return r


def full_period(a, b, c):
M = [step(1 << j, a, b, c) for j in range(N)] # column j = image of basis vector e_j
if matpow(M, PERIOD) != IDENT:
return False
return all(matpow(M, PERIOD // p) != IDENT for p in FACTORS)


def fmt(triples):
rows = []
for i in range(0, len(triples), 4):
rows.append("\t\t\t" + " ".join(f"new byte[] {{ {a}, {b}, {c} }}," for a, b, c in triples[i:i + 4]))
return "\n".join(rows).rstrip(",")


def main():
# non-extreme shifts (2..30) give better avalanche over the 16-word expansion
good = [(a, b, c)
for a in range(2, N - 1) for b in range(2, N - 1) for c in range(2, N - 1)
if full_period(a, b, c)]
print(f"original 12/25/27 full-period: {full_period(12, 25, 27)}")
print(f"curated full-period (>>a,<<b,>>c) triples, shifts 2..30: {len(good)} "
f"({math.log2(len(good)):.1f} bits)")

cs = f"""namespace Confuser.Protections.Constants {{
\t/// <summary>
\t/// Curated full-period xorshift32 triples (a, b, c) for the constant key generator
\t/// n ^= n >> a; n ^= n << b; n ^= n >> c. One triple is chosen per module and injected
\t/// into the runtime Constant.Initialize (Mutation.KeyI2/KeyI3/KeyI4), replacing the fixed
\t/// 12/25/27 that de4dot/AV pattern-match. Both the obfuscator and the runtime use the same
\t/// triple, so the round-trip holds for any shifts; each listed triple is additionally a
\t/// full-period generator (period 2^32-1, verified by GF(2) matrix order) with non-extreme
\t/// shifts, so the expanded key stream is well mixed -- unlike the historic 12/25/27, which
\t/// is not full period. Generated by scripts/curate_xorshift.py.
\t/// </summary>
\tinternal static class ConstantXorshift {{
\t\tinternal static readonly byte[][] Triples = {{
{fmt(good)}
\t\t}};
\t}}
}}
"""
with open(OUT, "w", newline="\n") as fh:
fh.write(cs)
print(f"wrote {os.path.normpath(OUT)}")


if __name__ == "__main__":
main()