This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Grim Reaper's profile picture

Protected Program Executer

Started by Grim Reaper, 16 May 2013 - 10:43 PM
Grim Reaper #1
Posted 17 May 2013 - 12:43 AM
I've seen a couple of anti-virus programs floating around the forums and wanted to give my go at it and this just happens to be the finished product. Initially, I wanted to create something that would scan the entire file system (disregarding the 'rom', of course) and checking all files individually for potential malicious function calls. However, I found that while working towards this goal I would constantly think of another work-around for the system I was developing, so I switched the goal to something more realistic.

This program allows you to execute programs with protection so that any time a program wants to call a potentially malicious function, you are prompted for your permission the program to execute said call.
For example, if I wasn't sure if a program I just downloaded would wipe my in-game computer clean and didn't want to read through the thousands of lines of code or messy code then I could type the following into my shell:

av programName
Of course, the program doesn't have to be named 'av' nor does the program you're executing need to be named 'programName'. I digress. This command would cause the program to be executed, but if it tried to call 'fs.delete' I would get the following prompt. I could either press 'Y' or 'N' depending on whether or not I wanted to allow the call or not. That's the gist of it, but hopefully it will save a few people's work or perhaps just be an interesting concept to think about.

In advance, I apologize for the simple UI, but I really didn't think this program needed anything extravagant in order to serve its purpose.

Here is the code:
Spoiler


--[[
	Protected Execute	   Trystan Cannon
							16 May 2013

	This program allows for users to run other
	programs without the risk of potentially
	malicious functions to execute without their
	allowing so. Currently, this program only
	prevents unauthorized 'fs' and 'os' calls.

	Version: 1.0 Updated 16 May 2013
--]]

-- Variables ------------------------------------
local NATIVE_TERMINAL = term.native -- A reference to the native terminal functions.
_G.screenBuffer	   = {} -- A table containing the visual contents of the screen.

local WARNING_BOX_WIDTH, WARNING_BOX_HEIGHT = 25, 7 -- The width and height of every warning box that is drawn.
local oldFunctions						  = {}
-- Variables ------------------------------------

-- Screen Buffer Functions ------------------------------------
-- Checks if the given color is a valid color on the computer. Returns true or false depending on the aforementioned condition.
local function isColorValid (color)
	for colorIndex, colorValue in pairs (colors) do
		if colorValue == color and type (colorValue) == "number" then
			return true
		end
	end

	return false
end

-- Initializes the global table 'screenBuffer' table to keep track of the screen's contents. Also, this method redirects the terminal
-- to write to the screen buffer and the screen.
local function initScreenBuffer()
	-- Initialize the screen buffer to be an empty canvas of cells whose text color is white and background color is black.
	screenBuffer.currentTextColor	   = colors.white
	screenBuffer.currentBackgroundColor = colors.black

	local screenWidth, screenHeight = term.getSize()
	for height = 1, screenHeight do
		screenBuffer[height] = {}

		for width = 1, screenWidth do
			screenBuffer[height][width] = {text = ' ', textColor = colors.white, backgroundColor = colors.black}
		end
	end

	-- Create a new terminal table which will have all of the native functions with some edited functionality.
	local terminal = {}
	for functionName, functionObject in pairs (NATIVE_TERMINAL) do
		terminal[functionName] = functionObject
	end

	-- Overrides the term.write method to write to the screen buffer and the screen.
	terminal.write = function (text)
		local cursorX, cursorY = term.getCursorPos()

		if cursorX < 1 or cursorX > screenWidth or cursorY < 1 or cursorY > screenHeight then
			return NATIVE_TERMINAL.write (text)
		end

		for charIndex = 1, text:len() do
			screenBuffer[cursorY][cursorX].text			= text:sub (charIndex, charIndex)
			screenBuffer[cursorY][cursorX].textColor	   = screenBuffer.currentTextColor
			screenBuffer[cursorY][cursorX].backgroundColor = screenBuffer.currentBackgroundColor
			cursorX = cursorX + 1

			if cursorX > screenWidth then
				break
			end
		end

		return NATIVE_TERMINAL.write (text)
	end

	-- Overrides the term.setTextColor method to set the current text color of the screen buffer and the terminal.
	terminal.setTextColor = function (textColor)
		if isColorValid (textColor) then
			screenBuffer.currentTextColor = textColor
		end

		return NATIVE_TERMINAL.setTextColor (textColor)
	end

	-- Overrides the term.setBackgroundColor method to set the current background color of the screen buffer and the terminal.
	terminal.setBackgroundColor = function (backgroundColor)
		if isColorValid (backgroundColor) then
			screenBuffer.currentBackgroundColor = backgroundColor
		end

		return NATIVE_TERMINAL.setBackgroundColor (backgroundColor)
	end

	-- Overrides the term.clear method to clear out the entire buffer and the terminal.
	terminal.clear = function()
		for line = 1, screenHeight do
			for width = 1, screenWidth do
				local cell = screenBuffer[line][width]

				cell.text			= ' '
				cell.textColor	   = screenBuffer.currentTextColor
				cell.backgroundColor = screenBuffer.currentBackgroundColor
			end
		end

		return NATIVE_TERMINAL.clear()
	end

	-- Overrides the term.clearLine method to clear out only the current line in the buffer and terminal.
	terminal.cearLine = function()
		local _, currentLine = term.getCursorPos()

		if currentLine > 0 and currentLine <= screenHeight then
			for width = 1, screenWidth do
				local cell = screenBuffer[currentLine][width]

				cell.text			= ' '
				cell.textColor	   = screenBuffer.currentTextColor
				cell.backgroundColor = screenBuffer.currentBackgroundColor
			end
		end

		return NATIVE_TERMINAL.clearLine()
	end

	term.redirect (terminal)
end

-- Kills the screen buffer by emptying the buffer and restoring the terminal to its native functionality.
local function killScreenBuffer()
	screenBuffer = {}
	term.redirect (NATIVE_TERMINAL)
end
-- Screen Buffer Functions ------------------------------------

-- Anti Virus Functions ------------------------------------
-- Prompts the user if a program being executed is trying to execute a malicious function. The user
-- will be asked if they want the program to be allowed to execute the function. However, if the user says
-- no, then the program might not work properly. Returns true or false depending on the Y/N value of the
-- user's input.
local function promptUserOfMaliciousFunctionCall (functionName)
	local cursorX, cursorY		  = term.getCursorPos()
	local screenWidth, screenHeight = term.getSize()
	local MAX_MESSAGE_WIDTH		 = WARNING_BOX_WIDTH - 4

	-- Draws a string in the center of the screen.
	local function drawCentered (myString, line)
		term.setCursorPos (screenWidth / 2 - myString:len() / 2, line)
		NATIVE_TERMINAL.write (myString)
	end

	-- Draw a simple black and white box in the middle of the screen which describes the situation.
	local currentTextColor, currentBackgroundColor = screenBuffer.currentTextColor, screenBuffer.currentBackgroundColor
	term.setTextColor (colors.white)
	term.setBackgroundColor (colors.black)

	drawCentered ('+' .. string.rep ('-', WARNING_BOX_WIDTH - 2) .. '+', screenHeight / 2 - WARNING_BOX_HEIGHT / 2)
	for line = 1, WARNING_BOX_HEIGHT do
		drawCentered ('|' .. string.rep (' ', WARNING_BOX_WIDTH - 2) .. '|', line + screenHeight / 2 - WARNING_BOX_HEIGHT / 2)
	end
	drawCentered ('+' .. string.rep ('-', WARNING_BOX_WIDTH - 2) .. '+', screenHeight / 2 + WARNING_BOX_HEIGHT / 2)

	drawCentered ("Malicious call:", screenHeight / 2 - WARNING_BOX_HEIGHT / 2 + 1)
	drawCentered (functionName:sub (1, MAX_MESSAGE_WIDTH), screenHeight / 2 - WARNING_BOX_HEIGHT / 2 + 3)
	drawCentered ("Allow call? Y \\ N?", screenHeight / 2 - WARNING_BOX_HEIGHT / 2 + 5)

	-- Wait for the user to press 'Y' or 'N' signaling their answer.
	local char = nil
	while not (char == 'y' or char == 'n') do
		_, char = oldFunctions.os.pullEvent ("char")
	end

	-- Restore the screen to the way it was.
	for lineNumber = 1, screenHeight do
		for cellNumber = 1, screenWidth do
			local cell = screenBuffer[lineNumber][cellNumber]

			NATIVE_TERMINAL.setTextColor (cell.textColor)
			NATIVE_TERMINAL.setBackgroundColor (cell.backgroundColor)
			NATIVE_TERMINAL.setCursorPos (cellNumber, lineNumber)
			NATIVE_TERMINAL.write (cell.text)
		end
	end
	NATIVE_TERMINAL.setCursorPos (cursorX, cursorY)
	NATIVE_TERMINAL.setTextColor (screenBuffer.currentTextColor)
	NATIVE_TERMINAL.setBackgroundColor (screenBuffer.currentBackgroundColor)

	return char == 'y'
end

-- Secures all of the functions in the APIs specified in 'maliciousAPIs' to prompt the user if they want to allow some program to execute
-- a potentially malicious function.
-- Returns all of the old functions in a table.
local function secureMaliciousAPIs()
	local oldFunctions = {}

	-- Secure the os API.
	oldFunctions.os = {}
	for functionName, functionObject in pairs (os) do
		oldFunctions.os[functionName] = functionObject

		os[functionName] = function (...)
			if promptUserOfMaliciousFunctionCall ("os." .. functionName) then
				return oldFunctions.os[functionName] (...)
			else
				return nil
			end
		end
	end

	-- Clean up the exceptions for the os API.
	os.startTimer   = oldFunctions.os.startTimer
	os.pullEvent	= oldFunctions.os.pullEvent
	os.pullEventRaw = oldFunctions.os.pullEventRaw

	-- Secure the fs API.
	oldFunctions.fs = {}
	for functionName, functionObject in pairs (fs) do
		oldFunctions.fs[functionName] = functionObject

		fs[functionName] = function (...)
			if promptUserOfMaliciousFunctionCall ("fs." .. functionName) then
				return oldFunctions.fs[functionName] (...)
			else
				return nil
			end
		end
	end

	-- Clean up the exceptions for the fs API.
	fs.isDir	  = oldFunctions.fs.isDir
	fs.exists	 = oldFunctions.fs.exists
	fs.combine	= oldFunctions.fs.combine
	fs.getName	= oldFunctions.fs.getName
	fs.list	   = oldFunctions.fs.list
	fs.isReadOnly = oldFunctions.fs.isReadOnly

	return oldFunctions
end

-- Restores all of the secured APIs to their previous states given a table containing the states of all of the malicious APIs.
local function restoreMaliciousAPIs (oldFunctions)
	for apiName, api in pairs (oldFunctions) do
		for functionName, functionObject in pairs (api) do
			local currentEnvironment = getfenv (1)

			if _G[apiName] ~= nil then
				_G[apiName][functionName] = functionObject
			elseif currentEnvironment[apiName] ~= nil then
				currentEnvironment[apiName][functionName] = functionObject
			end
		end
	end
end
-- Anti Virus Functions ------------------------------------

-- Get the potential file path that we need to check for malicious calls. If there wasn't a valid file path then print the program usage instructions.
local tArgs	   = { ... }
local path		= (tArgs[1] ~= nil) and shell.resolve (tArgs[1]) or ""
local currentPath = shell.resolve (shell.getRunningProgram())

if path ~= currentPath and fs.exists (path) and not fs.isDir (path) then
	initScreenBuffer()
	oldFunctions = secureMaliciousAPIs()

	local success, errorMessage = pcall (dofile, path)
	restoreMaliciousAPIs (oldFunctions)
	killScreenBuffer()
else
	print ("Usage: " .. fs.getName (currentPath) .. " <filePath>.")
end
Or, you could download it via pastebin.

Thanks for reading, Grim.
theoriginalbit #2
Posted 17 May 2013 - 01:14 AM
Nice work Grim. I really do think that this program is the answer over an av, the av's that have been made to date are shoddy at best… and there are too many ways to get around them, like getting around just a plan detection of fs.delete you can easily do _G.fs['delete'] or _G['fs'].delete or _G['_G']['fs']['delete'] …. I'm sure you see my point and I'm sure you came to the same conclustion…

Can I make one suggestion however. I think that when it is using some fs functions it maybe needs to tell the user which files it is attempting to modify, maybe if you could do some form of detection and just say "It would like access to its temporary files" or something if that is what it is doing, idk, i'll leave that up to you, but I think it might be something nice while still trying to obscure those hidden files for the programs.
wilcomega #3
Posted 17 May 2013 - 04:21 PM
very nice. had something like this planned. by the way, have you ever heard of environments? just wondering :)/>
Grim Reaper #4
Posted 17 May 2013 - 08:54 PM
very nice. had something like this planned. by the way, have you ever heard of environments? just wondering :)/>

Indeed I have. I also think I know what you're hinting at as well in terms of a possible technique to accomplish something similar to what I've done here ;)/>
superaxander #5
Posted 18 May 2013 - 01:33 PM
Nice
Grim Reaper #6
Posted 18 May 2013 - 09:17 PM
Nice

I guess that's one way to get your post count up.
superaxander #7
Posted 19 May 2013 - 04:28 AM
Nice

I guess that's one way to get your post count up.
That was definatly not to get my post count up but just to say I like it. Why be so harsh if I give you a compliment?
Espen #8
Posted 19 May 2013 - 05:16 AM
As this is mostly off-topic, but doesn't really fit into its own topic and is a direct answer to the last post, I'll put it into a spoiler so it doesn't take away too much "view space", if you know what I mean. ^_^/>

Spoiler
That was definatly not to get my post count up but just to say I like it. Why be so harsh if I give you a compliment?
Because it doesn't add anything useful to the topic. And on top of that you're doing it a lot. (Hence, I guess, the "upping" of the post count was mentioned.)
Instead of one-word posts to give a compliment you could just as well click +1 on his OP to show your appreciation.
Just imagine if every single person would make 1-word posts just to say "nice", "good", "lol", or whatever.

It feels like someone on twitter who always says what he/she's doing at the moment:
Feeling good today
Saw a dog
I like the weather
Blue is my favourite color
Cool
Lol
Hehe

Be honest, if you read something like that, would that be of any interest to you? Does it provide you with any useful information?
Maybe on Twitter, if you follow a specific person and your whole point of following the person is to find out about what that person is like.
But on a discussion forum for a Game-Mod it's just a twittery-like blurb that conveys nothing of value. I can't imagine someone thinking anything else than "Aha" after reading it and maybe thinking: "Why not just +1?".

I'm trying not to get on people's wrong side and thus I'm seldomly as direct as this when trying to explain what the perceived animosity about their behaviour is all about. But when someone asks and would like to know, then I'll try my best to explain the situation as I perceive it.
Not to scold you, but as a genuine attempt to help you see/understand what's going on and what you might be able to do in order to help improve the situation.
I don't believe in getting in people's face, because that accomplishes at worst the complete opposite.
And even at its best it will not foster understanding or even acknowledgement in the recipients mind, but just a bad feeling which leads to anger and spite.

So I hope that in explaining all of this, you recognize my genuine attempt trying to improve the situation by (hopefully) illuminating the issue at hand.
We all make mistakes from time to time (I've done some uber-derps too). As long as we want to improve ourselves and have an open ear for constructive criticism, there's no shame in admitting one's failures.

Sorry for the long post, but when does one get the chance to brush such a meta topic.
superaxander #9
Posted 19 May 2013 - 08:15 AM
As this is mostly off-topic, but doesn't really fit into its own topic and is a direct answer to the last post, I'll put it into a spoiler so it doesn't take away too much "view space", if you know what I mean. ^_^/>

Spoiler
That was definatly not to get my post count up but just to say I like it. Why be so harsh if I give you a compliment?
Because it doesn't add anything useful to the topic. And on top of that you're doing it a lot. (Hence, I guess, the "upping" of the post count was mentioned.)
Instead of one-word posts to give a compliment you could just as well click +1 on his OP to show your appreciation.
Just imagine if every single person would make 1-word posts just to say "nice", "good", "lol", or whatever.

It feels like someone on twitter who always says what he/she's doing at the moment:
Feeling good today
Saw a dog
I like the weather
Blue is my favourite color
Cool
Lol
Hehe

Be honest, if you read something like that, would that be of any interest to you? Does it provide you with any useful information?
Maybe on Twitter, if you follow a specific person and your whole point of following the person is to find out about what that person is like.
But on a discussion forum for a Game-Mod it's just a twittery-like blurb that conveys nothing of value. I can't imagine someone thinking anything else than "Aha" after reading it and maybe thinking: "Why not just +1?".

I'm trying not to get on people's wrong side and thus I'm seldomly as direct as this when trying to explain what the perceived animosity about their behaviour is all about. But when someone asks and would like to know, then I'll try my best to explain the situation as I perceive it.
Not to scold you, but as a genuine attempt to help you see/understand what's going on and what you might be able to do in order to help improve the situation.
I don't believe in getting in people's face, because that accomplishes at worst the complete opposite.
And even at its best it will not foster understanding or even acknowledgement in the recipients mind, but just a bad feeling which leads to anger and spite.

So I hope that in explaining all of this, you recognize my genuine attempt trying to improve the situation by (hopefully) illuminating the issue at hand.
We all make mistakes from time to time (I've done some uber-derps too). As long as we want to improve ourselves and have an open ear for constructive criticism, there's no shame in admitting one's failures.

Sorry for the long post, but when does one get the chance to brush such a meta topic.
I am sorry I just wanted to say I liked it I should have taken more time to write that post
Lion4ever #10
Posted 23 August 2015 - 12:45 AM
Grim, you were refering to this in an other post: It needs to be term.native() instead of term.native now to get the table.

Secondly: There are unsafe programs which want parameters it would be useful to be able to forward them to the unsafe program (e.g. "yourProg edit startup") and since you do not use shell.run, shell.getRunningProgramm() returns your program and not the unsafe one.

Thirdly: Because programs tend to call the same funcitons over and over again it would be useful to permatently allow or permanently deny some function &amp; parameter combinations.

local whiteList = {}
local blackList = {}
if whiteList[table.concat({...},";")] then
return oldFunctions.fs[functionName] (...)
elseif blackList[table.concat({...},";")] then
return nil
else
local answer = promptUserOfMaliciousFunctionCall ("fs." .. functionName,...)
if answer == "a" --allways
  whiteList[table.concat({...},";")] = true
elseif answer == "b"  then --block permanent
  blackList[table.concat({...},";")] = true
end
if answer == "y" or answer == "a" then
  return oldFunctions.fs[functionName] (...)
else
  return nil
end
end
You would need to modify the asking function to return the char and accept "a" and "b".
Anavrins #11
Posted 23 August 2015 - 03:41 AM
Nice, I had also thought of doing this, mostly inspired by the old Firewolf's prompt based antivirus.
This is really nice to see this made :D/>
クデル #12
Posted 23 August 2015 - 05:17 AM
Neat, maybe add a second argument to scan the rom too? Might be worried about some shady server owners.
Grim Reaper #13
Posted 01 September 2015 - 12:24 AM
Grim, you were refering to this in an other post: It needs to be term.native() instead of term.native now to get the table.

Secondly: There are unsafe programs which want parameters it would be useful to be able to forward them to the unsafe program (e.g. "yourProg edit startup") and since you do not use shell.run, shell.getRunningProgramm() returns your program and not the unsafe one.

Thirdly: Because programs tend to call the same funcitons over and over again it would be useful to permatently allow or permanently deny some function &amp; parameter combinations.

local whiteList = {}
local blackList = {}
if whiteList[table.concat({...},";")] then
return oldFunctions.fs[functionName] (...)
elseif blackList[table.concat({...},";")] then
return nil
else
local answer = promptUserOfMaliciousFunctionCall ("fs." .. functionName,...)
if answer == "a" --allways
  whiteList[table.concat({...},";")] = true
elseif answer == "b"  then --block permanent
  blackList[table.concat({...},";")] = true
end
if answer == "y" or answer == "a" then
  return oldFunctions.fs[functionName] (...)
else
  return nil
end
end
You would need to modify the asking function to return the char and accept "a" and "b".

These are all valid suggestions. Seeing as this is rather old and more of a proof-of-concept than anything else, I don't think I'll be changing it. Thanks for your input, though! :)/>