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.
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!