I try to make a simple console modding project in C# where I have my program that contain a list of an abstract class named ElementInGame. I want to be able to create others class that inherit ElementInGame from a .txt file. The class ElementInGame will contain some basic methods (virtual and not virtual). But I don't want these other modded class execute malicious code, I would like that they can only access the methods/properties from the inherited class. Here is my ElementInGame code :
(My C# program #1)
using System;
namespace Modding
{
//The class itself inherit from MarshalByRefObject to be available in 2 differents Domains
public abstract class ElementInGame : MarshalByRefObject
{
public ElementInGame()
{
Console.WriteLine("ElementInGame class created");
}
public virtual int GetNumber()
{
return 10;
}
public void CountToTen()
{
for (int i = 0; i <= 10; i++)
{
Console.WriteLine(i);
}
}
}
}
Then I have my .txt file stored at "C:\program.txt"
(My original .txt file)
using System;
namespace Test
{
public class HelloWorld
{
public HelloWorld()
{
Console.WriteLine("Called Constructor() !");
}
public static int TestMethod()
{
Console.WriteLine("Called TestMethod() !");
return 11;
}
}
}
So I code the main program to read the .txt file, compile it with restrictions, and execute it :
(My C# program #2 in a second .cs file, long code warning)
using System;
using System.CodeDom.Compiler;
using System.IO;
using Microsoft.CSharp;
using System.Reflection;
using System.Security.Permissions;
using System.Security;
using System.Security.Policy;
using System.Runtime.Remoting;
using System.Collections.Generic;
namespace Modding
{
public class Program : MarshalByRefObject
{
public static void Main(string[] args)
{
string assemblyPath = @"C:\program.txt"; // Where the .txt file is stored
string code = File.ReadAllText(assemblyPath); //The code to compile
CompilerResults compile = CompileFromCode(code); //Compile the code in the temporary files
string fullPath = compile.PathToAssembly; //sample : C:\Users\MY_USER_NAME\AppData\Local\Temp\5v2p3qki.dll
string pathWithoutFile = Path.GetDirectoryName(fullPath); //sample : C:\Users\MY_USER_NAME\AppData\Local\Temp
string pathNameOnly = Path.GetFileNameWithoutExtension(fullPath); //sample : 5v2p3qki
Program newDomainInstance = GetOtherProtectedDomainInstance(pathWithoutFile);
newDomainInstance.CallMethod(pathNameOnly, "Test.HelloWorld", "TestMethod", null, null);
newDomainInstance.CreateObject(pathNameOnly,"Test.HelloWorld");
List<ElementInGame> allElement = new List<ElementInGame>();
//allElement.Add ***?***
Console.ReadKey();
}
public static Program GetOtherProtectedDomainInstance(string pathWithoutFile)
{
AppDomainSetup adSetup = new AppDomainSetup();
adSetup.ApplicationBase = pathWithoutFile;
//Set some permissions to avoid malicious code
PermissionSet permSet = new PermissionSet(PermissionState.None);
permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
StrongName fullTrustAssembly = new StrongName(
new StrongNamePublicKeyBlob(typeof(Program).Assembly.GetName().GetPublicKey()),
typeof(Program).Assembly.GetName().Name,
typeof(Program).Assembly.GetName().Version);
AppDomain newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, fullTrustAssembly);
ObjectHandle handle = Activator.CreateInstanceFrom(
newDomain, typeof(Program).Assembly.ManifestModule.FullyQualifiedName,
typeof(Program).FullName
);
Program newDomainInstance = (Program)handle.Unwrap();
return newDomainInstance;
}
public static CompilerResults CompileFromCode(string code)
{
//Compile the code in a .dll locate in the temporary files
//The following code is based on https://stackoverflow.com/questions/10314815/trying-to-compile-and-execute-c-sharp-code-programmatically
CompilerParameters CompilerParams = new CompilerParameters();
string outputDirectory = Directory.GetCurrentDirectory();
CompilerParams.GenerateInMemory = false;
CompilerParams.TreatWarningsAsErrors = false;
CompilerParams.GenerateExecutable = false;
CompilerParams.CompilerOptions = "/optimize";
//Adding a reference to the current project to allow the .txt file to inherit the class "ElementInGame" later
string[] references = { "System.dll", Assembly.GetEntryAssembly().Location };
CompilerParams.ReferencedAssemblies.AddRange(references);
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerResults compile = provider.CompileAssemblyFromSource(CompilerParams, code);
if (compile.Errors.HasErrors)
{
string text = "Compile error: ";
foreach (CompilerError ce in compile.Errors)
{
text += "rn" + ce.ToString();
}
throw new Exception(text);
}
return compile;
}
public static void DisplaySomething()//Useful for later
{
Console.WriteLine("This isn't supposed to be display");
}
//Calling a method from the restricted Domain
public void CallMethod(string assemblyName, string typeName, string entryPoint, object objectToExecute = null, object[] parameters = null)
{
MethodInfo target = Assembly.Load(assemblyName).GetType(typeName).GetMethod(entryPoint);
try
{
target.Invoke(objectToExecute, parameters);
}
catch
{
Console.WriteLine("Security Error with Method " + assemblyName + " namespace : " + typeName + " method : " + entryPoint);
}
}
//Create an instance from the restricted Domain
public void CreateObject(string assemblyName, string typeName)
{
try
{
object o = Assembly.Load(assemblyName).CreateInstance(typeName);
}
catch
{
Console.WriteLine("Security Error with Constructor " + assemblyName + " namespace : " + typeName);
}
}
}
}
For the moment the .txt file don't have any link at all with my C# program. The code work properly and I got the following output :
Called TestMethod() !
Called Constructor() !
Then I edit my code in my .txt file to inherit from Modding.ElementInGame :
(My edited .txt file)
using System;
namespace Test
{
public class HelloWorld : Modding.ElementInGame
{
public HelloWorld() : base()
{
Console.WriteLine("Called Constructor() !");
}
public static int TestMethod()
{
Console.WriteLine("Called TestMethod() !");
return 11;
}
}
}
So I expected an output like :
Called TestMethod() !
ElementInGame class created
Called Constructor() !
But after this change, the program crash with a System.NullReferenceException
at when calling the TestMethod : newDomainInstance.CallMethod(pathNameOnly, "Test.HelloWorld", "TestMethod", null, null);
However creating an instance of HelloWorld (the .txt file): newDomainInstance.CreateObject(pathNameOnly,"Test.HelloWorld");
seem to works (no crash, the code stay in the try part when doing the try/catch), but my there is nothings print in my console, so it doesn't work I guess ?
Changing the permission of the AppDomain change nothing.
PermissionSet permSet = new PermissionSet(PermissionState.Unrestricted);
permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.AllFlags));
So my question is : How can I create and store an instance of the .txt file in my program that inherit from ElementInGame (and add it to the list of ElementInGame) ?
That way I can use from my program the virtual method GetNumber(). I don't want the .txt file have access to the program itself (like calling the method DisplaySomething()), just communicate with ElementInGame.
You are generating and loading reference assemblies from different locations. You did set the current directory for output but forgot to assign it to compiler parameters.
string outputDirectory = Directory.GetCurrentDirectory();
CompilerParams.OutputAssembly = Path.Combine(outputDirectory, "Test.dll");
This should fix the issue.