Je pensais donc écrire un compilateur et un environnement d'exécution C# en ligne. Et bien sûr, le problème n°1 est la sécurité. J'ai fini par créer un domaine d'application peu privilégié pour le code utilisateur et le lancer dans un nouveau processus dont la consommation de processeur et de mémoire est étroitement surveillée. Les espaces de noms standard des applications de la console sont disponibles. Ma question est donc la suivante : pouvez-vous imaginer des moyens de casser quelque chose d'une manière ou d'une autre ? Vous pouvez tester vos idées sur place rundotnet .
Edit2 Si quelqu'un s'intéresse au code, il y a maintenant un fork open source de ce projet : rextester sur github
Edit1 En réponse à l'un des commentaires, voici quelques exemples de code.
Donc, en gros, vous créez une application console. Je vais juste poster un gros morceau de celui-ci :
class Sandboxer : MarshalByRefObject
{
private static object[] parameters = { new string[] { "parameter for the curious" } };
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
string pathToUntrusted = args[0].Replace("|_|", " ");
string untrustedAssembly = args[1];
string entryPointString = args[2];
string[] parts = entryPointString.Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries);
string name_space = parts[0];
string class_name = parts[1];
string method_name = parts[2];
//Setting the AppDomainSetup. It is very important to set the ApplicationBase to a folder
//other than the one in which the sandboxer resides.
AppDomainSetup adSetup = new AppDomainSetup();
adSetup.ApplicationBase = Path.GetFullPath(pathToUntrusted);
//Setting the permissions for the AppDomain. We give the permission to execute and to
//read/discover the location where the untrusted code is loaded.
PermissionSet permSet = new PermissionSet(PermissionState.None);
permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
//Now we have everything we need to create the AppDomain, so let's create it.
AppDomain newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, null);
//Use CreateInstanceFrom to load an instance of the Sandboxer class into the
//new AppDomain.
ObjectHandle handle = Activator.CreateInstanceFrom(
newDomain, typeof(Sandboxer).Assembly.ManifestModule.FullyQualifiedName,
typeof(Sandboxer).FullName
);
//Unwrap the new domain instance into a reference in this domain and use it to execute the
//untrusted code.
Sandboxer newDomainInstance = (Sandboxer)handle.Unwrap();
Job job = new Job(newDomainInstance, untrustedAssembly, name_space, class_name, method_name, parameters);
Thread thread = new Thread(new ThreadStart(job.DoJob));
thread.Start();
thread.Join(10000);
if (thread.ThreadState != ThreadState.Stopped)
{
thread.Abort();
Console.Error.WriteLine("Job taking too long. Aborted.");
}
AppDomain.Unload(newDomain);
}
public void ExecuteUntrustedCode(string assemblyName, string name_space, string class_name, string method_name, object[] parameters)
{
MethodInfo target = null;
try
{
target = Assembly.Load(assemblyName).GetType(name_space+"."+class_name).GetMethod(method_name);
if (target == null)
throw new Exception();
}
catch (Exception)
{
Console.Error.WriteLine("Entry method '{0}' in class '{1}' in namespace '{2}' not found.", method_name, class_name, name_space);
return;
}
...
//Now invoke the method.
try
{
target.Invoke(null, parameters);
}
catch (Exception e)
{
...
}
}
}
class Job
{
Sandboxer sandboxer = null;
string assemblyName;
string name_space;
string class_name;
string method_name;
object[] parameters;
public Job(Sandboxer sandboxer, string assemblyName, string name_space, string class_name, string method_name, object[] parameters)
{
this.sandboxer = sandboxer;
this.assemblyName = assemblyName;
this.name_space = name_space;
this.class_name = class_name;
this.method_name = method_name;
this.parameters = parameters;
}
public void DoJob()
{
try
{
sandboxer.ExecuteUntrustedCode(assemblyName, name_space, class_name, method_name, parameters);
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message);
}
}
}
Vous compilez ce qui précède et vous obtenez un exécutable que vous démarrez et surveillez dans un nouveau processus :
using (Process process = new Process())
{
try
{
double TotalMemoryInBytes = 0;
double TotalThreadCount = 0;
int samplesCount = 0;
process.StartInfo.FileName = /*path to sandboxer*/;
process.StartInfo.Arguments = folder.Replace(" ", "|_|") + " " + assemblyName + " Rextester|Program|Main"; //assemblyName - assembly that contains compiled user code
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
DateTime start = DateTime.Now;
process.Start();
OutputReader output = new OutputReader(process.StandardOutput);
Thread outputReader = new Thread(new ThreadStart(output.ReadOutput));
outputReader.Start();
OutputReader error = new OutputReader(process.StandardError);
Thread errorReader = new Thread(new ThreadStart(error.ReadOutput));
errorReader.Start();
do
{
// Refresh the current process property values.
process.Refresh();
if (!process.HasExited)
{
try
{
var proc = process.TotalProcessorTime;
// Update the values for the overall peak memory statistics.
var mem1 = process.PagedMemorySize64;
var mem2 = process.PrivateMemorySize64;
//update stats
TotalMemoryInBytes += (mem1 + mem2);
TotalThreadCount += (process.Threads.Count);
samplesCount++;
if (proc.TotalSeconds > 5 || mem1 + mem2 > 100000000 || process.Threads.Count > 100 || start + TimeSpan.FromSeconds(10) < DateTime.Now)
{
var time = proc.TotalSeconds;
var mem = mem1 + mem2;
process.Kill();
...
}
}
catch (InvalidOperationException)
{
break;
}
}
}
while (!process.WaitForExit(10)); //check process every 10 milliseconds
process.WaitForExit();
...
}
...
class OutputReader
{
StreamReader reader;
public string Output
{
get;
set;
}
StringBuilder sb = new StringBuilder();
public StringBuilder Builder
{
get
{
return sb;
}
}
public OutputReader(StreamReader reader)
{
this.reader = reader;
}
public void ReadOutput()
{
try
{
int bufferSize = 40000;
byte[] buffer = new byte[bufferSize];
int outputLimit = 200000;
int count;
bool addMore = true;
while (true)
{
Thread.Sleep(10);
count = reader.BaseStream.Read(buffer, 0, bufferSize);
if (count != 0)
{
if (addMore)
{
sb.Append(Encoding.UTF8.GetString(buffer, 0, count));
if (sb.Length > outputLimit)
{
sb.Append("\n\n...");
addMore = false;
}
}
}
else
break;
}
Output = sb.ToString();
}
catch (Exception e)
{
...
}
}
}
Les assemblages que le code utilisateur peut utiliser sont ajoutés au moment de la compilation :
CompilerParameters cp = new CompilerParameters();
cp.GenerateExecutable = false;
cp.OutputAssembly = ...
cp.GenerateInMemory = false;
cp.TreatWarningsAsErrors = false;
cp.WarningLevel = 4;
cp.IncludeDebugInformation = false;
cp.ReferencedAssemblies.Add("System.dll");
cp.ReferencedAssemblies.Add("System.Core.dll");
cp.ReferencedAssemblies.Add("System.Data.dll");
cp.ReferencedAssemblies.Add("System.Data.DataSetExtensions.dll");
cp.ReferencedAssemblies.Add("System.Xml.dll");
cp.ReferencedAssemblies.Add("System.Xml.Linq.dll");
using (CodeDomProvider provider = CodeDomProvider.CreateProvider(/*language*/))
{
cr = provider.CompileAssemblyFromSource(cp, new string[] { data.Program });
}