Skip to content
API Documentation
BastionGuard™
ntfs_probe.cpp
Go to the documentation of this file.
1#include "ntfs_probe.hpp"
2#include "i18n.hpp"
3
4#include <array>
5#include <cmath>
6#include <set>
7#include <sstream>
8#include <string>
9
10namespace {
11constexpr uint64_t SECTOR_SIZE = 512ull;
12
13uint16_t le16(const unsigned char* p) {
14 return static_cast<uint16_t>(p[0]) | (static_cast<uint16_t>(p[1]) << 8);
15}
16
17uint64_t le64(const unsigned char* p) {
18 return static_cast<uint64_t>(p[0]) |
19 (static_cast<uint64_t>(p[1]) << 8) |
20 (static_cast<uint64_t>(p[2]) << 16) |
21 (static_cast<uint64_t>(p[3]) << 24) |
22 (static_cast<uint64_t>(p[4]) << 32) |
23 (static_cast<uint64_t>(p[5]) << 40) |
24 (static_cast<uint64_t>(p[6]) << 48) |
25 (static_cast<uint64_t>(p[7]) << 56);
26}
27
28bool is_ntfs_boot_sector(const std::array<unsigned char, 512>& sector) {
29 return std::string(reinterpret_cast<const char*>(sector.data() + 3), 8) == "NTFS " &&
30 sector[510] == 0x55 && sector[511] == 0xAA;
31}
32
33uint64_t record_size_from_descriptor(int8_t descriptor, uint64_t cluster_size) {
34 if (descriptor > 0) {
35 return static_cast<uint64_t>(descriptor) * cluster_size;
36 }
37 // Bug #3/#4 fix: descriptor==0 means invalid; descriptor < 0 is shift exponent.
38 // Range valid for NTFS: 1..30 (records from 2 bytes to 1 GiB).
39 if (descriptor == 0) return 0;
40 const int exponent = -static_cast<int>(descriptor);
41 if (exponent >= 1 && exponent <= 30) {
42 return 1ull << exponent;
43 }
44 return 0; // invalid: caller must reject volume
45}
46}
47
48std::vector<ScanResult> NtfsProbe::analyze(DiskReader& reader,
49 const std::vector<ScanResult>& seed_results,
50 std::vector<std::string>& logs) {
51 std::vector<ScanResult> out;
52 std::set<uint64_t> seen_offsets;
53
54 logs.push_back(trs("NTFS probe: analysis started."));
55
56 for (const auto& seed : seed_results) {
57 const bool interesting_seed =
58 seed.kind == ResultKind::Partition ||
61 if (!interesting_seed || seen_offsets.count(seed.offset_bytes) != 0) {
62 continue;
63 }
64 seen_offsets.insert(seed.offset_bytes);
65
66 std::array<unsigned char, 512> sector{};
67 if (!reader.read_at(seed.offset_bytes, sector.data(), sector.size()) || !is_ntfs_boot_sector(sector)) {
68 continue;
69 }
70
71 const uint16_t bytes_per_sector = le16(sector.data() + 11);
72 const uint8_t sectors_per_cluster = sector[13];
73 const uint64_t total_sectors = le64(sector.data() + 40);
74 const uint64_t mft_lcn = le64(sector.data() + 48);
75 const uint64_t mftmirr_lcn = le64(sector.data() + 56);
76 const int8_t record_descriptor = static_cast<int8_t>(sector[64]);
77
78 if (bytes_per_sector == 0 || sectors_per_cluster == 0 || total_sectors == 0) {
79 continue;
80 }
81
82 const uint64_t cluster_size = static_cast<uint64_t>(bytes_per_sector) * sectors_per_cluster;
83 const uint64_t volume_size = total_sectors * bytes_per_sector;
84 const uint64_t record_size = record_size_from_descriptor(record_descriptor, cluster_size);
85 // Bug #3/#4 fix: record_size==0 means invalid, skip volume
86 if (record_size == 0) {
87 continue;
88 }
89 const uint64_t mft_offset = seed.offset_bytes + (mft_lcn * cluster_size);
90 const uint64_t mftmirr_offset = seed.offset_bytes + (mftmirr_lcn * cluster_size);
91
92 std::array<char, 4> mft_sig{};
93 bool mft_ok = reader.read_at(mft_offset, mft_sig.data(), mft_sig.size()) &&
94 std::string(mft_sig.data(), mft_sig.size()) == "FILE";
95
96 std::array<char, 4> mftmirr_sig{};
97 bool mftmirr_ok = reader.read_at(mftmirr_offset, mftmirr_sig.data(), mftmirr_sig.size()) &&
98 std::string(mftmirr_sig.data(), mftmirr_sig.size()) == "FILE";
99
100 std::ostringstream detail;
101 detail << trs("Bytes/sector") << ": " << bytes_per_sector
102 << ", " << trs("Sectors/cluster") << ": " << static_cast<unsigned>(sectors_per_cluster)
103 << ", " << trs("MFT record size") << ": " << record_size
104 << ", " << trs("MFT") << ": " << (mft_ok ? "OK" : "missing")
105 << ", " << trs("MFTMirr") << ": " << (mftmirr_ok ? "OK" : "missing");
106
107 double confidence = 0.85;
108 if (mft_ok) confidence += 0.10;
109 if (mftmirr_ok) confidence += 0.03;
110 if (confidence > 0.99) confidence = 0.99;
111
112 out.push_back({ResultKind::NtfsVolume,
113 seed.offset_bytes,
114 volume_size,
115 "NTFS volume",
116 "NTFS probe",
117 detail.str(),
118 confidence});
119
120 if (mft_ok) {
121 out.push_back({ResultKind::NtfsMetadata,
122 mft_offset,
123 record_size,
124 "NTFS $MFT",
125 "NTFS probe",
126 trs("Primary MFT record signature validated"),
127 0.96});
128 }
129 if (mftmirr_ok) {
130 out.push_back({ResultKind::NtfsMetadata,
131 mftmirr_offset,
132 record_size,
133 "NTFS $MFTMirr",
134 "NTFS probe",
135 trs("MFT mirror record signature validated"),
136 0.93});
137 }
138 }
139
140 logs.push_back(trs("NTFS probe: analysis completed."));
141 return out;
142}
bool read_at(uint64_t offset, void *buffer, size_t len)
static std::vector< ScanResult > analyze(DiskReader &reader, const std::vector< ScanResult > &seed_results, std::vector< std::string > &logs)
std::string trs(const char *msgid)
Definition i18n.cpp:22
@ Partition
Definition models.hpp:8
@ LostPartitionCandidate
Definition models.hpp:9
@ NtfsMetadata
Definition models.hpp:13
@ FilesystemCandidate
Definition models.hpp:10
uint64_t le64(const unsigned char *p)
uint64_t record_size_from_descriptor(int8_t descriptor, uint64_t cluster_size)
uint16_t le16(const unsigned char *p)
bool is_ntfs_boot_sector(const std::array< unsigned char, 512 > &sector)