383 lines
15 KiB
C#
383 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Reflection.Emit;
|
|
|
|
namespace io.github.hatayama.uLoopMCP
|
|
{
|
|
/// <summary>
|
|
/// Post-compilation security validator using IL byte inspection.
|
|
/// Scans call/callvirt/newobj/ldtoken opcodes to detect dangerous API usage
|
|
/// that source-level scanning cannot catch (e.g. using aliases, fully-qualified names).
|
|
/// </summary>
|
|
internal sealed class IlSecurityValidator
|
|
{
|
|
private static readonly OpCode[] SingleByteOpCodes = BuildOpCodeTable(singleByte: true);
|
|
private static readonly OpCode[] MultiByteOpCodes = BuildOpCodeTable(singleByte: false);
|
|
|
|
public SecurityValidationResult Validate(Assembly assembly)
|
|
{
|
|
SecurityValidationResult result = new SecurityValidationResult
|
|
{
|
|
IsValid = true,
|
|
Violations = new List<SecurityViolation>()
|
|
};
|
|
|
|
Type[] types;
|
|
try
|
|
{
|
|
types = assembly.GetTypes();
|
|
}
|
|
catch (ReflectionTypeLoadException ex)
|
|
{
|
|
types = Array.FindAll(ex.Types, static t => t != null);
|
|
}
|
|
|
|
foreach (Type type in types)
|
|
{
|
|
ValidateTypeHierarchy(type, result);
|
|
ValidateMethodReferences(type, result);
|
|
}
|
|
|
|
result.IsValid = result.Violations.Count == 0;
|
|
return result;
|
|
}
|
|
|
|
private static void ValidateTypeHierarchy(Type type, SecurityValidationResult result)
|
|
{
|
|
ValidateReferencedType(
|
|
type.BaseType,
|
|
result,
|
|
typeName => $"Inheriting from dangerous type: {typeName}",
|
|
typeName => $"Type '{type.FullName}' inherits from blocked type '{typeName}'");
|
|
|
|
foreach (Type iface in type.GetInterfaces())
|
|
{
|
|
ValidateReferencedType(
|
|
iface,
|
|
result,
|
|
typeName => $"Implementing dangerous interface: {typeName}",
|
|
typeName => $"Type '{type.FullName}' implements blocked interface '{typeName}'");
|
|
}
|
|
|
|
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static |
|
|
BindingFlags.DeclaredOnly;
|
|
|
|
foreach (FieldInfo field in type.GetFields(flags))
|
|
{
|
|
ValidateReferencedType(
|
|
field.FieldType,
|
|
result,
|
|
typeName => $"Using dangerous type as field: {typeName}",
|
|
typeName => $"Field '{field.Name}' of type '{typeName}' in {type.FullName}");
|
|
}
|
|
|
|
foreach (PropertyInfo property in type.GetProperties(flags))
|
|
{
|
|
ValidateReferencedType(
|
|
property.PropertyType,
|
|
result,
|
|
typeName => $"Using dangerous type as property: {typeName}",
|
|
typeName => $"Property '{property.Name}' of type '{typeName}' in {type.FullName}");
|
|
|
|
foreach (System.Reflection.ParameterInfo indexParameter in property.GetIndexParameters())
|
|
{
|
|
ValidateReferencedType(
|
|
indexParameter.ParameterType,
|
|
result,
|
|
typeName => $"Using dangerous type as property index parameter: {typeName}",
|
|
typeName => $"Property index parameter '{indexParameter.Name}' of type '{typeName}' in {type.FullName}.{property.Name}");
|
|
}
|
|
}
|
|
|
|
foreach (EventInfo eventInfo in type.GetEvents(flags))
|
|
{
|
|
ValidateReferencedType(
|
|
eventInfo.EventHandlerType,
|
|
result,
|
|
typeName => $"Using dangerous type as event handler: {typeName}",
|
|
typeName => $"Event '{eventInfo.Name}' of type '{typeName}' in {type.FullName}");
|
|
}
|
|
}
|
|
|
|
private static void ValidateMethodReferences(Type type, SecurityValidationResult result)
|
|
{
|
|
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static |
|
|
BindingFlags.DeclaredOnly;
|
|
|
|
foreach (MethodInfo method in type.GetMethods(flags))
|
|
{
|
|
ValidateMethodBody(method, result);
|
|
}
|
|
|
|
foreach (ConstructorInfo ctor in type.GetConstructors(flags))
|
|
{
|
|
ValidateMethodBody(ctor, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateMethodBody(MethodBase method, SecurityValidationResult result)
|
|
{
|
|
MethodBody body;
|
|
try
|
|
{
|
|
body = method.GetMethodBody();
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Abstract/extern methods have no body
|
|
return;
|
|
}
|
|
if (body == null) return;
|
|
|
|
foreach (LocalVariableInfo local in body.LocalVariables)
|
|
{
|
|
string typeName = local.LocalType?.FullName;
|
|
if (typeName != null && DangerousApiCatalog.IsDangerousType(typeName))
|
|
{
|
|
result.Violations.Add(new SecurityViolation
|
|
{
|
|
Type = SecurityViolationType.DangerousApiCall,
|
|
ApiName = typeName,
|
|
Message = $"Using dangerous type as local variable: {typeName}",
|
|
Description = $"Local variable of type '{typeName}' in {method.DeclaringType?.FullName}.{method.Name}"
|
|
});
|
|
}
|
|
}
|
|
|
|
foreach (System.Reflection.ParameterInfo param in method.GetParameters())
|
|
{
|
|
string typeName = param.ParameterType?.FullName;
|
|
if (typeName != null && DangerousApiCatalog.IsDangerousType(typeName))
|
|
{
|
|
result.Violations.Add(new SecurityViolation
|
|
{
|
|
Type = SecurityViolationType.DangerousApiCall,
|
|
ApiName = typeName,
|
|
Message = $"Using dangerous type as parameter: {typeName}",
|
|
Description = $"Parameter '{param.Name}' of type '{typeName}' in {method.DeclaringType?.FullName}.{method.Name}"
|
|
});
|
|
}
|
|
}
|
|
|
|
byte[] il = body.GetILAsByteArray();
|
|
if (il == null) return;
|
|
|
|
ValidateIlCalls(method, il, result);
|
|
}
|
|
|
|
private static void ValidateReferencedType(
|
|
Type referencedType,
|
|
SecurityValidationResult result,
|
|
Func<string, string> createMessage,
|
|
Func<string, string> createDescription)
|
|
{
|
|
foreach (string typeName in ExpandReferencedTypes(referencedType))
|
|
{
|
|
if (!DangerousApiCatalog.IsDangerousType(typeName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result.Violations.Add(new SecurityViolation
|
|
{
|
|
Type = SecurityViolationType.DangerousApiCall,
|
|
ApiName = typeName,
|
|
Message = createMessage(typeName),
|
|
Description = createDescription(typeName)
|
|
});
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<string> ExpandReferencedTypes(Type referencedType)
|
|
{
|
|
if (referencedType == null)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
if (referencedType.HasElementType)
|
|
{
|
|
foreach (string typeName in ExpandReferencedTypes(referencedType.GetElementType()))
|
|
{
|
|
yield return typeName;
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
string fullName = referencedType.FullName;
|
|
if (!string.IsNullOrEmpty(fullName))
|
|
{
|
|
yield return fullName;
|
|
}
|
|
|
|
if (!referencedType.IsGenericType)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
foreach (Type genericArgument in referencedType.GetGenericArguments())
|
|
{
|
|
foreach (string typeName in ExpandReferencedTypes(genericArgument))
|
|
{
|
|
yield return typeName;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ValidateIlCalls(MethodBase method, byte[] il, SecurityValidationResult result)
|
|
{
|
|
int offset = 0;
|
|
Module module = method.Module;
|
|
Type[] typeArgs = method.DeclaringType?.IsGenericType == true
|
|
? method.DeclaringType.GetGenericArguments()
|
|
: null;
|
|
Type[] methodArgs = method.IsGenericMethod ? method.GetGenericArguments() : null;
|
|
|
|
while (offset < il.Length)
|
|
{
|
|
OpCode opCode = ReadOpCode(il, ref offset);
|
|
|
|
if (opCode.OperandType == OperandType.InlineMethod)
|
|
{
|
|
int token = ReadInt32(il, offset);
|
|
|
|
if (opCode == OpCodes.Call || opCode == OpCodes.Callvirt || opCode == OpCodes.Newobj ||
|
|
opCode == OpCodes.Ldftn || opCode == OpCodes.Ldvirtftn)
|
|
{
|
|
MethodBase calledMethod;
|
|
try
|
|
{
|
|
calledMethod = module.ResolveMethod(token, typeArgs, methodArgs);
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
offset += 4;
|
|
continue;
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
offset += 4;
|
|
continue;
|
|
}
|
|
|
|
string declaringType = calledMethod.DeclaringType?.FullName;
|
|
string memberName = calledMethod.Name;
|
|
|
|
if (declaringType != null && DangerousApiCatalog.IsDangerousApi(declaringType, memberName))
|
|
{
|
|
result.Violations.Add(new SecurityViolation
|
|
{
|
|
Type = SecurityViolationType.DangerousApiCall,
|
|
ApiName = $"{declaringType}.{memberName}",
|
|
Message = $"Dangerous API call: {declaringType}.{memberName}",
|
|
Description = $"IL {opCode.Name} to '{declaringType}.{memberName}' in {method.DeclaringType?.FullName}.{method.Name}"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// ldtoken with InlineTok operand: detect typeof(DangerousType)
|
|
if (opCode == OpCodes.Ldtoken && opCode.OperandType == OperandType.InlineTok)
|
|
{
|
|
int token = ReadInt32(il, offset);
|
|
|
|
Type resolvedType;
|
|
try
|
|
{
|
|
resolvedType = module.ResolveType(token, typeArgs, methodArgs);
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
// ldtoken can also reference fields/methods, not just types
|
|
offset += GetOperandSize(opCode.OperandType, il, offset);
|
|
continue;
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// ldtoken can also reference fields/methods, not just types
|
|
offset += GetOperandSize(opCode.OperandType, il, offset);
|
|
continue;
|
|
}
|
|
|
|
string fullName = resolvedType.FullName;
|
|
if (fullName != null && DangerousApiCatalog.IsDangerousType(fullName))
|
|
{
|
|
result.Violations.Add(new SecurityViolation
|
|
{
|
|
Type = SecurityViolationType.DangerousApiCall,
|
|
ApiName = fullName,
|
|
Message = $"typeof() reference to dangerous type: {fullName}",
|
|
Description = $"typeof({fullName}) in {method.DeclaringType?.FullName}.{method.Name}"
|
|
});
|
|
}
|
|
}
|
|
|
|
offset += GetOperandSize(opCode.OperandType, il, offset);
|
|
}
|
|
}
|
|
|
|
private static OpCode ReadOpCode(byte[] il, ref int offset)
|
|
{
|
|
byte first = il[offset++];
|
|
if (first != 0xFE) return SingleByteOpCodes[first];
|
|
byte second = il[offset++];
|
|
return MultiByteOpCodes[second];
|
|
}
|
|
|
|
private static int ReadInt32(byte[] il, int offset)
|
|
{
|
|
return il[offset] | (il[offset + 1] << 8) | (il[offset + 2] << 16) | (il[offset + 3] << 24);
|
|
}
|
|
|
|
private static int GetOperandSize(OperandType operandType, byte[] il, int offset)
|
|
{
|
|
switch (operandType)
|
|
{
|
|
case OperandType.InlineNone: return 0;
|
|
case OperandType.ShortInlineBrTarget:
|
|
case OperandType.ShortInlineI:
|
|
case OperandType.ShortInlineVar: return 1;
|
|
case OperandType.InlineVar: return 2;
|
|
case OperandType.InlineI:
|
|
case OperandType.InlineBrTarget:
|
|
case OperandType.InlineField:
|
|
case OperandType.InlineMethod:
|
|
case OperandType.InlineSig:
|
|
case OperandType.InlineString:
|
|
case OperandType.InlineTok:
|
|
case OperandType.InlineType:
|
|
case OperandType.ShortInlineR: return 4;
|
|
case OperandType.InlineI8:
|
|
case OperandType.InlineR: return 8;
|
|
case OperandType.InlineSwitch:
|
|
int count = ReadInt32(il, offset);
|
|
return 4 + (count * 4);
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
private static OpCode[] BuildOpCodeTable(bool singleByte)
|
|
{
|
|
OpCode[] table = new OpCode[0x100];
|
|
foreach (FieldInfo field in typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static))
|
|
{
|
|
if (field.GetValue(null) is not OpCode opCode) continue;
|
|
ushort value = unchecked((ushort)opCode.Value);
|
|
if (singleByte && value <= 0xFF)
|
|
{
|
|
table[value] = opCode;
|
|
}
|
|
else if (!singleByte && (value & 0xFF00) == 0xFE00)
|
|
{
|
|
table[value & 0xFF] = opCode;
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
}
|
|
}
|