Legato is a tool that allows you as a developer to do almost anything you want: change registry keys, move files around, and create or delete application setting files. Because there’s a great deal of power behind Legato, sometimes we want our scripts to be run by only certain people, perhaps, for example, only by company IT members. How do we make it so that a script can be deployed globally in a company but yet make it so that only some people can run it? The answer lies in using the power of Legato to secure our scripts.
Friday, September 15. 2017
LDC #52: How to Make Your Script Secure
One of the easiest parts of security is stopping unauthorized use of scripts. We can do this in several different ways using the many different tools that Legato gives to us. Let’s pause for a moment and take a look at the high level overview of what we can do to secure scripts. Computer security is defined as the protection of computer systems from the theft or damage to their hardware, software, or information, as well as from disruption or misdirection of the services they provide. What does that mean for us as developers? Essentially, it is our job to identify threats before they happen and attempt to mitigate the risk of a script before it can be used maliciously or incorrectly.
Practically that means that as we write scripts we need to determine if there is a risk factor to what we are doing. If our script is modifying the content of an open file in the current Edit Object, there is little risk to the system incurred that action. If our script is modifying option files or registry keys, perhaps we should consider locking down access to the script. This can be done by limiting the computers or users that can run the script.
There are multiple approaches to doing that. You could hard code a user’s password in the script file and then prompt the user for his or her password as part of the script initialization. This seems like an easy option, but it has a multitude of drawbacks. First, it can be bothersome to continually have to enter your credentials as a user to execute a script. Additionally, and probably more importantly, the password of every user allowed to access the script must be stored in the script itself. This is dangerous as anyone with access to the script’s code could theoretically access those passwords and change them. You could “crunch” the code to protect it, but you can see how this may not be the ideal option.
Another method of increasing script security is to make use of Windows’ security features. If you read our blog regularly, you’ll remember a few weeks ago I talked about Administrative Rights in Windows. One thing I mentioned was users in a Windows environment are associated with groups. We can check if the logged in user is part of a certain group and can stop a script’s execution if a user is not part of the intended group. Another option in this branch is to always ask a user for their username and password, then check if the entered credentials are part of the group. This would allow users who are a part of the group to put their credentials in while someone else is logged in. The downside to this is that users have to enter their username and password every single time they want to run the script. As always, ease of use should be considered. The higher security you have in place on a script, the more annoying a script may be to run.
Another tactic for determining if a script should be run is to check a computer’s information. You can make sure a computer is part of a domain or limit the script’s execution to certain named computers. This is a broader attempt to limit access, and while it may remove the need to verify particular users, it could be considered less secure since anyone who has access to that computer thus has access to the script. In addition, non-domain computers can be renamed, so that severely hinders this method of securing a script.
All of these options have advantages and drawbacks. Let’s demonstrate one approach to increasing the security of a Legato script. As an example this week, I’m going to use a script we have previously discussed: unregistering and changing your software key. Obviously this is an operation that should not be open to all users. I’m going to show a modified version of that script with a few new lines:
*** SECURITY HERE ***
When reading the script, the line above shows where you would add security measures. I'm then going to give you three examples of code that limits execution.
// // // GoFiler Legato Script - Align Outlined Text // ------------------------------------------ // // Rev 07/05/2017 // // (c) 2017 Novaworks, LLC -- All rights reserved. // // Unregisters GoFiler with server, asks if user wants to change software key while unregistering. #define BAT_NAME "RegisterGF.bat" #define SOFTKEY_REGEX "^([A-Za-z0-9]{5}-){4}[A-Za-z0-9]{5}$" #define TIMEOUT "timeout /t 3 /nobreak > nul\r\n" #define CLOSE_OPEN_WINDOWS_WRN "Please close any open windows before unregistering the application." #define INVALID_KEY_ERR "Invalid software key. Enter a valid key." #include "ChangeKey.rc" int run (int f_id, string mode);/* Call from Hook Processor */ string new_key; /* new key to use */ /****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Define Function */ item["Code"] = "UNREGISTER_EXIT"; /* Function Code */ item["MenuText"] = "&Unregister and Exit"; /* Menu Text */ item["Description"] = "<B>Unregister and Exit</B>"; /* Description (long) */ item["Description"]+= "\r\rCloses and Unregisters GoFiler."; /* * description */ /* * Check for Existing */ rc = MenuFindFunctionID(item["Code"]); /* Look for existing */ if (IsNotError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ /* * Registration */ rc = MenuAddFunction(item); /* Add the item */ if (IsError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuSetHook(item["Code"], fnScript, "run"); /* Set the Test Hook */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /****************************************/ int main() { /* Initialize from Hook Processor */ /****************************************/ setup(); /* Add to the menu */ return ERROR_NONE; } /* end setup */ /****************************************/ int run(int f_id, string mode) { /* Call from Hook Processor */ /****************************************/ string temp_folder; /* temp folder */ boolean change_key; /* true if changing key */ int rc; /* return code */ string edit_windows[][]; /* open edit windows */ string gofiler; /* path to GoFiler */ string bat_contents; /* contents of bat file */ string bat_file; /* location of bat file */ /* */ if (mode!="preprocess"){ /* if not preprocess */ return ERROR_NONE; /* return without error */ } /* */ *** SECURITY HERE *** edit_windows = EnumerateEditWindows(); /* get all open windows */ if (ArrayGetAxisDepth(edit_windows)>0){ /* if there are any open windows */ MessageBox('i',CLOSE_OPEN_WINDOWS_WRN); /* display warning */ return ERROR_NONE; /* return */ } /* */ gofiler = "\""+AddPaths(GetApplicationExecuteFolder(), /* path to GoFiler */ "GoFiler.exe\""); /* */ change_key = YesNoBox('q',"Change software key?"); /* ask if changing software key */ bat_contents = TIMEOUT; /* add a sleep to bat file */ bat_contents += gofiler+ " /unregister -silent\r\n"; /* */ if (change_key == IDYES){ /* if changing the software key */ rc = DialogBox("ChangeKey", "change_"); /* open dialog */ if (new_key == ""){ /* if no new key selected */ return ERROR_CANCEL; /* return cancelled. */ } /* */ bat_contents += TIMEOUT; /* add a sleep to bat file */ bat_contents += gofiler+ " /register:"+new_key+"\r\n"; /* add registration command to bat */ } /* */ temp_folder = GetTempFileFolder(); /* get temp folder */ bat_file = AddPaths(temp_folder, BAT_NAME); /* get full path to bat file */ StringToFile(bat_contents,bat_file); /* write out bat file */ RunProgram("cmd.exe", "/C \""+bat_file+"\""); /* run the bat file */ RunMenuFunction("FILE_EXIT"); /* quit GoFiler */ return ERROR_NONE; } /* end setup */ /****************************************/ int change_validate(){ /* validate key */ /****************************************/ string key; /* key from dialog */ /* */ new_key = ""; /* reset variable */ key = EditGetText(SOFTKEY); /* get text */ if (IsRegexMatch(key,SOFTKEY_REGEX)==false){ /* if invalid key */ MessageBox('x',INVALID_KEY_ERR); /* display message */ return ERROR_EXIT; /* return an error */ } /* */ new_key = key; /* store key for later use */ return ERROR_NONE; }
Now let’s talk about a couple of options to put here. First let’s check the currently logged in user, and see if this person is a member of management.
if (IsUserInGroup("Management") == FALSE) { /* Check if user is in group */ MessageBox('X', "You do not have access to this script.\r\n" + /* Inform User */ "Please contact an administrator to run this script."); /* they can't do this */ return ERROR_NONE; /* Return */ } /* end check user is in group */
The IsUserInGroup SDK function returns a boolean of true or false. We will add additional parameters to this function in the next example, but for right now all it does is check the user currently logged in to see the groups of which he or she is a part. The function returns true or false if a group matches the provided string parameter.
The nifty part about the IsUserInGroup function is that we can add additional parameters to check users who are not currently logged into the system. We do this by providing username and password strings as two additional parameters. In order to get these strings, we’ll be using another built in Legato function: the PasswordUsernameBox function.
string user[]; /* User variables */ user = PasswordUsernameBox("Please enter credentials", /* Get Credentials */ "Enter authorization to run this script."); /* to run the script */ if (IsUserInGroup("Management", user["Username"], user["Password"]) == FALSE) {/* Check if user is in group */ MessageBox('X', "You do not have access to this script.\r\n" + /* Inform User */ "Please contact an administrator to run this script."); /* they can't do this */ SecureClear(user); /* Clear user data */ return ERROR_NONE; /* Return */ } /* end check user is in group */ SecureClear(user); /* Clear user */
The PasswordUsernameBox function allows us to show a box that collects and stores a username and password. This function can also be extended to get domain information, but in this case we don’t care about that. Also note that we use the SecureClear function to make sure that we are not storing a username and password in memory for longer than we have to. The SecureClear function clears out the referenced memory and also clears the internal working string pool, ensuring no trace of the variable we just deleted exists anywhere in memory.
For more ease of use we could even combine these options together. Check the current user and if that doesn’t work prompt for credentials.
string user[]; /* User variables */ if (IsUserInGroup("Management") == FALSE) { /* Check if user is in group */ user = PasswordUsernameBox("Please enter credentials", /* Get Credentials */ "Enter authorization to run this script."); /* to run the script */ if (IsUserInGroup("Management", user["Username"], /* Check if user */ user["Password"]) == FALSE) { /* is in group */ MessageBox('X', "You do not have access to this script.\r\n" + /* Inform User */ "Please contact an administrator to run this script."); /* they can't do this */ SecureClear(user); /* Clear user data */ return ERROR_NONE; /* Return */ } /* end check user is in group */ } /* end check user is in group */ SecureClear(user); /* Clear user */
Finally we have another option for security: making sure the computer is a part of the domain. To do this, we merely ask the computer.
string domain; /* Domain name */ domain = GetComputerName(1); /* get domain name */ if (domain != "ABC") { /* Check if computer is in domain */ MessageBox('X', "You do not have access to this script.\r\n" + /* Inform User */ "Your computer must be a member of ABC domain to run this script.");/* they can't do this */ return ERROR_NONE; /* Return */ } /* end check user is in group */
The GetComputerName function takes a single integer parameter, a 0, 1, or 2. It will return either the computer name (0), the domain name (1), or the qualified domain name (2). If nothing is passed to the function, it will return the computer name. Here we check to see if the computer is a part of the specified domain. If it is not, the script returns without doing anything. Note that for this script to be fully secure it should be crunched; otherwise anyone could modify the domain for which we’re checking, rendering this bit of security useless.
Security is something with which we as developers must contend on a daily basis. For every step we take, there is always someone out there who would be more than willing to set us back two steps. To handle with this, we must make every precaution to ensure that the scripts we are writing and executing are secure. It’s a dangerous world out there, so remember to be always responsible with your scripts.
Joshua Kwiatkowski is a developer at Novaworks, primarily working on Novaworks’ cloud-based solution, GoFiler Online. He is a graduate of the Rochester Institute of Technology with a Bachelor of Science degree in Game Design and Development. He has been with the company since 2013. |
Additional Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato