1、前言
U盘作为移动存储设备之一,是我们日常生活中接触最多和最常用的存储介质。正因如此,针对U盘内容的管理也因为使用场景的多样和复杂性,变得难以实现。
我们的设计思路是先通过某些手段对U盘进行病毒,并在查杀完成后对U盘进行认证。在后续使用过程检测认证U盘是否被篡改以保证U盘的安全性。
2、设计
本方案的设计主要包含两个部分:U盘认证和U盘检测。
2.1 认证
- 检测当前设备是否USB存储设备
- 根据U盘内容创建合适的布隆过滤器
- 遍历U盘文件,以文件名 + 时间戳 + 文件大小的方式生成标志字符串,添加到布隆过滤器中
- 保存布隆过滤器
- 保存布隆过滤器的时间戳
2.2 检测
- 检测当前设备是否USB存储设备
- 根据布隆过滤器的时间戳,计算当前系统的时间偏差
- 加载布隆过滤器
- 遍历U盘文件,以文件名 + 时间戳 + 文件大小的方式生成标志字符串,检测是否命中
3、实现
3.1 设备检测
#ifdef _WIN32
if (driver_[0] != 'A' && driver_[0] != 'B' &&
// 判断设备是否可移动介质
GetDriveTypeA(driver_.c_str()) == DRIVE_REMOVABLE) {
// 判断设备是否加载
if (GetVolumeInformationA(driver_.c_str(), NULL, NULL, NULL, NULL, NULL,
NULL, 0)) {
return true;
}
}
#else
std::ifstream mounts("/proc/mounts");
if (mounts.is_open()) {
std::string line;
while (std::getline(mounts, line)) {
std::stringstream ss(line);
std::string device, mount_point;
ss >> device >> mount_point;
if (driver_.substr(0, driver_.length() - 1) == mount_point) {
if (0 == strncmp("/dev/sd", device.c_str(), 7)) {
ss.str("");
ss << "/sys/block/";
// 获取块设备
for (size_t i = 5; i <= device.length(); ++i) { //
if ('0' <= device[i] && '9' >= device[i]) {
break;
}
ss << device[i];
}
std::error_code err;
std::string dev_pci_name =
fs::read_symlink(ss.str(), err).generic_string();
// 检测pci总线中是否包含"usb"标识
if (dev_pci_name.find("usb") != std::string::npos) {
return true;
}
}
return false;
}
}
}
#endif
return false;
3.2 文件遍历
std::stringstream ss;
try {
for (fs::recursive_directory_iterator it(driver_path);
it != fs::recursive_directory_iterator(); it++) {
std::string path = it->path().generic_u8string().substr(
driver_path.size(), std::string::npos);
// 跳过认证文件
if (strcmp(kBloomFilterPath, path.c_str()) == 0 ||
strcmp(kTimeMarkPath, path.c_str()) == 0 ||
strcmp(kUsbAuthDir, path.c_str()) == 0)
continue;
if (!fs::is_directory(it->path()) && !fs::is_regular_file(it->path()))
continue;
ss.str("");
ss << path;
// 获取文件的filetime
auto time = tm_::duration_cast<tm_::seconds>(
it->last_write_time().time_since_epoch())
.count();
ss << (time - elapse);
size_t file_size = 0x1000;
if (fs::is_regular_file(it->path())) {
file_size = it->file_size();
}
ss << file_size;
if (!bf_.BloomAdd(ss.str())) {
return error_code::kIOFailure;
}
}
} catch (fs::filesystem_error &e) {
return error_code::kIOFailure;
}
3.3 布隆过滤器
// Hash算法:Fnv1a_64
uint64_t hash = 0xcbf29ce484222325ull;
const uint8_t *bytes = static_cast<const uint8_t *>(data);
for (size_t i = 0; i < numBytes; ++i) {
hash ^= bytes[i];
hash *= 0x100000001b3ull;
}
uint8_t seed_arr[32] = {0};
#ifdef _WIN32
numBytes = sprintf_s((char *)seed_arr, sizeof(seed_arr) - 1, "%d", seed);
#else
numBytes = snprintf((char *)seed_arr, sizeof(seed_arr) - 1, "%d", seed);
#endif
for (size_t i = 0; i < numBytes; ++i) {
hash ^= seed_arr[i];
hash *= 0x100000001b3ull;
}
return hash;
4、难点记录
4.1 时间戳获取
C++17标准中提供的std::filesystem::last_write_time()
函数返回的类型为std::filesystem::file_time_type
。
查阅官方文档可知,file_time_type
在C++17中的实现是基于平台的,不支持跨平台。
在MSVC的代码中可以发现,Windows平台提供的file_time_type
是基于FileTime实现的。即在Windows C++中,FileTime的计算方式是从1601年1月1日开始的100纳秒为单位的时间。
因此当我们需要将std::filesystem::last_write_time()
转换为UTC时间戳时,首先需要使用duration_cast
将单位转换为秒。
auto time = tm_::duration_cast<tm_::seconds>(it->last_write_time().time_since_epoch()).count();
通过上一步,我们取得了从1601年1月1日开始的秒为单位的时间戳。下一步,我们需要减去FileTime和UTC时间之前的差值。
从C++文档中我们可以查到std::chrono::system_clock
使用的正是1970年开始的UTC时间,因此可以通过以下手段获取秒为单位的差值。
auto elapse = tm_::duration_cast<tm_::seconds>
(fs::file_time_type::clock::now().time_since_epoch() - tm_::system_clock::now().time_since_epoch()).count();
最后,通过以上两个数据,我们计算出文件修改时间的UTC时间戳。
auto mtime = time - elapse
4.2 时间戳不一致
在实际测试中发现,即同一个文件在Windows和Linux系统中获取的时间戳在某些情况会出现不一致的问题。
经过调查发现,是由于Windows 与 Linux 看待硬件时间的方式不同。
Windows 把电脑的硬件时钟(RTC)看成是本地时间,即 RTC = Local Time,Windows 会直接显示硬件时间;
而 Linux 则是把电脑的硬件时钟看成 UTC 时间,即 RTC = UTC,那么 Linux 显示的时间就是硬件时间加上时区。
针对这个问题,和团队成员讨论后最终决定了一个方案。通过在认证阶段生成一个标识时间戳,在检测阶段计算和标识时间戳的差值来重新对齐时间。
// 生成标识时间
auto time = tm_::duration_cast<tm_::seconds>(fs::last_write_time(bf_path).time_since_epoch()).count();
struct {
uint64_t magic;
time_t base_time;
} bin = {kTmMagic, time - elapse};
// 获取标识时间, 计算差值
auto time = tm_::duration_cast<tm_::seconds>(fs::last_write_time(bf_path, err).time_since_epoch()).count();
auto time_diff = bin.base_time - (time - elapse); // Get System Time Diff between auth and scan