Mercurial > trustbridge
view cinst/nssstore_win.c @ 1041:6235b11a22a0
Test plan updated.
author | Emanuel Schuetze <emanuel@intevation.de> |
---|---|
date | Thu, 04 Sep 2014 16:11:50 +0200 |
parents | 6684e5012b7a |
children | 317ee9dc4684 |
line wrap: on
line source
/* Copyright (C) 2014 by Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH * * This file is Free Software under the GNU GPL (v>=2) * and comes with ABSOLUTELY NO WARRANTY! * See LICENSE.txt for details. */ #ifdef WIN32 /* @file @brief Windows implementation of nssstore process control. The windows process will write an instructions file for the mozilla process into the current users temp directory (%APPDATA%/Local/Temp/) and start the NSS installation process to exectute those instructions. If the current process is elevated the NSS process is run with a restricted token. The execution of the mozilla process is not monitored. You have to refer to the system log to check which certificates were installed / removed by it. If the installation process is running elevated it will create the file in the ProgramData directory in a subdirectory with the defined application name. %PROGRAMDATA%/$APPLICATION_NAME with the file name: current_selection.txt The folder will have restricted permissions so that only Administrators are allowed to access it. Additionally if this process is Elevated it also starts the NSS installation process in default profile mode once to change the default NSS certificate databases for new profiles. The process then adds a new RunOnce registry key for each user on the system that executes the NSS installation process on login to make sure it is launched once in the security context of that user. */ #include <windows.h> #include <winsafer.h> #include <sddl.h> #include <stdio.h> #include <stdbool.h> #include <userenv.h> #include <io.h> #include <accctrl.h> #include <aclapi.h> #include "logging.h" #include "util.h" #include "strhelp.h" #ifndef APPNAME #define APPNAME L"cinst" #endif /**@def The name of the nss installation process */ #define NSS_APP_NAME L"mozilla.exe" #ifndef SELECTION_FILE_NAME #define SELECTION_FILE_NAME L"currently_selected.txt" #endif /**@def The maximum time to wait for the NSS Process */ #define PROCESS_TIMEOUT 30000 /**@def The registry key to look for user profile directories */ #define PROFILE_LIST L"Software\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList" #define RUNONCE_PATH L"Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce" struct profile_key_path { char *sid; char *hive_path; struct profile_key_path *next; }; /** * @brief combination of sid and hive path */ typedef struct profile_key_path pkp_t; static void pkp_t_free (pkp_t *item) { if (!item) { return; } xfree (item->sid); xfree (item->hive_path); if (item->next) { pkp_t_free (item->next); } xfree (item); } /** @brief get a restricted access token to execute nss process * * This function uses the Software Restriction API to obtain the * access token for a process run als normal user. * * @returns A restricted handle or NULL on error. */ static HANDLE get_restricted_token() { SAFER_LEVEL_HANDLE user_level = NULL; HANDLE retval = NULL; SID_IDENTIFIER_AUTHORITY medium_identifier = {SECURITY_MANDATORY_LABEL_AUTHORITY}; PSID medium_sid = NULL; TOKEN_MANDATORY_LABEL integrity_label; memset (&integrity_label, 0, sizeof (integrity_label)); if (!SaferCreateLevel(SAFER_SCOPEID_USER, SAFER_LEVELID_NORMALUSER, SAFER_LEVEL_OPEN, &user_level, NULL)) { PRINTLASTERROR ("Failed to create user level.\n"); return NULL; } if (!SaferComputeTokenFromLevel(user_level, NULL, &retval, 0, NULL)) { SaferCloseLevel(user_level); return NULL; } SaferCloseLevel(user_level); /* Set the SID to medium it will still be high otherwise. Even if there is no high access allowed. */ if (!AllocateAndInitializeSid(&medium_identifier, 1, SECURITY_MANDATORY_MEDIUM_RID, 0, 0, 0, 0, 0, 0, 0, &medium_sid)) { PRINTLASTERROR ("Failed to initialize sid.\n"); return NULL; } integrity_label.Label.Attributes = SE_GROUP_INTEGRITY; integrity_label.Label.Sid = medium_sid; if (!SetTokenInformation(retval, TokenIntegrityLevel, &integrity_label, sizeof(TOKEN_MANDATORY_LABEL))) { PRINTLASTERROR ("Failed to set token integrity.\n"); return NULL; } return retval; } /**@brief Write strv of instructions to a handle * * Writes the null terminated list of instructions to * the handle. * * @param [in] certificates base64 encoded der certificate to write * @param [in] write_handle handle to write to * @param [in] remove weather the certificate should be installed or removed * * @returns true on success, false on failure */ static bool write_instructions(char **certificates, HANDLE write_handle, bool remove) { bool retval = false; int i = 0; const char *line_end = "\r\n"; char *line_start = NULL; if (!certificates) { return true; } line_start = remove ? "R:" : "I:"; for (i = 0; certificates[i]; i++) { DWORD written = 0; DWORD inst_len = strlen (certificates[i]); retval = WriteFile (write_handle, (LPCVOID) line_start, 2, &written, NULL); if (!retval) { PRINTLASTERROR ("Failed to write line start\n"); return false; } if (written != 2) { ERRORPRINTF ("Failed to write line start\n"); retval = false; return false; } written = 0; retval = WriteFile (write_handle, (LPCVOID) certificates[i], inst_len, &written, NULL); if (!retval) { PRINTLASTERROR ("Failed to write certificate\n"); return false; } if (inst_len != written) { ERRORPRINTF ("Failed to write everything\n"); retval = false; return false; } written = 0; retval = WriteFile (write_handle, (LPCVOID) line_end, 2, &written, NULL); if (!retval) { PRINTLASTERROR ("Failed to write line end\n"); return false; } if (written != 2) { ERRORPRINTF ("Failed to write full line end\n"); retval = false; return false; } } return true; } /**@brief Get the path to all users default registry hive * * Enumerates the keys in #PROFILE_LIST and retuns a * list of their profile path / sid pairs with the utf-8 encoded paths to * their suggestedregistry hive location. * * Users with an SID not starting with S-1-5-21- are ignored * as is the current user. * * The return value should be freed with pkp_t_free * * @returns a newly allocated strv of the paths to the registry hives or NULL */ static pkp_t* locate_other_hives() { HKEY profile_list = NULL; int ret = 0; DWORD index = 0, key_len = 257; /* According to http://msdn.microsoft.com/en-us/library/windows/desktop/ms724872%28v=vs.85%29.aspx a registry key is limited to 255 characters. But according to http://www.sepago.de/e/holger/2010/07/20/how-long-can-a-registry-key-name-really-be the actual limit is 256 + \0 thus we create a buffer for 257 wchar_t's*/ wchar_t key_name[257], *current_user_sid = NULL; pkp_t *retval = NULL, *cur_item = NULL; bool error = true; PSID current_user = NULL; ret = RegOpenKeyExW (HKEY_LOCAL_MACHINE, PROFILE_LIST, 0, KEY_READ, &profile_list); if (ret != ERROR_SUCCESS) { ERRORPRINTF ("Failed to open profile list. Error: %i", ret); return NULL; } /* Obtain the current user sid to prevent it from being returned. */ current_user = get_process_owner (GetCurrentProcess()); if (!current_user) { ERRORPRINTF ("Failed to get the current user."); goto done; } if (!ConvertSidToStringSidW (current_user, ¤t_user_sid)) { PRINTLASTERROR ("Failed to convert sid to string."); goto done; } while ((ret = RegEnumKeyExW (profile_list, index++, key_name, &key_len, NULL, NULL, NULL, NULL)) == ERROR_SUCCESS) { char *profile_path = NULL; wchar_t *key_path = NULL; size_t key_path_len = 0, profile_path_len = 0; if (key_len == 257) { ERRORPRINTF ("Registry key too long."); goto done; } /* Reset key_len to buffer size */ key_len = 257; if (wcsncmp (L"S-1-5-21-", key_name, 9) != 0 || wcscmp (current_user_sid, key_name) == 0) { /* S-1-5-21 is the well known prefix for local users. Skip all others and the current user*/ continue; } key_path_len = key_len + wcslen(PROFILE_LIST L"\\") + 1; key_path = xmalloc (key_path_len * sizeof (wchar_t)); wcscpy_s (key_path, key_path_len, PROFILE_LIST L"\\"); wcscat_s (key_path, key_path_len, key_name); key_path[key_path_len - 1] = '\0'; DEBUGPRINTF ("Key : %S", key_name); profile_path = read_registry_string (HKEY_LOCAL_MACHINE, key_path, L"ProfileImagePath"); xfree (key_path); if (profile_path == NULL) { ERRORPRINTF ("Failed to get profile path."); continue; } profile_path_len = strlen (profile_path); str_append_str (&profile_path, &profile_path_len, "\\ntuser.dat", 11); if (retval == NULL) { retval = xmalloc (sizeof (pkp_t)); cur_item = retval; } else { cur_item->next = xmalloc (sizeof(pkp_t)); cur_item = cur_item->next; } cur_item->hive_path = profile_path; cur_item->sid = wchar_to_utf8 (key_name, wcslen(key_name)); cur_item->next = NULL; DEBUGPRINTF ("Trying to access registry hive: %s", profile_path); } if (ret != ERROR_NO_MORE_ITEMS) { ERRORPRINTF ("Failed to enumeratre profile list. Error: %i", ret); goto done; } error = false; done: xfree (current_user); RegCloseKey (profile_list); if (current_user_sid) { LocalFree (current_user_sid); } if (error) { pkp_t_free (retval); retval = NULL; } return retval; } /** @brief Build the command line for the NSS installation process * * Caller has to free the return value * * @param [in] selection_file the certificates to install * * @returns the command line to install the certificates. */ static wchar_t* get_command_line(wchar_t *selection_file) { LPWSTR retval; char *install_dir = get_install_dir(); wchar_t *w_inst_dir; size_t cmd_line_len = 0; if (install_dir == NULL) { ERRORPRINTF ("Failed to get installation directory"); return NULL; } w_inst_dir = utf8_to_wchar (install_dir, strlen(install_dir)); xfree (install_dir); if (w_inst_dir == NULL) { ERRORPRINTF ("Failed to convert installation directory"); return NULL; } /* installdir + dirsep + quotes + process name + space + quotes + selection_file + NULL */ cmd_line_len = wcslen (w_inst_dir) + 1 + 2 + wcslen (NSS_APP_NAME) + + 1 + 2 + wcslen(selection_file) + 1; retval = xmalloc (cmd_line_len * sizeof(wchar_t)); wcscpy_s (retval, cmd_line_len, L"\""); wcscat_s (retval, cmd_line_len, w_inst_dir); wcscat_s (retval, cmd_line_len, L"\\"); wcscat_s (retval, cmd_line_len, NSS_APP_NAME); wcscat_s (retval, cmd_line_len, L"\" \""); wcscat_s (retval, cmd_line_len, selection_file); wcscat_s (retval, cmd_line_len, L"\""); return retval; } /** @brief Increase the privileges of the current token to allow registry access * * To load another users registry you need SE_BACKUP_NAME and SE_RESTORE_NAME * privileges. Normally if we are running elevated we can obtain them. * * @returns true if the privileges could be obtained. False otherwise */ static bool get_backup_restore_priv() { HANDLE hToken = NULL; PTOKEN_PRIVILEGES psToken = NULL; DWORD token_size = 0, dwI = 0, token_size_new = 0, privilege_size = 128; char privilege_name[128]; bool retval = false; bool backup_found = false; bool restore_found = false; if (!OpenProcessToken (GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) { PRINTLASTERROR ("Failed to get process token."); return false; } /* Get the size for the token */ GetTokenInformation (hToken, TokenPrivileges, NULL, 0, &token_size); if (token_size == 0) { PRINTLASTERROR ("Failed to get token size."); goto done; } psToken = xmalloc(token_size); if (!GetTokenInformation (hToken, TokenPrivileges, psToken, token_size, &token_size_new)) { PRINTLASTERROR ("Failed to get token information."); goto done; } if (token_size != token_size_new) { ERRORPRINTF ("Size changed."); goto done; } for(dwI = 0; dwI < psToken->PrivilegeCount; dwI++) { privilege_size = sizeof (privilege_name); if (!LookupPrivilegeNameA (NULL, &psToken->Privileges[dwI].Luid, privilege_name, &privilege_size)) { PRINTLASTERROR ("Failed to lookup privilege name"); } if(strcmp(privilege_name, "SeRestorePrivilege") == 0) { psToken->Privileges[dwI].Attributes |= SE_PRIVILEGE_ENABLED; restore_found = true; continue; } if(strcmp(privilege_name, "SeBackupPrivilege") == 0) { psToken->Privileges[dwI].Attributes |= SE_PRIVILEGE_ENABLED; backup_found = true; continue; } if (backup_found && restore_found) { break; } } if (backup_found && restore_found) { if(!AdjustTokenPrivileges (hToken, 0, psToken, token_size, NULL, NULL)) { PRINTLASTERROR ("Failed to adjust token privileges."); } else { retval = true; } } done: if (hToken != NULL) { CloseHandle(hToken); } xfree(psToken); return retval; } /**@brief Register NSS process as runOnce for other users * * Loads the registry hives of other users on the system and * adds a RunOnce registry key to start the NSS process to * install the current selection on their next login. * * This should avoid conflicts with their firefox / thunderbird * while making the certificates available for their applications. * * This function needs SE_BACKUP_NAME and SE_RESTORE_NAME * privileges. * * @param [in] selection_file filename of the file containing * the users install / remove selection. */ static void register_proccesses_for_others (wchar_t *selection_file) { pkp_t *pkplist = locate_other_hives(), *cur = NULL; wchar_t *run_command = NULL; if (pkplist == NULL) { DEBUGPRINTF ("No hives found."); return; } if (!get_backup_restore_priv()) { ERRORPRINTF ("Failed to obtain backup / restore privileges."); return; } run_command = get_command_line (selection_file); for (cur = pkplist; cur != NULL; cur = cur->next) { LONG ret = 0; wchar_t *hivepath = utf8_to_wchar (cur->hive_path, strlen(cur->hive_path)); HKEY key_handle = NULL; bool key_loaded = false; if (hivepath == NULL) { ERRORPRINTF ("Failed to read hive path"); continue; } ret = RegLoadKeyW (HKEY_LOCAL_MACHINE, APPNAME L"_tmphive", hivepath); xfree (hivepath); hivepath = NULL; if (ret != ERROR_SUCCESS) { /* This is somewhat expected if the registry is not located in the standard location or already loaded. Try to access the loaded registry in that case*/ wchar_t *user_key = NULL, *w_sid = NULL; size_t user_key_len = 0; SetLastError((DWORD)ret); PRINTLASTERROR ("Failed to load hive. Trying to access already loaded hive."); w_sid = utf8_to_wchar (cur->sid, strlen(cur->sid)); if (!w_sid) { ERRORPRINTF ("Failed to read sid."); continue; } user_key_len = wcslen (L"\\" RUNONCE_PATH) + wcslen(w_sid) + 1; user_key = xmalloc (user_key_len * sizeof (wchar_t)); wcscpy_s (user_key, user_key_len, w_sid); wcscat_s (user_key, user_key_len, L"\\" RUNONCE_PATH); user_key[user_key_len - 1] = '\0'; xfree (w_sid); w_sid = NULL; ret = RegOpenKeyExW (HKEY_USERS, user_key, 0, KEY_WRITE, &key_handle); xfree (user_key); if (ret != ERROR_SUCCESS) { ERRORPRINTF ("Failed to find RunOnce key for sid: %s in HKEY_USERS.", cur->sid); continue; } } else { key_loaded = true; ret = RegOpenKeyExW (HKEY_LOCAL_MACHINE, APPNAME L"_tmphive\\" RUNONCE_PATH, 0, KEY_WRITE, &key_handle); if (ret != ERROR_SUCCESS) { ERRORPRINTF ("Failed to find RunOnce key in other registry."); RegUnLoadKey (HKEY_LOCAL_MACHINE, APPNAME L"_tmphive"); continue; } } ret = RegSetValueExW (key_handle, APPNAME, 0, REG_SZ, (LPBYTE) run_command, (wcslen(run_command) + 1) * sizeof(wchar_t)); if (ret != ERROR_SUCCESS) { ERRORPRINTF ("Failed to write RunOnce key."); } RegCloseKey (key_handle); if (key_loaded) { ret = RegUnLoadKeyW (HKEY_LOCAL_MACHINE, APPNAME L"_tmphive"); if (ret != ERROR_SUCCESS) { SetLastError ((DWORD)ret); PRINTLASTERROR ("Failed to unload hive."); } } } xfree (run_command); pkp_t_free (pkplist); } /**@brief Start the process to install / remove * * Starts the NSS installation process for the current user * * @param [in] selection_file filename of the file containing * the users install / remove selection. * * @param [in] drop_privileges weather or not elevated privileges * should be dropped before starting the process. * * @returns true on success, false on error. */ static bool start_procces_for_user (wchar_t *selection_file, bool drop_privileges) { HANDLE hToken = NULL; LPWSTR lpApplicationPath = NULL, lpCommandLine = NULL; PROCESS_INFORMATION piProcInfo = {0}; STARTUPINFOW siStartInfo = {0}; BOOL success = FALSE; char *install_dir = get_install_dir(); wchar_t *w_inst_dir; size_t w_path_len = 0; if (!selection_file) { ERRORPRINTF ("Invalid call\n"); return false; } /* Set up the application path. It's installdir + NSS_APP_NAME */ if (install_dir == NULL) { ERRORPRINTF ("Failed to get installation directory"); return FALSE; } w_inst_dir = utf8_to_wchar (install_dir, strlen(install_dir)); xfree (install_dir); install_dir = NULL; w_path_len = wcslen(w_inst_dir) + wcslen(L"\\" NSS_APP_NAME) + 1; lpApplicationPath = xmalloc(w_path_len * sizeof (wchar_t)); wcscpy_s (lpApplicationPath, w_path_len, w_inst_dir); xfree (w_inst_dir); w_inst_dir = NULL; wcscat_s (lpApplicationPath, w_path_len, L"\\" NSS_APP_NAME); /* set up handles. stdin and stdout go to the same stdout*/ siStartInfo.cb = sizeof (STARTUPINFO); if (is_elevated() && drop_privileges) { /* Start the child process as normal user */ hToken = get_restricted_token (); if (hToken == NULL) { ERRORPRINTF ("Failed to get user level token."); return false; } } else if(!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken)) { PRINTLASTERROR("Failed to get current handle."); xfree (lpApplicationPath); return false; } lpCommandLine = get_command_line (selection_file); if (lpCommandLine == NULL) { ERRORPRINTF ("Failed to build command line."); xfree (lpApplicationPath); return false; } DEBUGPRINTF ("Starting %S with command line %S\n", lpApplicationPath, lpCommandLine); success = CreateProcessAsUserW (hToken, lpApplicationPath, lpCommandLine, /* Commandline */ NULL, /* Process attributes. Take hToken */ NULL, /* Thread attribues. Take hToken */ FALSE, /* Inherit Handles */ 0, /* Creation flags. */ NULL, /* Inherit environment */ NULL, /* Current working directory */ &siStartInfo, &piProcInfo); xfree (lpApplicationPath); xfree (lpCommandLine); if (!success) { PRINTLASTERROR ("Failed to create process.\n"); return false; } if (WaitForSingleObject (piProcInfo.hProcess, PROCESS_TIMEOUT) != WAIT_OBJECT_0) { /* Should not happen... */ ERRORPRINTF ("Failed to wait for process.\n"); if (piProcInfo.hProcess) CloseHandle (piProcInfo.hProcess); if (piProcInfo.hThread) CloseHandle (piProcInfo.hThread); return false; } if (piProcInfo.hProcess) CloseHandle (piProcInfo.hProcess); if (piProcInfo.hThread) CloseHandle (piProcInfo.hThread); return true; } /**@brief Writes the selection file containing the instructions * * If the process is running elevated the instructions are * written to the global ProgramData directory otherwise * they are written in the directory of the current user. * * If the return value is not NULL it needs to be freed by the caller. * The returned path will contain backslashes as directory seperators. * * @param[in] to_install Certificates that should be installed * @param[in] to_remove Certificates that should be removed * @returns pointer to the absolute filename of the selection file or NULL */ wchar_t * write_selection_file (char **to_install, char **to_remove) { wchar_t *folder_name = NULL, *path = NULL; HANDLE hFile = NULL; size_t path_len; folder_name = get_program_data_folder(); if (!folder_name) { ERRORPRINTF("Failed to look up ProgramData folder.\n"); return NULL; } path_len = wcslen (folder_name) + wcslen (APPNAME) + 2; /* path + dirsep + \0 */ path_len += wcslen (SELECTION_FILE_NAME) + 1; /* filename + dirsep */ if (path_len >= MAX_PATH) { /* We could go and use the full 32,767 characters but this should be a very weird setup if this is neccessary. */ ERRORPRINTF ("Path too long.\n"); return NULL; } path = xmalloc (path_len * sizeof (wchar_t)); if (wcscpy_s (path, path_len, folder_name) != 0) { ERRORPRINTF ("Failed to copy folder name.\n"); CoTaskMemFree (folder_name); return NULL; } CoTaskMemFree (folder_name); if (wcscat_s (path, path_len, L"\\") != 0) { ERRORPRINTF ("Failed to cat dirsep.\n"); xfree(path); return NULL; } if (wcscat_s (path, path_len, APPNAME) != 0) { ERRORPRINTF ("Failed to cat appname.\n"); xfree(path); return NULL; } /* Security: if someone has created this directory before it might be a symlink to another place that a users wants us to grant read access to or makes us overwrite something */ if(!create_restricted_directory (path)) { ERRORPRINTF ("Failed to create directory\n"); xfree(path); return NULL; } if (wcscat_s (path, path_len, L"\\") != 0) { ERRORPRINTF ("Failed to cat dirsep.\n"); xfree(path); return NULL; } if (wcscat_s (path, path_len, SELECTION_FILE_NAME) != 0) { ERRORPRINTF ("Failed to cat filename.\n"); xfree(path); return NULL; } hFile = CreateFileW(path, GENERIC_WRITE, 0, /* don't share */ NULL, /* use the security attributes from the folder */ OPEN_ALWAYS | TRUNCATE_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_NOT_FOUND) { hFile = CreateFileW(path, GENERIC_WRITE, 0, /* don't share */ NULL, /* use the security attributes from the folder */ CREATE_NEW, 0, NULL); } if (hFile == INVALID_HANDLE_VALUE) { PRINTLASTERROR ("Failed to create file\n"); xfree(path); return NULL; } if (!write_instructions (to_install, hFile, false)) { ERRORPRINTF ("Failed to write install instructions.\n"); CloseHandle(hFile); xfree(path); return NULL; } if (!write_instructions (to_remove, hFile, true)) { ERRORPRINTF ("Failed to write remove instructions.\n"); CloseHandle(hFile); xfree(path); return NULL; } CloseHandle(hFile); return path; } int write_stores_nss (char **to_install, char **to_remove) { wchar_t *selection_file_name = NULL; selection_file_name = write_selection_file (to_install, to_remove); if (!selection_file_name) { ERRORPRINTF ("Failed to write instructions.\n"); return -1; } DEBUGPRINTF ("Wrote selection file. Loc: %S\n", selection_file_name); if (is_elevated()) { register_proccesses_for_others (selection_file_name); /* Start the NSS process once with elevated rights to install into the default profile directories. */ if (!start_procces_for_user (selection_file_name, false)) { ERRORPRINTF ("Failed to run NSS installation process for default folders.\n"); xfree(selection_file_name); return -1; } } if (!start_procces_for_user (selection_file_name, true)) { ERRORPRINTF ("Failed to run NSS installation process.\n"); xfree(selection_file_name); return -1; } xfree(selection_file_name); return 0; } #endif