Финансы Сайт на котором знают все про финансы

Google Chrome и Secure Preferences

На нашем форуме и не только, с некоторой периодичностью люди интересуются алгоритмом генерации «защитного кода» в файле Secure Preferences для браузера Google Chrome.
Зачем он браузеру? Этот код используется для проверки целостности настроек расширений и некоторых других параметров, проще говоря — HMAC. Зачем он людям? Вероятно, это необходимый этап для тихой установки расширений или изменения настроек браузера. Давайте разберемся, где и как происходит генерация этих HMAC’ов.

Для начала заглянем в исходный код проекта Chromium. Беглый поиск по слову hash вывел меня на файл pref_hash_calculator.cc. И, что характерно, именно здесь все и происходит. Процитирую код основных методов, которые отвечают за генерацию:

std::string PrefHashCalculator::Calculate(const std::string& path, const base::Value* value) const
{
	return GetDigestString(seed_, GetMessage(device_id_, path, ValueAsString(value)));
}

// Concatenates |device_id|, |path|, and |value_as_string| to give the hash input.
std::string GetMessage(const std::string& device_id, const std::string& path, const std::string& value_as_string)
{
	std::string message;
	message.reserve(device_id.size() + path.size() + value_as_string.size());
	message.append(device_id);
	message.append(path);
	message.append(value_as_string);
	return message;
}

// Calculates an HMAC of |message| using |key|, encoded as a hexadecimal string.
std::string GetDigestString(const std::string& key, const std::string& message)
{
	crypto::HMAC hmac(crypto::HMAC::SHA256);
	std::vector digest(hmac.DigestLength());
	if (!hmac.Init(key) || !hmac.Sign(message, &digest[0], digest.size()))
	{
		NOTREACHED();
		return std::string();
	}

	return base::HexEncode(&digest[0], digest.size());
}

Авторское форматирование было задвинуто подальше, так как у меня личная неприязнь к такому стилю. Код выглядит довольно простым, осталось понять, что вообще подается на вход. Чтобы не скачивать весь исходный код Chromium и не собирать его, поступим следующим образом: поставим официальный Chrome, скачаем один лишь файл pref_hash_calculator.cc и укажем в Visual Studio путь к отладочным символам для Google Chrome:

https://chromium-browser-symsrv.commondatastorage.googleapis.com

Все готово к исследованию. Запускаем браузер, атачимся к процессу, ждем, пока прогрузятся отладочные символы (можно в настройках указать загрузку символов только для модуля chrome.dll, этого будет достаточно), открываем файл pref_hash_calculator.cc в MSVC и ставим брейкпоинт на методе Calculate. Теперь нам необходимо совершить какое-нибудь действие, которое приведет к вычислению хэша, например, установить произвольное расширение из Chrome Web Store. Устанавливаем и попадем на наш брейкопинт.

Мы видим значения seed_ (перевел в hex для удобства):

e748f336d85ea5f9dcdf25d8f347a65b4cdf667600f02df6724a2af18a212d26b788a25086910cf3a90313696871f3dc05823730c91df8ba5c4fd9c884b505a8

И device_id:

7CD6D9C7354E9165A3A4CBE097021669F4C026E7AA578B0CA9

Причем seed_ — постоянная величина (но, скорее всего, может меняться от версии к версии), а device_id — уникальный идентификатор компьютера. Откуда берется seed_? Не вдаваясь в подробности поиска, скажу, что он содержится в файле resources.pak, который находится в директории с браузером. Формат содержимого файла известный и давно описан, например, тут. Давайте попробуем самостоятельно извлечь seed_ из resources.pak. Для этого я напишу простенький скрипт на Perl:

use strict;
use warnings;
use File::Basename;
use Fcntl ':seek';
use constant NL => $^O eq 'MSWin32' ? "\015\012" : "\012";

$| = 1;

if(scalar @ARGV != 1)
{
	print "Usage: " , basename(__FILE__), " resources.pak", NL;
	exit 1;
}

open my $fh, '<', $ARGV[0] or die $!;

# Читаем заголовок resources.pak и извлекаем информацию о версии, кол-ве ресурсов и кодировке
# 4 byte version number + 4 byte number of resources + 1 byte encoding
read $fh, my $header, 9;

my ($version, $resources_total, $encoding) = unpack 'VVC', $header;

print "Version: $version", NL, "Resources: $resources_total", NL, "Encoding: ", $encoding, NL, NL;

# Перечисляем ресурсы и их смещения в файле
# 2 byte resource id  + 4 byte resource offset in file

# Учитываем ресурс с ID 0, означающий конец списка
my ($last_id, $last_offset) = (0, 0);
for(my $i = 0; $i < $resources_total + 1; $i++)
{
	read $fh, $header, 6;
	my ($id, $offset) = unpack 'SV', $header;

	if($last_id)
	{
		my $size = $offset - $last_offset;
		# Делаем предположение, что seed всегда состоит из 64 символов
		if($size == 64)
		{
			print "ID: $last_id", NL, "Offset: $last_offset", NL, "Size: $size", NL;

			seek $fh, $last_offset, SEEK_SET;
			read $fh, my $data, 64;

			print "Resource HEX data: ", unpack('H*', $data), NL;

			exit 0;
		}
	}

	$last_id = $id;
	$last_offset = $offset;

	if($id == 0)
	{
		last;
	}
}

Делаем тестовый прогон и узнаем, что seed_ содержится в ресурсе с ID 609, который скорее всего тоже меняется.

Теперь нам необходимо получить device_id. Откуда его берет Chrome? Не буду вас утомлять отладчиком, просто скажу, что нас интересует функция GetMachineId из файла machine_id.cc. Приведу ее исходный код на всякий случай:

bool GetMachineId(std::string* machine_id)
{
	if (!machine_id)
		return false;

	static std::string calculated_id;
	static bool calculated = false;
	if (calculated)
	{
		*machine_id = calculated_id;
		return true;
	}

	base::string16 sid_string;
	int volume_id;
	if (!GetRawMachineId(&sid_string, &volume_id))
    	return false;

	if (!testing::GetMachineIdImpl(sid_string, volume_id, machine_id))
		return false;

	calculated = true;
	calculated_id = *machine_id;
	return true;
}

Этот код является частью сторонней библиотеки RLZ. Я не стал особо копаться в логике вызовов, а просто выдрал код, немного подредактировал и сделал из него отдельный файл, который можно смело собирать и тестировать под Windows (генерация device_id отличается в зависимости от ОС). Ссылка на проект для Microsoft Visual Studio 2013 в конце статьи.
Итак, у нас есть seed_, device_id, осталось обратить свое внимание на два оставшихся аргумента, которые передаются в GetMessage — это path и value. Зайдем сразу внутрь функции GetMessage и посмотрим, что она формирует.

Мы видим, что path содержит путь к настройкам расширения в Secure Preferences:

extensions.settings.aapocclcgogkmnckokdopfmhonfmgoek

А value (value_as_string) — настройки расширения в JSON:

{"ack_external":true,"app_launcher_ordinal":"zs","creation_flags":137,"from_bookmark":false,"from_webstore":true,"initial_keybindings_set":true,"install_time":"13074357069686027","lastpingday":"13074332401422358","location":1,"manifest":{"api_console_project_id":"889782162350","app":{"launch":{"local_path":"main.html"}},"container":"GOOGLE_DRIVE","current_locale":"en_US","default_locale":"en_US","description":"Create and edit presentations ","icons":{"128":"icon_128.png","16":"icon_16.png"},"key":"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLOGW2Hoztw8m2z6SmCjm7y4Oe2o6aRqO+niYKCXhZab572by7acqFIFF0On3e3a967SwNijsTx2n+7Mt3KqWzEKtnwUZqzHYSsdZZK64vWIHIduawP0EICWRMf2RGIBEdDC6I1zErtcDiSrJWeRlnb0DHWXDXlt1YseM7RiON9wIDAQAB","manifest_version":2,"name":"Google Slides","offline_enabled":true,"update_url":"https://clients2.google.com/service/update2/crx","version":"0.9"},"page_ordinal":"n","path":"aapocclcgogkmnckokdopfmhonfmgoek\\0.9_0","state":1,"was_installed_by_default":true,"was_installed_by_oem":false}

Подытожим логику вычисления:

key = seed_
message = device_id + extension_path + extension_settings

hmac(message, key)

В качестве hmac используется HMAC SHA256, это видно по коду. Приведу пример простого скрипта, который парсит расширения из файла Secure Preferences и вычисляет hmac для каждого из них, а также super_mac. Алгоритм вычисления прост:

key = seed_
message = device_id + {security_preferences->protection->macs}

hmac(message, key)

super_mac используется для проверки целостности некоторых настроек браузера и массива пар ид_расширения — hmac. Наконец-таки скрипт:

use strict;
use warnings;
use JSON;
use File::Basename;
use Digest::SHA qw(hmac_sha256_hex);
use constant NL => $^O eq 'MSWin32' ? "\015\012" : "\012";


$| = 1;

if(scalar @ARGV != 3)
{
	print "Usage: " , basename(__FILE__), " device_id seed SecurePreferences ", NL;
	exit 1;
}

my ($device_id, $seed, $sec_pref_file) = @ARGV;

# Считаем и распарсим содержимое Secure Preferences
open my $fh, '<', $sec_pref_file or die $!;
undef $/;
my $json_raw_data = <$fh>;
close $fh;

# Необходимо, чтобы избежать досадную ситуацию:
# в Secure Preferences некоторые < представлены в виде \u003C, # а парсер JSON их декодирует, однако в вычислении HMAC они используются as-is $json_raw_data =~ s/\\u/\\\\u/g; my $json = JSON->new->allow_nonref;
my $sec_pref = $json->decode($json_raw_data);

my $reference_hmacs = $sec_pref->{protection}->{macs}->{extensions}->{settings};

# Пройдемся по всем расширениям и посчитаем для них HMAC
my %ext_settings = %{$sec_pref->{extensions}->{settings}};
# seed используется в бинарном виде
my $key = pack 'H*', $seed;

print NL;
while(my ($ext_name, $ext_prefs) = each %ext_settings)
{
	# Пустые JSON-объекты игнорируются
	remove_empty($ext_prefs);

	my $extension_path = 'extensions.settings.' . $ext_name;
	my $extension_settings = $json->canonical->encode($ext_prefs);
	$extension_settings =~ s/\\\\u/\\u/g;
	my $message = $device_id . $extension_path . $extension_settings;

	my $hmac = uc hmac_sha256_hex($message, $key);

	# Печатаем вычисленный нами и эталонный HMAC'и
	# Печатаем только первые и последние 4 символа, а то слишком длинно выходит
	my $ref_hmac = $reference_hmacs->{$ext_name};

	my $our_hmac_part = substr($hmac, 0, 4) . '...' . substr($hmac, -4);
	my $ref_hmac_part = substr($ref_hmac, 0, 4) . '...' . substr($ref_hmac, -4);

	print $ext_name, ' - our: ', $our_hmac_part, '; ref: ', $ref_hmac_part, '; ', ($hmac eq $ref_hmac ? 'OK' : 'ERR'), NL;
}

# Посчитаем super_maс
my $json_macs = $sec_pref->{protection}->{macs};
my $raw_macs = $json->canonical->encode($json_macs);

my $message = $device_id . $raw_macs;

my $hmac = uc hmac_sha256_hex($message, $key);
my $ref_hmac = $sec_pref->{protection}->{super_mac};

my $our_hmac_part = substr($hmac, 0, 4) . '...' . substr($hmac, -4);
my $ref_hmac_part = substr($ref_hmac, 0, 4) . '...' . substr($ref_hmac, -4);

print NL, 'Super mac - our: ', $our_hmac_part, '; ref: ', $ref_hmac_part, '; ', ($hmac eq $ref_hmac ? 'OK' : 'ERR'), NL;


sub remove_empty
{
	my $hash_ref = shift;

	my @queue = ($hash_ref);
	while(my $ref = shift @queue)
	{
		next unless ref $ref eq ref {};

		foreach my $key(keys %$ref)
		{
			if(ref $ref->{$key} eq ref {})
			{
				if(scalar values $ref->{$key} == 0)
				{
					push @queue, $hash_ref;
					delete $ref->{$key};
					next;
				}

				push @queue, $ref->{$key};
			}
			elsif(ref $ref->{$key} eq ref [])
			{
				if(@{$ref->{$key}} == 0)
				{
					push @queue, $hash_ref;
					delete $ref->{$key} ;
				}
			}
		}
	}
}

Запустим скрипт, указав в качестве аргументов device_id, seed и путь к файлу Secure Preferences:

Как мы видим, скрипт успешно отработал и корректно вычислил HMAC’и.

Скрипты из статьи и проект для MSVC 2013, вычисляющий device_id: скачать