Side-Channel Attacks (旁路攻击) 在浏览器中的实际利用案例 (如缓存定时攻击)。

admin
Wed, 23 Jul 2025 11:00:23 +0000
Javascript, channel, side, 攻击

大家好，欢迎来到“浏览器里的那些小秘密：旁路攻击实战演练”讲座！今天咱们不整那些虚头巴脑的，直接上手，聊聊浏览器里那些让人头疼的旁路攻击，尤其是缓存定时攻击。 一、啥是旁路攻击？（别告诉我你不知道！） 简单来说，旁路攻击就是不直接攻击密码算法本身，而是通过观察算法运行时的“边角料”信息，比如时间、功耗、电磁辐射，甚至是声音（真的！），来推断出密钥或者敏感数据。这就像你撬不开门锁，就听听屋里人走路的节奏，猜猜他们在哪儿，然后绕到窗户偷看一样。 二、浏览器里的战场：缓存定时攻击 浏览器，作为一个复杂的系统，到处都是缓存。CPU有缓存，内存有缓存，硬盘有缓存，就连网络请求也有缓存。这些缓存本来是为了提升性能，但如果使用不当，就会变成攻击者的乐园。 缓存定时攻击就是利用了缓存机制带来的时间差异。攻击者通过测量不同操作的执行时间，来判断某些数据是否被缓存过，从而推断出敏感信息。 三、实战演练：密码猜测器（简化版） 咱们来做一个简化版的密码猜测器，看看缓存定时攻击是怎么工作的。 场景： 目标网站有一个登录页面，用户名是固定的，但密码是未知的。 登录页面在验证密码时，会逐个字符比较用户输入的密码和正 …

继续阅读“Side-Channel Attacks (旁路攻击) 在浏览器中的实际利用案例 (如缓存定时攻击)。”

The post Side-Channel Attacks (旁路攻击) 在浏览器中的实际利用案例 (如缓存定时攻击)。 first appeared on 智猿学院-前后端，数据库，人工智能，云计算等领域前沿技术讲座.

大家好，欢迎来到“浏览器里的那些小秘密：旁路攻击实战演练”讲座！今天咱们不整那些虚头巴脑的，直接上手，聊聊浏览器里那些让人头疼的旁路攻击，尤其是缓存定时攻击。

一、啥是旁路攻击？（别告诉我你不知道！）

简单来说，旁路攻击就是不直接攻击密码算法本身，而是通过观察算法运行时的“边角料”信息，比如时间、功耗、电磁辐射，甚至是声音（真的！），来推断出密钥或者敏感数据。这就像你撬不开门锁，就听听屋里人走路的节奏，猜猜他们在哪儿，然后绕到窗户偷看一样。

二、浏览器里的战场：缓存定时攻击

浏览器，作为一个复杂的系统，到处都是缓存。CPU有缓存，内存有缓存，硬盘有缓存，就连网络请求也有缓存。这些缓存本来是为了提升性能，但如果使用不当，就会变成攻击者的乐园。

缓存定时攻击就是利用了缓存机制带来的时间差异。攻击者通过测量不同操作的执行时间，来判断某些数据是否被缓存过，从而推断出敏感信息。

三、实战演练：密码猜测器（简化版）

咱们来做一个简化版的密码猜测器，看看缓存定时攻击是怎么工作的。

场景：

目标网站有一个登录页面，用户名是固定的，但密码是未知的。

登录页面在验证密码时，会逐个字符比较用户输入的密码和正确密码。

如果输入的字符与正确字符匹配，验证时间会稍微长一点（因为要继续比较下一个字符）。

攻击者可以提交不同的密码，并测量每次提交所需的时间。

攻击思路：

攻击者可以逐个字符尝试猜测密码。如果某个字符匹配，服务器的验证时间会稍微延长。通过测量时间差异，攻击者可以逐步推断出完整的密码。

代码实现 (JavaScript):

// 模拟登录验证函数 (服务器端)
function verifyPassword(password) {
  const correctPassword = "SecretPassword"; // 真正的密码
  let startTime = performance.now(); //记录开始时间
  for (let i = 0; i < password.length; i++) {
    if (i >= correctPassword.length) {
      break; // 防止输入密码过长
    }
    if (password[i] === correctPassword[i]) {
      // 模拟耗时操作
      for (let j = 0; j < 10000; j++) {
        //空循环，增加耗时
      }
    } else {
      break; // 密码不匹配，直接返回
    }
  }
  let endTime = performance.now(); //记录结束时间
  return endTime - startTime;
}

// 客户端代码 (模拟攻击)
async function attack() {
  const username = "attacker"; // 已知的用户名
  let guessedPassword = "";
  let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // 密码可能包含的字符集

  while (true) {
    let bestChar = "";
    let bestTime = 0;

    for (let char of alphabet) {
      let testPassword = guessedPassword + char;
      let startTime = performance.now();
      //模拟网络请求
      await new Promise(resolve => setTimeout(resolve, 50)); //模拟网络延时50ms

      let duration = verifyPassword(testPassword); // 调用模拟的验证函数

      let endTime = performance.now(); //记录结束时间
      let timeTaken = duration;

      console.log(`尝试密码: ${testPassword}, 耗时: ${timeTaken.toFixed(2)} ms`);

      if (bestChar === "" || timeTaken > bestTime) {
        bestChar = char;
        bestTime = timeTaken;
      }
    }

    if (bestTime <= 0) {
      console.log("无法猜测下一个字符，攻击结束。");
      break;
    }

    guessedPassword += bestChar;
    console.log(`猜测到的密码: ${guessedPassword}`);

    // 停止条件：密码长度达到预期，或者没有显著的时间差异
    if (guessedPassword.length >= 16) {
      console.log("猜测完毕，最终密码:", guessedPassword);
      break;
    }
  }
}

attack();

代码解释：

1. verifyPassword(password) (模拟服务器端): 这个函数模拟了服务器端的密码验证过程。它会逐个字符比较用户输入的密码和正确密码。如果字符匹配，会执行一个简单的循环来模拟耗时操作。

2. attack() (客户端): 这个函数模拟了攻击者的行为。

它会循环尝试不同的字符来猜测密码的下一个字符。

对于每个字符，它会调用 verifyPassword() 函数来模拟密码验证过程，并测量验证所需的时间。

它会记录耗时最长的字符，并将其添加到已猜测的密码中。

它会重复这个过程，直到猜测出完整的密码。

3. performance.now(): 这个函数用于获取高精度的时间戳，可以用来测量代码块的执行时间。

4. await new Promise(resolve => setTimeout(resolve, 50)):模拟网络请求的延迟。在真实环境中，攻击者需要通过网络请求将密码发送到服务器，因此需要考虑网络延迟的影响。

运行结果分析：

运行上面的代码，你会发现，随着猜测的密码越来越接近正确密码，每次验证所需的时间也会逐渐增加。攻击者可以通过分析这些时间差异，逐步推断出完整的密码。

注意： 这只是一个简化的示例，实际的攻击会更加复杂，需要考虑更多的因素，比如网络延迟、服务器负载等。

四、更高级的玩法：Flush+Reload

Flush+Reload是一种更隐蔽、更精确的缓存定时攻击技术。它的原理是：

1. Flush: 攻击者通过某种方式，将目标数据所在的缓存行从缓存中清除（Flush）。

2. Access (受害者): 受害者访问目标数据，导致数据被重新加载到缓存中。

3. Reload: 攻击者再次访问目标数据，并测量访问时间。如果数据已经被缓存，访问时间会非常短。

通过测量访问时间，攻击者可以判断受害者是否访问过目标数据。

应用场景：

网站访问记录: 攻击者可以利用Flush+Reload来判断用户是否访问过特定的网站。

加密密钥: 攻击者可以利用Flush+Reload来窃取加密密钥。

代码示例 (JavaScript + SharedArrayBuffer):

这个例子需要使用 SharedArrayBuffer，并且需要在支持 SharedArrayBuffer 的环境中运行（比如开启了 Cross-Origin-Embedder-Policy 和 Cross-Origin-Opener-Policy 的服务器）。




Flush+Reload Demo


Flush+Reload Demo
Open the console to see the results.


    // 创建一个 SharedArrayBuffer
    const sab = new SharedArrayBuffer(1024);
    const array = new Int32Array(sab);

    // 要探测的目标地址（这里只是一个示例地址）
    const targetAddress = array.byteOffset;

    // 探测函数
    async function probe(address) {
      // Flush: 尝试将目标地址的数据从缓存中清除
      // (这个操作在 JavaScript 中无法直接实现，只能通过一些间接的方式，比如访问大量内存)
      for (let i = 0; i < 10000; i++) {
        volatileRead(array, i % array.length); // 访问大量内存
      }

      // 开始计时
      const startTime = performance.now();

      // Reload: 访问目标地址的数据
      volatileRead(array, 0); // 访问目标地址

      // 结束计时
      const endTime = performance.now();

      // 返回访问时间
      return endTime - startTime;
    }

    // 防止编译器优化
    function volatileRead(arr, index) {
        // 这段代码的目的是防止编译器优化，确保每次都实际读取内存
        let dummy = arr[index];
        return dummy;
    }

    // 模拟受害者访问数据
    async function victimFunction() {
      console.log("Victim: Accessing data...");
      await new Promise(resolve => setTimeout(resolve, 100)); // 模拟一些操作
      volatileRead(array, 0); // 模拟访问目标数据
      console.log("Victim: Data access complete.");
    }

    // 攻击函数
    async function attackerFunction() {
      console.log("Attacker: Starting attack...");

      // 第一次探测
      let time1 = await probe(targetAddress);
      console.log(`Attacker: First probe time: ${time1.toFixed(4)} ms`);

      // 模拟受害者访问数据
      await victimFunction();

      // 第二次探测
      let time2 = await probe(targetAddress);
      console.log(`Attacker: Second probe time: ${time2.toFixed(4)} ms`);

      // 分析结果
      if (time2 < time1 * 0.8) { // 阈值可以根据实际情况调整
        console.log("Attacker: Victim accessed the data!");
      } else {
        console.log("Attacker: Victim did not access the data.");
      }
    }

    // 启动攻击
    attackerFunction();




代码解释：

1. SharedArrayBuffer: SharedArrayBuffer 允许在不同的 JavaScript 上下文（比如不同的 Worker 线程）之间共享内存。这对于 Flush+Reload 攻击非常有用，因为攻击者需要能够精确地控制缓存的状态。

2. probe(address): 这个函数执行 Flush+Reload 操作。

Flush: 它会访问大量内存，试图将目标地址的数据从缓存中清除。 (注意：在JavaScript中无法直接控制缓存，所以这里Flush操作是间接的，效果可能不理想)

Reload: 它会访问目标地址的数据，并测量访问时间。

3. victimFunction(): 这个函数模拟了受害者访问数据的行为。

4. attackerFunction(): 这个函数执行攻击。

它首先执行一次探测，记录访问时间。

然后，它模拟受害者访问数据。

最后，它再次执行探测，并比较两次访问时间。如果第二次访问时间明显小于第一次，就说明受害者访问过目标数据。

5. volatileRead(arr, index): 防止编译器优化，确保每次都实际读取内存。

运行结果分析：

运行上面的代码，你会发现，如果受害者访问过目标数据，第二次探测的访问时间会明显小于第一次。攻击者可以通过分析这些时间差异，判断受害者是否访问过目标数据。

重要提示： 由于 JavaScript 无法直接控制缓存，所以上面的 Flush+Reload 示例的效果可能不理想。在实际攻击中，攻击者通常会使用更底层的技术，比如汇编语言，来直接控制缓存。

五、防御策略：亡羊补牢，犹未晚矣

旁路攻击防不胜防，但也不是完全没有办法。以下是一些常见的防御策略：

常量时间算法: 确保算法的执行时间不依赖于输入数据。比如，在比较密码时，不要使用短路逻辑，而是始终比较完整的字符串。

// 不安全的写法 (存在定时攻击风险)
function insecureCompare(a, b) {
  if (a.length !== b.length) {
    return false;
  }
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) {
      return false;
    }
  }
  return true;
}

// 安全的写法 (常量时间比较)
function secureCompare(a, b) {
  if (a.length !== b.length) {
    return false;
  }
  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= (a.charCodeAt(i) ^ b.charCodeAt(i)); // 使用位运算，确保所有字符都被比较
  }
  return result === 0;
}

缓存分区: 将敏感数据和非敏感数据存储在不同的缓存区域，防止攻击者利用缓存来推断敏感信息。

随机化: 在算法中引入随机性，使得攻击者难以预测算法的执行时间。

限制时间精度: 降低时间测量的精度，使得攻击者难以区分不同的操作。

HTTPS: 使用 HTTPS 加密所有网络流量，防止攻击者窃听通信内容。

Same-Site Cookies: 使用 Same-Site Cookies 来防止跨站请求伪造 (CSRF) 攻击，从而减少攻击者利用缓存的机会。

HTTP Header Security: 设置适当的 HTTP Header，比如 Cache-Control: no-store 和 Pragma: no-cache，来禁用缓存。 但是请注意，这些Header并非完全可靠，某些浏览器或者代理服务器可能会忽略它们。

Subresource Integrity (SRI): 使用 SRI 来验证第三方资源的完整性，防止恶意代码被注入到页面中。

Content Security Policy (CSP): 使用 CSP 来限制页面可以加载的资源，防止恶意脚本被执行。

六、总结：道高一尺，魔高一丈

旁路攻击是一种非常隐蔽和难以防御的攻击方式。作为开发者，我们需要时刻保持警惕，了解常见的旁路攻击技术，并采取适当的防御措施。

当然，防御旁路攻击是一个持续不断的过程。随着技术的不断发展，新的攻击方式也会不断涌现。我们需要不断学习和更新知识，才能更好地保护我们的系统。

今天就到这里，希望大家有所收获！如果以后有机会，咱们再聊聊其他有趣的浏览器安全话题。 谢谢大家！