Skip to content

How Meltdown Works #14

Open
Open
@yfractal

Description

@yfractal

Can one tab's JavaScript read the other tab's data?

In theory, it's not possible as the operating system guarantees isolation.

How the operating system guarantees isolation and provides multiplexing

Kernel Mode and User Mode:

Screen Shot 2023-12-04 at 7 47 17 PM

  • A process can only affect itself in user mode.
  • When accessing shared resources, it must be under OS control through kernel mode.
    • Shared resources include disk, network card, and kernel memory.
    • Procedure for accessing shared resources:
      The user program calls a system call (e.g., read file).
      User mode switches to kernel mode.
      The job is performed in kernel mode, and the data is returned (copied) to the user program.
  • When a process is in user mode, it cannot access other processes' data or kernel data.
  • This concept is similar(very very roughly) to a front-end and back-end setting:
    • Front-end app: User mode process, accessing its own data and functions.
    • Back-end server: Controls shared resources, performing operations requested by the front-end app.
      e.g. when a user wants to withdraw money, the backend checks whether its account has enough money or not first.
    • RPC: Similar to a system call

Virtual memory ensures data privilege:

  • Virtual memory is a hardware-based mapping that maps virtual addresses to physical addresses.
  • How it works:
    • When data is requested, virtual memory translates the virtual address to the corresponding physical address.
      Screen Shot 2023-12-04 at 7 48 56 PM
      def read(virtual_address)
        pte = pte_through_virtual_address(virtual_address)
        physical_address = physical_address_through_pte(pte)
        read_data(physical_address)
      end
    • Before returning the data, virtual memory checks for the appropriate privilege.
      Screen Shot 2023-12-04 at 7 50 09 PM
      def read(virtual_address)
        pte = pte_through_virtual_address(virtual_address)
        can_read!(pte)
        physical_address = physical_address_through_pte(pte)
        read_data(physical_address)
      end
      
      def can_read!(pte)
        if required_mode(pte) == :kernel_mode && current_mode == :user_mode
        raise "No right"
      end

Reading Kernel Memory in User Mode:

Plan:

  • Reading kernel data in user mode.
  • Making the result visible to the user program.

Reading kernel data in user mode:

  • Precondition: user mode has kernel memory mapping.
    Screen Shot 2023-12-04 at 7 51 37 PM

    • Kernel memory is mapped in high memory addresses for faster system calls,
    • which was nearly universal until these attacks were discovered.
  • speculative execution executes unprivileged Instructions:

    if a # load a through memory
      do_some_thing
    else
     do_other_thing
    end
    • CPU may execute "do_something" without knowing the value of "a."
    • If the CPU guesses correctly, it saves time; if not, it must revert changes.
    • CPU should not raise exceptions if "do_something" raises an exception before knowing a's value.
  • accessing kernel memory:

    if a # load data from memory
     read_a_kernel_address
    end
    • CPU accesses "read_a_kernel_address" due to speculative execution.
    • However, the CPU doesn't check the privilege, bypassing the security checks(I don't know the detail, the CPU is a black box..).
    • It accesses the kernel data, but it's not visible to the user program.

Making Data Visible to User:

  • CPU guarantees that such actions are invisible to programs and cannot be directly accessed.

  • We can determine if data is read in the CPU cache by measuring time using instructions like "rdtsc" for x86 architecture.
    L1 hit: A few cycles.
    L2 hit: A dozen or two cycles.
    RAM: Around 300 cycles.

    Screen Shot 2023-12-04 at 8 12 15 PM

Goal: Read one kernel memory bit.

  • Plan:

    1. Make speculative execution visible.
    2. Read one kernel memory bit.
  • Making speculative execution visible:

    char buf[8192];
    clflush(buf[0]);
    clflush(some_condition);
    
    if(some_condition) { // some_condition is false, need to read from memory
      // speculative execution starts
      a = buf[0];
      // speculative execution ends
    }
    
    c0 = rdtsc;
    a = buf[0];
    c1 = rdtsc;
    
    if (c1 - c0 < x00) {
      // buf[0] had been loaded into memory
    } else {
      // buf[0] hadn't been loaded into memory
    }
    • Read data during speculative execution, then read it again and measure the time.
    • If the duration is fast, it means the address has been loaded into the cache, indicating visible speculative execution.
  • Reading one bit in the kernel.

    char buf[8192];
    clflush(buf[0]);
    clflush(buf[4096]);
    clflush(some_condition);
    
    if(some_condition) { // some_condition is false, but need to read from memory
      // speculative execution starts
    
      r1 = a_kernel_virtual_address;
      r2 = *r1;         // read content
      r2 = r2 & 1;      // first bit
      r2 = r2 * 4096;   // 0 or 4096
      r3 = buf[r2];     // access the data
      // speculative execution end
     }
    
    c0 = rdtsc;
    a = buf[0];
    c1 = rdtsc;
    b = buf[4096];
    c2 = rdtsc;
    
    if (c1 - c0 > c2 - c2) {
      // low bit was probably 0;
    } else {
      // low bit was probably 1;
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions