blob: 0c6f411234ee9aa9823d308c32bf7f2ff960f1f7 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/chrome_cleaner/parsers/shortcut_parser/target/lnk_parser.h"
#include <stdio.h>
#include <windows.h>
#include <memory>
#include <utility>
#include <vector>
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/scoped_handle.h"
#include "base/win/shortcut.h"
#include "chrome/chrome_cleaner/os/disk_util.h"
#include "chrome/chrome_cleaner/test/test_file_util.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace chrome_cleaner {
namespace internal {
class LnkParserTest : public testing::Test {
public:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.GetPath(),
&target_file_path_));
}
base::win::ScopedHandle CreateAndOpenShortcut(
const base::win::ShortcutProperties& properties) {
base::FilePath shortcut_path =
temp_dir_.GetPath().AppendASCII("test_shortcut.lnk");
if (!base::win::CreateOrUpdateShortcutLink(
shortcut_path, properties,
base::win::ShortcutOperation::SHORTCUT_CREATE_ALWAYS)) {
LOG(ERROR) << "Could not create shortcut";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
base::File lnk_file(shortcut_path, base::File::Flags::FLAG_OPEN |
base::File::Flags::FLAG_READ);
base::win::ScopedHandle handle(lnk_file.TakePlatformFile());
if (!handle.IsValid())
LOG(ERROR) << "Error opening the lnk file";
return handle;
}
base::win::ScopedHandle CreateAndOpenShortcutFromBuffer(
const std::vector<BYTE>& shortcut_buffer) {
base::FilePath shortcut_path =
temp_dir_.GetPath().AppendASCII("test_shortcut_from_buffer.lnk");
base::File lnk_file(shortcut_path, base::File::Flags::FLAG_CREATE |
base::File::Flags::FLAG_WRITE |
base::File::Flags::FLAG_READ);
// Write the contents of the buffer to a new file.
uint64_t written_bytes = lnk_file.Write(
/*offset=*/0, reinterpret_cast<const char*>(shortcut_buffer.data()),
shortcut_buffer.size());
if (written_bytes != shortcut_buffer.size()) {
LOG(ERROR) << "Error writing shortcut from buffer";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
// Rewind the file to the start.
if (lnk_file.Seek(base::File::Whence::FROM_BEGIN, /*offset=*/0) == -1) {
LOG(ERROR) << "Could not rewind file to the begining";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
base::win::ScopedHandle lnk_handle(lnk_file.TakePlatformFile());
if (!lnk_handle.IsValid()) {
LOG(ERROR) << "Cannot open lnk from buffer for writing";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
return lnk_handle;
}
void CheckParsedShortcut(ParsedLnkFile* parsed_shortcut,
base::FilePath target_path,
base::string16 arguments,
base::FilePath icon_location) {
base::FilePath parsed_file_path(parsed_shortcut->target_path);
ASSERT_TRUE(PathEqual(parsed_file_path, target_path));
ASSERT_EQ(parsed_shortcut->command_line_arguments, arguments);
base::FilePath parsed_icon_location(parsed_shortcut->icon_location);
ASSERT_TRUE(PathEqual(parsed_icon_location, icon_location));
}
bool CreateFileWithUTF16Name(base::FilePath* file_path) {
base::string16 UTF16_file_name = L"NormalFile\x0278";
*file_path = temp_dir_.GetPath().Append(UTF16_file_name);
return CreateFileInFolder(temp_dir_.GetPath(), UTF16_file_name);
}
bool LoadShortcutIntoMemory(base::File* shortcut,
std::vector<BYTE>* shortcut_contents) {
int16_t file_size = shortcut->GetLength();
if (file_size == -1) {
LOG(ERROR) << "Cannot get lnk file size";
return false;
}
shortcut_contents->resize(file_size);
int64_t bytes_read = shortcut->Read(
/*offset=*/0, reinterpret_cast<char*>(shortcut_contents->data()),
file_size);
if (bytes_read != file_size) {
LOG(ERROR) << "Error reading lnk file, bytes read: " << bytes_read
<< "file size: " << file_size;
return false;
}
// Assert that the file was created with more than just the
// header.
if (file_size <= kHeaderSize) {
LOG(ERROR) << "Not enough data on lnk file";
return false;
}
return true;
}
base::win::ScopedHandle CreateAndOpenShortcutToNetworkAndLocalLocation(
const base::win::ShortcutProperties& properties) {
base::win::ScopedHandle local_lnk_handle =
CreateAndOpenShortcut(properties);
if (!local_lnk_handle.IsValid()) {
LOG(ERROR) << "Error creating lnk pointing to local file";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
// Load the shortcut into memory to modify the LinkInfoFlags.
base::File local_lnk(std::move(local_lnk_handle));
std::vector<BYTE> shortcut_buffer;
if (!LoadShortcutIntoMemory(&local_lnk, &shortcut_buffer)) {
LOG(ERROR) << "Error loading shortcut into memory";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
internal::LnkInfoPartialHeader* partial_header =
internal::LocateAndParseLnkInfoPartialHeader(&shortcut_buffer, nullptr);
if (!partial_header)
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
// The value 0x03 means that this link file points to a local file that
// is also in a shared folder (local and network location paths).
constexpr BYTE kLocalAndNetworkLnkFlag = 0x03;
partial_header->flags = kLocalAndNetworkLnkFlag;
// Write the lnk file back to disk.
base::FilePath local_and_network_shortcut_path =
temp_dir_.GetPath().AppendASCII("local_and_network_shortcut.lnk");
base::File local_and_network_shortcut(local_and_network_shortcut_path,
base::File::Flags::FLAG_CREATE |
base::File::Flags::FLAG_WRITE |
base::File::Flags::FLAG_READ);
uint64_t written_bytes = local_and_network_shortcut.Write(
/*offset=*/0, reinterpret_cast<const char*>(shortcut_buffer.data()),
shortcut_buffer.size());
if (written_bytes != shortcut_buffer.size()) {
LOG(ERROR) << "Error writing modified shortcut";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
return base::win::ScopedHandle(
local_and_network_shortcut.TakePlatformFile());
}
base::win::ScopedHandle CreateAndOpenCorruptedLnkFile(
const base::win::ShortcutProperties& properties) {
base::win::ScopedHandle good_lnk_handle = CreateAndOpenShortcut(properties);
if (!good_lnk_handle.IsValid()) {
LOG(ERROR) << "Error creating and opening good lnk file";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
// Load the shortcut into memory to corrupt it.
base::File good_lnk_file(std::move(good_lnk_handle));
std::vector<BYTE> shortcut_buffer;
if (!LoadShortcutIntoMemory(&good_lnk_file, &shortcut_buffer)) {
LOG(ERROR) << "Error loading shortcut into memory";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
// Leave the header intact and then corrupt every second byte
// on the file, this was an arbitrary choice.
uint64_t current_byte = kHeaderSize + 0x02;
while (current_byte < shortcut_buffer.size()) {
shortcut_buffer[current_byte] = 0xff;
current_byte += 2;
}
return CreateAndOpenShortcutFromBuffer(shortcut_buffer);
}
// Creates a shortcut file and pads |extra_size| extra bytes to the end of it.
base::win::ScopedHandle CreateAndOpenPaddedLnkFile(
const base::win::ShortcutProperties& properties,
size_t extra_size) {
base::win::ScopedHandle good_lnk_handle = CreateAndOpenShortcut(properties);
if (!good_lnk_handle.IsValid()) {
LOG(ERROR) << "Error creating and opening good lnk file";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
// Load the shortcut into memory.
base::File good_lnk_file(std::move(good_lnk_handle));
std::vector<BYTE> shortcut_buffer;
if (!LoadShortcutIntoMemory(&good_lnk_file, &shortcut_buffer)) {
LOG(ERROR) << "Error loading shortcut into memory";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
// Add extra bytes at the end of the buffer.
size_t initial_size = shortcut_buffer.size();
shortcut_buffer.resize(initial_size + extra_size);
std::fill(shortcut_buffer.begin() + initial_size, shortcut_buffer.end(), 1);
return CreateAndOpenShortcutFromBuffer(shortcut_buffer);
}
// Returns the last found instance of a given "StringData" in the given
// buffer. A StringData consists of a 16-bit unsigned integer which represents
// the size for a string followed by the variable-length unicode string. See
// https://msdn.microsoft.com/en-us/library/dd871305.aspx for more
// information.
bool FindStringDataInBuffer(const std::vector<BYTE>& buffer,
const base::string16& expected_string,
DWORD* found_location) {
size_t length = expected_string.length();
if (buffer.size() < length + 2)
return false;
bool found = false;
char size_upper = length >> 8;
char size_lower = length % (1 << 8);
for (size_t i = 0; i < buffer.size() - length - 1; i++) {
if (buffer[i] == size_lower && buffer[i + 1] == size_upper) {
const wchar_t* string_ptr =
reinterpret_cast<const wchar_t*>(buffer.data() + i + 2);
base::string16 found_string(string_ptr, length);
if (found_string == expected_string) {
found = true;
*found_location = i;
}
}
}
return found;
}
// Creates a shortcut file and modifies its bytes containing the size of
// the command-line arguments.
base::win::ScopedHandle CreateAndOpenCorruptedArgumentSizeLnkFile(
const base::win::ShortcutProperties& properties,
SHORT new_size) {
base::win::ScopedHandle good_lnk_handle = CreateAndOpenShortcut(properties);
if (!good_lnk_handle.IsValid()) {
LOG(ERROR) << "Error creating and opening good lnk file";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
// Load the shortcut into memory.
base::File good_lnk_file(std::move(good_lnk_handle));
std::vector<BYTE> shortcut_buffer;
if (!LoadShortcutIntoMemory(&good_lnk_file, &shortcut_buffer)) {
LOG(ERROR) << "Error loading shortcut into memory";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
// Find the location of the command-line argument size bytes.
DWORD argument_size_location;
if (!FindStringDataInBuffer(shortcut_buffer, properties.arguments,
&argument_size_location)) {
LOG(ERROR) << "Error finding argument locations";
return base::win::ScopedHandle(INVALID_HANDLE_VALUE);
}
// Set the command-line argument bytes to |new_size|.
shortcut_buffer[argument_size_location] = new_size % (1 << 8);
shortcut_buffer[argument_size_location + 1] = new_size >> 8;
return CreateAndOpenShortcutFromBuffer(shortcut_buffer);
}
protected:
base::FilePath target_file_path_;
base::ScopedTempDir temp_dir_;
};
TEST_F(LnkParserTest, ParseLnkWithoutArgumentsTest) {
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
base::win::ScopedHandle lnk_handle = CreateAndOpenShortcut(properties);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile parsed_shortcut;
ASSERT_EQ(ParseLnk(std::move(lnk_handle), &parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
CheckParsedShortcut(&parsed_shortcut, target_file_path_, L"",
base::FilePath(L""));
}
TEST_F(LnkParserTest, ParseLnkWithArgumentsTest) {
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
const base::string16 kArguments = L"Seven";
properties.set_arguments(kArguments.c_str());
base::win::ScopedHandle lnk_handle = CreateAndOpenShortcut(properties);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile parsed_shortcut;
ASSERT_EQ(ParseLnk(std::move(lnk_handle), &parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
CheckParsedShortcut(&parsed_shortcut, target_file_path_, kArguments,
base::FilePath(L""));
}
TEST_F(LnkParserTest, ParseLnkWithExtraStringStructures) {
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
const base::string16 kArguments =
L"lavidaesbella --arg1 --argmento2 --argumento3 /a --Seven";
properties.set_arguments(kArguments.c_str());
properties.set_working_dir(temp_dir_.GetPath());
properties.set_app_id(L"id");
base::win::ScopedHandle lnk_handle = CreateAndOpenShortcut(properties);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile parsed_shortcut;
ASSERT_EQ(ParseLnk(std::move(lnk_handle), &parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
CheckParsedShortcut(&parsed_shortcut, target_file_path_, kArguments,
base::FilePath(L""));
}
TEST_F(LnkParserTest, UTF16FileNameParseTest) {
base::FilePath utf16_path;
ASSERT_TRUE(CreateFileWithUTF16Name(&utf16_path));
base::win::ShortcutProperties properties;
properties.set_target(utf16_path);
base::win::ScopedHandle lnk_handle = CreateAndOpenShortcut(properties);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile parsed_shortcut;
ASSERT_EQ(ParseLnk(std::move(lnk_handle), &parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
CheckParsedShortcut(&parsed_shortcut, utf16_path, L"", base::FilePath(L""));
}
TEST_F(LnkParserTest, InvalidHandleTest) {
base::win::ScopedHandle invalid_handle(INVALID_HANDLE_VALUE);
ParsedLnkFile unused_parsed_shortcut;
ASSERT_NE(ParseLnk(std::move(invalid_handle), &unused_parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
}
TEST_F(LnkParserTest, NoShortcutFileTest) {
base::File no_shortcut(target_file_path_, base::File::Flags::FLAG_OPEN |
base::File::Flags::FLAG_READ);
base::win::ScopedHandle handle(no_shortcut.TakePlatformFile());
ASSERT_TRUE(handle.IsValid());
ParsedLnkFile unused_parsed_shortcut;
ASSERT_NE(ParseLnk(std::move(handle), &unused_parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
}
TEST_F(LnkParserTest, CorruptedShortcutTest) {
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
properties.set_working_dir(temp_dir_.GetPath());
base::win::ScopedHandle bad_lnk_handle =
CreateAndOpenCorruptedLnkFile(properties);
ASSERT_TRUE(bad_lnk_handle.IsValid());
ParsedLnkFile unused_parsed_shortcut;
ASSERT_NE(ParseLnk(std::move(bad_lnk_handle), &unused_parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
}
TEST_F(LnkParserTest, ReasonablyLargeFileSizeShortcutTest) {
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
properties.set_working_dir(temp_dir_.GetPath());
// Create a LNK file with an extra kilobyte of data appended to it.
base::win::ScopedHandle lnk_handle =
CreateAndOpenPaddedLnkFile(properties, 1024);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile parsed_shortcut;
ASSERT_EQ(ParseLnk(std::move(lnk_handle), &parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
CheckParsedShortcut(&parsed_shortcut, target_file_path_, L"",
base::FilePath(L""));
}
TEST_F(LnkParserTest, TooLargeFileSizeShortcutTest) {
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
properties.set_working_dir(temp_dir_.GetPath());
// Create a LNK file with an extra megabyte of data appended to it.
base::win::ScopedHandle lnk_handle =
CreateAndOpenPaddedLnkFile(properties, 1024 * 1024);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile unused_parsed_shortcut;
ASSERT_NE(ParseLnk(std::move(lnk_handle), &unused_parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
}
TEST_F(LnkParserTest, ArgumentsSizeCorruptedShortcutTest_TooLarge) {
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
properties.set_working_dir(temp_dir_.GetPath());
const base::string16 kArguments = L"foo --bar";
properties.set_arguments(kArguments.c_str());
// Create a LNK file which thinks its arguments are much longer than they
// really are.
base::win::ScopedHandle lnk_handle =
CreateAndOpenCorruptedArgumentSizeLnkFile(properties, 256);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile unused_parsed_shortcut;
ASSERT_NE(ParseLnk(std::move(lnk_handle), &unused_parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
}
TEST_F(LnkParserTest, ArgumentsSizeCorruptedShortcutTest_ZeroSize) {
const base::string16 kArguments = L"foo --bar";
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
properties.set_working_dir(temp_dir_.GetPath());
properties.set_arguments(kArguments.c_str());
// Create a LNK file which thinks its arguments are of size zero.
base::win::ScopedHandle lnk_handle =
CreateAndOpenCorruptedArgumentSizeLnkFile(properties, 0);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile unused_parsed_shortcut;
ASSERT_NE(ParseLnk(std::move(lnk_handle), &unused_parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
}
TEST_F(LnkParserTest, ArgumentsSizeCorruptedShortcutTest_NegativeSize) {
const base::string16 kArguments = L"foo --bar";
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
properties.set_working_dir(temp_dir_.GetPath());
properties.set_arguments(kArguments.c_str());
// Create a LNK file which thinks its arguments are of a negative size.
// The size is supposed to be unsigned, so a negative size should have the
// same behavior as if the size was much larger than the real file.
base::win::ScopedHandle lnk_handle =
CreateAndOpenCorruptedArgumentSizeLnkFile(properties, -42);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile unused_parsed_shortcut;
ASSERT_NE(ParseLnk(std::move(lnk_handle), &unused_parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
}
TEST_F(LnkParserTest, ArgumentsSizeCorruptedShortcutTest_Smaller) {
const base::string16 kArguments = L"foo --bar";
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
properties.set_working_dir(temp_dir_.GetPath());
properties.set_arguments(kArguments.c_str());
// Create a LNK file which thinks its arguments are smaller than they
// really are.
base::win::ScopedHandle lnk_handle =
CreateAndOpenCorruptedArgumentSizeLnkFile(properties, 3);
ASSERT_TRUE(lnk_handle.IsValid());
ParsedLnkFile parsed_shortcut;
ASSERT_EQ(ParseLnk(std::move(lnk_handle), &parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
CheckParsedShortcut(&parsed_shortcut, target_file_path_, L"foo",
base::FilePath(L""));
}
TEST_F(LnkParserTest, LocalAndNetworkShortcutTest) {
base::win::ShortcutProperties properties;
properties.set_target(target_file_path_);
properties.set_working_dir(temp_dir_.GetPath());
base::win::ScopedHandle local_and_network_lnk_handle =
CreateAndOpenShortcutToNetworkAndLocalLocation(properties);
ASSERT_TRUE(local_and_network_lnk_handle.IsValid());
ParsedLnkFile parsed_shortcut;
ASSERT_EQ(ParseLnk(std::move(local_and_network_lnk_handle), &parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
CheckParsedShortcut(&parsed_shortcut, target_file_path_, L"",
base::FilePath(L""));
}
TEST_F(LnkParserTest, ParseIconLocationTest) {
base::FilePath txt_file_path = temp_dir_.GetPath().Append(L"file.txt");
base::File txt_file(txt_file_path, base::File::Flags::FLAG_CREATE |
base::File::Flags::FLAG_READ);
ASSERT_TRUE(txt_file.IsValid());
base::win::ShortcutProperties properties;
properties.set_target(txt_file_path);
properties.set_icon(txt_file_path, /*icon_index=*/0);
base::win::ScopedHandle txt_lnk_handle = CreateAndOpenShortcut(properties);
ASSERT_TRUE(txt_lnk_handle.IsValid());
ParsedLnkFile parsed_shortcut;
EXPECT_EQ(ParseLnk(std::move(txt_lnk_handle), &parsed_shortcut),
mojom::LnkParsingResult::SUCCESS);
CheckParsedShortcut(&parsed_shortcut, txt_file_path, L"", txt_file_path);
}
} // namespace internal
} // namespace chrome_cleaner