mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-15 17:59:22 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
560 lines
14 KiB
C#
560 lines
14 KiB
C#
using Microsoft.CodeAnalysis;
|
|
using Mono.Cecil;
|
|
using Mono.Cecil.Cil;
|
|
using Mono.Cecil.Rocks;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Sandbox;
|
|
|
|
[SkipHotload]
|
|
public class AccessControlResult
|
|
{
|
|
public bool Success { get; internal set; }
|
|
public ConcurrentBag<string> Errors { get; } = new();
|
|
public ConcurrentBag<(string Name, AccessControl.CodeLocation[] Locations)> WhitelistErrors { get; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// An assembly instance to verify against access control rules.
|
|
/// </summary>
|
|
[SkipHotload]
|
|
internal partial class AssemblyAccess
|
|
{
|
|
public AccessControl Global { get; }
|
|
|
|
public ConcurrentDictionary<string, Access> Touched = new();
|
|
public AssemblyDefinition Assembly { get; private set; }
|
|
|
|
public AccessControlResult Result { get; private set; }
|
|
|
|
private byte[] bytes;
|
|
|
|
public AssemblyAccess( AccessControl global, byte[] _bytes )
|
|
{
|
|
Global = global;
|
|
Result = new AccessControlResult();
|
|
bytes = _bytes;
|
|
|
|
LoadAssemblyDefinition();
|
|
}
|
|
|
|
private void LoadAssemblyDefinition()
|
|
{
|
|
var ms = new MemoryStream( bytes );
|
|
|
|
var readerParams = new ReaderParameters
|
|
{
|
|
ReadingMode = ReadingMode.Immediate,
|
|
InMemory = true,
|
|
AssemblyResolver = Global,
|
|
ReadSymbols = true,
|
|
};
|
|
|
|
try
|
|
{
|
|
Assembly = AssemblyDefinition.ReadAssembly( ms, readerParams );
|
|
}
|
|
catch ( Mono.Cecil.Cil.SymbolsNotFoundException )
|
|
{
|
|
ms.Seek( 0, SeekOrigin.Begin );
|
|
|
|
// Fallback: may not have symbols
|
|
readerParams.ReadSymbols = false;
|
|
Assembly = AssemblyDefinition.ReadAssembly( ms, readerParams );
|
|
}
|
|
|
|
Global.ForgetOlderAssemblyDefinitions( Assembly.Name );
|
|
|
|
// Keep this around, we might need it again if we load an assembly that depends on it
|
|
Global.Assemblies[Assembly.Name] = Assembly;
|
|
}
|
|
|
|
internal void Verify( out TrustedBinaryStream outStream )
|
|
{
|
|
outStream = null;
|
|
|
|
try
|
|
{
|
|
//
|
|
// All assemblies are compiled with the name package.*
|
|
// Shouldn't let them masquerade as anything because they could say they're Sandbox.Test and have access to all InternalsVisibleTo
|
|
//
|
|
if ( !Assembly.Name.Name.StartsWith( "package." ) )
|
|
{
|
|
Result.Errors.Add( "Assembly name is invalid" );
|
|
Result.Success = false;
|
|
return;
|
|
}
|
|
|
|
if ( Global.CheckSafeAssembly( Assembly.Name.Name, bytes ) )
|
|
{
|
|
outStream = TrustedBinaryStream.CreateInternal( bytes );
|
|
Result.Success = true;
|
|
return;
|
|
}
|
|
|
|
// do the heavy work of touching everything
|
|
InitTouches( bytes );
|
|
|
|
if ( !CheckPassesRules() )
|
|
{
|
|
Result.Success = false;
|
|
return;
|
|
}
|
|
|
|
if ( Result.Errors.Count > 0 )
|
|
{
|
|
Result.Success = false;
|
|
return;
|
|
}
|
|
|
|
outStream = TrustedBinaryStream.CreateInternal( bytes );
|
|
Result.Success = true;
|
|
}
|
|
catch ( System.Exception e )
|
|
{
|
|
Result.Errors.Add( $"{e.Message} ({e.GetType()})\n{e.StackTrace}" );
|
|
Result.Success = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear the touches and fill them with everything this dll touches
|
|
/// </summary>
|
|
private void InitTouches( byte[] dll )
|
|
{
|
|
Touched.Clear();
|
|
|
|
// Test assembly attributes (these are defined differently to module attributes)
|
|
TestAttributes( Assembly.CustomAttributes );
|
|
|
|
foreach ( var module in Assembly.Modules )
|
|
{
|
|
Location = new AccessControl.CodeLocation( module.FileName );
|
|
|
|
TestModule( module );
|
|
}
|
|
|
|
RemoveLocalTouches();
|
|
}
|
|
|
|
private void TestModule( ModuleDefinition module )
|
|
{
|
|
if ( module.HasExportedTypes )
|
|
{
|
|
Touch( "System.Private.CoreLib/System.Runtime.CompilerServices.TypeForwardedToAttribute", "attribute" );
|
|
}
|
|
|
|
TestAttributes( module.CustomAttributes );
|
|
|
|
#if true
|
|
{
|
|
Parallel.ForEach( EnumerateTypes( module.Types ), new ParallelOptions { MaxDegreeOfParallelism = 16 }, TestTypeInThread );
|
|
}
|
|
#else
|
|
{
|
|
foreach ( var type in module.Types )
|
|
{
|
|
TestType( type );
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
IEnumerable<TypeDefinition> EnumerateTypes( Mono.Collections.Generic.Collection<TypeDefinition> types )
|
|
{
|
|
foreach ( var t in types )
|
|
{
|
|
yield return t;
|
|
|
|
if ( t.HasNestedTypes )
|
|
{
|
|
foreach ( var nt in EnumerateTypes( t.NestedTypes ) )
|
|
{
|
|
yield return nt;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Explicit layouts can be exploited but have legitimate usages in generated code.
|
|
// Can't just check for CompilerGeneratedAttribute, so only allow specific stuff.
|
|
private bool TypeAllowedExplicitLayout( TypeDefinition type )
|
|
{
|
|
// Allow static array initializers
|
|
if ( type.DeclaringType?.Name == "<PrivateImplementationDetails>" && type.Name.StartsWith( "__StaticArrayInitTypeSize=" ) )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void TestTypeInThread( TypeDefinition type )
|
|
{
|
|
try
|
|
{
|
|
TestType( type );
|
|
}
|
|
catch ( System.Exception e )
|
|
{
|
|
Result.Errors.Add( $"{e.Message}\n{e.StackTrace}" );
|
|
}
|
|
}
|
|
|
|
private void TestType( TypeDefinition type )
|
|
{
|
|
Location = new AccessControl.CodeLocation( type.ToString() );
|
|
//Console.WriteLine( $"Touching {$"{type.Module.Assembly.Name.Name}/{type.FullName}"}" );
|
|
|
|
if ( (type.Attributes & Mono.Cecil.TypeAttributes.ExplicitLayout) != 0 && !TypeAllowedExplicitLayout( type ) )
|
|
{
|
|
Touch( $"System.Private.CoreLib/System.Runtime.InteropServices.StructLayout", "attribute" );
|
|
}
|
|
|
|
TestAttributes( type.CustomAttributes );
|
|
|
|
if ( type.IsArray )
|
|
{
|
|
TestType( type.GetElementType().Resolve() );
|
|
}
|
|
|
|
// log.Info( $"Type: {type}" );
|
|
|
|
foreach ( var member in type.Fields )
|
|
{
|
|
TestField( member );
|
|
TestAttributes( member.CustomAttributes );
|
|
}
|
|
|
|
foreach ( var member in type.Properties )
|
|
{
|
|
TestProperty( member );
|
|
TestAttributes( member.CustomAttributes );
|
|
}
|
|
|
|
foreach ( var inter in type.Interfaces )
|
|
{
|
|
Touch( inter.InterfaceType );
|
|
}
|
|
|
|
if ( type.BaseType is not null )
|
|
{
|
|
TestBaseType( type, type.BaseType );
|
|
}
|
|
|
|
Parallel.ForEach( type.Methods, new ParallelOptions { MaxDegreeOfParallelism = 8, TaskScheduler = TaskScheduler.Default }, member =>
|
|
{
|
|
TestMethod( member );
|
|
TestAttributes( member.CustomAttributes );
|
|
} );
|
|
}
|
|
|
|
private void TestBaseType( TypeDefinition parent, TypeReference baseType )
|
|
{
|
|
var typeInformation = baseType.Resolve();
|
|
var baseTypeName = $"{typeInformation.Module.Assembly.Name.Name}{typeInformation.FullName}";
|
|
|
|
// this is a struct - we can trust it
|
|
if ( baseTypeName == "System.Private.CoreLibSystem.ValueType" ) return;
|
|
|
|
// this is an enum - we can trust it
|
|
if ( baseTypeName == "System.Private.CoreLibSystem.Enum" ) return;
|
|
|
|
// this is basically no baseclass, or a static class
|
|
if ( baseTypeName == "System.Private.CoreLibSystem.Object" ) return;
|
|
|
|
var constructors = typeInformation.GetConstructors().ToArray();
|
|
var parentConstructors = parent.GetConstructors().ToArray();
|
|
|
|
//
|
|
// no parent constructors - then what are they doing? Have they modified the assembly?
|
|
// in this case check that every child constructor is allowed
|
|
//
|
|
if ( parentConstructors.Length == 0 )
|
|
{
|
|
Console.WriteLine( $"Type {parent} had no constructors {baseTypeName}" );
|
|
TestType( typeInformation );
|
|
|
|
foreach ( var c in constructors )
|
|
{
|
|
Touch( c );
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//
|
|
// The assumption here is that their class constructor is going to call the base
|
|
// class constructor. If that's allowed then it's going to get picked up in the
|
|
// normal method checks.
|
|
//
|
|
// Could it be a problem if the base class has no constructor? Is that legal?
|
|
// Is it possible to edit the assembly so the constructor of this class never calls
|
|
// the base class and triggers this stuff?
|
|
// If the base class has a constructor, should we be making sure that at least one
|
|
// of them is being called from our constructors?
|
|
//
|
|
}
|
|
|
|
private void TestProperty( PropertyDefinition member )
|
|
{
|
|
if ( member.PropertyType.IsGenericParameter )
|
|
return;
|
|
|
|
Touch( member.PropertyType );
|
|
}
|
|
|
|
private void TestAttributes( Mono.Collections.Generic.Collection<CustomAttribute> attributes )
|
|
{
|
|
if ( attributes == null )
|
|
return;
|
|
|
|
foreach ( var attr in attributes )
|
|
{
|
|
TestAttribute( attr );
|
|
}
|
|
|
|
}
|
|
|
|
private void TestField( FieldDefinition member )
|
|
{
|
|
if ( member.ContainsGenericParameter )
|
|
return;
|
|
|
|
if ( member.HasLayoutInfo )
|
|
{
|
|
Touch( $"System.Private.CoreLib/System.Runtime.InteropServices.FieldOffset", "attribute" );
|
|
}
|
|
|
|
if ( member.HasMarshalInfo )
|
|
{
|
|
Touch( $"System.Private.CoreLib/System.Runtime.InteropServices.MarshalAs", "attribute" );
|
|
}
|
|
|
|
Touch( member.FieldType );
|
|
}
|
|
|
|
private void TestMethod( MethodDefinition member )
|
|
{
|
|
if ( member.IsNative ) Touch( $"System.Private.CoreLib/System.Runtime.InteropServices.DllImportAttribute", "attribute" );
|
|
if ( member.IsPInvokeImpl ) Touch( $"System.Private.CoreLib/System.Runtime.InteropServices.DllImportAttribute", "attribute" );
|
|
if ( member.IsUnmanagedExport ) Touch( $"System.Private.CoreLib/System.Runtime.InteropServices.DllImportAttribute", "attribute" );
|
|
|
|
if ( member.DebugInformation.HasSequencePoints && member.DebugInformation.SequencePoints.FirstOrDefault( x => !x.IsHidden ) is { } sequencePoint )
|
|
UpdateLocation( sequencePoint );
|
|
else
|
|
Location = new AccessControl.CodeLocation( member.FullName );
|
|
|
|
Touch( member );
|
|
|
|
if ( ShouldSkipExploration( member.Module.Assembly ) )
|
|
return;
|
|
|
|
Touch( member.MethodReturnType.ReturnType );
|
|
|
|
if ( member.HasBody )
|
|
{
|
|
foreach ( var variable in member.Body.Variables )
|
|
{
|
|
if ( variable.IsPinned )
|
|
Touch( $"System.Private.CoreLib/System.Security.UnverifiableCodeAttribute", "attribute" );
|
|
}
|
|
|
|
foreach ( var instruction in member.Body.Instructions )
|
|
{
|
|
var l = Location;
|
|
UpdateLocation( member.DebugInformation.GetSequencePoint( instruction ) );
|
|
TestInstruction( member, instruction );
|
|
Location = l;
|
|
}
|
|
}
|
|
|
|
foreach ( var parameter in member.Parameters )
|
|
{
|
|
foreach ( var attribute in parameter.CustomAttributes )
|
|
{
|
|
Touch( attribute.AttributeType );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void TestAttribute( CustomAttribute attr )
|
|
{
|
|
Touch( attr.AttributeType );
|
|
|
|
foreach ( var arg in attr.ConstructorArguments )
|
|
{
|
|
Touch( arg.Type );
|
|
}
|
|
}
|
|
|
|
void TestOpCode( OpCode opcode )
|
|
{
|
|
// todo - whitelist instead of blacklist?
|
|
|
|
if ( opcode == OpCodes.Cpobj )
|
|
{
|
|
Touch( $"System.Private.CoreLib/System.Runtime.Opcode.Cpobj", "class" );
|
|
}
|
|
|
|
//Touch( $"System.Private.CoreLib/System.Runtime.Opcode.{opcode}", "class" );
|
|
}
|
|
|
|
private void TestInstruction( MethodDefinition method, Instruction instruction )
|
|
{
|
|
TestOpCode( instruction.OpCode );
|
|
|
|
// Skipping these just so the log at the bottom of this function isn't spammy
|
|
if ( instruction.Operand is string ) return;
|
|
if ( instruction.Operand is float ) return;
|
|
if ( instruction.Operand is double ) return;
|
|
if ( instruction.Operand is int ) return;
|
|
if ( instruction.Operand is sbyte ) return;
|
|
|
|
if ( instruction.Operand is Instruction[] instructions )
|
|
{
|
|
foreach ( var i in instructions )
|
|
{
|
|
TestInstruction( method, i );
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if ( instruction.Operand is MethodReference methodref )
|
|
{
|
|
if ( methodref.DeclaringType.IsArray )
|
|
{
|
|
Touch( methodref.DeclaringType.Resolve() );
|
|
}
|
|
else
|
|
{
|
|
Touch( methodref );
|
|
}
|
|
|
|
|
|
Touch( methodref.ReturnType );
|
|
|
|
if ( methodref.DeclaringType is GenericInstanceType git )
|
|
{
|
|
foreach ( var param in git.GenericArguments )
|
|
{
|
|
Touch( param );
|
|
}
|
|
}
|
|
|
|
if ( instruction.Operand is GenericInstanceMethod gim )
|
|
{
|
|
foreach ( var m in gim.GenericArguments )
|
|
{
|
|
Touch( m );
|
|
}
|
|
}
|
|
|
|
foreach ( var param in methodref.GenericParameters )
|
|
{
|
|
Touch( param );
|
|
}
|
|
|
|
foreach ( var param in methodref.Parameters )
|
|
{
|
|
Touch( param.ParameterType );
|
|
}
|
|
|
|
//
|
|
// The compiler catches base.Finalize() or this.Finalize() and errors as CS0245
|
|
// However you can get a reference and invoke that action just fine, which can be abused and should be invalid.
|
|
// We check against ldftn/ldvirtftn only as call is valid runtime usage from the compiler
|
|
//
|
|
if ( methodref.Name == Microsoft.CodeAnalysis.WellKnownMemberNames.DestructorName && (instruction.OpCode.Code == Code.Ldftn || instruction.OpCode.Code == Code.Ldvirtftn) )
|
|
{
|
|
// Dummy invalid method
|
|
Touch( $"System.Private.CoreLib/System.InvalidFinalizeMethodReference", "method" );
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if ( instruction.Operand is VariableDefinition vardef )
|
|
{
|
|
Touch( vardef.VariableType );
|
|
return;
|
|
}
|
|
|
|
if ( instruction.Operand is FieldDefinition fielddef )
|
|
{
|
|
Touch( fielddef.FieldType );
|
|
return;
|
|
}
|
|
|
|
if ( instruction.Operand is TypeReference typeRef )
|
|
{
|
|
Touch( typeRef );
|
|
return;
|
|
}
|
|
|
|
if ( instruction.Operand is FieldReference fieldRef )
|
|
{
|
|
Touch( fieldRef.FieldType );
|
|
return;
|
|
}
|
|
|
|
if ( instruction.Operand is ParameterDefinition paramDef )
|
|
{
|
|
Touch( paramDef.ParameterType );
|
|
return;
|
|
}
|
|
|
|
if ( instruction.Operand is Instruction instrct && instrct != instruction )
|
|
{
|
|
TestInstruction( method, instrct );
|
|
return;
|
|
}
|
|
|
|
if ( instruction.Operand != null )
|
|
{
|
|
//log.Trace( $"Unhandled Instruction: {instruction.Operand.GetType()}" );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private bool ShouldSkipExploration( AssemblyDefinition candidate )
|
|
{
|
|
if ( Assembly == candidate ) return false;
|
|
|
|
// Everything else is either a dependent addon (which we've already checked)
|
|
// Or a system/game dll (which we don't need to explore the contents of)
|
|
|
|
// If we let them use their own dlls we'll need to check those too
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove touches that are inside addon depends or this dll.
|
|
/// </summary>
|
|
void RemoveLocalTouches()
|
|
{
|
|
var whitelist = new List<string>();
|
|
whitelist.AddRange( Global.SafeAssemblies.Select( x => $"{x.Key}/" ) );
|
|
whitelist.Add( $"{Assembly.Name.Name}/" );
|
|
|
|
Parallel.ForEach( Touched.Keys, ( key ) =>
|
|
{
|
|
//
|
|
// Don't bother looking at them if they're in our list of approved
|
|
//
|
|
if ( whitelist.Any( x => key.StartsWith( x ) ) )
|
|
{
|
|
Touched.Remove( key, out var _ );
|
|
return;
|
|
}
|
|
} );
|
|
|
|
}
|
|
}
|