Run .net assembly from memory in 3ds Max

A question by momo on cgTalk sparked this article. He asks if it’s possible to run an exe file from memory instead directly from file. The answer is: yes, but only if it’s a .net exe. I’ve looked around to see how it’s done in Visual Studio and then translated that to 3dsMax. For instance here and here.

Why

I can think of two reasons why this would be useful. First of all you could encrypt the bytes of an exe file and distribute it with your other programs. At runtime a launcher would read the bytes into memory, decrypt them and execute the program. Another use is more specific to 3dsMax. This method allows you to store the exe as an actual array of numbers in your maxscript. This makes it easier to distribute with your other files. It’s a similar approach to storing images as base64 strings in scripts described here by LoneRobot.

Execute .net assemblies
Injury by Aha-Soft from the Noun Project

There’s a downside as well. Since you can distribute exe files as ordinary text it’s a great way to keep harmful software unseen. I in no way endorse that of course.

The .NET app

First I’ve created a .net console app.  It prints out a line of text and then it prints all string arguments passed to it. You can set up a new console application in Visual Studio and use this code as the Program class

class Program
{
	static void Main(string[] args)
	{
		Console.WriteLine("This is a test app.");
		foreach (string s in args)
		{
			Console.WriteLine(s);
		}
	}
}

Now you just build the solution and voila! You have your test application. You can name it whatever you like, I’ve called it TestApp.exe.

Converting the app

To use the app in the examples I’ll convert it into two other forms: a textfile with a byte on each line and a Base64 string.

First read the testapp.exe file and write each byte onto a separate line in a text file:

inputPath = @"\\MY\input\path\TestApp.exe"
outputPath = @"\\MY\output\path\TestApp_Bytes.txt"

theFileStream = (dotNetClass "System.io.file").open inputPath (dotnetClass "system.io.filemode").open
theReader = dotnetObject "System.IO.BinaryReader" theFileStream
assemblyBytes = theReader.ReadBytes((dotnetClass "System.Convert").ToInt32 theFileStream.length)
strBytes = "" as StringStream
for n = 1 to assemblyBytes.count do
(
	format "%" assemblyBytes[n] to:strBytes
	if n < assemblyBytes.count do format "\r\n" to:strBytes
)
theFileStream.close()
theReader.close()
(dotNetClass "System.IO.File").WriteAllText outputPath (strBytes as string)

Second, read the testapp and convert it to a Base64 string. This is not stored in a separate file but can be written directly into maxscript files.

inputPath = @"\\MY\input\path\TestApp.exe"
theFileStream = (dotNetClass "System.io.file").open inputPath (dotnetClass "system.io.filemode").open
theLength = theFileStream.length
memstream = dotnetobject "System.IO.MemoryStream"
theReader = dotnetObject "System.IO.BinaryReader" theFileStream
assemblyBytes = theReader.ReadBytes((dotnetClass "System.Convert").ToInt32 theFileStream.length)
memstream.Write assemblyBytes 0 ((dotnetClass "System.Convert").ToInt32 theFileStream.length)
Base64string = (dotnetclass "system.convert").ToBase64String (memstream.ToArray())
theReader.close()
theFileStream.close()
memstream.close()
print Base64string

 

Work the binary file

I’m showing a few methods here you can use separately. The first method will load the testapp.exe file as a byte array. This means you still have to bundle the app with your scripts. The second method loads the test app from a textfile. The third method reads a base64 string. A base64 string is more compact and probably preferable over lines of numbers. Finally you can omit loading any file and just provide an array of bytes yourself. This is the easiest way for distribution since you can hardcode this array directly into your scriptfile.

First, let’s load the testapp from disk:

function fn_binaryFileAsBytes filePath =
(
	/*
	Description
		Reads a binary file and returns a byte array
	Arguments
		 filePath: the path to the file
	Return
		<System.Byte[]> a .net byte array
	*/
	
	local theFileStream = (dotNetClass "System.io.file").open filePath (dotnetClass "system.io.filemode").open
	local theReader = dotnetObject "System.IO.BinaryReader" theFileStream
	local assemblyBytes = theReader.ReadBytes((dotnetClass "System.Convert").ToInt32 theFileStream.length)
	theFileStream.close()
	theReader.close()
	assemblyBytes
)

Here you see I first read the file as a filestream and then read the bytes of the stream in one go. Don’t forget to close the stream and the reader. If the app you’re loading is rather large you can also load it in chunks. However, since you’re loading the entire app in memory anyway there’s no real advantage to that.

Second, load the testapp from a textfile where each line contains one byte

function fn_textFileAsBytes filePath =
(
	/*
	Description
		Reads a textfile line by line, converts each line to a byte and return a byte array
	Arguments
		 filePath: the path to the textfile
	Return
		 an array with bytes
	*/
	
	local theLines = (dotNetClass "System.io.file").ReadLines filepath
	local assemblyBytes = #()
	local enumerator = theLines.GetEnumerator()
	while enumerator.MoveNext() do
	(
		append assemblyBytes (enumerator.current as integer)
	)	
	assemblyBytes
)

Here I read the textfile into separate lines and iterate over each line. Each line is read as an integer into an ordinary array.

The next method converts a Base64 string to a byte array. A Base64 string looks something like this

Qk0YEQAAAAAAADYAAAAoAAAAIwAAACgAAAABABgAAAAAAOIQAAAgLgAAIC4AAAAAAAAAAAAAM2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0NGc1Nmg2NWk2NWk2Nmo3Nms3NWs2NWo2Nms3Nms3NWo3NWk2NWg2NWg2NGc1M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0AAAAM2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0NWo3N203MGkxKGQqJmAnJF0lI1skJlwnLmEvMWIyKl8rJV0mJV4mJl8nKGIoLGYtNGs1Nms3NGg1M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2Y0AAAAM2Y0M2Y0M2Y0M2Y0M2Y0M2Y0M2c0NGg2MmYzG04bGUEZJUEmLkMuMUMwJz0nFjEWEDMPGDwYEzETHzYeKj0qLkAuKEAoID8gFkQXI1okNWg1NGc1M

assemblyBytes = (dotnetclass "System.Convert").FromBase64String Base64string

Finally you can just write an array of bytes directly into your script. I won’t show the entire byte array for my testapp but it looks something like this. You can see it’s just a regular array with numbers ranging from 0 to 255.

assemblyBytes = #(77, 90, 144, 0, 3, 0, 0, 0, 4, 0, 0, 0, 255, 255, 0, 0, 184, 0, 0, 0, ...)

 

Using the app in memory

Once you’ve got a byte array in memory you can invoke it. Depending on your app you need to supply one or more arguments. In my case my app takes an array of strings as argument. I need to package this array of strings into an array of objects. It’s a bit specific but let’s roll with it. If you don’t have any arguments, just replace the arguments with an undefined value.

Here’s the method which actually runs the byte array as an exe. I’ve also added a routine which captures the console output to a string. My testapp prints out stuff to the console, but if I don’t grab that it will get lost without ever being seen. If your app doesn’t have output like this, you can ignore or remove that.

function fn_invokeAssemblyAsBytes assemblyBytes invokeArgs: =
(
	/*
	Description
		invokes an assembly supplied as a byte array. Works exclusively with .net assemblies
	Arguments
		<System.Byte[]> assemblyBytes: the assembly in the shape of a .net byte array
		<System.Object[]> invokeArgs: supply this if the assembly takes arguments
	Return
		 console output, if any
	*/
	
	local theAssembly = (dotnetClass "System.Reflection.Assembly").Load assemblyBytes
	
	--If there is output to a console we can catch that output in order to display it in 3dsMax
	--https://saezndaree.wordpress.com/2009/03/29/how-to-redirect-the-consoles-output-to-a-textbox-in-c/
	local memStream = dotnetObject "System.IO.MemoryStream" 1000
	local streamWriter = dotnetObject "System.IO.StreamWriter" memStream
	(dotnetClass "System.Console").SetOut streamWriter
	
	--actually execute the assembly
	--http://www.vcskicks.com/exe-from-memory.php
	if invokeArgs == unsupplied then invokeArgs = undefined
	theAssembly.EntryPoint.Invoke undefined invokeArgs
	
	--process the output
	streamWriter.close()
	local outputString = (dotnetClass "System.Text.Encoding").Default.GetString (memStream.ToArray())
	memStream.close()
	outputString
)

Putting it all together it looks something like this

--my .net console app takes an array of strings as an argument. We have to feed this string array as an object array into the invoke method
stringArgs = dotnet.valuetodotnetobject #("Hello","World") (dotnetclass "System.String[]")
invokeArgs = dotnetObject "System.Object[]" 1
invokeArgs.SetValue stringArgs 0
textFilePath = @"\\MY\path\TestApp_Bytes.txt"
bytes = fn_textFileAsBytes textFilePath
outputString = fn_invokeAssemblyAsBytes bytes invokeArgs:invokeArgs

I hope this is useful to you and helps you distribute and run benign software within 3dsMax!

 

Leave a Reply

* Will not be published