If we look at the mojo interface gzipper.mojom (services/data_decoder/public/mojom/gzipper.mojom):
// An interface that lets callers compress and uncompress data
using gzip.
interface Gzipper {
// Compresses |data| using gzip and returns it as
|compressed_data|. Returns
// null if compression fails.
Compress(mojo_base.mojom.BigBuffer data)
=> (mojo_base.mojom.BigBuffer? compressed_data);
// Uncompresses |compressed_data| using gzip and returns it as
|data|. Returns
// null if uncompression fails.
Uncompress(mojo_base.mojom.BigBuffer compressed_data)
=> (mojo_base.mojom.BigBuffer? data);
};
It's interesting already since the service appears to be doing a
gzip decompression
directly from data in shared memory. However, this is architected
so that the caller
of these interfaces is (at present) always a trusted process, and
that the service
implementing this service is a less-privileged sandboxed utility
service.
We can see that Uncompress returns a BigBuffer, which may be
backed by shared
memory. If we look at the callsites for
DataDecoder::GzipUncompress, this leads
us to SandboxedUnpacker:
void SandboxedUnpacker::UnzipDone(const base::FilePath&
zip_file,
const base::FilePath& unzip_dir,
const std::string& error) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!error.empty()) {
ReportFailure(SandboxedUnpackerFailureReason::UNZIP_FAILED,
l10n_util::GetStringUTF16(IDS_EXTENSION_PACKAGE_UNZIP_ERROR));
return;
}
base::FilePath verified_contents_path =
file_util::GetVerifiedContentsPath(extension_root_);
// If the verified contents are already present in the _metadata
folder, we
// can ignore the verified contents in the header.
if (compressed_verified_contents_.empty() ||
base::PathExists(verified_contents_path)) {
Unpack(unzip_dir);
return;
}
data_decoder_.GzipUncompress(
compressed_verified_contents_,
base::BindOnce(&SandboxedUnpacker::OnVerifiedContentsUncompressed,
this,
unzip_dir));
}
Once this receives the response from the DataDecoder, it passes
the memory
returned in the BigBuffer directly into
StoreVerifiedContentsInExtensionDir:
void SandboxedUnpacker::OnVerifiedContentsUncompressed(
const base::FilePath& unzip_dir,
data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer>
result) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!result.value) {
ReportFailure(
SandboxedUnpackerFailureReason::
CRX_HEADER_VERIFIED_CONTENTS_UNCOMPRESSING_FAILURE,
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
ASCIIToUTF16(
\"CRX_HEADER_VERIFIED_CONTENTS_UNCOMPRESSING_FAILURE\")));
return;
}
if
(!StoreVerifiedContentsInExtensionDir(result.value.value().byte_span()))
return;
Unpack(unzip_dir);
}
bool SandboxedUnpacker::StoreVerifiedContentsInExtensionDir(
base::span<const uint8_t> verified_contents) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!VerifiedContents::Create(
ContentVerifierKey(kWebstoreSignaturesPublicKey,
kWebstoreSignaturesPublicKeySize),
{reinterpret_cast<const char*>(verified_contents.data()),
verified_contents.size()})) {
ReportFailure(SandboxedUnpackerFailureReason::MALFORMED_VERIFIED_CONTENTS,
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
ASCIIToUTF16(\"MALFORMED_VERIFIED_CONTENTS\")));
return false;
}
//...
Following this leads us further down:
std::unique_ptr<VerifiedContents>
VerifiedContents::Create(
base::span<const uint8_t> public_key,
base::StringPiece contents) {
ScopedUMARecorder<kUMAVerifiedContentsInitTime,
kUMAVerifiedContentsInitResult>
uma_recorder;
// Note: VerifiedContents constructor is private.
auto verified_contents = base::WrapUnique(new
VerifiedContents(public_key));
std::string payload;
if (!verified_contents->GetPayload(contents, &payload))
return nullptr;
And the first thing that VerifiedContents::GetPayload does is to
parse this
data as JSON. Note that at this point, the StringPiece contents is
still backed
by shared memory.
bool VerifiedContents::GetPayload(base::StringPiece
contents,
std::string* payload) {
base::Optional<base::Value> top_list =
base::JSONReader::Read(contents);
if (!top_list || !top_list->is_list())
return false;
I haven't verified that base::JSONReader does anything dangerous
when given an
input backed by shared memory; it actually looks like most of the
code in there
will manage to handle this unexpected situation fairly gracefully,
although it
definitely seems to be outside of the intended usage (and eg. ICU
code comes in
to play here as well...)
In any case, the next thing that
StoreVerifiedContentsInExtensionDir does is
(after verifying the signatures and stuff in the JSON it parsed) to
write the
contents of the shared memory out to disk; the attacker could
change those
contents between the JSON parse and the final version being written
to disk,
and they should be able to effectively bypass all of the checks
in
VerifiedContents::Create.
bool SandboxedUnpacker::StoreVerifiedContentsInExtensionDir(
base::span<const uint8_t> verified_contents) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!VerifiedContents::Create(
ContentVerifierKey(kWebstoreSignaturesPublicKey,
kWebstoreSignaturesPublicKeySize),
{reinterpret_cast<const char*>(verified_contents.data()),
verified_contents.size()})) {
ReportFailure(SandboxedUnpackerFailureReason::MALFORMED_VERIFIED_CONTENTS,
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
ASCIIToUTF16(\"MALFORMED_VERIFIED_CONTENTS\")));
return false;
}
base::FilePath metadata_path =
extension_root_.Append(kMetadataFolder);
if (!base::CreateDirectory(metadata_path)) {
ReportFailure(
SandboxedUnpackerFailureReason::COULD_NOT_CREATE_METADATA_DIRECTORY,
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
ASCIIToUTF16(\"COULD_NOT_CREATE_METADATA_DIRECTORY\")));
return false;
}
base::FilePath verified_contents_path =
file_util::GetVerifiedContentsPath(extension_root_);
// Cannot write the verified contents file.
if (!base::WriteFile(verified_contents_path, verified_contents)) {
// <-- verified_contents is still pointing to the shared
memory
ReportFailure(
SandboxedUnpackerFailureReason::
COULD_NOT_WRITE_VERIFIED_CONTENTS_INTO_FILE,
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
ASCIIToUTF16(\"COULD_NOT_WRITE_VERIFIED_CONTENTS_INTO_FILE\")));
return false;
}
return true;
}
This bug is subject to a 90 day disclosure deadline. After 90
days elapse,
the bug report will become visible to the public. The scheduled
disclosure
date is 2021-06-11. Disclosure at an earlier date is possible
if
agreed upon by all parties.
Found by:
Read more https://packetstormsecurity.com/files/163130/GS20210614154942.txt