Click here to Skip to main content
15,867,330 members
Articles / Desktop Programming / Win32
Article

mscript 2.0.2: DLL Authoring, Registry DLL, exec() Error Handling

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
6 Apr 2022Apache2 min read 7.7K   87   4   8
Minor release improves DLL security, adds registry DLL, enhances exec() error handling
With version 2.0.0, DLL integrations were made possible, but they were not secure. Registry operations are something not possible from .bat files, so I added that. Most importantly, the exec() function used to ignore errors; it now raises errors by default, and there's an option you can pass in to ignore them as needed.

Introduction

This article covers the 2.0.2 minor release of the mscript scripting language and runtime

If you're new to mscript, check out mscript.io for the latest documentation and downloads.

For a developer's perspective, you can also check out the original article about version 1.0 and the second article about version 2.0.

The project is open source on GitHub.

DLL Security

When I added DLL integration to mscript with version 2.0, I took what you gave me with the path to the DLL, and I used LoadLibrary() and hoped for the best. With this version, I'm requiring that the DLL be alongside the mscript EXE, in C:\Program Files (x86)\mscript on most systems, and no slashes, no ..'s, just right in that folder. Also, I'm requiring that DLLs be signed by the same subject and publisher as the mscript EXE. I'd like all DLL development to be done in the GitHub mscript solution, where I can build it and sign it and make it part of the installer. If you don't care about being part of the installer or do any signing, know that if the EXE is unsigned that the DLLs it loads can be unsigned. The DLL directory restriction stands.

Here's the code for determining the subject and publisher of an EXE or DLL:

C++
bin_crypt.h

#pragma once

#include <string>

namespace mscript
{
    struct bin_crypt_info
    {
        std::wstring publisher;
        std::wstring subject;
    };
    bin_crypt_info getBinCryptInfo(const std::wstring& filePath);
}
C++
bin_crypt.cpp

#include "pch.h"
#include "bin_crypt.h"
#include "utils.h"

#include <wincrypt.h>
#include <wintrust.h>
#pragma comment(lib, "crypt32.lib")

#define ENCODING (X509_ASN_ENCODING | PKCS_7_ASN_ENCODING)

mscript::bin_crypt_info mscript::getBinCryptInfo(const std::wstring & filePath)
{
    mscript::bin_crypt_info crypt_info;

    // Error handling with Win32 "objects" is fun and easy!
    std::string exp_msg;
#define BIN_CRYPT_ERR(msg) { exp_msg = (msg); goto Cleanup; }

    // Pre-declare *everything*
    DWORD dwEncoding = 0, dwContentType = 0, dwFormatType = 0, dwSignerInfo = 0, dwData = 0;
    HCERTSTORE hStore = NULL;
    HCRYPTMSG hMsg = NULL;
    BOOL fResult = FALSE;
    PCMSG_SIGNER_INFO pSignerInfo = NULL;
    PCCERT_CONTEXT pCertContext = NULL;
    LPTSTR szName = NULL;

    // Get the certificate from the file
    fResult =
        CryptQueryObject
        (
            CERT_QUERY_OBJECT_FILE,
            filePath.c_str(),
            CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
            CERT_QUERY_FORMAT_FLAG_BINARY,
            0,
            &dwEncoding,
            &dwContentType,
            &dwFormatType,
            &hStore,
            &hMsg,
            NULL
        );
    if (!fResult)
        return crypt_info;

    // Get signer info
    fResult = CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, NULL, &dwSignerInfo);
    if (!fResult)
        BIN_CRYPT_ERR("Getting binary security signer info failed");
    pSignerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, dwSignerInfo);
    if (!fResult)
        BIN_CRYPT_ERR("Allocating signer info memory failed");
    fResult = CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, pSignerInfo, &dwSignerInfo);
    if (!fResult)
        BIN_CRYPT_ERR("Getting signer info failed");

    // Get the cert from the store
    CERT_INFO CertInfo;
    CertInfo.Issuer = pSignerInfo->Issuer;
    CertInfo.SerialNumber = pSignerInfo->SerialNumber;
    pCertContext = 
        CertFindCertificateInStore
        (
            hStore,
            ENCODING,
            0,
            CERT_FIND_SUBJECT_CERT,
            (PVOID)&CertInfo,
            NULL
        );
    if (!fResult)
        BIN_CRYPT_ERR("Finding signing certificate failed");

    // Get the issuer
    dwData =
        CertGetNameString
        (
            pCertContext,
            CERT_NAME_SIMPLE_DISPLAY_TYPE,
            CERT_NAME_ISSUER_FLAG,
            NULL,
            NULL,
            0
        );
    if (dwData == 0)
        BIN_CRYPT_ERR("Getting certificate issuer length failed");
    szName = (LPTSTR)LocalAlloc(LPTR, dwData * sizeof(TCHAR));
    if (!szName)
        BIN_CRYPT_ERR("Allocating memory for certificate issuer failed");
    if (!(CertGetNameString(pCertContext,
        CERT_NAME_SIMPLE_DISPLAY_TYPE,
        CERT_NAME_ISSUER_FLAG,
        NULL,
        szName,
        dwData)))
    {
        BIN_CRYPT_ERR("Getting certificate issuer failed");
    }
    crypt_info.publisher = szName;
    LocalFree(szName);
    szName = NULL;

    // Get the subject
    dwData =
        CertGetNameString
        (
            pCertContext,
            CERT_NAME_SIMPLE_DISPLAY_TYPE,
            0,
            NULL,
            NULL,
            0
        );
    if (dwData == 0)
        BIN_CRYPT_ERR("Getting certificate subject length failed");
    szName = (LPTSTR)LocalAlloc(LPTR, dwData * sizeof(TCHAR));
    if (!szName)
        BIN_CRYPT_ERR("Allocating memory for certificate subject failed");
    if (!(CertGetNameString(pCertContext,
        CERT_NAME_SIMPLE_DISPLAY_TYPE,
        0,
        NULL,
        szName,
        dwData)))
    {
        BIN_CRYPT_ERR("Getting certificate subject failed");
    }
    crypt_info.subject = szName;
    LocalFree(szName);
    szName = NULL;

Cleanup:
    if (pSignerInfo != NULL) LocalFree(pSignerInfo);
    if (szName != NULL) LocalFree(szName);
    if (pCertContext != NULL) CertFreeCertificateContext(pCertContext);
    if (hStore != NULL) CertCloseStore(hStore, 0);
    if (hMsg != NULL) CryptMsgClose(hMsg);

    // raiseError is an mscript utility function
    // you might throw std::runtime_error(exp_msg.c_str())
    if (!exp_msg.empty())
        raiseError(exp_msg.c_str()); 
    else
        return crypt_info;
}

Registry DLL

For this version, I've added a new DLL - mscript-registry.dll - for working with registry keys:

msreg_create_key(key)
 - ensure that a registry key exists

msreg_delete_key(key)
 - ensure that a registry key no longer exists

msreg_get_sub_keys(key)
 - get a list of the names of the sub-keys of a key
 - just the names of the sub-keys, not full keys

msreg_put_settings(key, settings_index)
 - add settings to a key with the name-values in an index
 - you can send in number and string settings
 - to remove a setting, pass null as the index value

msreg_get_settings(key)
 - get the settings on a key in a name-values index

NOTE: You can only put REG_DWORD and REG_SV settings. You only get REG_DWORD and REG_SV settings out, others are ignored.

Here's the source code that powers this DLL:

C++
registry.h

#pragma once
#include "pch.h"

using namespace mscript;

class registry
{
public:
	registry(const object::list& params)
		: m_params(params)
		, m_root_key(nullptr)
		, m_local_key(nullptr)
	{
		if (m_params.empty() || m_params[0].type() != object::STRING)
			raiseError("Registry functions take a first registry key string parameter");
		m_input_path = m_params[0].stringVal();

		size_t first_slash = m_input_path.find('\\');
		if (first_slash == std::wstring::npos)
			raiseWError(L"Invalid registry key (no slash): " + m_input_path);

		m_path = m_input_path.substr(first_slash + 1);
		if (m_path.empty())
			raiseWError(L"Invalid registry key (empty key): " + m_input_path);

		std::wstring reg_root;
		reg_root = toUpper(m_input_path.substr(0, first_slash));
		if (reg_root.empty())
			raiseWError(L"Invalid registry key (invalid m_root_key): " + m_input_path);

		if (reg_root == L"HKCR" || reg_root == L"HKEY_CLASSES_ROOT")
			m_root_key = HKEY_CLASSES_ROOT;
		else if (reg_root == L"HKCC" || reg_root == L"HKEY_CURRENT_CONFIG")
			m_root_key = HKEY_CURRENT_CONFIG;
		else if (reg_root == L"HKCU" || reg_root == L"HKEY_CURRENT_USER")
			m_root_key = HKEY_CURRENT_USER;
		else if (reg_root == L"HKLM" || reg_root == L"HKEY_LOCAL_MACHINE")
			m_root_key = HKEY_LOCAL_MACHINE;
		else if (reg_root == L"HKU" || reg_root == L"HKEY_USERS")
			m_root_key = HKEY_USERS;
		else
			raiseWError(L"Invalid registry key (unknown root): " + 
                          m_input_path + L" (" + reg_root + L")");
	}

	~registry()
	{
		if (m_local_key != nullptr)
			::RegCloseKey(m_local_key);
	}

	void createKey()
	{
		DWORD dwError = ::RegCreateKey(m_root_key, m_path.c_str(), &m_local_key);
		if (dwError != ERROR_SUCCESS)
			raiseWError(L"Creating key failed: " + m_input_path + L": " + 
                          getLastErrorMsg(dwError));
	}

	void deleteKey()
	{
		{
			DWORD dwError = ::RegOpenKey(m_root_key, m_path.c_str(), &m_local_key);
			if (dwError != ERROR_SUCCESS)
				raiseWError(L"Opening key failed: " + m_input_path + L": " + 
                              getLastErrorMsg(dwError));
		}

		{
			DWORD dwError = ::RegDeleteTree(m_local_key, m_path.c_str());
			if (dwError != ERROR_SUCCESS && dwError != ERROR_FILE_NOT_FOUND && 
			    dwError != ERROR_TOO_MANY_OPEN_FILES)
				raiseWError(L"Deleting key failed: " + m_input_path + L": " + 
                              getLastErrorMsg(dwError));
		}
	}

	object::list getSubKeys()
	{
		DWORD dwError = ::RegOpenKey(m_root_key, m_path.c_str(), &m_local_key);
		if (dwError != ERROR_SUCCESS)
			raiseWError(L"Opening key failed: " + m_input_path + L": " + 
                          getLastErrorMsg(dwError));

		const size_t MAX_VALUE_LEN = 16 * 1024;
		std::unique_ptr<wchar_t[]> value_name(new wchar_t[MAX_VALUE_LEN + 1]);
		value_name[MAX_VALUE_LEN] = '\0';

		object::list retVal;
		DWORD result = ERROR_SUCCESS;
		for (DWORD e = 0; ; ++e)
		{
			result = ::RegEnumKey(m_local_key, e, value_name.get(), MAX_VALUE_LEN);
			if (result == ERROR_NO_MORE_ITEMS)
				break;
			else if (result == ERROR_SUCCESS)
				retVal.push_back(std::wstring(value_name.get()));
			else
				raiseWError(L"Enumerating key failed: " + m_input_path + L": " + 
                              getLastErrorMsg(dwError));
		}
		return retVal;
	}

	void putKeySettings()
	{
		if (m_params.size() != 2 || m_params[1].type() != object::INDEX)
			raiseError("msreg_put_settings takes a registry key 
                        and an index of settings to put");
		object::index index = m_params[1].indexVal();

		DWORD dwError = ::RegOpenKey(m_root_key, m_path.c_str(), &m_local_key);
		if (dwError != ERROR_SUCCESS)
			raiseWError(L"Opening key failed: " + m_input_path + L": " + 
                          getLastErrorMsg(dwError));

		for (const auto& name : index.keys())
		{
			if (name.type() != object::STRING)
			{
				raiseWError(L"msreg_put_settings index key is not a string: " 
                            + name.toString());
			}

			object val = index.get(name);
			if (val.type() == object::NOTHING)
			{
				dwError = ::RegDeleteValue(m_local_key, name.stringVal().c_str());
				if (dwError != ERROR_SUCCESS)
					break;
			}
			else if (val.type() == object::NUMBER)
			{
				int64_t num_val = int64_t(round(val.numberVal()));
				if (num_val < 0)
					raiseWError(L"msreg_put_settings value must be a positive integer: " 
                                  + name.toString());
				else if (num_val > MAXDWORD)
					raiseWError(L"msreg_put_settings value must not exceed DWORD capacity: " 
                                + name.toString());
				DWORD dw_val = DWORD(num_val);
				dwError = ::RegSetKeyValue(m_local_key, nullptr, name.stringVal().c_str(), 
				REG_DWORD, LPCWSTR(&dw_val), sizeof(dw_val));
				if (dwError != ERROR_SUCCESS)
					raiseWError(L"Setting number value failed: " + m_input_path + L": " + 
					name.stringVal() + L": " + getLastErrorMsg(dwError));
			}
			else if (val.type() == object::STRING)
			{
				dwError = ::RegSetKeyValue(m_local_key, nullptr, name.stringVal().c_str(), 
				REG_SZ, val.stringVal().c_str(), 
                (val.stringVal().length() + 1) * sizeof(wchar_t));
				if (dwError != ERROR_SUCCESS)
					raiseWError(L"Setting string value failed: " + m_input_path + L": " + 
					name.stringVal() + L": " + getLastErrorMsg(dwError));
			}
			else
				raiseWError(L"msreg_put_settings value is not null, number, or string: " 
                              + val.toString());
		}
	}

	object::index getKeySettings()
	{
		object::index ret_val;
		DWORD dwError = ::RegOpenKey(m_root_key, m_path.c_str(), &m_local_key);
		if (dwError != ERROR_SUCCESS)
			raiseWError(L"Opening key failed: " + m_input_path + L": " 
                        + getLastErrorMsg(dwError));

		const size_t MAX_VALUE_LEN = 16 * 1024;
		std::unique_ptr<wchar_t[]> value_name(new wchar_t[MAX_VALUE_LEN + 1]);
		value_name[MAX_VALUE_LEN] = '\0';
		for (DWORD i = 0; ; ++i)
		{
			DWORD val_len = MAX_VALUE_LEN;
			DWORD dwValRet =
				::RegEnumValue(m_local_key, i, value_name.get(), &val_len, 
                               nullptr, nullptr, nullptr, nullptr);
			if (dwValRet != ERROR_SUCCESS)
				break;

			const DWORD flags = RRF_RT_REG_DWORD | RRF_RT_REG_SZ;
			DWORD type = 0;
			DWORD data_len = 0;
			dwError =
				::RegGetValue(m_local_key, nullptr, value_name.get(), 
                              flags, &type, nullptr, &data_len);
			if (dwError != ERROR_SUCCESS && dwError != ERROR_MORE_DATA)
				break;
			if (type == REG_DWORD)
			{
				DWORD data_val = 0;
				dwError =
					::RegGetValue(m_local_key, nullptr, value_name.get(), 
                                  flags, &type, &data_val, &data_len);
				if (dwError != ERROR_SUCCESS)
					raiseWError(L"Getting DWORD value failed: " + 
                                m_input_path + L": " + getLastErrorMsg(dwError));
				ret_val.set(std::wstring(value_name.get()), double(data_val));
			}
			else if (type == REG_SZ)
			{
				std::unique_ptr<wchar_t[]> value(new wchar_t[data_len + 1]);
				dwError =
					::RegGetValue(m_local_key, nullptr, value_name.get(), 
                                  flags, &type, value.get(), &data_len);
				if (dwError != ERROR_SUCCESS)
					raiseWError(L"Getting string value failed: " + 
                                m_input_path + L": " + getLastErrorMsg(dwError));
				ret_val.set(std::wstring(value_name.get()), std::wstring(value.get()));
			}
			//else - omitted
		}

		return ret_val;
	}

private:
	object::list m_params;

	std::wstring m_input_path;

	HKEY m_root_key;
	std::wstring m_path;

	HKEY m_local_key;
};

exec() Error Handling

exec() now raises an error if the exit code of the called program is not zero. exec() takes a second parameter, an index of options, one of which is "ignore_errors". When set to true, non-zero exit codes do not raise errors. You can still get the error code in the returned index, with the key "exit_code".

As an example of a real-world mscript, it was a pain dealing with the .bat file, so how about an mscript to...build mscript: (mscript-builder.exe is a copy of a recent mscript EXE, gotta start somewhere).

BAT
? arguments.length() != 1
	>> Usage: mscript-builder.exe build.ms version
	>> version should be like "2.0.2"
	* exit(0)
}

$ version = arguments.get(0)
> "Finalizing version " + version

$ ignore_errors = index("ignore_errors", true)

>> Binaries...
* exec("rmdir /S /Q binaries", ignore_errors)
* exec("mkdir binaries")
* exec("xcopy /Y ..\mscript\Win32\Release\*.exe binaries\.")
* exec("xcopy /Y ..\mscript\Win32\Release\*.dll binaries\.")

>> Samples...
* exec("rmdir /S /Q samples", ignore_errors)
* exec("mkdir samples")
* exec("xcopy /Y ..\mscript\mscript-examples\*.* samples\.")

>> Resource hacking and signing...
$ modules = list("mscript2")
* modules.add("mscript-dll-sample")
* modules.add("mscript-timestamp")
* modules.add("mscript-registry")
@ module : modules
	> "..." + module
	
	$ ext = "dll"
	? module = "mscript2"
		& ext = "exe"
	}
	
	$ filename = module + "." + ext

	* exec("del resources.res", ignore_errors)
	* exec("ResourceHacker.exe -open resources-" + module + 
           ".rc -save resources.res -action compile")
	* exec("ResourceHacker.exe -open binaries\" + filename + 
           " -save binaries\" + filename + " -action addoverwrite -resource resources.res")
	* exec("signtool sign /f mscript.pfx /p foobar binaries\" + filename)
}

>> Building installer
* exec("AdvancedInstaller.com /rebuild mscript.aip")

>> Building site
* exec("rmdir /S /Q ..\mscript.io\releases\" + version, ignore_errors)
* exec("mkdir ..\mscript.io\releases\" + version)

* exec("mkdir ..\mscript.io\releases\" + version  + "\samples")
* exec("xcopy samples\*.* ..\mscript.io\releases\" + version + "\samples\.")

@ module : modules
	$ ext = "dll"
	? module = "mscript2"
		& ext = "exe"
	}
	
	$ filename = module + "." + ext
	* exec("xcopy /Y binaries\" + filename + " ..\mscript.io\releases\" + version + "\.")
}

>> Finalizing installer
* exec("xcopy /Y mscript-SetupFiles\mscript.msi ..\mscript.io\releases\" + version + "\.")
* exec("signtool sign /f mscript.pfx /p foobar ..\mscript.io\releases\" + 
        version + "\mscript.msi")

>> All done!

Conclusion and Points of Interest

DLL security is much improved, the core exec() routine now supports reasonable error handling, and you have a simple DLL for working with the registry.

Enjoy!

History

  • 5th April, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Software Developer
United States United States
Michael Balloni is a manager of software development at a cybersecurity software and services provider.

Check out https://www.michaelballoni.com for all the programming fun he's done over the years.

He has been developing software since 1994, back when Mosaic was the web browser of choice. IE 4.0 changed the world, and Michael rode that wave for five years at a .com that was a cloud storage system before the term "cloud" meant anything. He moved on to a medical imaging gig for seven years, working up and down the architecture of a million-lines-code C++ system.

Michael has been at his current cybersecurity gig since then, making his way into management. He still loves to code, so he sneaks in as much as he can at work and at home.

Comments and Discussions

 
Suggestionchanging directories Pin
Member 1222600110-Apr-22 8:21
Member 1222600110-Apr-22 8:21 
GeneralRe: changing directories Pin
Michael Sydney Balloni11-Apr-22 8:18
professionalMichael Sydney Balloni11-Apr-22 8:18 
Questionchanging directories Pin
Member 122260018-Apr-22 17:27
Member 122260018-Apr-22 17:27 
AnswerRe: changing directories Pin
Michael Sydney Balloni10-Apr-22 7:42
professionalMichael Sydney Balloni10-Apr-22 7:42 
SuggestionDebugging a dll - all three file locations together are important (Visual Studio 2022) Pin
Member 122260018-Apr-22 11:35
Member 122260018-Apr-22 11:35 
GeneralRe: Debugging a dll - all three file locations together are important (Visual Studio 2022) Pin
Michael Sydney Balloni8-Apr-22 14:20
professionalMichael Sydney Balloni8-Apr-22 14:20 
SuggestionDLL Authoring (module signing); one fix to compile Pin
Member 122260018-Apr-22 5:40
Member 122260018-Apr-22 5:40 
GeneralRe: DLL Authoring (module signing); one fix to compile Pin
Michael Sydney Balloni8-Apr-22 14:18
professionalMichael Sydney Balloni8-Apr-22 14:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.