Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions SPECS/sleuthkit/CVE-2026-40024.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
From 24e3bb6f9ce8c7d3a93a508e5d01918f57ca3938 Mon Sep 17 00:00:00 2001
From: Brian Carrier <carrier@sleuthkit.org>
Date: Sat, 28 Feb 2026 16:36:48 -0500
Subject: [PATCH] Clean up any path traversal symbols in export path. Reported
by Mobasi

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://github.com/sleuthkit/sleuthkit/commit/a3f96b3bc36a8bb1a00c297f77110d4a6e7dd31b.patch
---
tools/autotools/tsk_recover.cpp | 49 +++++++++++++++++++++++++++++----
1 file changed, 44 insertions(+), 5 deletions(-)

diff --git a/tools/autotools/tsk_recover.cpp b/tools/autotools/tsk_recover.cpp
index c56edc8..336aa3d 100755
--- a/tools/autotools/tsk_recover.cpp
+++ b/tools/autotools/tsk_recover.cpp
@@ -46,6 +46,17 @@ usage()
exit(1);
}

+
+// special characters we do not want to have in the name when writing out.
+#ifdef TSK_WIN32
+#define TSK_IS_SPL_FILE_CHAR(x) \
+ (((x) == 0x3A) || ((x) == 0x5C))
+#else
+#define TSK_IS_SPL_FILE_CHAR(x) \
+ ((x) == 0x2F)
+#endif
+
+
#ifdef TSK_WIN32
#include <windows.h>
#include "shlobj.h"
@@ -136,8 +147,20 @@ uint8_t TskRecover::writeFile(TSK_FS_FILE * a_fs_file, const char *a_path)

// clean up any control characters
for (size_t i = 0; i < ilen; i++) {
- if (TSK_IS_CNTRL(path8[i]))
+ if (TSK_IS_CNTRL(path8[i])) {
path8[i] = '^';
+ }
+
+ if (path8[i] == '/')
+ path8[i] = '\\';
+
+ // make sure there is no \..\ path traversal
+ if (i + 4 < ilen) {
+ if ((path8[i] == '\\') && (path8[i+1] == '.') && (path8[i+2] == '.') && (path8[i+3] == '\\')) {
+ path8[i+1] = '^';
+ path8[i+2] = '^';
+ }
+ }
}

//convert path from utf8 to utf16
@@ -169,6 +192,8 @@ uint8_t TskRecover::writeFile(TSK_FS_FILE * a_fs_file, const char *a_path)
for (size_t i = 0; i < len; i++) {
if (path16full[i] == L'/')
path16full[i] = L'\\';
+
+ // break at path seperator to make that directory
if (((i > 0) && (path16full[i] == L'\\') && (path16full[i - 1] != L'\\'))
|| ((path16full[i] != L'\\') && (i == len - 1))) {
uint8_t
@@ -198,8 +223,10 @@ uint8_t TskRecover::writeFile(TSK_FS_FILE * a_fs_file, const char *a_path)
char name8[FILENAME_MAX];
strncpy(name8, a_fs_file->name->name, FILENAME_MAX);
for (int i = 0; name8[i] != '\0'; i++) {
- if (TSK_IS_CNTRL(name8[i]))
+ //make sure there is no slash, which could lead to path traversal
+ if (TSK_IS_CNTRL(name8[i]) || TSK_IS_SPL_FILE_CHAR(name8[i])) {
name8[i] = '^';
+ }
}

//convert file name from utf8 to utf16
@@ -257,13 +284,23 @@ uint8_t TskRecover::writeFile(TSK_FS_FILE * a_fs_file, const char *a_path)
for (size_t i = 0; i < strlen(fbuf); i++) {
if (TSK_IS_CNTRL(fbuf[i]))
fbuf[i] = '^';
+
+ // make sure there is no /../ path traversal
+ if (i + 4 < strlen(fbuf)) {
+ if ((fbuf[i] == '/') && (fbuf[i+1] == '.') && (fbuf[i+2] == '.') && (fbuf[i+3] == '/')) {
+ fbuf[i+1] = '^';
+ fbuf[i+2] = '^';
+ }
+ }
}

// see if the directory already exists. Create, if not.
+ // and all of the missing layers
if (0 != lstat(fbuf, &statds)) {
size_t
len = strlen(fbuf);
for (size_t i = 0; i < len; i++) {
+ // stop if we are at a path separator to make that folder
if (((i > 0) && (fbuf[i] == '/') && (fbuf[i - 1] != '/'))
|| ((fbuf[i] != '/') && (i == len - 1))) {
uint8_t
@@ -290,15 +327,17 @@ uint8_t TskRecover::writeFile(TSK_FS_FILE * a_fs_file, const char *a_path)
if (fbuf[strlen(fbuf) - 1] != '/')
strncat(fbuf, "/", PATH_MAX - strlen(fbuf)-1);

+ int nstart = strlen(fbuf);
strncat(fbuf, a_fs_file->name->name, PATH_MAX - strlen(fbuf)-1);

//do name mangling of the file name that was just added
- for (int i = strlen(fbuf)-1; fbuf[i] != '/'; i--) {
- if (TSK_IS_CNTRL(fbuf[i]))
+ for (int i = nstart; fbuf[i] != '\0'; i++) {
+ // need to make sure it doesn't have any slashes in it, which could lead
+ // to path traversal
+ if (TSK_IS_CNTRL(fbuf[i]) || TSK_IS_SPL_FILE_CHAR(fbuf[i]))
fbuf[i] = '^';
}

-
// open the file
if ((hFile = fopen(fbuf, "w+")) == NULL) {
fprintf(stderr, "Error opening file for writing (%s)\n", fbuf);
--
2.45.4

272 changes: 272 additions & 0 deletions SPECS/sleuthkit/CVE-2026-40025.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
From a66da89b3cc4782c72174d2040c0634492345416 Mon Sep 17 00:00:00 2001
From: Brian Carrier <carrier@sleuthkit.org>
Date: Sat, 28 Feb 2026 16:40:19 -0500
Subject: [PATCH] Fix bounds overrun in APFS. Reported by Mobasi

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://github.com/sleuthkit/sleuthkit/commit/8b9c9e7d493bd68624f3b1a3963edd45c3ff7611.patch
---
tsk/fs/apfs.cpp | 73 ++++++++++++++++++++++++++-------------------
tsk/fs/tsk_apfs.hpp | 22 ++++++++++++--
2 files changed, 62 insertions(+), 33 deletions(-)

diff --git a/tsk/fs/apfs.cpp b/tsk/fs/apfs.cpp
index 229a044..7d38010 100644
--- a/tsk/fs/apfs.cpp
+++ b/tsk/fs/apfs.cpp
@@ -51,22 +51,24 @@ static __forceinline int lsbset(long x) {
#endif // _MSC_VER

class wrapped_key_parser {
- // TODO(JTS): This code assume a well-formed input. It needs some sanity
- // checking!
-
using tag = uint8_t;
using view = span<const uint8_t>;

const uint8_t* _data;
+ const uint8_t* _end; // one-past-the-end of the buffer

- size_t get_length(const uint8_t** pos) const noexcept {
+ // Returns true and leaves *pos unchanged on any bounds violation.
+ bool is_eob(const uint8_t** pos, size_t* out_len) const noexcept {
auto data = *pos;

+ if (data >= _end) return true;
size_t len = *data++;

if (len & 0x80) {
+ size_t enc_len = len & 0x7F;
len = 0;
- auto enc_len = len & 0x7F;
+ if (enc_len == 0 || static_cast<size_t>(_end - data) < enc_len)
+ return true;
while (enc_len--) {
len <<= 8;
len |= *data++;
@@ -74,15 +76,23 @@ class wrapped_key_parser {
}

*pos = data;
- return len;
+ *out_len = len;
+ return false;
}

+ // Returns an invalid (empty) view if the tag is not found or a bounds
+ // violation is detected.
const view get_tag(tag t) const noexcept {
auto data = _data;

- while (true) {
+ while (data < _end) {
const auto tag = *data++;
- const auto len = get_length(&data);
+
+ size_t len = 0;
+ if (is_eob(&data, &len)) break;
+
+ // Ensure the value bytes are within the buffer.
+ if (static_cast<size_t>(_end - data) < len) break;

if (tag == t) {
return {data, len};
@@ -90,6 +100,9 @@ class wrapped_key_parser {

data += len;
}
+
+ // Tag not found or buffer overrun — return an invalid view.
+ return {};
}

// Needed for the recursive variadic to compile, but should never be
@@ -99,7 +112,9 @@ class wrapped_key_parser {
}

public:
- wrapped_key_parser(const void* data) noexcept : _data{(const uint8_t*)data} {}
+ wrapped_key_parser(const void* data, size_t size) noexcept
+ : _data{static_cast<const uint8_t*>(data)},
+ _end{static_cast<const uint8_t*>(data) + size} {}

template <typename... Args>
const view get_data(tag t, Args... args) const noexcept {
@@ -109,7 +124,8 @@ class wrapped_key_parser {
return data;
}

- return wrapped_key_parser{data.data()}.get_data(args...);
+ // Recurse into the nested TLV value; its buffer is exactly `data`.
+ return wrapped_key_parser{data.data(), data.count()}.get_data(args...);
}

template <typename... Args>
@@ -346,10 +362,10 @@ APFSFileSystem::APFSFileSystem(const APFSPool& pool,
}

APFSFileSystem::wrapped_kek::wrapped_kek(TSKGuid&& id,
- const std::unique_ptr<uint8_t[]>& kp)
+ const APFS_sized_key_data& kp)
: uuid{std::forward<TSKGuid>(id)} {
// Parse KEK
- wrapped_key_parser wp{kp.get()};
+ wrapped_key_parser wp{kp.get(), kp.size};

// Get flags
flags = wp.get_number(0x30, 0xA3, 0x82);
@@ -398,12 +414,12 @@ void APFSFileSystem::init_crypto_info() {
const auto container_kb = _pool.nx()->keybag();

auto data = container_kb.get_key(uuid(), APFS_KB_TYPE_VOLUME_KEY);
- if (data == nullptr) {
+ if (!data) {
throw std::runtime_error(
"APFSFileSystem: can not find volume encryption key");
}

- wrapped_key_parser wp{ data.get() };
+ wrapped_key_parser wp{ data.get(), data.size };

// Get Wrapped VEK
auto kek_data = wp.get_data(0x30, 0xA3, 0x83);
@@ -424,7 +440,7 @@ void APFSFileSystem::init_crypto_info() {
std::memcpy(_crypto.vek_uuid, kek_data.data(), sizeof(_crypto.vek_uuid));

data = container_kb.get_key(uuid(), APFS_KB_TYPE_UNLOCK_RECORDS);
- if (data == nullptr) {
+ if (!data) {
throw std::runtime_error(
"APFSFileSystem: can not find volume recovery key");
}
@@ -443,7 +459,7 @@ void APFSFileSystem::init_crypto_info() {

data = recs.get_key(uuid(), APFS_KB_TYPE_PASSPHRASE_HINT);

- if (data != nullptr) {
+ if (data) {
_crypto.password_hint = std::string((const char*)data.get());
}

@@ -1000,10 +1016,10 @@ APFSKeybag::APFSKeybag(const APFSPool& pool, const apfs_block_num block_num,
}
}

-std::unique_ptr<uint8_t[]> APFSKeybag::get_key(const TSKGuid& uuid,
- uint16_t type) const {
+APFS_sized_key_data APFSKeybag::get_key(const TSKGuid& uuid,
+ uint16_t type) const {
if (kb()->num_entries == 0) {
- return nullptr;
+ return {};
}

// First key is immediately after the header
@@ -1012,20 +1028,17 @@ std::unique_ptr<uint8_t[]> APFSKeybag::get_key(const TSKGuid& uuid,
for (auto i = 0U; i < kb()->num_entries; i++) {
if (next_key->type == type &&
std::memcmp(next_key->uuid, uuid.bytes().data(), 16) == 0) {
- // We've found a matching key. Copy it's data to a pointer and return it.
+ // We've found a matching key. Copy its data to a pointer and return it.
const auto data = reinterpret_cast<const uint8_t*>(next_key + 1);

- // We're padding the data with an extra byte so we can null-terminate
- // any data strings. There might be a better way.
+ // +1 byte for null-terminator guard on string values
auto dp = std::make_unique<uint8_t[]>(next_key->length + 1);
-
std::memcpy(dp.get(), data, next_key->length);

- return dp;
+ return {std::move(dp), next_key->length};
}

// Calculate address of next key (ensuring alignment)
-
const auto nk_addr =
(uintptr_t)next_key +
((sizeof(*next_key) + next_key->length + 0x0F) & ~0x0FULL);
@@ -1034,7 +1047,7 @@ std::unique_ptr<uint8_t[]> APFSKeybag::get_key(const TSKGuid& uuid,
}

// Not Found
- return nullptr;
+ return {};
}

std::vector<APFSKeybag::key> APFSKeybag::get_keys() const {
@@ -1046,13 +1059,13 @@ std::vector<APFSKeybag::key> APFSKeybag::get_keys() const {
for (auto i = 0U; i < kb()->num_entries; i++) {
const auto data = reinterpret_cast<const uint8_t*>(next_key + 1);

- // We're padding the data with an extra byte so we can null-terminate
- // any data strings. There might be a better way.
+ // +1 byte for null-terminator guard on string values
auto dp = std::make_unique<uint8_t[]>(next_key->length + 1);
-
std::memcpy(dp.get(), data, next_key->length);

- keys.emplace_back(key{{next_key->uuid}, std::move(dp), next_key->type});
+ keys.emplace_back(key{{next_key->uuid},
+ APFS_sized_key_data{std::move(dp), next_key->length},
+ next_key->type});

// Calculate address of next key (ensuring alignment)
const auto nk_addr =
diff --git a/tsk/fs/tsk_apfs.hpp b/tsk/fs/tsk_apfs.hpp
index 700cd54..33629e0 100755
--- a/tsk/fs/tsk_apfs.hpp
+++ b/tsk/fs/tsk_apfs.hpp
@@ -37,6 +37,22 @@ constexpr T bitfield_value(T bitfield, int bits, int shift) noexcept {

class APFSPool;

+// An owning buffer that also carries its own length, so callers never need to
+// track the size separately. Drop-in replacement for unique_ptr<uint8_t[]>
+// at call sites — supports operator bool() and .get() for compatibility.
+struct APFS_sized_key_data {
+ std::unique_ptr<uint8_t[]> ptr;
+ size_t size{0};
+
+ // Allows `if (data)` / `if (!data)` checks to keep working.
+ explicit operator bool() const noexcept { return ptr != nullptr; }
+
+ // Mimic unique_ptr's .get() so existing call sites need minimal changes.
+ const uint8_t* get() const noexcept { return ptr.get(); }
+};
+
+
+
class APFSObject : public APFSBlock {
protected:
inline const apfs_obj_header *obj() const noexcept {
@@ -856,7 +872,7 @@ class APFSKeybag : public APFSObject {

using key = struct {
TSKGuid uuid;
- std::unique_ptr<uint8_t[]> data;
+ APFS_sized_key_data data;
uint16_t type;
};

@@ -864,7 +880,7 @@ class APFSKeybag : public APFSObject {
APFSKeybag(const APFSPool &pool, const apfs_block_num block_num,
const uint8_t *key, const uint8_t *key2 = nullptr);

- std::unique_ptr<uint8_t[]> get_key(const TSKGuid &uuid, uint16_t type) const;
+ APFS_sized_key_data get_key(const TSKGuid &uuid, uint16_t type) const;

std::vector<key> get_keys() const;
};
@@ -993,7 +1009,7 @@ class APFSFileSystem : public APFSObject {
uint64_t iterations;
uint64_t flags;
uint8_t salt[0x10];
- wrapped_kek(TSKGuid &&uuid, const std::unique_ptr<uint8_t[]> &);
+ wrapped_kek(TSKGuid &&uuid, const APFS_sized_key_data &);

inline bool hw_crypt() const noexcept {
// If this bit is set, some sort of hardware encryption is used.
--
2.45.4

Loading
Loading