blob: ea6ae660b2127473a449456e3634c3c2871e8528 [file] [log] [blame]
// Copyright 2007-2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================
//
// Implementation of the Google Update recovery mechanism to be included in
// Google apps.
#include "omaha/common/google_update_recovery.h"
#include <shellapi.h>
#include <wininet.h>
#include <atlstr.h>
#include "omaha/common/const_addresses.h"
#include "omaha/common/signaturevalidator.h"
#include "omaha/third_party/smartany/scoped_any.h"
namespace omaha {
namespace {
const int kRollbackWindowDays = 100;
const TCHAR* const kMachineRepairArgs = _T("/recover /machine");
const TCHAR* const kUserRepairArgs = _T("/recover");
// TODO(omaha): Add a Code Red lib version that is manually updated when
// we check in the lib.
const TCHAR* const kQueryStringFormat =
_T("?appid=%s&appversion=%s&applang=%s&machine=%u")
_T("&version=%s&machineid=%s&userid=%s")
_T("&osversion=%s&servicepack=%s");
// Information about where to obtain Omaha info.
// This must never change in Omaha.
const TCHAR* const kRegValueProductVersion = _T("pv");
const TCHAR* const kRegValueUserId = _T("ui");
const TCHAR* const kRegValueMachineId = _T("mi");
const TCHAR* const kRelativeGoopdateRegPath = _T("Software\\Google\\Update\\");
const TCHAR* const kRelativeClientsGoopdateRegPath =
_T("Software\\Google\\Update\\Clients\\")
_T("{430FD4D0-B729-4F61-AA34-91526481799D}");
// Starts another process via ::CreateProcess.
HRESULT StartProcess(const TCHAR* process_name,
TCHAR* command_line) {
if (!process_name && !command_line) {
return E_INVALIDARG;
}
PROCESS_INFORMATION pi = {0};
STARTUPINFO si = {sizeof(si), 0};
// Feedback cursor is off while the process is starting.
si.dwFlags = STARTF_FORCEOFFFEEDBACK;
BOOL success = ::CreateProcess(
process_name, // Module name
command_line, // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
0, // No creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi); // Pointer to PROCESS_INFORMATION structure
if (!success) {
return HRESULT_FROM_WIN32(::GetLastError());
}
::CloseHandle(pi.hProcess);
::CloseHandle(pi.hThread);
return S_OK;
}
// Check if a string starts with another string. Case-sensitive.
bool StringStartsWith(const TCHAR *str, const TCHAR *start_str) {
if (!start_str || !str) {
return false;
}
while (0 != *str) {
// Check for matching characters
TCHAR c1 = *str;
TCHAR c2 = *start_str;
// Reached the end of start_str?
if (0 == c2)
return true;
if (c1 != c2)
return false;
++str;
++start_str;
}
// If str is shorter than start_str, no match. If equal size, match.
return 0 == *start_str;
}
// Escape and unescape strings (shlwapi-based implementation).
// The intended usage for these APIs is escaping strings to make up
// URLs, for example building query strings.
//
// Pass false to the flag segment_only to escape the url. This will not
// cause the conversion of the # (%23), ? (%3F), and / (%2F) characters.
// Characters that must be encoded include any characters that have no
// corresponding graphic character in the US-ASCII coded character
// set (hexadecimal 80-FF, which are not used in the US-ASCII coded character
// set, and hexadecimal 00-1F and 7F, which are control characters),
// blank spaces, "%" (which is used to encode other characters),
// and unsafe characters (<, >, ", #, {, }, |, \, ^, ~, [, ], and ').
//
// The input and output strings can't be longer than INTERNET_MAX_URL_LENGTH
HRESULT StringEscape(const CString& str_in,
bool segment_only,
CString* escaped_string) {
if (!escaped_string) {
return E_INVALIDARG;
}
DWORD buf_len = INTERNET_MAX_URL_LENGTH + 1;
HRESULT hr = ::UrlEscape(str_in,
escaped_string->GetBufferSetLength(buf_len),
&buf_len,
segment_only ?
URL_ESCAPE_PERCENT | URL_ESCAPE_SEGMENT_ONLY :
URL_ESCAPE_PERCENT);
if (SUCCEEDED(hr)) {
escaped_string->ReleaseBuffer();
}
return hr;
}
// Gets the temporary files directory for the current user.
// The directory returned may not exist.
// The returned path ends with a '\'.
// Fails if the path is longer than MAX_PATH.
HRESULT GetTempDir(CString* temp_path) {
if (!temp_path) {
return E_INVALIDARG;
}
temp_path->Empty();
TCHAR buffer[MAX_PATH] = {0};
DWORD num_chars = ::GetTempPath(MAX_PATH, buffer);
if (!num_chars) {
return HRESULT_FROM_WIN32(::GetLastError());
} else if (num_chars >= MAX_PATH) {
return E_FAIL;
}
*temp_path = buffer;
return S_OK;
}
// Creates the specified directory.
HRESULT CreateDir(const CString& dir) {
if (!::CreateDirectory(dir, NULL)) {
DWORD error = ::GetLastError();
if (ERROR_FILE_EXISTS != error && ERROR_ALREADY_EXISTS != error) {
return HRESULT_FROM_WIN32(error);
}
}
return S_OK;
}
HRESULT GetAndCreateTempDir(CString* temp_path) {
if (!temp_path) {
return E_INVALIDARG;
}
HRESULT hr = GetTempDir(temp_path);
if (FAILED(hr)) {
return hr;
}
if (temp_path->IsEmpty()) {
return E_FAIL;
}
// Create this dir if it doesn't already exist.
return CreateDir(*temp_path);
}
// Create a unique temporary file and returns the full path.
HRESULT CreateUniqueTempFile(const CString& user_temp_dir,
CString* unique_temp_file_path) {
if (user_temp_dir.IsEmpty() || !unique_temp_file_path) {
return E_INVALIDARG;
}
TCHAR unique_temp_filename[MAX_PATH] = {0};
if (!::GetTempFileName(user_temp_dir,
_T("GUR"), // prefix
0, // form a unique filename
unique_temp_filename)) {
return HRESULT_FROM_WIN32(::GetLastError());
}
*unique_temp_file_path = unique_temp_filename;
if (unique_temp_file_path->IsEmpty()) {
return E_FAIL;
}
return S_OK;
}
// Obtains the OS version and service pack.
HRESULT GetOSInfo(CString* os_version, CString* service_pack) {
if (!os_version || !service_pack) {
return E_INVALIDARG;
}
OSVERSIONINFO os_version_info = { 0 };
os_version_info.dwOSVersionInfoSize = sizeof(os_version_info);
if (!::GetVersionEx(&os_version_info)) {
HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
return hr;
} else {
os_version->Format(_T("%d.%d"),
os_version_info.dwMajorVersion,
os_version_info.dwMinorVersion);
*service_pack = os_version_info.szCSDVersion;
}
return S_OK;
}
// Reads the specified string value from the specified registry key.
// Only supports value types REG_SZ and REG_EXPAND_SZ.
// REG_EXPAND_SZ strings are not expanded.
HRESULT GetRegStringValue(bool is_machine_key,
const CString& relative_key_path,
const CString& value_name,
CString* value) {
if (!value) {
return E_INVALIDARG;
}
value->Empty();
HKEY root_key = is_machine_key ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
HKEY key = NULL;
LONG res = ::RegOpenKeyEx(root_key, relative_key_path, 0, KEY_READ, &key);
if (res != ERROR_SUCCESS) {
return HRESULT_FROM_WIN32(res);
}
// First get the size of the string buffer.
DWORD type = 0;
DWORD byte_count = 0;
res = ::RegQueryValueEx(key, value_name, NULL, &type, NULL, &byte_count);
if (ERROR_SUCCESS != res) {
::RegCloseKey(key);
return HRESULT_FROM_WIN32(res);
}
if ((type != REG_SZ && type != REG_EXPAND_SZ) || (0 == byte_count)) {
::RegCloseKey(key);
return E_FAIL;
}
CString local_value;
// GetBuffer throws when not able to allocate the requested buffer.
TCHAR* buffer = local_value.GetBuffer(byte_count / sizeof(TCHAR));
res = ::RegQueryValueEx(key,
value_name,
NULL,
NULL,
reinterpret_cast<byte*>(buffer),
&byte_count);
if (ERROR_SUCCESS == res) {
local_value.ReleaseBufferSetLength(byte_count / sizeof(TCHAR));
*value = local_value;
}
::RegCloseKey(key);
return HRESULT_FROM_WIN32(res);
}
// Obtains information about the current Omaha installation.
// Attempts to obtain as much information as possible even if errors occur.
// Therefore, return values of GetRegStringValue are ignored.
HRESULT GetOmahaInformation(bool is_machine_app,
CString* omaha_version,
CString* machine_id,
CString* user_id) {
if (!omaha_version || !machine_id || !user_id) {
return E_INVALIDARG;
}
if (FAILED(GetRegStringValue(is_machine_app,
kRelativeClientsGoopdateRegPath,
kRegValueProductVersion,
omaha_version))) {
*omaha_version = _T("0.0.0.0");
}
GetRegStringValue(true, // Machine ID is always in HKLM.
kRelativeGoopdateRegPath,
kRegValueMachineId,
machine_id);
GetRegStringValue(is_machine_app,
kRelativeGoopdateRegPath,
kRegValueUserId,
user_id);
return S_OK;
}
// Builds the query portion of the recovery url.
// This method obtains values necessary to build the query that are not provided
// as parameters.
// Attempts to build with as much information as possible even if errors occur.
HRESULT BuildUrlQueryPortion(const CString& app_guid,
const CString& app_version,
const CString& app_language,
bool is_machine_app,
CString* query) {
if (!query) {
return E_INVALIDARG;
}
CString omaha_version;
CString machine_id;
CString user_id;
GetOmahaInformation(is_machine_app, &omaha_version, &machine_id, &user_id);
CString os_version;
CString os_service_pack;
GetOSInfo(&os_version, &os_service_pack);
// All parameters must be escaped individually before building the query.
CString app_guid_escaped;
CString app_version_escaped;
CString app_language_escaped;
CString omaha_version_escaped;
CString machine_id_escaped;
CString user_id_escaped;
CString os_version_escaped;
CString os_service_pack_escaped;
StringEscape(app_guid, true, &app_guid_escaped);
StringEscape(app_version, true, &app_version_escaped);
StringEscape(app_language, true, &app_language_escaped);
StringEscape(omaha_version, true, &omaha_version_escaped);
StringEscape(machine_id, true, &machine_id_escaped);
StringEscape(user_id, true, &user_id_escaped);
StringEscape(os_version, true, &os_version_escaped);
StringEscape(os_service_pack, true, &os_service_pack_escaped);
query->Format(kQueryStringFormat,
app_guid_escaped,
app_version_escaped,
app_language_escaped,
is_machine_app ? 1 : 0,
omaha_version_escaped,
machine_id_escaped,
user_id_escaped,
os_version_escaped,
os_service_pack_escaped);
return S_OK;
}
// Returns the full path to save the downloaded file to.
// The path is based on a unique temporary filename to avoid a conflict
// between multiple apps downloading to the same location.
// The path to this file is also returned. The caller is responsible for
// deleting the temporary file after using the download target path.
// If it cannot create the unique directory, it attempts to use the user's
// temporary directory and a constant filename.
HRESULT GetDownloadTargetPath(CString* download_target_path,
CString* temp_file_path) {
if (!download_target_path || !temp_file_path) {
return E_INVALIDARG;
}
CString user_temp_dir;
HRESULT hr = GetAndCreateTempDir(&user_temp_dir);
if (FAILED(hr)) {
return hr;
}
hr = CreateUniqueTempFile(user_temp_dir, temp_file_path);
if (SUCCEEDED(hr) && !temp_file_path->IsEmpty()) {
*download_target_path = *temp_file_path;
// Ignore the return value. A .tmp filename is better than none.
download_target_path->Replace(_T(".tmp"), _T(".exe"));
} else {
// Try a static filename in the temp directory as a fallback.
*download_target_path = user_temp_dir + _T("GoogleUpdateSetup.exe");
*temp_file_path = _T("");
}
return S_OK;
}
HRESULT DownloadRepairFile(const CString& download_target_path,
const CString& app_guid,
const CString& app_version,
const CString& app_language,
bool is_machine_app,
DownloadCallback download_callback,
void* context) {
CString query;
BuildUrlQueryPortion(app_guid,
app_version,
app_language,
is_machine_app,
&query);
CString url = omaha::kUrlCodeRedCheck + query;
return download_callback(url, download_target_path, context);
}
// Makes sure the path is enclosed with double quotation marks.
void EnclosePath(CString* path) {
if (path) {
return;
}
if (!path->IsEmpty() && path->GetAt(0) != _T('"')) {
path->Insert(0, _T('"'));
path->AppendChar(_T('"'));
}
}
HRESULT RunRepairFile(const CString& file_path, bool is_machine_app) {
const TCHAR* repair_file_args = is_machine_app ? kMachineRepairArgs :
kUserRepairArgs;
CString command_line(file_path);
EnclosePath(&command_line);
command_line.AppendChar(_T(' '));
command_line.Append(repair_file_args);
return StartProcess(NULL, command_line.GetBuffer());
}
} // namespace
// Verifies the file's integrity and that it is signed by Google.
// We cannot prevent rollback attacks by using a version because the client
// may not be able to determine the current version if the files and/or
// registry entries have been deleted/corrupted.
// Therefore, we check that the file was signed recently.
HRESULT VerifyFileSignature(const CString& filename) {
// Use Authenticode/WinVerifyTrust to verify the file.
// Allow the revocation check to use the network.
HRESULT hr = VerifySignature(filename, true);
if (FAILED(hr)) {
return hr;
}
// Verify that there is a Google certificate and that it has not expired.
if (!VerifySigneeIsGoogle(filename)) {
return CERT_E_CN_NO_MATCH;
}
// Check that the file was signed recently to limit the window for
// rollback attacks.
return VerifyFileSignedWithinDays(filename, kRollbackWindowDays);
}
// Verifies the file contains the special markup resource for repair files.
HRESULT VerifyRepairFileMarkup(const CString& filename) {
const TCHAR* kMarkupResourceName = MAKEINTRESOURCE(1);
const TCHAR* kMarkupResourceType = _T("GOOGLEUPDATEREPAIR");
const DWORD kMarkupResourceExpectedValue = 1;
scoped_library module(::LoadLibraryEx(filename, 0, LOAD_LIBRARY_AS_DATAFILE));
if (!module) {
return HRESULT_FROM_WIN32(::GetLastError());
}
HRSRC resource(::FindResource(get(module),
kMarkupResourceName,
kMarkupResourceType));
if (!resource) {
return HRESULT_FROM_WIN32(::GetLastError());
}
if (sizeof(kMarkupResourceExpectedValue) !=
::SizeofResource(get(module), resource)) {
return E_UNEXPECTED;
}
HGLOBAL loaded_resource(::LoadResource(get(module), resource));
if (!loaded_resource) {
return HRESULT_FROM_WIN32(::GetLastError());
}
const DWORD* value = static_cast<DWORD*>(::LockResource(loaded_resource));
if (!value) {
return E_HANDLE;
}
if (kMarkupResourceExpectedValue != *value) {
return E_UNEXPECTED;
}
return S_OK;
}
// Verifies the filename is not UNC name, the file exists, has a valid signature
// chain, is signed by Google, and contains the special markup resource for
// repair files.
HRESULT VerifyIsValidRepairFile(const CString& filename) {
// Make sure file exists.
if (!::PathFileExists(filename)) {
return HRESULT_FROM_WIN32(::GetLastError());
}
HRESULT hr = VerifyFileSignature(filename);
if (FAILED(hr)) {
return hr;
}
return VerifyRepairFileMarkup(filename);
}
} // namespace omaha
// If a repair file is run, the file will not be deleted until reboot. Delete
// after reboot will only succeed when executed by an admin or LocalSystem.
HRESULT FixGoogleUpdate(const TCHAR* app_guid,
const TCHAR* app_version,
const TCHAR* app_language,
bool is_machine_app,
DownloadCallback download_callback,
void* context) {
if (!app_guid || !app_version || !app_language || !download_callback) {
return E_INVALIDARG;
}
CString download_target_path;
CString temp_file_path;
HRESULT hr = omaha::GetDownloadTargetPath(&download_target_path,
&temp_file_path);
if (FAILED(hr)) {
return hr;
}
if (download_target_path.IsEmpty()) {
hr = E_FAIL;
}
// After calling DownloadRepairFile, don't return until the repair file and
// temp file have been deleted.
hr = omaha::DownloadRepairFile(download_target_path,
app_guid,
app_version,
app_language,
is_machine_app,
download_callback,
context);
if (SUCCEEDED(hr)) {
hr = omaha::VerifyIsValidRepairFile(download_target_path);
}
if (FAILED(hr)) {
::DeleteFile(download_target_path);
::DeleteFile(temp_file_path);
return hr;
}
hr = omaha::RunRepairFile(download_target_path, is_machine_app);
::MoveFileEx(download_target_path, NULL, MOVEFILE_DELAY_UNTIL_REBOOT);
::DeleteFile(temp_file_path);
return hr;
}