|
| 1 | +From 0ec7b97fa5ec1fa9af4838c737ae72c71e9c434f Mon Sep 17 00:00:00 2001 |
| 2 | +From: Brian Carrier <carrier@sleuthkit.org> |
| 3 | +Date: Sat, 28 Feb 2026 16:40:19 -0500 |
| 4 | +Subject: [PATCH] Fix bounds overrun in APFS. Reported by Mobasi |
| 5 | + |
| 6 | +Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> |
| 7 | +Upstream-reference: https://github.com/sleuthkit/sleuthkit/commit/8b9c9e7d493bd68624f3b1a3963edd45c3ff7611.patch |
| 8 | +--- |
| 9 | + tsk/fs/apfs.cpp | 73 ++++++++++++++++++++++++++------------------- |
| 10 | + tsk/fs/tsk_apfs.hpp | 22 ++++++++++++-- |
| 11 | + 2 files changed, 62 insertions(+), 33 deletions(-) |
| 12 | + |
| 13 | +diff --git a/tsk/fs/apfs.cpp b/tsk/fs/apfs.cpp |
| 14 | +index 229a044..7d38010 100644 |
| 15 | +--- a/tsk/fs/apfs.cpp |
| 16 | ++++ b/tsk/fs/apfs.cpp |
| 17 | +@@ -51,22 +51,24 @@ static __forceinline int lsbset(long x) { |
| 18 | + #endif // _MSC_VER |
| 19 | + |
| 20 | + class wrapped_key_parser { |
| 21 | +- // TODO(JTS): This code assume a well-formed input. It needs some sanity |
| 22 | +- // checking! |
| 23 | +- |
| 24 | + using tag = uint8_t; |
| 25 | + using view = span<const uint8_t>; |
| 26 | + |
| 27 | + const uint8_t* _data; |
| 28 | ++ const uint8_t* _end; // one-past-the-end of the buffer |
| 29 | + |
| 30 | +- size_t get_length(const uint8_t** pos) const noexcept { |
| 31 | ++ // Returns true and leaves *pos unchanged on any bounds violation. |
| 32 | ++ bool is_eob(const uint8_t** pos, size_t* out_len) const noexcept { |
| 33 | + auto data = *pos; |
| 34 | + |
| 35 | ++ if (data >= _end) return true; |
| 36 | + size_t len = *data++; |
| 37 | + |
| 38 | + if (len & 0x80) { |
| 39 | ++ size_t enc_len = len & 0x7F; |
| 40 | + len = 0; |
| 41 | +- auto enc_len = len & 0x7F; |
| 42 | ++ if (enc_len == 0 || static_cast<size_t>(_end - data) < enc_len) |
| 43 | ++ return true; |
| 44 | + while (enc_len--) { |
| 45 | + len <<= 8; |
| 46 | + len |= *data++; |
| 47 | +@@ -74,15 +76,23 @@ class wrapped_key_parser { |
| 48 | + } |
| 49 | + |
| 50 | + *pos = data; |
| 51 | +- return len; |
| 52 | ++ *out_len = len; |
| 53 | ++ return false; |
| 54 | + } |
| 55 | + |
| 56 | ++ // Returns an invalid (empty) view if the tag is not found or a bounds |
| 57 | ++ // violation is detected. |
| 58 | + const view get_tag(tag t) const noexcept { |
| 59 | + auto data = _data; |
| 60 | + |
| 61 | +- while (true) { |
| 62 | ++ while (data < _end) { |
| 63 | + const auto tag = *data++; |
| 64 | +- const auto len = get_length(&data); |
| 65 | ++ |
| 66 | ++ size_t len = 0; |
| 67 | ++ if (is_eob(&data, &len)) break; |
| 68 | ++ |
| 69 | ++ // Ensure the value bytes are within the buffer. |
| 70 | ++ if (static_cast<size_t>(_end - data) < len) break; |
| 71 | + |
| 72 | + if (tag == t) { |
| 73 | + return {data, len}; |
| 74 | +@@ -90,6 +100,9 @@ class wrapped_key_parser { |
| 75 | + |
| 76 | + data += len; |
| 77 | + } |
| 78 | ++ |
| 79 | ++ // Tag not found or buffer overrun — return an invalid view. |
| 80 | ++ return {}; |
| 81 | + } |
| 82 | + |
| 83 | + // Needed for the recursive variadic to compile, but should never be |
| 84 | +@@ -99,7 +112,9 @@ class wrapped_key_parser { |
| 85 | + } |
| 86 | + |
| 87 | + public: |
| 88 | +- wrapped_key_parser(const void* data) noexcept : _data{(const uint8_t*)data} {} |
| 89 | ++ wrapped_key_parser(const void* data, size_t size) noexcept |
| 90 | ++ : _data{static_cast<const uint8_t*>(data)}, |
| 91 | ++ _end{static_cast<const uint8_t*>(data) + size} {} |
| 92 | + |
| 93 | + template <typename... Args> |
| 94 | + const view get_data(tag t, Args... args) const noexcept { |
| 95 | +@@ -109,7 +124,8 @@ class wrapped_key_parser { |
| 96 | + return data; |
| 97 | + } |
| 98 | + |
| 99 | +- return wrapped_key_parser{data.data()}.get_data(args...); |
| 100 | ++ // Recurse into the nested TLV value; its buffer is exactly `data`. |
| 101 | ++ return wrapped_key_parser{data.data(), data.count()}.get_data(args...); |
| 102 | + } |
| 103 | + |
| 104 | + template <typename... Args> |
| 105 | +@@ -346,10 +362,10 @@ APFSFileSystem::APFSFileSystem(const APFSPool& pool, |
| 106 | + } |
| 107 | + |
| 108 | + APFSFileSystem::wrapped_kek::wrapped_kek(TSKGuid&& id, |
| 109 | +- const std::unique_ptr<uint8_t[]>& kp) |
| 110 | ++ const APFS_sized_key_data& kp) |
| 111 | + : uuid{std::forward<TSKGuid>(id)} { |
| 112 | + // Parse KEK |
| 113 | +- wrapped_key_parser wp{kp.get()}; |
| 114 | ++ wrapped_key_parser wp{kp.get(), kp.size}; |
| 115 | + |
| 116 | + // Get flags |
| 117 | + flags = wp.get_number(0x30, 0xA3, 0x82); |
| 118 | +@@ -398,12 +414,12 @@ void APFSFileSystem::init_crypto_info() { |
| 119 | + const auto container_kb = _pool.nx()->keybag(); |
| 120 | + |
| 121 | + auto data = container_kb.get_key(uuid(), APFS_KB_TYPE_VOLUME_KEY); |
| 122 | +- if (data == nullptr) { |
| 123 | ++ if (!data) { |
| 124 | + throw std::runtime_error( |
| 125 | + "APFSFileSystem: can not find volume encryption key"); |
| 126 | + } |
| 127 | + |
| 128 | +- wrapped_key_parser wp{ data.get() }; |
| 129 | ++ wrapped_key_parser wp{ data.get(), data.size }; |
| 130 | + |
| 131 | + // Get Wrapped VEK |
| 132 | + auto kek_data = wp.get_data(0x30, 0xA3, 0x83); |
| 133 | +@@ -424,7 +440,7 @@ void APFSFileSystem::init_crypto_info() { |
| 134 | + std::memcpy(_crypto.vek_uuid, kek_data.data(), sizeof(_crypto.vek_uuid)); |
| 135 | + |
| 136 | + data = container_kb.get_key(uuid(), APFS_KB_TYPE_UNLOCK_RECORDS); |
| 137 | +- if (data == nullptr) { |
| 138 | ++ if (!data) { |
| 139 | + throw std::runtime_error( |
| 140 | + "APFSFileSystem: can not find volume recovery key"); |
| 141 | + } |
| 142 | +@@ -443,7 +459,7 @@ void APFSFileSystem::init_crypto_info() { |
| 143 | + |
| 144 | + data = recs.get_key(uuid(), APFS_KB_TYPE_PASSPHRASE_HINT); |
| 145 | + |
| 146 | +- if (data != nullptr) { |
| 147 | ++ if (data) { |
| 148 | + _crypto.password_hint = std::string((const char*)data.get()); |
| 149 | + } |
| 150 | + |
| 151 | +@@ -1000,10 +1016,10 @@ APFSKeybag::APFSKeybag(const APFSPool& pool, const apfs_block_num block_num, |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | +-std::unique_ptr<uint8_t[]> APFSKeybag::get_key(const TSKGuid& uuid, |
| 156 | +- uint16_t type) const { |
| 157 | ++APFS_sized_key_data APFSKeybag::get_key(const TSKGuid& uuid, |
| 158 | ++ uint16_t type) const { |
| 159 | + if (kb()->num_entries == 0) { |
| 160 | +- return nullptr; |
| 161 | ++ return {}; |
| 162 | + } |
| 163 | + |
| 164 | + // First key is immediately after the header |
| 165 | +@@ -1012,20 +1028,17 @@ std::unique_ptr<uint8_t[]> APFSKeybag::get_key(const TSKGuid& uuid, |
| 166 | + for (auto i = 0U; i < kb()->num_entries; i++) { |
| 167 | + if (next_key->type == type && |
| 168 | + std::memcmp(next_key->uuid, uuid.bytes().data(), 16) == 0) { |
| 169 | +- // We've found a matching key. Copy it's data to a pointer and return it. |
| 170 | ++ // We've found a matching key. Copy its data to a pointer and return it. |
| 171 | + const auto data = reinterpret_cast<const uint8_t*>(next_key + 1); |
| 172 | + |
| 173 | +- // We're padding the data with an extra byte so we can null-terminate |
| 174 | +- // any data strings. There might be a better way. |
| 175 | ++ // +1 byte for null-terminator guard on string values |
| 176 | + auto dp = std::make_unique<uint8_t[]>(next_key->length + 1); |
| 177 | +- |
| 178 | + std::memcpy(dp.get(), data, next_key->length); |
| 179 | + |
| 180 | +- return dp; |
| 181 | ++ return {std::move(dp), next_key->length}; |
| 182 | + } |
| 183 | + |
| 184 | + // Calculate address of next key (ensuring alignment) |
| 185 | +- |
| 186 | + const auto nk_addr = |
| 187 | + (uintptr_t)next_key + |
| 188 | + ((sizeof(*next_key) + next_key->length + 0x0F) & ~0x0FULL); |
| 189 | +@@ -1034,7 +1047,7 @@ std::unique_ptr<uint8_t[]> APFSKeybag::get_key(const TSKGuid& uuid, |
| 190 | + } |
| 191 | + |
| 192 | + // Not Found |
| 193 | +- return nullptr; |
| 194 | ++ return {}; |
| 195 | + } |
| 196 | + |
| 197 | + std::vector<APFSKeybag::key> APFSKeybag::get_keys() const { |
| 198 | +@@ -1046,13 +1059,13 @@ std::vector<APFSKeybag::key> APFSKeybag::get_keys() const { |
| 199 | + for (auto i = 0U; i < kb()->num_entries; i++) { |
| 200 | + const auto data = reinterpret_cast<const uint8_t*>(next_key + 1); |
| 201 | + |
| 202 | +- // We're padding the data with an extra byte so we can null-terminate |
| 203 | +- // any data strings. There might be a better way. |
| 204 | ++ // +1 byte for null-terminator guard on string values |
| 205 | + auto dp = std::make_unique<uint8_t[]>(next_key->length + 1); |
| 206 | +- |
| 207 | + std::memcpy(dp.get(), data, next_key->length); |
| 208 | + |
| 209 | +- keys.emplace_back(key{{next_key->uuid}, std::move(dp), next_key->type}); |
| 210 | ++ keys.emplace_back(key{{next_key->uuid}, |
| 211 | ++ APFS_sized_key_data{std::move(dp), next_key->length}, |
| 212 | ++ next_key->type}); |
| 213 | + |
| 214 | + // Calculate address of next key (ensuring alignment) |
| 215 | + const auto nk_addr = |
| 216 | +diff --git a/tsk/fs/tsk_apfs.hpp b/tsk/fs/tsk_apfs.hpp |
| 217 | +index 700cd54..33629e0 100755 |
| 218 | +--- a/tsk/fs/tsk_apfs.hpp |
| 219 | ++++ b/tsk/fs/tsk_apfs.hpp |
| 220 | +@@ -37,6 +37,22 @@ constexpr T bitfield_value(T bitfield, int bits, int shift) noexcept { |
| 221 | + |
| 222 | + class APFSPool; |
| 223 | + |
| 224 | ++// An owning buffer that also carries its own length, so callers never need to |
| 225 | ++// track the size separately. Drop-in replacement for unique_ptr<uint8_t[]> |
| 226 | ++// at call sites — supports operator bool() and .get() for compatibility. |
| 227 | ++struct APFS_sized_key_data { |
| 228 | ++ std::unique_ptr<uint8_t[]> ptr; |
| 229 | ++ size_t size{0}; |
| 230 | ++ |
| 231 | ++ // Allows `if (data)` / `if (!data)` checks to keep working. |
| 232 | ++ explicit operator bool() const noexcept { return ptr != nullptr; } |
| 233 | ++ |
| 234 | ++ // Mimic unique_ptr's .get() so existing call sites need minimal changes. |
| 235 | ++ const uint8_t* get() const noexcept { return ptr.get(); } |
| 236 | ++}; |
| 237 | ++ |
| 238 | ++ |
| 239 | ++ |
| 240 | + class APFSObject : public APFSBlock { |
| 241 | + protected: |
| 242 | + inline const apfs_obj_header *obj() const noexcept { |
| 243 | +@@ -856,7 +872,7 @@ class APFSKeybag : public APFSObject { |
| 244 | + |
| 245 | + using key = struct { |
| 246 | + TSKGuid uuid; |
| 247 | +- std::unique_ptr<uint8_t[]> data; |
| 248 | ++ APFS_sized_key_data data; |
| 249 | + uint16_t type; |
| 250 | + }; |
| 251 | + |
| 252 | +@@ -864,7 +880,7 @@ class APFSKeybag : public APFSObject { |
| 253 | + APFSKeybag(const APFSPool &pool, const apfs_block_num block_num, |
| 254 | + const uint8_t *key, const uint8_t *key2 = nullptr); |
| 255 | + |
| 256 | +- std::unique_ptr<uint8_t[]> get_key(const TSKGuid &uuid, uint16_t type) const; |
| 257 | ++ APFS_sized_key_data get_key(const TSKGuid &uuid, uint16_t type) const; |
| 258 | + |
| 259 | + std::vector<key> get_keys() const; |
| 260 | + }; |
| 261 | +@@ -993,7 +1009,7 @@ class APFSFileSystem : public APFSObject { |
| 262 | + uint64_t iterations; |
| 263 | + uint64_t flags; |
| 264 | + uint8_t salt[0x10]; |
| 265 | +- wrapped_kek(TSKGuid &&uuid, const std::unique_ptr<uint8_t[]> &); |
| 266 | ++ wrapped_kek(TSKGuid &&uuid, const APFS_sized_key_data &); |
| 267 | + |
| 268 | + inline bool hw_crypt() const noexcept { |
| 269 | + // If this bit is set, some sort of hardware encryption is used. |
| 270 | +-- |
| 271 | +2.45.4 |
| 272 | + |
0 commit comments