Add paramPadSettings to insert random junk parameters into method signatures
parent
c88021dba9
commit
d85a37c7ad
|
|
@ -35,11 +35,12 @@ namespace Obfuz.ObfusPasses
|
|||
ExprObfus = 0x400,
|
||||
ControlFlowObfus = 0x800,
|
||||
EvalStackObfus = 0x1000,
|
||||
ParamPad = 0x2000,
|
||||
|
||||
RemoveConstField = 0x100000,
|
||||
WaterMark = 0x200000,
|
||||
|
||||
AllObfus = SymbolObfus | CallObfus | ExprObfus | ControlFlowObfus | EvalStackObfus,
|
||||
AllObfus = SymbolObfus | CallObfus | ExprObfus | ControlFlowObfus | EvalStackObfus | ParamPad,
|
||||
AllEncrypt = ConstEncrypt | FieldEncrypt,
|
||||
|
||||
MethodBodyObfusOrEncrypt = ConstEncrypt | CallObfus | ExprObfus | ControlFlowObfus | EvalStackObfus,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dc5d3560708a4cfb94fded33575958e7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright 2025 Code Philosophy
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
using dnlib.DotNet;
|
||||
using Obfuz.ObfusPasses.SymbolObfus;
|
||||
using Obfuz.Settings;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Obfuz.ObfusPasses.ParamPad
|
||||
{
|
||||
public class ParamPadPass : ObfuscationPassBase
|
||||
{
|
||||
private readonly ParamPadSettingsFacade _settings;
|
||||
private IObfuscationPolicy _renamePolicy;
|
||||
|
||||
public override ObfuscationPassType Type => ObfuscationPassType.ParamPad;
|
||||
|
||||
public ParamPadPass(ParamPadSettingsFacade settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
_renamePolicy = SymbolRename.CreateDefaultRenamePolicy(_settings.ruleFiles, _settings.customRenamePolicyTypes, ObfuscationPassType.ParamPad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deliberately empty. The work happens in Stop(), see the comment there.
|
||||
/// </summary>
|
||||
public override void Process()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Padding runs in Stop(), not Process(), and is registered after CallObfus.
|
||||
///
|
||||
/// CallObfus generates its dispatch proxy BODIES in Stop(). Running before it means those
|
||||
/// bodies do not exist yet and their calls to padded methods keep the old argument count
|
||||
/// (a broken build). Running after it in Process() is impossible for the same reason. But
|
||||
/// Stop() runs in registration order, so padding last in Stop() sees the finished proxies.
|
||||
///
|
||||
/// That ordering is what keeps the proxies useful. CallObfus groups call targets by shared
|
||||
/// signature, so if it saw padded signatures the pool would fragment — measured on the real
|
||||
/// game, hubs went from 3209 (mean 6.2 callees) to 9717 (mean 2.46, median 1), i.e. mostly
|
||||
/// one-to-one indirections that any tool collapses. Padding afterwards leaves the proxy
|
||||
/// signatures unpadded, so the hubs stay dense, and the junk arguments get materialised
|
||||
/// once inside each proxy case instead of at every call site that funnels through it.
|
||||
///
|
||||
/// Everything else has already run by now, which is also why the junk constants are never
|
||||
/// const-encrypted and the consume fold is never re-obfuscated by ExprObfus or flattened
|
||||
/// by ControlFlowObfus.
|
||||
/// </summary>
|
||||
public override void Stop()
|
||||
{
|
||||
var ctx = ObfuscationPassContext.Current;
|
||||
int seed = _settings.randomSeed != 0 ? _settings.randomSeed : (Guid.NewGuid().GetHashCode() | 1);
|
||||
Debug.Log($"[ParamPad] padding parameters with seed {seed}, count range [{_settings.minCount},{_settings.maxCount}].");
|
||||
|
||||
var padding = new ParameterPadding(seed, _settings.minCount, _settings.maxCount, IsSafeToPad);
|
||||
padding.Process(ctx.modulesToObfuscate, ctx.allObfuscationRelativeModules);
|
||||
Debug.Log($"[ParamPad] padded {padding.PaddedMethodCount} of {padding.CandidateCount} candidate methods ({padding.VetoedCount} vetoed because a call site could not be rewritten).");
|
||||
}
|
||||
|
||||
private bool IsSafeToPad(MethodDef method)
|
||||
{
|
||||
var ctx = ObfuscationPassContext.Current;
|
||||
if (ctx.whiteList.IsInWhiteList(method.Module) || ctx.whiteList.IsInWhiteList(method.DeclaringType) || ctx.whiteList.IsInWhiteList(method))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!Support(ctx.passPolicy.GetMethodObfuscationPasses(method)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ctx.obfuzIgnoreScopeComputeCache.HasSelfOrDeclaringOrEnclosingOrInheritObfuzIgnoreScope(method, method.DeclaringType, ObfuzScope.MethodParameter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// the rename policy already encodes every contract that binds a method from outside
|
||||
// the IL: MonoBehaviour messages, DOTS and source generated types, MonoPInvokeCallback,
|
||||
// delegate members, plus the project's own rule files and custom policies.
|
||||
return _renamePolicy.NeedRename(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 03ae54b9c1b04b12bf0cf6140e3db2bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,992 @@
|
|||
// Copyright 2025 Code Philosophy
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
using dnlib.DotNet;
|
||||
using dnlib.DotNet.Emit;
|
||||
using Obfuz.Editor;
|
||||
using Obfuz.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Obfuz.ObfusPasses.ParamPad
|
||||
{
|
||||
public enum JunkKind
|
||||
{
|
||||
Int32,
|
||||
UInt32,
|
||||
Int64,
|
||||
Single,
|
||||
Double,
|
||||
Boolean,
|
||||
Byte,
|
||||
Int16,
|
||||
Char,
|
||||
}
|
||||
|
||||
/// <summary>How the junk parameters are combined into one value.</summary>
|
||||
public enum FoldOp { Xor, Add, Sub, Mul, Or, And }
|
||||
|
||||
/// <summary>How that value is driven to zero. Two of these need no literal zero at all.</summary>
|
||||
public enum ZeroOp { MulZero, DupXor, DupSub, AndZero }
|
||||
|
||||
/// <summary>Where the zero goes, so that the junk loads are never dead.</summary>
|
||||
public enum SinkKind { BranchPair, SwitchOne, ThreadIntoReturn }
|
||||
|
||||
public class PadPlan
|
||||
{
|
||||
public MethodDef method;
|
||||
// the per-method recipe for consuming the junk. Randomising these is what stops every
|
||||
// padded method from opening with one greppable prologue.
|
||||
public FoldOp foldOp;
|
||||
public ZeroOp zeroOp;
|
||||
public SinkKind sink;
|
||||
public int[] junkFoldOrder;
|
||||
// one entry per slot of the new parameter list. -1 marks a junk slot, otherwise
|
||||
// the index of the original parameter that lives there.
|
||||
public int[] slotToReal;
|
||||
// junk descriptor per slot, only meaningful where slotToReal is -1.
|
||||
public JunkKind[] slotKind;
|
||||
public object[] slotValue;
|
||||
// original parameter index -> new slot index.
|
||||
public int[] realToSlot;
|
||||
|
||||
public int RealCount => realToSlot.Length;
|
||||
public int SlotCount => slotToReal.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts junk parameters at random positions into eligible methods and fixes up every
|
||||
/// definition, reference and call site so the result still runs.
|
||||
///
|
||||
/// Pure dnlib on purpose: no ObfuscationPassContext, no Unity, so Tests~/ParamPad can
|
||||
/// compile this file directly the way Tests~/MemberReorder compiles MemberReorder.cs.
|
||||
/// </summary>
|
||||
public class ParameterPadding
|
||||
{
|
||||
private readonly Random _random;
|
||||
private readonly int _minCount;
|
||||
private readonly int _maxCount;
|
||||
private readonly Func<MethodDef, bool> _isSafe;
|
||||
|
||||
private static readonly JunkKind[] s_junkKinds = (JunkKind[])Enum.GetValues(typeof(JunkKind));
|
||||
|
||||
public ParameterPadding(int seed, int minCount, int maxCount, Func<MethodDef, bool> isSafe)
|
||||
{
|
||||
if (minCount < 1 || maxCount < minCount)
|
||||
{
|
||||
throw new ArgumentException($"invalid parameter padding range [{minCount},{maxCount}]");
|
||||
}
|
||||
_random = new Random(seed);
|
||||
_minCount = minCount;
|
||||
_maxCount = maxCount;
|
||||
_isSafe = isSafe;
|
||||
}
|
||||
|
||||
private class CallSite
|
||||
{
|
||||
public MethodDef host;
|
||||
public Instruction inst;
|
||||
public PadPlan plan;
|
||||
// parameter types as seen at this call site, already valid in the host module.
|
||||
public TypeSig[] argTypes;
|
||||
}
|
||||
|
||||
public int PaddedMethodCount { get; private set; }
|
||||
|
||||
/// <summary>Methods the safety predicate and the structural checks accepted.</summary>
|
||||
public int CandidateCount { get; private set; }
|
||||
|
||||
/// <summary>Candidates dropped because a call site could not be rewritten safely.</summary>
|
||||
public int VetoedCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Method operands that could not be resolved at all. Every candidate sharing a name with
|
||||
/// one of these is vetoed, because an unresolvable reference might BE that candidate.
|
||||
/// </summary>
|
||||
public int UnresolvedReferenceCount { get; private set; }
|
||||
|
||||
public void Process(List<ModuleDef> toObfuscate, List<ModuleDef> allModules)
|
||||
{
|
||||
var candidates = new HashSet<MethodDef>();
|
||||
foreach (ModuleDef mod in toObfuscate)
|
||||
{
|
||||
foreach (TypeDef type in mod.GetTypes())
|
||||
{
|
||||
foreach (MethodDef method in type.Methods)
|
||||
{
|
||||
if (IsCandidate(method))
|
||||
{
|
||||
candidates.Add(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// a module can be loaded more than once, in which case resolving a reference hands
|
||||
// back a MethodDef from the other instance. Matching on identity alone would then
|
||||
// silently miss the call site and ship a broken assembly, so match on token too.
|
||||
var byToken = new Dictionary<string, MethodDef>();
|
||||
foreach (MethodDef method in candidates)
|
||||
{
|
||||
byToken[TokenKey(method.Module, method.MDToken.Raw)] = method;
|
||||
}
|
||||
|
||||
CandidateCount = candidates.Count;
|
||||
var vetoed = new HashSet<MethodDef>();
|
||||
var rawSites = new List<CallSite>();
|
||||
var refsByMethod = new Dictionary<MethodDef, HashSet<MemberRef>>();
|
||||
var unresolvedNames = new HashSet<string>();
|
||||
IndexReferences(allModules, byToken, vetoed, rawSites, refsByMethod, unresolvedNames);
|
||||
|
||||
// An operand we could not resolve may well be one of our candidates, so we cannot tell
|
||||
// whether its call site needs rewriting. Every other "I do not understand this" path
|
||||
// in this pass vetoes; this one must too, or the method is padded with a stale call
|
||||
// site left behind and the assembly ships broken.
|
||||
if (unresolvedNames.Count > 0)
|
||||
{
|
||||
foreach (MethodDef candidate in candidates)
|
||||
{
|
||||
if (unresolvedNames.Contains(candidate.Name))
|
||||
{
|
||||
vetoed.Add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates.ExceptWith(vetoed);
|
||||
VetoedCount = CandidateCount - candidates.Count;
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// a method whose call sites we could not fully index is dropped wholesale, so the
|
||||
// transform is never half applied.
|
||||
var sites = rawSites.Where(s => candidates.Contains(s.plan.method)).ToList();
|
||||
|
||||
var plans = new Dictionary<MethodDef, PadPlan>();
|
||||
foreach (MethodDef method in candidates.OrderBy(m => m.Module.Name.String, StringComparer.Ordinal).ThenBy(m => m.MDToken.Raw))
|
||||
{
|
||||
plans.Add(method, BuildPlan(method));
|
||||
}
|
||||
foreach (CallSite site in sites)
|
||||
{
|
||||
site.plan = plans[site.plan.method];
|
||||
}
|
||||
|
||||
foreach (PadPlan plan in plans.Values)
|
||||
{
|
||||
ApplyToDefinition(plan);
|
||||
}
|
||||
foreach (var e in refsByMethod)
|
||||
{
|
||||
if (!plans.TryGetValue(e.Key, out PadPlan plan))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (MemberRef memberRef in e.Value)
|
||||
{
|
||||
ApplyToReference(memberRef, plan);
|
||||
}
|
||||
}
|
||||
RewriteCallSites(sites);
|
||||
PaddedMethodCount = plans.Count;
|
||||
|
||||
// CleanUpInstructionPass runs in the Process() phase, which is already over by the
|
||||
// time this pass works, so nothing else will compact what we emit.
|
||||
var touched = new HashSet<MethodDef>(plans.Keys);
|
||||
foreach (CallSite site in sites)
|
||||
{
|
||||
touched.Add(site.host);
|
||||
}
|
||||
foreach (MethodDef method in touched)
|
||||
{
|
||||
CilBody body = method.Body;
|
||||
body.OptimizeMacros();
|
||||
body.OptimizeBranches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attributes that pin a method's ARGUMENT LIST, as opposed to its name. The rename
|
||||
/// policy does not cover these: a serialization callback is located by attribute, so it
|
||||
/// is perfectly safe to rename and still fatal to re-sign — BinaryFormatter and
|
||||
/// Newtonsoft both validate the signature and throw. Unity's ContextMenu and the editor
|
||||
/// callbacks are invoked with a fixed (usually empty) argument list for the same reason.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> s_signaturePinningAttributes = new HashSet<string>
|
||||
{
|
||||
"System.Runtime.Serialization.OnSerializingAttribute",
|
||||
"System.Runtime.Serialization.OnSerializedAttribute",
|
||||
"System.Runtime.Serialization.OnDeserializingAttribute",
|
||||
"System.Runtime.Serialization.OnDeserializedAttribute",
|
||||
"System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute",
|
||||
"UnityEngine.RuntimeInitializeOnLoadMethodAttribute",
|
||||
"UnityEngine.ContextMenu",
|
||||
"UnityEditor.MenuItem",
|
||||
"UnityEditor.InitializeOnLoadMethodAttribute",
|
||||
"UnityEditor.Callbacks.DidReloadScripts",
|
||||
"UnityEditor.Callbacks.PostProcessBuildAttribute",
|
||||
"UnityEditor.Callbacks.PostProcessSceneAttribute",
|
||||
"UnityEditor.Callbacks.OnOpenAssetAttribute",
|
||||
};
|
||||
|
||||
private static bool HasSignaturePinningAttribute(MethodDef method)
|
||||
{
|
||||
foreach (CustomAttribute ca in method.CustomAttributes)
|
||||
{
|
||||
ITypeDefOrRef attrType = ca.AttributeType;
|
||||
if (attrType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (s_signaturePinningAttributes.Contains(attrType.FullName)
|
||||
|| attrType.Name == ConstValues.MonoPInvokeCallbackAttributeName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsCandidate(MethodDef method)
|
||||
{
|
||||
if (!method.HasBody || method.Body.Instructions.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (method.IsPinvokeImpl || method.IsInternalCall || method.IsNative || method.IsRuntime || method.IsUnmanagedExport)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (method.IsRuntimeSpecialName || method.IsConstructor || method.IsStaticConstructor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// vtable slots, interface contracts and MethodImpl entries: same pin set as
|
||||
// MemberReorder.IsPositionPinnedMethod.
|
||||
if (method.IsVirtual || method.IsAbstract || method.HasOverrides || method.IsNewSlot)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// PropertyDef/EventDef carry their own signature and nothing keeps them in step.
|
||||
if (method.SemanticsAttributes != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
MethodSig sig = method.MethodSig;
|
||||
if (sig == null || sig.IsVarArg || sig.ParamsAfterSentinel != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
TypeDef declaringType = method.DeclaringType;
|
||||
if (declaringType == null || declaringType.IsDelegate || declaringType.IsInterface)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (method.Module != null && method.Module.EntryPoint == method)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (method.Parameters.Any(p => p.Type != null && p.Type.ElementType == ElementType.TypedByRef))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// `this` as an explicit signature entry would shift the ldarg remap by one.
|
||||
if (sig.ExplicitThis)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (HasSignaturePinningAttribute(method))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return _isSafe(method);
|
||||
}
|
||||
|
||||
private static string TokenKey(ModuleDef module, uint token)
|
||||
{
|
||||
return (module?.Name.String ?? "?") + "!" + token.ToString("X8");
|
||||
}
|
||||
|
||||
private void IndexReferences(List<ModuleDef> allModules, Dictionary<string, MethodDef> byToken,
|
||||
HashSet<MethodDef> vetoed, List<CallSite> sites, Dictionary<MethodDef, HashSet<MemberRef>> refsByMethod,
|
||||
HashSet<string> unresolvedNames)
|
||||
{
|
||||
var resolveCache = new Dictionary<IMethod, MethodDef>();
|
||||
foreach (ModuleDef mod in allModules)
|
||||
{
|
||||
foreach (TypeDef type in mod.GetTypes())
|
||||
{
|
||||
foreach (MethodDef host in type.Methods)
|
||||
{
|
||||
if (!host.HasBody)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
IList<Instruction> instructions = host.Body.Instructions;
|
||||
for (int i = 0; i < instructions.Count; i++)
|
||||
{
|
||||
Instruction inst = instructions[i];
|
||||
if (!(inst.Operand is IMethod operand) || !operand.IsMethod)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
MethodDef resolved = Resolve(operand, resolveCache);
|
||||
if (resolved == null)
|
||||
{
|
||||
if (unresolvedNames.Add(operand.Name))
|
||||
{
|
||||
UnresolvedReferenceCount++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!byToken.TryGetValue(TokenKey(resolved.Module, resolved.MDToken.Raw), out MethodDef target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
switch (inst.OpCode.Code)
|
||||
{
|
||||
case Code.Call:
|
||||
case Code.Callvirt:
|
||||
{
|
||||
Instruction prev = i > 0 ? instructions[i - 1] : null;
|
||||
if (prev != null && (prev.OpCode.Code == Code.Constrained || prev.OpCode.Code == Code.Tailcall))
|
||||
{
|
||||
vetoed.Add(target);
|
||||
break;
|
||||
}
|
||||
TypeSig[] argTypes = TryGetCallSiteArgTypes(operand);
|
||||
if (argTypes == null || argTypes.Length != target.MethodSig.Params.Count)
|
||||
{
|
||||
vetoed.Add(target);
|
||||
break;
|
||||
}
|
||||
sites.Add(new CallSite
|
||||
{
|
||||
host = host,
|
||||
inst = inst,
|
||||
plan = new PadPlan { method = target },
|
||||
argTypes = argTypes,
|
||||
});
|
||||
break;
|
||||
}
|
||||
// the signature is pinned by a delegate type or handed to reflection.
|
||||
case Code.Ldftn:
|
||||
case Code.Ldvirtftn:
|
||||
case Code.Ldtoken:
|
||||
case Code.Newobj:
|
||||
case Code.Jmp:
|
||||
default:
|
||||
{
|
||||
vetoed.Add(target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
CollectMemberRef(operand, target, refsByMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectMemberRef(IMethod operand, MethodDef target, Dictionary<MethodDef, HashSet<MemberRef>> refsByMethod)
|
||||
{
|
||||
MemberRef memberRef = operand as MemberRef ?? (operand as MethodSpec)?.Method as MemberRef;
|
||||
if (memberRef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!refsByMethod.TryGetValue(target, out HashSet<MemberRef> set))
|
||||
{
|
||||
set = new HashSet<MemberRef>();
|
||||
refsByMethod.Add(target, set);
|
||||
}
|
||||
set.Add(memberRef);
|
||||
}
|
||||
|
||||
private static MethodDef Resolve(IMethod method, Dictionary<IMethod, MethodDef> cache)
|
||||
{
|
||||
if (method is MethodDef def)
|
||||
{
|
||||
return def;
|
||||
}
|
||||
if (cache.TryGetValue(method, out MethodDef cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
MethodDef resolved = null;
|
||||
try
|
||||
{
|
||||
resolved = method.ResolveMethodDef();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
resolved = null;
|
||||
}
|
||||
cache.Add(method, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static TypeSig[] TryGetCallSiteArgTypes(IMethod operand)
|
||||
{
|
||||
try
|
||||
{
|
||||
MethodSig sig = MetaUtil.GetInflatedMethodSig(operand, null);
|
||||
if (sig == null || sig.IsVarArg || sig.ParamsAfterSentinel != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (sig.Params.Any(p => p == null || p.ElementType == ElementType.TypedByRef))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return sig.Params.ToArray();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PadPlan BuildPlan(MethodDef method)
|
||||
{
|
||||
int realCount = method.MethodSig.Params.Count;
|
||||
int junkCount = _random.Next(_minCount, _maxCount + 1);
|
||||
int slotCount = realCount + junkCount;
|
||||
|
||||
// choose which slots hold junk
|
||||
var junkSlots = new HashSet<int>();
|
||||
while (junkSlots.Count < junkCount)
|
||||
{
|
||||
junkSlots.Add(_random.Next(slotCount));
|
||||
}
|
||||
|
||||
// and shuffle the real parameters across the slots left over. Free: the call site
|
||||
// already spills every real argument to a local and re-pushes it, so an arbitrary
|
||||
// permutation costs exactly the same instructions as the identity one. Arguments are
|
||||
// still EVALUATED in source order - only the push order changes - so side effects in
|
||||
// argument expressions keep their sequence.
|
||||
var realOrder = new int[realCount];
|
||||
for (int i = 0; i < realCount; i++)
|
||||
{
|
||||
realOrder[i] = i;
|
||||
}
|
||||
for (int i = realCount - 1; i > 0; i--)
|
||||
{
|
||||
int j = _random.Next(i + 1);
|
||||
int tmp = realOrder[i];
|
||||
realOrder[i] = realOrder[j];
|
||||
realOrder[j] = tmp;
|
||||
}
|
||||
|
||||
var plan = new PadPlan
|
||||
{
|
||||
method = method,
|
||||
slotToReal = new int[slotCount],
|
||||
slotKind = new JunkKind[slotCount],
|
||||
slotValue = new object[slotCount],
|
||||
realToSlot = new int[realCount],
|
||||
};
|
||||
int nextReal = 0;
|
||||
for (int slot = 0; slot < slotCount; slot++)
|
||||
{
|
||||
if (junkSlots.Contains(slot))
|
||||
{
|
||||
plan.slotToReal[slot] = -1;
|
||||
JunkKind kind = s_junkKinds[_random.Next(s_junkKinds.Length)];
|
||||
plan.slotKind[slot] = kind;
|
||||
plan.slotValue[slot] = MakeJunkValue(kind);
|
||||
}
|
||||
else
|
||||
{
|
||||
int real = realOrder[nextReal++];
|
||||
plan.slotToReal[slot] = real;
|
||||
plan.realToSlot[real] = slot;
|
||||
}
|
||||
}
|
||||
plan.foldOp = (FoldOp)_random.Next(6);
|
||||
plan.zeroOp = (ZeroOp)_random.Next(4);
|
||||
plan.sink = (SinkKind)_random.Next(3);
|
||||
|
||||
// fold the junk in a shuffled order too, so even the ldarg sequence differs
|
||||
var junkOrder = new List<int>();
|
||||
for (int slot = 0; slot < slotCount; slot++)
|
||||
{
|
||||
if (plan.slotToReal[slot] < 0)
|
||||
{
|
||||
junkOrder.Add(slot);
|
||||
}
|
||||
}
|
||||
for (int i = junkOrder.Count - 1; i > 0; i--)
|
||||
{
|
||||
int j = _random.Next(i + 1);
|
||||
int tmp = junkOrder[i];
|
||||
junkOrder[i] = junkOrder[j];
|
||||
junkOrder[j] = tmp;
|
||||
}
|
||||
plan.junkFoldOrder = junkOrder.ToArray();
|
||||
return plan;
|
||||
}
|
||||
|
||||
private object MakeJunkValue(JunkKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case JunkKind.Int32: return _random.Next(int.MinValue, int.MaxValue);
|
||||
case JunkKind.UInt32: return _random.Next(int.MinValue, int.MaxValue);
|
||||
case JunkKind.Int64: return ((long)_random.Next() << 32) | (uint)_random.Next();
|
||||
case JunkKind.Single: return (float)(_random.NextDouble() * 1000.0);
|
||||
case JunkKind.Double: return _random.NextDouble() * 1000.0;
|
||||
case JunkKind.Boolean: return _random.Next(2);
|
||||
case JunkKind.Byte: return _random.Next(256);
|
||||
case JunkKind.Int16: return _random.Next(short.MinValue, short.MaxValue + 1);
|
||||
case JunkKind.Char: return _random.Next(char.MaxValue + 1);
|
||||
default: throw new NotSupportedException(kind.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static TypeSig JunkTypeSig(ICorLibTypes corLibTypes, JunkKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case JunkKind.Int32: return corLibTypes.Int32;
|
||||
case JunkKind.UInt32: return corLibTypes.UInt32;
|
||||
case JunkKind.Int64: return corLibTypes.Int64;
|
||||
case JunkKind.Single: return corLibTypes.Single;
|
||||
case JunkKind.Double: return corLibTypes.Double;
|
||||
case JunkKind.Boolean: return corLibTypes.Boolean;
|
||||
case JunkKind.Byte: return corLibTypes.Byte;
|
||||
case JunkKind.Int16: return corLibTypes.Int16;
|
||||
case JunkKind.Char: return corLibTypes.Char;
|
||||
default: throw new NotSupportedException(kind.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static Instruction PushJunk(PadPlan plan, int slot)
|
||||
{
|
||||
object value = plan.slotValue[slot];
|
||||
switch (plan.slotKind[slot])
|
||||
{
|
||||
case JunkKind.Int64: return Instruction.Create(OpCodes.Ldc_I8, (long)value);
|
||||
case JunkKind.Single: return Instruction.Create(OpCodes.Ldc_R4, (float)value);
|
||||
case JunkKind.Double: return Instruction.Create(OpCodes.Ldc_R8, (double)value);
|
||||
default: return Instruction.Create(OpCodes.Ldc_I4, (int)value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyToDefinition(PadPlan plan)
|
||||
{
|
||||
MethodDef method = plan.method;
|
||||
CilBody body = method.Body;
|
||||
|
||||
// ldarg.0 and friends carry no operand, so the remap below cannot see them until
|
||||
// they are expanded. CleanUpInstructionPass re-compacts afterwards.
|
||||
body.SimplifyMacros(method.Parameters);
|
||||
body.SimplifyBranches();
|
||||
|
||||
int thisOffset = method.HasThis ? 1 : 0;
|
||||
var oldOperandIndex = new List<KeyValuePair<Instruction, int>>();
|
||||
foreach (Instruction inst in body.Instructions)
|
||||
{
|
||||
if (inst.Operand is Parameter param)
|
||||
{
|
||||
oldOperandIndex.Add(new KeyValuePair<Instruction, int>(inst, param.Index));
|
||||
}
|
||||
}
|
||||
|
||||
ICorLibTypes corLibTypes = method.Module.CorLibTypes;
|
||||
var oldParams = method.MethodSig.Params.ToList();
|
||||
method.MethodSig.Params.Clear();
|
||||
for (int slot = 0; slot < plan.SlotCount; slot++)
|
||||
{
|
||||
int real = plan.slotToReal[slot];
|
||||
method.MethodSig.Params.Add(real >= 0 ? oldParams[real] : JunkTypeSig(corLibTypes, plan.slotKind[slot]));
|
||||
}
|
||||
method.Parameters.UpdateParameterTypes();
|
||||
|
||||
// ParamDef.Sequence is 1 based over the explicit parameters, 0 being the return value.
|
||||
foreach (ParamDef paramDef in method.ParamDefs)
|
||||
{
|
||||
int oldReal = paramDef.Sequence - 1;
|
||||
if (oldReal >= 0 && oldReal < plan.RealCount)
|
||||
{
|
||||
paramDef.Sequence = (ushort)(plan.realToSlot[oldReal] + 1);
|
||||
}
|
||||
else if (paramDef.Sequence != 0)
|
||||
{
|
||||
// Sequence 0 is the return value and stays. Anything else out of range is
|
||||
// malformed metadata that would collide with a renumbered entry.
|
||||
throw new Exception($"parameter padding found ParamDef sequence {paramDef.Sequence} on `{method}`, "
|
||||
+ $"which has {plan.RealCount} parameters.");
|
||||
}
|
||||
}
|
||||
var sortedParamDefs = method.ParamDefs.OrderBy(p => p.Sequence).ToList();
|
||||
method.ParamDefs.Clear();
|
||||
foreach (ParamDef paramDef in sortedParamDefs)
|
||||
{
|
||||
method.ParamDefs.Add(paramDef);
|
||||
}
|
||||
method.Parameters.UpdateParameterTypes();
|
||||
|
||||
// dnlib parameters are addressed by index, so an untouched operand now means a
|
||||
// different parameter. Every one of them has to be re-pointed.
|
||||
foreach (var e in oldOperandIndex)
|
||||
{
|
||||
int oldIndex = e.Value;
|
||||
int newIndex;
|
||||
if (thisOffset == 1 && oldIndex == 0)
|
||||
{
|
||||
newIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
int oldReal = oldIndex - thisOffset;
|
||||
newIndex = plan.realToSlot[oldReal] + thisOffset;
|
||||
}
|
||||
e.Key.Operand = method.Parameters[newIndex];
|
||||
}
|
||||
|
||||
EmitConsumePrologue(plan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the junk parameters load-bearing without making them cost anything.
|
||||
///
|
||||
/// Every step is drawn per method — which operator folds the junk, in which order, how
|
||||
/// the result is driven to zero, and where the zero is consumed — so there is no single
|
||||
/// instruction sequence to grep for. That matters more than the individual tricks: a
|
||||
/// fixed prologue is a fingerprint of the obfuscator, and one script keyed on it strips
|
||||
/// every junk parameter in the assembly.
|
||||
///
|
||||
/// Whatever the recipe, the result is provably zero and is consumed by a branch or folded
|
||||
/// into a value the method already returns, so liveness alone cannot delete the parameter
|
||||
/// loads, while clang folds the arithmetic away during IL2CPP compilation. Only holds
|
||||
/// while this pass runs after ConstEncrypt, which would otherwise turn the literal
|
||||
/// constants into VM decrypt calls.
|
||||
/// </summary>
|
||||
private static void EmitConsumePrologue(PadPlan plan)
|
||||
{
|
||||
MethodDef method = plan.method;
|
||||
CilBody body = method.Body;
|
||||
int thisOffset = method.HasThis ? 1 : 0;
|
||||
|
||||
if (plan.junkFoldOrder.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var prologue = new List<Instruction>();
|
||||
bool first = true;
|
||||
foreach (int slot in plan.junkFoldOrder)
|
||||
{
|
||||
Parameter param = method.Parameters[slot + thisOffset];
|
||||
prologue.Add(Instruction.Create(OpCodes.Ldarg, param));
|
||||
switch (plan.slotKind[slot])
|
||||
{
|
||||
case JunkKind.Int64:
|
||||
prologue.Add(Instruction.Create(OpCodes.Conv_I4));
|
||||
break;
|
||||
case JunkKind.Single:
|
||||
prologue.Add(Instruction.Create(OpCodes.Ldc_R4, 0f));
|
||||
prologue.Add(Instruction.Create(OpCodes.Ceq));
|
||||
break;
|
||||
case JunkKind.Double:
|
||||
prologue.Add(Instruction.Create(OpCodes.Ldc_R8, 0d));
|
||||
prologue.Add(Instruction.Create(OpCodes.Ceq));
|
||||
break;
|
||||
}
|
||||
if (!first)
|
||||
{
|
||||
prologue.Add(Instruction.Create(FoldOpCode(plan.foldOp)));
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
|
||||
// drive the fold to zero
|
||||
switch (plan.zeroOp)
|
||||
{
|
||||
case ZeroOp.MulZero:
|
||||
prologue.Add(Instruction.Create(OpCodes.Ldc_I4_0));
|
||||
prologue.Add(Instruction.Create(OpCodes.Mul));
|
||||
break;
|
||||
case ZeroOp.AndZero:
|
||||
prologue.Add(Instruction.Create(OpCodes.Ldc_I4_0));
|
||||
prologue.Add(Instruction.Create(OpCodes.And));
|
||||
break;
|
||||
case ZeroOp.DupXor:
|
||||
prologue.Add(Instruction.Create(OpCodes.Dup));
|
||||
prologue.Add(Instruction.Create(OpCodes.Xor));
|
||||
break;
|
||||
case ZeroOp.DupSub:
|
||||
prologue.Add(Instruction.Create(OpCodes.Dup));
|
||||
prologue.Add(Instruction.Create(OpCodes.Sub));
|
||||
break;
|
||||
}
|
||||
|
||||
SinkKind sink = plan.sink;
|
||||
if (sink == SinkKind.ThreadIntoReturn && !TryThreadIntoReturn(plan, prologue))
|
||||
{
|
||||
sink = SinkKind.BranchPair;
|
||||
}
|
||||
if (sink != SinkKind.ThreadIntoReturn)
|
||||
{
|
||||
// The branch target has to be an instruction of our own, never the original first
|
||||
// instruction: in a Release build that is frequently the start of a try block, and
|
||||
// branching into a protected region is invalid IL.
|
||||
Instruction resume = Instruction.Create(OpCodes.Nop);
|
||||
if (sink == SinkKind.SwitchOne)
|
||||
{
|
||||
// Instruction[] specifically, not List<Instruction>: that is what dnlib
|
||||
// produces when reading a body, and what Obfuz's own BasicBlockCollection
|
||||
// type-checks for when a later pass walks this method.
|
||||
prologue.Add(new Instruction(OpCodes.Switch, new Instruction[] { resume }));
|
||||
}
|
||||
else
|
||||
{
|
||||
prologue.Add(Instruction.Create(OpCodes.Brfalse, resume));
|
||||
}
|
||||
prologue.Add(Instruction.Create(OpCodes.Br, resume));
|
||||
prologue.Add(resume);
|
||||
}
|
||||
|
||||
for (int i = prologue.Count - 1; i >= 0; i--)
|
||||
{
|
||||
body.Instructions.Insert(0, prologue[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static OpCode FoldOpCode(FoldOp op)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case FoldOp.Add: return OpCodes.Add;
|
||||
case FoldOp.Sub: return OpCodes.Sub;
|
||||
case FoldOp.Mul: return OpCodes.Mul;
|
||||
case FoldOp.Or: return OpCodes.Or;
|
||||
case FoldOp.And: return OpCodes.And;
|
||||
default: return OpCodes.Xor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stashes the zero and adds it into every returned value, so the junk parameters feed a
|
||||
/// value the method genuinely produces instead of a branch that exists only for them.
|
||||
/// Returns false when the return type cannot absorb an integer zero, leaving the caller to
|
||||
/// fall back to a branch sink.
|
||||
/// </summary>
|
||||
private static bool TryThreadIntoReturn(PadPlan plan, List<Instruction> prologue)
|
||||
{
|
||||
MethodDef method = plan.method;
|
||||
CilBody body = method.Body;
|
||||
TypeSig retType = method.MethodSig.RetType;
|
||||
if (retType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OpCode widen;
|
||||
switch (retType.ElementType)
|
||||
{
|
||||
case ElementType.I1:
|
||||
case ElementType.U1:
|
||||
case ElementType.I2:
|
||||
case ElementType.U2:
|
||||
case ElementType.I4:
|
||||
case ElementType.U4:
|
||||
case ElementType.Char:
|
||||
case ElementType.Boolean:
|
||||
widen = OpCodes.Nop;
|
||||
break;
|
||||
case ElementType.I8:
|
||||
case ElementType.U8:
|
||||
widen = OpCodes.Conv_I8;
|
||||
break;
|
||||
case ElementType.R4:
|
||||
widen = OpCodes.Conv_R4;
|
||||
break;
|
||||
case ElementType.R8:
|
||||
widen = OpCodes.Conv_R8;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
var returns = body.Instructions.Where(i => i.OpCode.Code == Code.Ret).ToList();
|
||||
if (returns.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sink = new Local(method.Module.CorLibTypes.Int32);
|
||||
body.Variables.Add(sink);
|
||||
prologue.Add(Instruction.Create(OpCodes.Stloc, sink));
|
||||
|
||||
foreach (Instruction ret in returns)
|
||||
{
|
||||
// mutate the ret in place so anything branching to it still runs the fold, then
|
||||
// re-emit the ret after it. Stack stays balanced on both paths.
|
||||
var tail = new List<Instruction> { Instruction.Create(OpCodes.Ldloc, sink) };
|
||||
if (widen != OpCodes.Nop)
|
||||
{
|
||||
tail.Add(Instruction.Create(widen));
|
||||
}
|
||||
tail.Add(Instruction.Create(OpCodes.Add));
|
||||
tail.Add(Instruction.Create(OpCodes.Ret));
|
||||
|
||||
int at = body.Instructions.IndexOf(ret);
|
||||
ret.OpCode = tail[0].OpCode;
|
||||
ret.Operand = tail[0].Operand;
|
||||
for (int k = tail.Count - 1; k >= 1; k--)
|
||||
{
|
||||
body.Instructions.Insert(at + 1, tail[k]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ApplyToReference(MemberRef memberRef, PadPlan plan)
|
||||
{
|
||||
MethodSig sig = memberRef.MethodSig;
|
||||
if (sig == null || sig.Params.Count != plan.RealCount)
|
||||
{
|
||||
// Skipping here would leave the definition padded and this reference stale, so the
|
||||
// call site would push the wrong number of arguments. Fail the build instead.
|
||||
throw new Exception($"parameter padding cannot retarget reference `{memberRef}` of `{plan.method}`: "
|
||||
+ $"expected {plan.RealCount} parameters, found {(sig == null ? "no signature" : sig.Params.Count.ToString())}.");
|
||||
}
|
||||
ICorLibTypes corLibTypes = memberRef.Module.CorLibTypes;
|
||||
var oldParams = sig.Params.ToList();
|
||||
sig.Params.Clear();
|
||||
for (int slot = 0; slot < plan.SlotCount; slot++)
|
||||
{
|
||||
int real = plan.slotToReal[slot];
|
||||
sig.Params.Add(real >= 0 ? oldParams[real] : JunkTypeSig(corLibTypes, plan.slotKind[slot]));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RewriteCallSites(List<CallSite> sites)
|
||||
{
|
||||
foreach (var byHost in sites.GroupBy(s => s.host))
|
||||
{
|
||||
MethodDef host = byHost.Key;
|
||||
CilBody body = host.Body;
|
||||
var siteByInst = byHost.ToDictionary(s => s.inst, s => s);
|
||||
|
||||
// inserting instructions can push a short branch out of range.
|
||||
body.SimplifyBranches();
|
||||
|
||||
var localPool = new List<List<Local>>();
|
||||
var final = new List<Instruction>(body.Instructions.Count + siteByInst.Count * 8);
|
||||
foreach (Instruction inst in body.Instructions)
|
||||
{
|
||||
if (!siteByInst.TryGetValue(inst, out CallSite site))
|
||||
{
|
||||
final.Add(inst);
|
||||
continue;
|
||||
}
|
||||
List<Instruction> output = BuildCallSite(body, site, localPool);
|
||||
|
||||
// the call may be a branch target, so it keeps its identity and becomes the
|
||||
// first emitted instruction. Same trick as InstructionObfuscationPassBase.
|
||||
inst.OpCode = output[0].OpCode;
|
||||
inst.Operand = output[0].Operand;
|
||||
final.Add(inst);
|
||||
for (int k = 1; k < output.Count; k++)
|
||||
{
|
||||
final.Add(output[k]);
|
||||
}
|
||||
}
|
||||
body.Instructions.Clear();
|
||||
foreach (Instruction inst in final)
|
||||
{
|
||||
body.Instructions.Add(inst);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Instruction> BuildCallSite(CilBody body, CallSite site, List<List<Local>> localPool)
|
||||
{
|
||||
PadPlan plan = site.plan;
|
||||
OpCode callOpCode = site.inst.OpCode;
|
||||
IMethod callOperand = (IMethod)site.inst.Operand;
|
||||
|
||||
var output = new List<Instruction>();
|
||||
var used = new List<Local>();
|
||||
var spilled = new Local[plan.RealCount];
|
||||
|
||||
// arguments are already on the stack in order, so pop them back to front.
|
||||
for (int real = plan.RealCount - 1; real >= 0; real--)
|
||||
{
|
||||
Local local = RentLocal(body, localPool, used, site.argTypes[real]);
|
||||
used.Add(local);
|
||||
spilled[real] = local;
|
||||
output.Add(Instruction.Create(OpCodes.Stloc, local));
|
||||
}
|
||||
for (int slot = 0; slot < plan.SlotCount; slot++)
|
||||
{
|
||||
int real = plan.slotToReal[slot];
|
||||
output.Add(real >= 0
|
||||
? Instruction.Create(OpCodes.Ldloc, spilled[real])
|
||||
: PushJunk(plan, slot));
|
||||
}
|
||||
output.Add(Instruction.Create(callOpCode, callOperand));
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rents a local of exactly this type that is not already spoken for at this call site.
|
||||
/// Matching is by type identity, never by TypeSig.FullName: that omits the assembly, so
|
||||
/// two same-named types from different assemblies would share one wrongly typed local.
|
||||
/// LocalVariableAllocator.AllocateLocal compares the same way.
|
||||
/// </summary>
|
||||
private static Local RentLocal(CilBody body, List<List<Local>> localPool, List<Local> used, TypeSig type)
|
||||
{
|
||||
foreach (List<Local> bucket in localPool)
|
||||
{
|
||||
if (bucket.Count == 0 || !TypeEqualityComparer.Instance.Equals(bucket[0].Type, type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (Local candidate in bucket)
|
||||
{
|
||||
if (!used.Contains(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
var extra = new Local(type);
|
||||
body.Variables.Add(extra);
|
||||
bucket.Add(extra);
|
||||
return extra;
|
||||
}
|
||||
var local = new Local(type);
|
||||
body.Variables.Add(local);
|
||||
localPool.Add(new List<Local> { local });
|
||||
return local;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7f5ad401929d488695f7edbf9690295f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -25,16 +25,18 @@ namespace Obfuz.ObfusPasses.SymbolObfus.Policies
|
|||
internal class SupportPassPolicy : ObfuscationPolicyBase
|
||||
{
|
||||
private readonly ConfigurablePassPolicy _policy;
|
||||
private readonly ObfuscationPassType _passType;
|
||||
|
||||
|
||||
private bool Support(ObfuscationPassType passType)
|
||||
{
|
||||
return passType.HasFlag(ObfuscationPassType.SymbolObfus);
|
||||
return passType.HasFlag(_passType);
|
||||
}
|
||||
|
||||
public SupportPassPolicy(ConfigurablePassPolicy policy)
|
||||
public SupportPassPolicy(ConfigurablePassPolicy policy, ObfuscationPassType passType = ObfuscationPassType.SymbolObfus)
|
||||
{
|
||||
_policy = policy;
|
||||
_passType = passType;
|
||||
}
|
||||
|
||||
public override bool NeedRename(TypeDef typeDef)
|
||||
|
|
|
|||
|
|
@ -88,13 +88,13 @@ namespace Obfuz.ObfusPasses.SymbolObfus
|
|||
BuildCustomAttributeArguments();
|
||||
}
|
||||
|
||||
public static IObfuscationPolicy CreateDefaultRenamePolicy(List<string> obfuscationRuleFiles, List<Type> customPolicyTypes)
|
||||
public static IObfuscationPolicy CreateDefaultRenamePolicy(List<string> obfuscationRuleFiles, List<Type> customPolicyTypes, ObfuscationPassType passType = ObfuscationPassType.SymbolObfus)
|
||||
{
|
||||
var ctx = ObfuscationPassContext.Current;
|
||||
var obfuscateRuleConfig = new ConfigurableRenamePolicy(ctx.coreSettings.assembliesToObfuscate, ctx.modulesToObfuscate, obfuscationRuleFiles);
|
||||
var totalRenamePolicies = new List<IObfuscationPolicy>
|
||||
{
|
||||
new SupportPassPolicy(ctx.passPolicy),
|
||||
new SupportPassPolicy(ctx.passPolicy, passType),
|
||||
new SystemRenamePolicy(ctx.obfuzIgnoreScopeComputeCache),
|
||||
new UnityRenamePolicy(),
|
||||
obfuscateRuleConfig,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ using Obfuz.ObfusPasses.ControlFlowObfus;
|
|||
using Obfuz.ObfusPasses.EvalStackObfus;
|
||||
using Obfuz.ObfusPasses.ExprObfus;
|
||||
using Obfuz.ObfusPasses.FieldEncrypt;
|
||||
using Obfuz.ObfusPasses.ParamPad;
|
||||
using Obfuz.ObfusPasses.RemoveConstField;
|
||||
using Obfuz.ObfusPasses.SymbolObfus;
|
||||
using Obfuz.ObfusPasses.Watermark;
|
||||
|
|
@ -251,6 +252,12 @@ namespace Obfuz
|
|||
{
|
||||
builder.AddPass(new WatermarkPass(settings.watermarkSettings.ToFacade()));
|
||||
}
|
||||
// Registered last on purpose: it works in Stop(), which runs in registration order, so
|
||||
// it must come after CallObfus to see the dispatch proxy bodies. See ParamPadPass.Stop.
|
||||
if (obfuscationPasses.HasFlag(ObfuscationPassType.ParamPad))
|
||||
{
|
||||
builder.AddPass(new ParamPadPass(settings.paramPadSettings.ToFacade()));
|
||||
}
|
||||
if (obfuscationPasses.HasFlag(ObfuscationPassType.SymbolObfus))
|
||||
{
|
||||
builder.AddPass(new SymbolObfusPass(settings.symbolObfusSettings.ToFacade()));
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ namespace Obfuz.Settings
|
|||
[Tooltip("encryption virtual machine settings")]
|
||||
public EncryptionVMSettings encryptionVMSettings;
|
||||
|
||||
[Tooltip("parameter padding settings")]
|
||||
public ParamPadSettings paramPadSettings;
|
||||
|
||||
[Tooltip("symbol obfuscation settings")]
|
||||
public SymbolObfuscationSettings symbolObfusSettings;
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ namespace Obfuz.Settings
|
|||
private SerializedProperty _secretSettings;
|
||||
private SerializedProperty _encryptionVMSettings;
|
||||
|
||||
private SerializedProperty _paramPadSettings;
|
||||
private SerializedProperty _symbolObfusSettings;
|
||||
private SerializedProperty _constEncryptSettings;
|
||||
private SerializedProperty _removeConstFieldSettings;
|
||||
|
|
@ -95,6 +96,7 @@ namespace Obfuz.Settings
|
|||
|
||||
_encryptionVMSettings = _serializedObject.FindProperty("encryptionVMSettings");
|
||||
|
||||
_paramPadSettings = _serializedObject.FindProperty("paramPadSettings");
|
||||
_symbolObfusSettings = _serializedObject.FindProperty("symbolObfusSettings");
|
||||
_constEncryptSettings = _serializedObject.FindProperty("constEncryptSettings");
|
||||
_removeConstFieldSettings = _serializedObject.FindProperty("removeConstFieldSettings");
|
||||
|
|
@ -128,6 +130,7 @@ namespace Obfuz.Settings
|
|||
|
||||
EditorGUILayout.PropertyField(_encryptionVMSettings);
|
||||
|
||||
EditorGUILayout.PropertyField(_paramPadSettings);
|
||||
EditorGUILayout.PropertyField(_symbolObfusSettings);
|
||||
EditorGUILayout.PropertyField(_constEncryptSettings);
|
||||
EditorGUILayout.PropertyField(_removeConstFieldSettings);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2025 Code Philosophy
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
using Obfuz.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Obfuz.Settings
|
||||
{
|
||||
public class ParamPadSettingsFacade
|
||||
{
|
||||
public int randomSeed;
|
||||
public int minCount;
|
||||
public int maxCount;
|
||||
public List<string> ruleFiles;
|
||||
public List<Type> customRenamePolicyTypes;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ParamPadSettings
|
||||
{
|
||||
[Tooltip("random seed for the junk parameters. 0 draws a fresh seed on every build, so every build has a different signature table")]
|
||||
public int randomSeed = 0;
|
||||
|
||||
[Tooltip("minimum number of junk parameters added to an eligible method")]
|
||||
[Range(1, 20)]
|
||||
public int minCount = 5;
|
||||
|
||||
[Tooltip("maximum number of junk parameters added to an eligible method. Every call to a padded method writes this many extra argument slots, so lower it if a build shows a measurable cost")]
|
||||
[Range(1, 20)]
|
||||
public int maxCount = 10;
|
||||
|
||||
[Tooltip("a method is only padded if it is also safe to rename, so these are the symbol obfuscation rule files")]
|
||||
public string[] ruleFiles;
|
||||
|
||||
[Tooltip("custom rename policy types, same contract as SymbolObfuscationSettings.customRenamePolicyTypes")]
|
||||
public string[] customRenamePolicyTypes;
|
||||
|
||||
public ParamPadSettingsFacade ToFacade()
|
||||
{
|
||||
return new ParamPadSettingsFacade
|
||||
{
|
||||
randomSeed = randomSeed,
|
||||
// an asset serialized before this section existed deserializes as zeros, which
|
||||
// the transform rejects; clamp rather than fail the build
|
||||
minCount = Math.Max(1, minCount),
|
||||
maxCount = Math.Max(Math.Max(1, minCount), maxCount),
|
||||
ruleFiles = ruleFiles?.ToList() ?? new List<string>(),
|
||||
customRenamePolicyTypes = customRenamePolicyTypes?.Select(typeName => ReflectionUtil.FindUniqueTypeInCurrentAppDomain(typeName)).ToList() ?? new List<Type>(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 82f587ee0f604d9b8cf68acbaa1f631a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
bin/
|
||||
obj/
|
||||
out/
|
||||
out[A-Z]/
|
||||
pristine/
|
||||
UnityEngine.CoreModule.dll
|
||||
|
|
@ -0,0 +1,483 @@
|
|||
// Copyright 2025 Code Philosophy
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
using dnlib.DotNet;
|
||||
using dnlib.DotNet.Emit;
|
||||
using Obfuz.ObfusPasses.ParamPad;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
static class Program
|
||||
{
|
||||
static int failures;
|
||||
|
||||
static void Check(bool ok, string what)
|
||||
{
|
||||
Console.WriteLine((ok ? "PASS " : "FAIL ") + what);
|
||||
if (!ok) failures++;
|
||||
}
|
||||
|
||||
class DirLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly string _dir;
|
||||
public DirLoadContext(string dir, string name) : base(name, isCollectible: false) { _dir = dir; }
|
||||
protected override Assembly Load(AssemblyName name)
|
||||
{
|
||||
string candidate = Path.Combine(_dir, name.Name + ".dll");
|
||||
return File.Exists(candidate) ? LoadFromAssemblyPath(candidate) : null;
|
||||
}
|
||||
}
|
||||
|
||||
static ModuleContext MakeContext(string dir)
|
||||
{
|
||||
var ctx = ModuleDef.CreateModuleContext();
|
||||
var res = (AssemblyResolver)ctx.AssemblyResolver;
|
||||
res.EnableTypeDefCache = true;
|
||||
res.DefaultModuleContext = ctx;
|
||||
res.PreSearchPaths.Add(Path.GetFullPath(dir));
|
||||
res.PreSearchPaths.Add(Path.GetDirectoryName(typeof(object).Assembly.Location));
|
||||
res.PostSearchPaths.Add(Path.GetDirectoryName(typeof(object).Assembly.Location));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// Mirrors the shape of the real predicate: the pass composes whitelist + pass policy +
|
||||
// rename policy, none of which are structural. Here we only pin by name, so what the
|
||||
// fixture actually exercises is ParameterPadding.IsCandidate.
|
||||
static readonly HashSet<string> PolicyPinned = new HashSet<string> { "Reflected" };
|
||||
|
||||
static bool IsSafe(MethodDef m) => !PolicyPinned.Contains(m.Name);
|
||||
|
||||
// every method the transform must refuse to touch, and why
|
||||
static readonly Dictionary<string, string> MustNotPad = new Dictionary<string, string>
|
||||
{
|
||||
{ "Fx.Square::Area", "implicit interface impl (virtual newslot)" },
|
||||
{ "Fx.Square::Describe", "implicit interface impl (virtual newslot)" },
|
||||
{ "Fx.Square::Sides", "abstract override" },
|
||||
{ "Fx.Square::Weight", "virtual override" },
|
||||
{ "Fx.Square::get_Side", "property accessor" },
|
||||
{ "Fx.Square::set_Side", "property accessor" },
|
||||
{ "Fx.Square::add_Resized", "event accessor" },
|
||||
{ "Fx.Square::remove_Resized", "event accessor" },
|
||||
{ "Fx.Square::.ctor", "constructor" },
|
||||
{ "Fx.Square::.cctor", "static constructor" },
|
||||
{ "Fx.ShapeBase::Sides", "abstract" },
|
||||
{ "Fx.ShapeBase::Weight", "virtual" },
|
||||
{ "Fx.ShapeBase::.ctor", "constructor" },
|
||||
{ "Fx.Explicit::Fx.IShape.Area", "explicit interface impl" },
|
||||
{ "Fx.Explicit::Fx.IShape.Describe", "explicit interface impl" },
|
||||
{ "Fx.IShape::Area", "interface declaration" },
|
||||
{ "Fx.IShape::Describe", "interface declaration" },
|
||||
{ "Fx.Ops::Add", "ldftn delegate target" },
|
||||
{ "Fx.Ops::Native", "DllImport" },
|
||||
{ "Fx.Ops::Reflected", "pinned by the safety predicate" },
|
||||
{ "Fx.Payload::AfterLoad", "[OnDeserialized]: renameable but arity-pinned" },
|
||||
{ "Fx.Payload::BeforeSave", "[OnSerializing]: renameable but arity-pinned" },
|
||||
{ "Fx.Combine::Invoke", "delegate member" },
|
||||
{ "Fx.Combine::.ctor", "delegate member" },
|
||||
};
|
||||
|
||||
// every method that must actually gain junk parameters
|
||||
static readonly string[] MustPad =
|
||||
{
|
||||
"Fx.Square::Scaled", "Fx.Square::Bare", "Fx.Ops::Mul", "Fx.Ops::Neg", "Fx.Ops::Zero",
|
||||
"Fx.Ops::Clamp", "Fx.Ops::Fact", "Fx.Ops::Split", "Fx.Ops::Pair", "Fx.Ops::Mixed",
|
||||
"Fx.Ops::Boom", "Fx.Ops::Sum", "Fx.Ops::Add3", "Fx.Ops::UseDelegate",
|
||||
"Fx.Ops::UseReflection", "Fx.Ops::Guarded", "Fx.Ops::Wrapped", "Fx.Ops::SameName", "Fx.Ops::TakeA", "Fx.Ops::TakeB", "Fx.Ops::Order", "Fx.Ops::Tick", "Fx.Counter::Step", "Fx.Counter::Value", "Fx.Entry::RunAll",
|
||||
};
|
||||
|
||||
static string Key(MethodDef m) => m.DeclaringType.FullName + "::" + m.Name;
|
||||
|
||||
static Dictionary<string, MethodSig> SnapshotSigs(ModuleDefMD mod)
|
||||
{
|
||||
var d = new Dictionary<string, MethodSig>();
|
||||
foreach (TypeDef t in mod.GetTypes())
|
||||
foreach (MethodDef m in t.Methods)
|
||||
d[Key(m)] = m.MethodSig;
|
||||
return d;
|
||||
}
|
||||
|
||||
const int MinCount = 5;
|
||||
const int MaxCount = 10;
|
||||
|
||||
static int Pad(string srcDir, string dstDir, int seed)
|
||||
{
|
||||
Directory.CreateDirectory(dstDir);
|
||||
var ctx = MakeContext(srcDir);
|
||||
ModuleDefMD fixtureMod = ModuleDefMD.Load(Path.Combine(srcDir, "fixture.dll"), ctx);
|
||||
ModuleDefMD callerMod = ModuleDefMD.Load(Path.Combine(srcDir, "caller.dll"), ctx);
|
||||
// Obfuz's AssemblyCache does this for the real pipeline; without it the resolver would
|
||||
// load a second copy of fixture.dll when caller.dll references it.
|
||||
var resolver = (AssemblyResolver)ctx.AssemblyResolver;
|
||||
resolver.AddToCache(fixtureMod.Assembly);
|
||||
resolver.AddToCache(callerMod.Assembly);
|
||||
|
||||
var padding = new ParameterPadding(seed, MinCount, MaxCount, IsSafe);
|
||||
padding.Process(new List<ModuleDef> { fixtureMod }, new List<ModuleDef> { fixtureMod, callerMod });
|
||||
|
||||
Verify(fixtureMod, srcDir);
|
||||
|
||||
fixtureMod.Write(Path.Combine(dstDir, "fixture.dll"));
|
||||
callerMod.Write(Path.Combine(dstDir, "caller.dll"));
|
||||
foreach (string extra in Directory.GetFiles(srcDir, "*.dll"))
|
||||
{
|
||||
string name = Path.GetFileName(extra);
|
||||
if (name != "fixture.dll" && name != "caller.dll")
|
||||
File.Copy(extra, Path.Combine(dstDir, name), true);
|
||||
}
|
||||
foreach (string cfg in Directory.GetFiles(srcDir, "*.json"))
|
||||
File.Copy(cfg, Path.Combine(dstDir, Path.GetFileName(cfg)), true);
|
||||
return padding.PaddedMethodCount;
|
||||
}
|
||||
|
||||
static void Verify(ModuleDefMD padded, string srcDir)
|
||||
{
|
||||
ModuleDefMD original = ModuleDefMD.Load(Path.Combine(srcDir, "fixture.dll"), MakeContext(srcDir));
|
||||
Dictionary<string, MethodSig> before = SnapshotSigs(original);
|
||||
|
||||
foreach (var e in MustNotPad)
|
||||
{
|
||||
MethodDef m = FindMethod(padded, e.Key);
|
||||
if (m == null) { Check(false, $"{e.Key} not found in fixture"); continue; }
|
||||
if (!before.TryGetValue(e.Key, out MethodSig oldSig)) { Check(false, $"{e.Key} missing baseline"); continue; }
|
||||
Check(m.MethodSig.Params.Count == oldSig.Params.Count,
|
||||
$"untouched: {e.Key} ({e.Value}) keeps {oldSig.Params.Count} params");
|
||||
}
|
||||
|
||||
foreach (string key in MustPad)
|
||||
{
|
||||
MethodDef m = FindMethod(padded, key);
|
||||
if (m == null) { Check(false, $"{key} not found in fixture"); continue; }
|
||||
int oldCount = before[key].Params.Count;
|
||||
int added = m.MethodSig.Params.Count - oldCount;
|
||||
Check(added >= MinCount && added <= MaxCount,
|
||||
$"padded: {key} gained {added} params (was {oldCount})");
|
||||
}
|
||||
|
||||
// every real parameter survives exactly once; its POSITION is free to move, so this is a
|
||||
// multiset check rather than a subsequence one.
|
||||
int reorderedMethods = 0;
|
||||
foreach (string key in MustPad)
|
||||
{
|
||||
MethodDef m = FindMethod(padded, key);
|
||||
if (m == null) continue;
|
||||
var oldParams = before[key].Params.Select(p => p.FullName).ToList();
|
||||
var newParams = m.MethodSig.Params.Select(p => p.FullName).ToList();
|
||||
var remaining = new List<string>(newParams);
|
||||
bool allPresent = oldParams.All(want => remaining.Remove(want));
|
||||
Check(allPresent, $"params: {key} still carries every real parameter");
|
||||
|
||||
// did this one actually get its real parameters shuffled?
|
||||
int idx = 0; bool inOrder = true;
|
||||
foreach (string want in oldParams)
|
||||
{
|
||||
int at = newParams.IndexOf(want, idx);
|
||||
if (at < 0) { inOrder = false; break; }
|
||||
idx = at + 1;
|
||||
}
|
||||
if (!inOrder) reorderedMethods++;
|
||||
}
|
||||
Check(reorderedMethods > 0, $"order: real parameters are permuted, not just interleaved ({reorderedMethods} methods reordered)");
|
||||
|
||||
// branching into a protected region is invalid IL per ECMA-335, and ilverify does not
|
||||
// check it. Release builds put a try at instruction 0, so the consume prologue has to
|
||||
// branch to an instruction of its own rather than to the original body start.
|
||||
int branchesIntoTry = 0;
|
||||
foreach (TypeDef t in padded.GetTypes())
|
||||
{
|
||||
foreach (MethodDef m in t.Methods)
|
||||
{
|
||||
if (!m.HasBody || m.Body.ExceptionHandlers.Count == 0) continue;
|
||||
var idx = new Dictionary<Instruction, int>();
|
||||
for (int i = 0; i < m.Body.Instructions.Count; i++) idx[m.Body.Instructions[i]] = i;
|
||||
foreach (ExceptionHandler eh in m.Body.ExceptionHandlers)
|
||||
{
|
||||
if (eh.TryStart == null || eh.TryEnd == null) continue;
|
||||
int lo = idx[eh.TryStart], hi = idx[eh.TryEnd];
|
||||
for (int i = 0; i < m.Body.Instructions.Count; i++)
|
||||
{
|
||||
if (i >= lo && i < hi) continue;
|
||||
if (m.Body.Instructions[i].Operand is Instruction tgt
|
||||
&& idx.TryGetValue(tgt, out int ti) && ti >= lo && ti < hi)
|
||||
{
|
||||
Console.WriteLine($" branch into try: {Key(m)} #{i} -> #{ti}");
|
||||
branchesIntoTry++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Check(branchesIntoTry == 0, "protected regions: no branch jumps into a try block");
|
||||
|
||||
// Downstream Obfuz passes walk these bodies with their own analyzers, which type-check
|
||||
// operands against what dnlib produces on read. A List<Instruction> switch operand is
|
||||
// valid IL and survives ilverify, but throws in BasicBlockCollection.BuildInOutGraph.
|
||||
int badOperands = 0;
|
||||
foreach (TypeDef t in padded.GetTypes())
|
||||
foreach (MethodDef m in t.Methods)
|
||||
if (m.HasBody)
|
||||
foreach (Instruction inst in m.Body.Instructions)
|
||||
if (inst.OpCode.Code == Code.Switch && !(inst.Operand is Instruction[])) badOperands++;
|
||||
Check(badOperands == 0, $"operands: every switch carries Instruction[], as dnlib and Obfuz's analyzers expect ({badOperands} bad)");
|
||||
|
||||
// the transform must leave every body with a coherent argument count at each call site
|
||||
foreach (TypeDef t in padded.GetTypes())
|
||||
{
|
||||
foreach (MethodDef m in t.Methods)
|
||||
{
|
||||
if (!m.HasBody) continue;
|
||||
foreach (Instruction inst in m.Body.Instructions)
|
||||
{
|
||||
if (inst.Operand is Parameter p)
|
||||
Check(p.Index < m.Parameters.Count, $"operand: {Key(m)} parameter operand in range");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static MethodDef FindMethod(ModuleDefMD mod, string key)
|
||||
{
|
||||
foreach (TypeDef t in mod.GetTypes())
|
||||
foreach (MethodDef m in t.Methods)
|
||||
if (Key(m) == key) return m;
|
||||
return null;
|
||||
}
|
||||
|
||||
static string Invoke(string dir, string tag)
|
||||
{
|
||||
var alc = new DirLoadContext(Path.GetFullPath(dir), tag);
|
||||
Assembly caller = alc.LoadFromAssemblyPath(Path.Combine(Path.GetFullPath(dir), "caller.dll"));
|
||||
Type entry = caller.GetType("Cl.Caller");
|
||||
return (string)entry.GetMethod("RunAll").Invoke(null, null);
|
||||
}
|
||||
|
||||
// abstract stack simulation: catches an unbalanced path that a single behaviour run may
|
||||
// simply never take, and that ilverify does not always reach.
|
||||
static string StackCheck(MethodDef m)
|
||||
{
|
||||
CilBody body = m.Body;
|
||||
IList<Instruction> ins = body.Instructions;
|
||||
var depth = new int[ins.Count];
|
||||
for (int i = 0; i < ins.Count; i++) depth[i] = int.MinValue;
|
||||
var idx = new Dictionary<Instruction, int>();
|
||||
for (int i = 0; i < ins.Count; i++) idx[ins[i]] = i;
|
||||
depth[0] = 0;
|
||||
foreach (ExceptionHandler eh in body.ExceptionHandlers)
|
||||
{
|
||||
if (eh.HandlerStart != null && idx.TryGetValue(eh.HandlerStart, out int h))
|
||||
depth[h] = eh.HandlerType == ExceptionHandlerType.Finally || eh.HandlerType == ExceptionHandlerType.Fault ? 0 : 1;
|
||||
if (eh.FilterStart != null && idx.TryGetValue(eh.FilterStart, out int f)) depth[f] = 1;
|
||||
}
|
||||
var work = new Stack<int>();
|
||||
for (int i = 0; i < ins.Count; i++) if (depth[i] != int.MinValue) work.Push(i);
|
||||
while (work.Count > 0)
|
||||
{
|
||||
int i = work.Pop();
|
||||
Instruction inst = ins[i];
|
||||
int d = depth[i];
|
||||
inst.CalculateStackUsage(out int push, out int pop);
|
||||
if (pop == -1) d = 0; else d -= pop;
|
||||
if (d < 0) return $"underflow at #{i} {inst.OpCode}";
|
||||
d += push;
|
||||
foreach (int nxt in Successors(ins, idx, i, inst))
|
||||
{
|
||||
if (nxt < 0 || nxt >= ins.Count) continue;
|
||||
if (depth[nxt] == int.MinValue) { depth[nxt] = d; work.Push(nxt); }
|
||||
else if (depth[nxt] != d) return $"mismatch at #{nxt} {ins[nxt].OpCode}: {depth[nxt]} vs {d}";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static IEnumerable<int> Successors(IList<Instruction> ins, Dictionary<Instruction, int> idx, int i, Instruction inst)
|
||||
{
|
||||
FlowControl fc = inst.OpCode.FlowControl;
|
||||
if (fc != FlowControl.Branch && fc != FlowControl.Return && fc != FlowControl.Throw) yield return i + 1;
|
||||
if (inst.Operand is Instruction t && idx.TryGetValue(t, out int ti)) yield return ti;
|
||||
if (inst.Operand is IList<Instruction> ts) foreach (Instruction x in ts) if (idx.TryGetValue(x, out int xi)) yield return xi;
|
||||
}
|
||||
|
||||
static int BranchesIntoTry(ModuleDefMD mod)
|
||||
{
|
||||
int bad = 0;
|
||||
foreach (TypeDef t in mod.GetTypes())
|
||||
{
|
||||
foreach (MethodDef m in t.Methods)
|
||||
{
|
||||
if (!m.HasBody || m.Body.ExceptionHandlers.Count == 0) continue;
|
||||
var idx = new Dictionary<Instruction, int>();
|
||||
for (int i = 0; i < m.Body.Instructions.Count; i++) idx[m.Body.Instructions[i]] = i;
|
||||
foreach (ExceptionHandler eh in m.Body.ExceptionHandlers)
|
||||
{
|
||||
if (eh.TryStart == null || eh.TryEnd == null) continue;
|
||||
int lo = idx[eh.TryStart], hi = idx[eh.TryEnd];
|
||||
for (int i = 0; i < m.Body.Instructions.Count; i++)
|
||||
{
|
||||
if (i >= lo && i < hi) continue;
|
||||
if (m.Body.Instructions[i].Operand is Instruction tgt
|
||||
&& idx.TryGetValue(tgt, out int ti) && ti >= lo && ti < hi) bad++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
// The consume code must not be one greppable shape. Fuzz many seeds, prove every one is
|
||||
// structurally sound, and count how many distinct prologue shapes the recipes produce.
|
||||
static void Fuzz(string srcDir, int seeds)
|
||||
{
|
||||
int stackFailures = 0, tryFailures = 0, padded = 0;
|
||||
var shapes = new Dictionary<string, int>();
|
||||
int paddedBodies = 0;
|
||||
for (int seed = 1; seed <= seeds; seed++)
|
||||
{
|
||||
var ctx = MakeContext(srcDir);
|
||||
ModuleDefMD fx = ModuleDefMD.Load(Path.Combine(srcDir, "fixture.dll"), ctx);
|
||||
ModuleDefMD cl = ModuleDefMD.Load(Path.Combine(srcDir, "caller.dll"), ctx);
|
||||
var resolver = (AssemblyResolver)ctx.AssemblyResolver;
|
||||
resolver.AddToCache(fx.Assembly);
|
||||
resolver.AddToCache(cl.Assembly);
|
||||
|
||||
var pad = new ParameterPadding(seed, MinCount, MaxCount, IsSafe);
|
||||
pad.Process(new List<ModuleDef> { fx }, new List<ModuleDef> { fx, cl });
|
||||
padded += pad.PaddedMethodCount;
|
||||
|
||||
paddedBodies += pad.PaddedMethodCount;
|
||||
foreach (ModuleDefMD mod in new[] { fx, cl })
|
||||
{
|
||||
tryFailures += BranchesIntoTry(mod);
|
||||
foreach (TypeDef t in mod.GetTypes())
|
||||
{
|
||||
foreach (MethodDef m in t.Methods)
|
||||
{
|
||||
if (!m.HasBody || m.Body.Instructions.Count == 0) continue;
|
||||
if (StackCheck(m) != null) stackFailures++;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (TypeDef t in fx.GetTypes())
|
||||
foreach (MethodDef m in t.Methods)
|
||||
if (m.HasBody && m.Body.Instructions.Count > 6)
|
||||
foreach (string w in Windows(m, 5)) { shapes.TryGetValue(w, out int c); shapes[w] = c + 1; }
|
||||
}
|
||||
Check(stackFailures == 0, $"fuzz: {seeds} seeds, {padded} padded methods, no unbalanced stack ({stackFailures} failures)");
|
||||
Check(tryFailures == 0, $"fuzz: no branch into a protected region across {seeds} seeds ({tryFailures} failures)");
|
||||
// The old fixed prologue ended `ldc.i4.0 mul brfalse br nop` in EVERY padded method, so one
|
||||
// 5-instruction grep found all of them. Measure that directly: how much of the padded
|
||||
// population does the single most common 5-instruction window cover?
|
||||
int worst = 0; string worstShape = "";
|
||||
foreach (var e in shapes) if (e.Value > worst) { worst = e.Value; worstShape = e.Key; }
|
||||
double share = paddedBodies == 0 ? 0 : 100.0 * worst / paddedBodies;
|
||||
Check(share < 25.0,
|
||||
$"fuzz: no single 5-instruction window identifies padded methods (most common covers {share:F1}%: {worstShape})");
|
||||
}
|
||||
|
||||
// distinct opcode windows of the given length, deduplicated within one method so a long body
|
||||
// does not inflate the count
|
||||
static IEnumerable<string> Windows(MethodDef m, int len)
|
||||
{
|
||||
IList<Instruction> ins = m.Body.Instructions;
|
||||
var seen = new HashSet<string>();
|
||||
for (int i = 0; i + len <= ins.Count && i < 40; i++)
|
||||
{
|
||||
var w = string.Join(" ", Enumerable.Range(i, len).Select(k => ins[k].OpCode.Name));
|
||||
if (seen.Add(w)) yield return w;
|
||||
}
|
||||
}
|
||||
|
||||
static int Main()
|
||||
{
|
||||
string bin = Path.GetFullPath("caller/bin/Release/net7.0");
|
||||
if (!File.Exists(Path.Combine(bin, "caller.dll")))
|
||||
{
|
||||
Console.WriteLine("FAIL caller.dll not built; run run.sh");
|
||||
return 1;
|
||||
}
|
||||
|
||||
string pristine = Path.GetFullPath("pristine");
|
||||
if (Directory.Exists(pristine)) Directory.Delete(pristine, true);
|
||||
Directory.CreateDirectory(pristine);
|
||||
foreach (string f in Directory.GetFiles(bin))
|
||||
File.Copy(f, Path.Combine(pristine, Path.GetFileName(f)), true);
|
||||
|
||||
string expected = Invoke(pristine, "pristine");
|
||||
Console.WriteLine("baseline: " + expected);
|
||||
|
||||
int padded = Pad(pristine, "out", 12345);
|
||||
Check(padded > 0, $"padded {padded} methods");
|
||||
|
||||
// the written assemblies must round-trip
|
||||
try
|
||||
{
|
||||
ModuleDefMD.Load(Path.GetFullPath("out/fixture.dll"), MakeContext("out"));
|
||||
ModuleDefMD.Load(Path.GetFullPath("out/caller.dll"), MakeContext("out"));
|
||||
Check(true, "roundtrip: padded assemblies reload through dnlib");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Check(false, "roundtrip: " + e.Message);
|
||||
}
|
||||
|
||||
// the decisive check: the padded build still behaves identically
|
||||
try
|
||||
{
|
||||
string actual = Invoke("out", "padded");
|
||||
Check(actual == expected, "behaviour: padded build produces identical output");
|
||||
if (actual != expected)
|
||||
{
|
||||
Console.WriteLine(" expected: " + expected);
|
||||
Console.WriteLine(" actual: " + actual);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Check(false, "behaviour: padded build threw " + e.GetType().Name + ": " + e.Message);
|
||||
Console.WriteLine(e.ToString());
|
||||
}
|
||||
|
||||
// determinism and variation
|
||||
int a = Pad(pristine, "outA", 777);
|
||||
int b = Pad(pristine, "outB", 777);
|
||||
int c = Pad(pristine, "outC", 999);
|
||||
Check(SigDump("outA") == SigDump("outB"), "seed: the same seed reproduces the same signatures");
|
||||
Check(SigDump("outA") != SigDump("outC"), "seed: a different seed produces different signatures");
|
||||
Check(Invoke("outC", "outC") == expected, "behaviour: a second seed also behaves identically");
|
||||
|
||||
Fuzz(pristine, 40);
|
||||
|
||||
Console.WriteLine(failures == 0 ? "ALL PASS" : $"{failures} FAILURES");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static string SigDump(string dir)
|
||||
{
|
||||
ModuleDefMD mod = ModuleDefMD.Load(Path.GetFullPath(Path.Combine(dir, "fixture.dll")), MakeContext(dir));
|
||||
var lines = new List<string>();
|
||||
foreach (TypeDef t in mod.GetTypes())
|
||||
foreach (MethodDef m in t.Methods)
|
||||
lines.Add(Key(m) + "(" + string.Join(",", m.MethodSig.Params.Select(p => p.FullName)) + ")");
|
||||
lines.Sort(StringComparer.Ordinal);
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using System.Text;
|
||||
|
||||
namespace Cl
|
||||
{
|
||||
// Stands in for a nonObfuscatedButReferencingObfuscated assembly: never padded itself,
|
||||
// but its call sites into the padded assembly must still be fixed up.
|
||||
public static class Caller
|
||||
{
|
||||
public static string RunAll()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(Fx.Entry.RunAll()).Append('#');
|
||||
sb.Append(Fx.Ops.Mul(9, 9)).Append(';');
|
||||
sb.Append(Fx.Ops.Clamp(500, 0, 99)).Append(';');
|
||||
sb.Append(Fx.Ops.Pair<int>(7, 8)).Append(';');
|
||||
var sq = new Fx.Square(4);
|
||||
sb.Append(sq.Scaled(2, 2)).Append(';');
|
||||
sb.Append(sq.Bare()).Append(';');
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<AssemblyName>caller</AssemblyName>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../fixture/fixture.csproj" />
|
||||
<ProjectReference Include="../libA/libA.csproj" />
|
||||
<ProjectReference Include="../libB/libB.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
extern alias LA;
|
||||
extern alias LB;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace Fx
|
||||
{
|
||||
public interface IShape
|
||||
{
|
||||
int Area(int scale);
|
||||
string Describe(string prefix);
|
||||
}
|
||||
|
||||
public delegate int Combine(int a, int b);
|
||||
|
||||
public abstract class ShapeBase
|
||||
{
|
||||
public abstract int Sides();
|
||||
public virtual int Weight(int density) { return density * 2; }
|
||||
}
|
||||
|
||||
public class Square : ShapeBase, IShape
|
||||
{
|
||||
private int _side;
|
||||
|
||||
// ctor: excluded
|
||||
public Square(int side) { _side = side; }
|
||||
static Square() { Origin = 7; }
|
||||
|
||||
public static int Origin;
|
||||
|
||||
// implicit interface impl: virtual newslot, excluded
|
||||
public int Area(int scale) { return _side * _side * scale; }
|
||||
public string Describe(string prefix) { return prefix + ":square:" + _side; }
|
||||
|
||||
// abstract impl: excluded
|
||||
public override int Sides() { return 4; }
|
||||
// virtual override: excluded
|
||||
public override int Weight(int density) { return density * 3; }
|
||||
|
||||
// property accessors: excluded
|
||||
public int Side { get { return _side; } set { _side = value; } }
|
||||
|
||||
// event accessors: excluded
|
||||
public event Action<int> Resized;
|
||||
public void RaiseResized(int v) { Resized?.Invoke(v); }
|
||||
|
||||
// ordinary instance method: PADDED
|
||||
public int Scaled(int factor, int offset) { return _side * factor + offset; }
|
||||
|
||||
// zero arg instance method: PADDED
|
||||
public int Bare() { return _side; }
|
||||
}
|
||||
|
||||
public class Explicit : IShape
|
||||
{
|
||||
// explicit interface impl: excluded
|
||||
int IShape.Area(int scale) { return scale; }
|
||||
string IShape.Describe(string prefix) { return prefix + ":explicit"; }
|
||||
}
|
||||
|
||||
public static class Ops
|
||||
{
|
||||
// taken with ldftn via method group conversion: excluded
|
||||
public static int Add(int a, int b) { return a + b; }
|
||||
|
||||
// PADDED, ordinary statics
|
||||
public static int Mul(int a, int b) { return a * b; }
|
||||
public static int Neg(int a) { return -a; }
|
||||
public static int Zero() { return 0; }
|
||||
|
||||
// PADDED, called as a nested argument and as a branch target
|
||||
public static int Clamp(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }
|
||||
|
||||
// PADDED, recursion
|
||||
public static int Fact(int n) { return n <= 1 ? 1 : n * Fact(n - 1); }
|
||||
|
||||
// PADDED, byref parameters
|
||||
public static void Split(int v, out int lo, ref int hi, in int bump)
|
||||
{
|
||||
lo = v & 0xFF;
|
||||
hi = (v >> 8) + bump;
|
||||
}
|
||||
|
||||
// PADDED, generic
|
||||
public static string Pair<T>(T a, T b) { return a + "|" + b; }
|
||||
|
||||
// PADDED, many argument types
|
||||
public static string Mixed(byte b, short s, long l, float f, double d, char c, bool t, string str)
|
||||
{
|
||||
return b + "/" + s + "/" + l + "/" + f.ToString("F1") + "/" + d.ToString("F1") + "/" + c + "/" + t + "/" + str;
|
||||
}
|
||||
|
||||
// PADDED, called inside a try/catch and throws
|
||||
public static int Boom(int v) { if (v > 0) throw new InvalidOperationException("boom" + v); return v; }
|
||||
|
||||
// PADDED, loop with a backward branch over a call
|
||||
public static int Sum(int n)
|
||||
{
|
||||
int acc = 0;
|
||||
for (int i = 0; i < n; i++) { acc = Add3(acc, i); }
|
||||
return acc;
|
||||
}
|
||||
|
||||
// PADDED, called from the loop above
|
||||
public static int Add3(int a, int b) { return a + b; }
|
||||
|
||||
// whole body wrapped in try/catch: the consume prologue must not branch into the
|
||||
// protected region
|
||||
public static int Guarded(int v)
|
||||
{
|
||||
try { return 100 / v; }
|
||||
catch (DivideByZeroException) { return -1; }
|
||||
finally { Touched++; }
|
||||
}
|
||||
|
||||
public static int Touched;
|
||||
|
||||
// try block starting at the very first instruction, with a nested call
|
||||
public static string Wrapped(int a, int b)
|
||||
{
|
||||
try { return "w" + Mul(a, b); }
|
||||
catch (Exception e) { return e.Message; }
|
||||
}
|
||||
|
||||
// Two DIFFERENT types that share a namespace-qualified name. Spilling both at the same
|
||||
// ordinal must not reuse one local: TypeSig.FullName omits the assembly, so keying on it
|
||||
// would type the second spill as the first type.
|
||||
public static int TakeA(LA::Shared.Thing t, int n) { return t.V + n; }
|
||||
public static int TakeB(LB::Shared.Thing t, int n) { return t.V * n; }
|
||||
|
||||
public static int SameName()
|
||||
{
|
||||
return TakeA(new LA::Shared.Thing(1), 2) + TakeB(new LB::Shared.Thing(3), 4);
|
||||
}
|
||||
|
||||
// Arguments must still be EVALUATED left to right even though they are PUSHED in a
|
||||
// permuted order. Tick records evaluation order; Order records value routing.
|
||||
public static string OrderLog = "";
|
||||
public static int Tick(int n) { OrderLog += n; return n; }
|
||||
public static int Order(int a, int b, int c) { return a * 100 + b * 10 + c; }
|
||||
|
||||
// ldtoken / reflection target: excluded
|
||||
public static int Reflected(int a) { return a + 1000; }
|
||||
|
||||
public static string UseReflection()
|
||||
{
|
||||
var m = typeof(Ops).GetMethod("Reflected");
|
||||
return m == null ? "null" : m.Name + ":" + m.GetParameters().Length;
|
||||
}
|
||||
|
||||
// [DllImport]: excluded (no body, never a candidate)
|
||||
[DllImport("nonexistent", EntryPoint = "never_called")]
|
||||
public static extern int Native(int a);
|
||||
|
||||
public static int UseDelegate(int a, int b)
|
||||
{
|
||||
Combine c = Add; // ldftn Ops::Add
|
||||
Func<int, int> lam = x => x * 5; // ldftn on the lambda
|
||||
return c(a, b) + lam(a);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Payload
|
||||
{
|
||||
public int Value;
|
||||
public static int AfterCount;
|
||||
|
||||
// located by attribute, so the rename policy permits renaming it - but BinaryFormatter and
|
||||
// Newtonsoft both validate the signature, so its argument list must not change.
|
||||
[OnDeserialized]
|
||||
internal void AfterLoad(StreamingContext ctx) { AfterCount++; }
|
||||
|
||||
[OnSerializing]
|
||||
internal void BeforeSave(StreamingContext ctx) { AfterCount += 2; }
|
||||
}
|
||||
|
||||
public class Counter
|
||||
{
|
||||
private int _n;
|
||||
public int Step(int by) { _n += by; return _n; }
|
||||
public int Value() { return _n; }
|
||||
}
|
||||
|
||||
public static class Entry
|
||||
{
|
||||
public static string RunAll()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var sq = new Square(5);
|
||||
sb.Append(sq.Area(2)).Append(';');
|
||||
sb.Append(sq.Describe("p")).Append(';');
|
||||
sb.Append(sq.Sides()).Append(';');
|
||||
sb.Append(sq.Weight(4)).Append(';');
|
||||
sq.Side = 6;
|
||||
sb.Append(sq.Side).Append(';');
|
||||
sb.Append(sq.Scaled(3, 1)).Append(';');
|
||||
sb.Append(sq.Bare()).Append(';');
|
||||
sb.Append(Square.Origin).Append(';');
|
||||
|
||||
int captured = -1;
|
||||
sq.Resized += v => captured = v;
|
||||
sq.RaiseResized(42);
|
||||
sb.Append(captured).Append(';');
|
||||
|
||||
IShape ex = new Explicit();
|
||||
sb.Append(ex.Area(9)).Append(';');
|
||||
sb.Append(ex.Describe("q")).Append(';');
|
||||
|
||||
sb.Append(Ops.Mul(6, 7)).Append(';');
|
||||
sb.Append(Ops.Neg(11)).Append(';');
|
||||
sb.Append(Ops.Zero()).Append(';');
|
||||
// nested call as an argument, exercises overlapping spills
|
||||
sb.Append(Ops.Clamp(Ops.Mul(3, 40), 10, 100)).Append(';');
|
||||
sb.Append(Ops.Clamp(5, Ops.Neg(-20), Ops.Mul(5, 5))).Append(';');
|
||||
sb.Append(Ops.Fact(6)).Append(';');
|
||||
sb.Append(Ops.Sum(10)).Append(';');
|
||||
|
||||
int lo, hi = 3, bump = 4;
|
||||
Ops.Split(0x1234, out lo, ref hi, in bump);
|
||||
sb.Append(lo).Append(',').Append(hi).Append(';');
|
||||
|
||||
sb.Append(Ops.Pair<int>(1, 2)).Append(';');
|
||||
sb.Append(Ops.Pair<string>("a", "b")).Append(';');
|
||||
sb.Append(Ops.Mixed(1, -2, 3L, 4.5f, 6.5, 'z', true, "s")).Append(';');
|
||||
|
||||
try { Ops.Boom(3); }
|
||||
catch (InvalidOperationException e) { sb.Append(e.Message).Append(';'); }
|
||||
sb.Append(Ops.Boom(0)).Append(';');
|
||||
|
||||
Ops.OrderLog = "";
|
||||
int ordered = Ops.Order(Ops.Tick(1), Ops.Tick(2), Ops.Tick(3));
|
||||
sb.Append(ordered).Append(',').Append(Ops.OrderLog).Append(';');
|
||||
sb.Append(Ops.SameName()).Append(';');
|
||||
sb.Append(Ops.Guarded(4)).Append(';');
|
||||
sb.Append(Ops.Guarded(0)).Append(';');
|
||||
sb.Append(Ops.Touched).Append(';');
|
||||
sb.Append(Ops.Wrapped(3, 4)).Append(';');
|
||||
sb.Append(Ops.UseDelegate(2, 3)).Append(';');
|
||||
sb.Append(Ops.UseReflection()).Append(';');
|
||||
|
||||
var c = new Counter();
|
||||
for (int i = 0; i < 4; i++) { c.Step(i); }
|
||||
sb.Append(c.Value()).Append(';');
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<AssemblyName>fixture</AssemblyName>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<NoWarn>CS0649;CS0067;CS0414</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../unitystub/unitystub.csproj" />
|
||||
<ProjectReference Include="../libA/libA.csproj" Aliases="LA" />
|
||||
<ProjectReference Include="../libB/libB.csproj" Aliases="LB" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1 @@
|
|||
namespace Shared { public class Thing { public int V; public Thing(int v) { V = v; } } }
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><TargetFramework>net7.0</TargetFramework><AssemblyName>libA</AssemblyName></PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1 @@
|
|||
namespace Shared { public class Thing { public int V; public Thing(int v) { V = v; } } }
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><TargetFramework>net7.0</TargetFramework><AssemblyName>libB</AssemblyName></PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<AssemblyName>probe</AssemblyName>
|
||||
<RootNamespace>probe</RootNamespace>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<NoWarn>CS0168;CS0219;CS0414;CS1998;CS0162</NoWarn>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="dnlib"><HintPath>../../Plugins/dnlib.dll</HintPath></Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="stubs.cs" />
|
||||
<Compile Include="Main.cs" />
|
||||
<Compile Include="../../Editor/ConstValues.cs" />
|
||||
<Compile Include="../../Editor/Utils/MetaUtil.cs" />
|
||||
<Compile Include="../../Editor/Utils/TypeSigUtil.cs" />
|
||||
<Compile Include="../../Editor/Utils/CachedDictionary.cs" />
|
||||
<Compile Include="../../Editor/Utils/GenericArgumentContext.cs" />
|
||||
<Compile Include="../../Editor/Utils/ThisArgType.cs" />
|
||||
<Compile Include="../../Editor/ObfusPasses/ParamPad/ParameterPadding.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
# Release: the shape that ships, and the one where a try block can start at instruction 0
|
||||
dotnet build libA/libA.csproj -c Release -v q --nologo
|
||||
dotnet build libB/libB.csproj -c Release -v q --nologo
|
||||
dotnet build fixture/fixture.csproj -c Release -v q --nologo
|
||||
dotnet build caller/caller.csproj -c Release -v q --nologo
|
||||
dotnet build probe.csproj -v q --nologo
|
||||
dotnet bin/Debug/net7.0/probe.dll
|
||||
|
||||
ilverify=$(command -v ilverify || echo "$HOME/.dotnet/tools/ilverify")
|
||||
if [ -x "$ilverify" ]; then
|
||||
refs=$(dirname "$(find /usr/share/dotnet/shared/Microsoft.NETCore.App/7.* -name System.Private.CoreLib.dll 2>/dev/null | head -1)")
|
||||
for dll in out/fixture.dll out/caller.dll; do
|
||||
"$ilverify" "$dll" -r "$refs/*.dll" -r "out/*.dll"
|
||||
done
|
||||
else
|
||||
echo "SKIP ilverify not installed (dotnet tool install -g dotnet-ilverify --version 7.0.0)"
|
||||
fi
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2025 Code Philosophy
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
namespace UnityEngine.Assertions {
|
||||
public static class Assert {
|
||||
public static void IsTrue(bool c) { if(!c) throw new System.Exception("assert"); }
|
||||
public static void IsTrue(bool c, string m) { if(!c) throw new System.Exception(m); }
|
||||
public static void IsNotNull(object o) { if(o==null) throw new System.Exception("null"); }
|
||||
}
|
||||
}
|
||||
namespace UnityEngine {
|
||||
public static class Debug {
|
||||
public static void Log(object o) { System.Console.WriteLine(o); }
|
||||
public static void LogWarning(object o) { System.Console.WriteLine(o); }
|
||||
public static void LogError(object o) { System.Console.WriteLine(o); }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2025 Code Philosophy
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
namespace UnityEngine
|
||||
{
|
||||
public class Object { }
|
||||
public class Component : Object { }
|
||||
public class Behaviour : Component { }
|
||||
public class MonoBehaviour : Behaviour { }
|
||||
public class ScriptableObject : Object { }
|
||||
[System.AttributeUsage(System.AttributeTargets.Method)]
|
||||
public class RuntimeInitializeOnLoadMethodAttribute : System.Attribute { }
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<AssemblyName>UnityEngine.CoreModule</AssemblyName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Loading…
Reference in New Issue