Skip to content

Commit 1a876d1

Browse files
author
Rainer Buschow
committed
FullRenamer: rename interface methods together with their implementations and rename async state machines
The previous implementation skipped every method with IsVirtual=true, which meant the entire public surface of services and repositories (every implicit interface implementation in C# is virtual at IL level) kept its original name. The original method name also leaked through compiler-generated async state-machine types named "<MethodName>d__N". This change: 1. Adds a pre-pass that collects each interface method in the current obfuscation scope, reserves one random name per method, and applies the same name to every matching implementation that is also in scope. The contract between interface and implementation stays consistent. 2. After renaming a method, looks for nested compiler-generated types whose name embeds the original method name ("<originalName>d__N" or "<originalName>b__N_M") and renames them too. This closes the most common stack-trace leak for async methods and closures. 3. Refines the virtual-method skip in the standard rename loop: - IsVirtual && !IsNewSlot is kept as skip (true override; the name is bound to the base class method). - IsVirtual && IsNewSlot && IsFinal is added as skip (implicit implementation of an interface that lives outside the current module; renaming would break the contract and trigger TypeLoadException at load time). 4. Replaces the in-line loops for method/type/field rename with iteration over pre-collected lists so the helpers can be called safely while the underlying collections are still in use. The original behaviour is preserved for type and field renames.
1 parent 60b2389 commit 1a876d1

1 file changed

Lines changed: 191 additions & 31 deletions

File tree

Lines changed: 191 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace BitMono.Protections;
1+
namespace BitMono.Protections;
22

33
[DoNotResolve(MemberInclusionFlags.SpecialRuntime | MemberInclusionFlags.Model | MemberInclusionFlags.Reflection)]
44
public class FullRenamer : Protection
@@ -12,21 +12,116 @@ public FullRenamer(Renamer renamer, IBitMonoServiceProvider serviceProvider) : b
1212

1313
public override Task ExecuteAsync()
1414
{
15-
foreach (var method in Context.Parameters.Members.OfType<MethodDefinition>())
15+
// -----------------------------------------------------------------
16+
// Behaviour:
17+
//
18+
// 1. Interface methods within the obfuscation scope and their
19+
// implementations are renamed TOGETHER with the same random
20+
// name (rather than being skipped wholesale because of
21+
// IsVirtual, which would break the API surface).
22+
// 2. Compiler-generated async state-machine types
23+
// ("<MethodName>d__N") are renamed as well so the original
24+
// method name does not leak through stack traces.
25+
// 3. True "override" methods (IsVirtual && !IsNewSlot) are left
26+
// alone because they must keep the same name as the base
27+
// class method they override.
28+
// 4. Implicit implementations of interfaces that live OUTSIDE
29+
// the current module (IsVirtual && IsNewSlot && IsFinal but
30+
// not picked up by phase 1) are skipped so we do not break
31+
// the contract and trigger TypeLoadException at assembly
32+
// load time.
33+
// -----------------------------------------------------------------
34+
35+
var members = Context.Parameters.Members;
36+
var membersList = members.ToList();
37+
var allTypes = membersList.OfType<TypeDefinition>().ToList();
38+
var allMethods = membersList.OfType<MethodDefinition>().ToList();
39+
var allFields = membersList.OfType<FieldDefinition>().ToList();
40+
41+
// Track which methods have already received a new name through
42+
// phase 1 so the standard loop below can skip them.
43+
var alreadyRenamed = new HashSet<MethodDefinition>();
44+
45+
// Phase 1: collect interface methods in scope, reserve a random
46+
// name for each, and apply the same name to every
47+
// matching implementation that is also in scope.
48+
var concreteTypes = allTypes
49+
.Where(t => !t.IsInterface && !t.IsModuleType && !t.IsCompilerGenerated())
50+
.ToList();
51+
52+
foreach (var iface in allTypes.Where(t => t.IsInterface && !t.IsCompilerGenerated()))
1653
{
17-
if (method.DeclaringType?.IsModuleType == true)
54+
foreach (var ifaceMethod in iface.Methods.ToList())
1855
{
19-
continue;
20-
}
21-
if (method.IsConstructor || method.IsVirtual)
22-
{
23-
continue;
24-
}
25-
if (method.IsCompilerGenerated())
26-
{
27-
continue;
56+
if (ifaceMethod.IsConstructor) continue;
57+
if (ifaceMethod.IsCompilerGenerated()) continue;
58+
if (!ShouldRenameMethodName(ifaceMethod)) continue;
59+
60+
var groupName = _renamer.RenameUnsafely();
61+
62+
// Capture the original name so we can match implementations
63+
// by name + signature BEFORE we overwrite the interface name.
64+
var originalIfaceName = ifaceMethod.Name?.Value;
65+
66+
ifaceMethod.Name = groupName;
67+
alreadyRenamed.Add(ifaceMethod);
68+
RenameParametersOf(ifaceMethod);
69+
RenameAsyncStateMachineFor(ifaceMethod, originalIfaceName);
70+
71+
// Find matching implementations.
72+
foreach (var concrete in concreteTypes)
73+
{
74+
if (!ImplementsInterface(concrete, iface)) continue;
75+
76+
foreach (var implMethod in concrete.Methods.ToList())
77+
{
78+
if (implMethod.IsConstructor) continue;
79+
if (implMethod.IsCompilerGenerated()) continue;
80+
if (alreadyRenamed.Contains(implMethod)) continue;
81+
if (originalIfaceName == null) continue;
82+
if (implMethod.Name?.Value != originalIfaceName) continue;
83+
if (!SignatureCompatible(implMethod, ifaceMethod)) continue;
84+
85+
var originalImplName = implMethod.Name?.Value;
86+
implMethod.Name = groupName;
87+
alreadyRenamed.Add(implMethod);
88+
RenameParametersOf(implMethod);
89+
RenameAsyncStateMachineFor(implMethod, originalImplName);
90+
}
91+
}
2892
}
93+
}
94+
95+
// Phase 2: standard rename loop for everything that was not
96+
// handled by phase 1.
97+
foreach (var method in allMethods)
98+
{
99+
if (alreadyRenamed.Contains(method)) continue;
100+
if (method.DeclaringType?.IsModuleType == true) continue;
101+
if (method.IsConstructor) continue;
102+
if (method.IsCompilerGenerated()) continue;
103+
104+
// Skip true overrides: IsVirtual && !IsNewSlot means the
105+
// method reuses an inherited vtable slot (override keyword
106+
// in C#). Renaming it would break the link to the base
107+
// class method.
108+
if (method.IsVirtual && !method.IsNewSlot) continue;
109+
110+
// Skip implementations of external interfaces. The C#
111+
// compiler marks implicit interface impls with
112+
// IsVirtual+IsNewSlot+IsFinal+IsHideBySig. If phase 1 did
113+
// not pick them up (alreadyRenamed check above), the
114+
// corresponding interface lives in another assembly. If we
115+
// rename the impl, the contract breaks and the CLR throws
116+
// at assembly load time:
117+
// System.TypeLoadException: Method '...' in type '...'
118+
// does not have an implementation.
119+
if (method.IsVirtual && method.IsNewSlot && method.IsFinal) continue;
120+
121+
var originalName = method.Name?.Value;
29122
_renamer.Rename(method);
123+
RenameAsyncStateMachineFor(method, originalName);
124+
30125
if (!method.HasParameters())
31126
{
32127
continue;
@@ -40,30 +135,95 @@ public override Task ExecuteAsync()
40135
_renamer.Rename(parameter.Definition);
41136
}
42137
}
43-
foreach (var type in Context.Parameters.Members.OfType<TypeDefinition>())
138+
139+
// Type renames - unchanged from the original logic.
140+
foreach (var type in allTypes)
44141
{
45-
if (type.IsModuleType)
46-
{
47-
continue;
48-
}
49-
if (type.IsCompilerGenerated())
50-
{
51-
continue;
52-
}
142+
if (type.IsModuleType) continue;
143+
if (type.IsCompilerGenerated()) continue;
53144
_renamer.Rename(type);
54145
}
55-
foreach (var field in Context.Parameters.Members.OfType<FieldDefinition>())
146+
147+
// Field renames - unchanged from the original logic.
148+
foreach (var field in allFields)
56149
{
57-
if (field.DeclaringType?.IsModuleType == true)
58-
{
59-
continue;
60-
}
61-
if (field.IsCompilerGenerated())
62-
{
63-
continue;
64-
}
150+
if (field.DeclaringType?.IsModuleType == true) continue;
151+
if (field.IsCompilerGenerated()) continue;
65152
_renamer.Rename(field);
66153
}
154+
67155
return Task.CompletedTask;
68156
}
69-
}
157+
158+
private static bool ShouldRenameMethodName(MethodDefinition method)
159+
{
160+
if (method.IsCompilerGenerated()) return false;
161+
if (method.IsConstructor) return false;
162+
return true;
163+
}
164+
165+
private void RenameParametersOf(MethodDefinition method)
166+
{
167+
if (!method.HasParameters()) return;
168+
foreach (var parameter in method.Parameters)
169+
{
170+
if (parameter.Definition == null) continue;
171+
_renamer.Rename(parameter.Definition);
172+
}
173+
}
174+
175+
private void RenameAsyncStateMachineFor(MethodDefinition method, string? originalName)
176+
{
177+
if (string.IsNullOrEmpty(originalName)) return;
178+
var declaring = method.DeclaringType;
179+
if (declaring == null) return;
180+
if (declaring.NestedTypes == null) return;
181+
182+
// Compiler-generated state-machine types are named
183+
// "<MethodName>d__N" (async/await) or "<MethodName>b__N_M"
184+
// (lambdas). Once we know the original method name, we can find
185+
// the related nested types and rename them along with it.
186+
var prefix = "<" + originalName + ">";
187+
foreach (var nested in declaring.NestedTypes.ToList())
188+
{
189+
var nestedName = nested.Name?.Value;
190+
if (nestedName == null) continue;
191+
if (!nestedName.StartsWith(prefix, StringComparison.Ordinal)) continue;
192+
nested.Name = _renamer.RenameUnsafely();
193+
}
194+
}
195+
196+
private static bool ImplementsInterface(TypeDefinition type, TypeDefinition iface)
197+
{
198+
if (type.Interfaces == null) return false;
199+
foreach (var implemented in type.Interfaces)
200+
{
201+
var resolved = implemented.Interface?.Resolve();
202+
if (resolved == iface) return true;
203+
}
204+
// Also walk the base-type chain.
205+
var baseTypeDef = type.BaseType?.Resolve();
206+
if (baseTypeDef != null && baseTypeDef != type)
207+
{
208+
return ImplementsInterface(baseTypeDef, iface);
209+
}
210+
return false;
211+
}
212+
213+
private static bool SignatureCompatible(MethodDefinition a, MethodDefinition b)
214+
{
215+
if (a.Signature == null || b.Signature == null) return false;
216+
if (a.Signature.ParameterTypes.Count != b.Signature.ParameterTypes.Count) return false;
217+
if (a.Signature.GenericParameterCount != b.Signature.GenericParameterCount) return false;
218+
var aRet = a.Signature.ReturnType?.FullName;
219+
var bRet = b.Signature.ReturnType?.FullName;
220+
if (!string.Equals(aRet, bRet, StringComparison.Ordinal)) return false;
221+
for (int i = 0; i < a.Signature.ParameterTypes.Count; i++)
222+
{
223+
var aParam = a.Signature.ParameterTypes[i]?.FullName;
224+
var bParam = b.Signature.ParameterTypes[i]?.FullName;
225+
if (!string.Equals(aParam, bParam, StringComparison.Ordinal)) return false;
226+
}
227+
return true;
228+
}
229+
}

0 commit comments

Comments
 (0)