Elementos internos de Linux: cómo / proc / self / mem escribe en la memoria no grabable



La extraña peculiaridad del pseudo archivo /proc/*/mem



radica en su semántica contundente. Las operaciones de escritura a través de este archivo se realizarán correctamente incluso si la memoria virtual de destino está marcada como no escribible. Esto es intencional, y proyectos como el compilador Julia JIT o el depurador rr utilizan activamente este comportamiento .



Pero la pregunta es: ¿el código privilegiado obedece a los permisos de memoria virtual? ¿Hasta qué punto el hardware puede afectar el acceso a la memoria del kernel?



Intentaremos responder a estas preguntas y considerar los matices de la interacción entre el sistema operativo y el hardware en el que se ejecuta. Exploremos los límites del procesador que pueden afectar al kernel y veamos cómo el kernel puede solucionarlos.



Parchear libc con / proc / self / mem



¿Cómo se ve esta semántica contundente? Considere el código:



#include <fstream>
#include <iostream>
#include <sys/mman.h>

/* Write @len bytes at @ptr to @addr in this address space using
 * /proc/self/mem.
 */
void memwrite(void *addr, char *ptr, size_t len) {
  std::ofstream ff("/proc/self/mem");
  ff.seekp(reinterpret_cast<size_t>(addr));
  ff.write(ptr, len);
  ff.flush();
}

int main(int argc, char **argv) {
  // Map an unwritable page. (read-only)
  auto mymap =
      (int *)mmap(NULL, 0x9000,
                  PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<<
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

  if (mymap == MAP_FAILED) {
    std::cout << "FAILED\n";
    return 1;
  }

  std::cout << "Allocated PROT_READ only memory: " << mymap << "\n";
  getchar();

  // Try to write to the unwritable page.
  memwrite(mymap, "\x40\x41\x41\x41", 4);
  std::cout << "did mymap[0] = 0x41414140 via proc self mem..";
  getchar();
  std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "\n";
  getchar();

  // Try to writ to the text segment (executable code) of libc.
  auto getchar_ptr = (char *)getchar;
  memwrite(getchar_ptr, "\xcc", 1);

  // Run the libc function whose code we modified. If the write worked,
  // we will get a SIGTRAP when the 0xcc executes.
  getchar();
}

      
      





Se /proc/self/mem



utiliza aquí para escribir en dos páginas de memoria no grabables. El primero contiene el código en sí y el segundo pertenece a libc



(la función getchar



). La última parte es de más interés: el código escribe el byte 0xcc (un punto de interrupción en aplicaciones x86-64), que, si se ejecuta, hará que el kernel proporcione a nuestro proceso un SIGTRAP. Esto literalmente cambia el ejecutable de libc. Y si en la próxima llamada getchar



obtenemos SIGTRAP, sabremos que el registro fue exitoso.



Así es como se ve cuando ejecuta el programa:





¡Obras! En el medio, se imprimen expresiones que prueban que el valor 0x41414140 se escribió y leyó correctamente de la memoria. El último resultado muestra que después de parchear, nuestro proceso recibió un SIGTRAP como resultado de nuestra llamada getchar



.



En el video:





Hemos visto cómo funciona esta característica desde la perspectiva del espacio de usuario. Profundicemos más. Para comprender completamente cómo funciona esto, debe observar cómo el hardware impone restricciones de memoria.



Equipo



En la plataforma x86-64, hay dos configuraciones de procesador que controlan la capacidad del kernel para acceder a la memoria. Son utilizados por la unidad de gestión de memoria (MMU).



El primer ajuste es el bit de protección contra escritura (CR0.WP). Del manual de Intel (Volumen 3, Sección 2.5) sabemos:



Protección contra escritura (16 bits CR0). Si se proporciona, evita que los procedimientos de nivel de supervisor escriban en páginas protegidas contra escritura. Si el bit está vacío, los procedimientos a nivel de supervisor pueden escribir en páginas protegidas contra escritura (independientemente de la configuración del bit U / S; consulte las Secciones 4.1.3 y 4.6).


Esto evita que el kernel escriba en páginas protegidas contra escritura, lo que naturalmente está permitido por defecto .



La segunda configuración es Prevención de acceso al modo supervisor (SMAP) (CR4.SMAP). La descripción completa en el Volumen 3, Sección 4.6, es detallada. En resumen, SMAP priva completamente al kernel de la capacidad de escribir o leer desde la memoria del espacio del usuario. Esto evita vulnerabilidades que inundan el espacio del usuario con datos maliciosos que el kernel debe leer durante la ejecución.



Si su código de kernel solo usa canales aprobados ( copy_to_user



etc.), entonces SMAP se puede ignorar con seguridad, estas funciones lo usarán automáticamente antes y después de acceder a la memoria. ¿Qué pasa con la protección contra escritura?



Si no se especifica CR0.WP, entonces la implementación del /proc/*/mem



kernel puede escribir sin ceremonias en la memoria del espacio de usuario protegida contra escritura.



Sin embargo, CR0.WP se establece en el arranque y normalmente permanece durante todo el tiempo de funcionamiento de los sistemas. En este caso, al intentar escribir, se emitirá un error de página. Es más una herramienta de copia en escritura que una herramienta de seguridad, por lo que no impone ninguna restricción real al kernel. En otras palabras, se requiere un manejo de fallas inconveniente, que no es necesario para un bit dado.



Averigüemos la implementación ahora.



Cómo funciona / proc / * / mem



/proc/*/mem



Está implementado en fs / proc / base.c .



La estructura file_operations



contiene las funciones del controlador y la función mem_rw () es totalmente compatible con el controlador de escritura. mem_rw()



usa access_remote_vm () para operaciones de escritura . Y access_remote_vm()



hace esto:



  • Llama get_user_pages_remote()



    para encontrar una trama física que coincida con la dirección virtual de destino.
  • Llama kmap()



    para marcar este marco como grabable en el espacio de direcciones virtuales del kernel.
  • Pide copy_to_user_page()



    la ejecución final de las operaciones de escritura.


¡Esta implementación evita por completo el problema de la capacidad del kernel para escribir en un espacio de usuario no modificable! El control del kernel sobre el subsistema de memoria virtual permite que la MMU se omita por completo, lo que permite que el kernel simplemente escriba en su propio espacio de direcciones grabables. Entonces, la discusión de CR0.WP se vuelve irrelevante.



Veamos cada uno de los pasos:



get_user_pages_remote ()



Para omitir la MMU, el kernel necesita hacer manualmente lo que hace la MMU en el hardware de la aplicación. Primero, debe convertir la dirección virtual de destino en una física. Esto lo hace la familia de funciones. get_user_pages()



... Recorren las tablas de páginas y buscan tramas de memoria física que coincidan con un rango determinado de direcciones virtuales.



La persona que llama proporciona el contexto y usa banderas para cambiar el comportamiento get_user_pages()



. La bandera FOLL_FORCE



que se transmite es especialmente interesante mem_rw()



. La bandera activa check_vma_flags (lógica de verificación de acceso get_user_pages()



) para ignorar las escrituras en páginas que no se pueden escribir y continuar buscando. La semántica "contundente" se refiere completamente a FOLL_FORCE



(mis comentarios):



static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)
{
        [...]
        if (write) { // If performing a write..
                if (!(vm_flags & VM_WRITE)) { // And the page is unwritable..
                        if (!(gup_flags & FOLL_FORCE)) // *Unless* FOLL_FORCE..
                                return -EFAULT; // Return an error
        [...]
        return 0; // Otherwise, proceed with lookup
}

      
      





get_user_pages()



También se adhiere a la semántica de copia en escritura (CoW). Si se especifica una escritura en una tabla de páginas no grabables, se emula un handle_mm_fault



error de página llamando al controlador de errores de la página principal. Esto inicia la rutina de procesamiento de copia en escritura adecuada do_wp_page



, que copia la página según sea necesario. Entonces, si las entradas a través /proc/*/mem



se ejecutan mediante un mapeo compartido privado, por ejemplo, libc, entonces son visibles solo dentro del proceso.



kmap ()



Una vez que se encuentra un marco físico, es necesario asignarlo al espacio de direcciones virtuales del kernel, que se puede escribir. Esto se hace con la ayuda de kmap()



.



En una plataforma x86 de 64 bits, toda la memoria física se asigna a través del área de asignación en línea del espacio de direcciones virtuales del kernel. En este caso, kmap()



funciona de manera muy simple: solo necesita agregar la dirección de inicio del mapeo lineal a la dirección física del marco para calcular la dirección virtual a la que se mapea este marco.



En una plataforma x86 de 32 bits, el mapeo en línea contiene un subconjunto de memoria física, por lo que una función kmap()



puede necesitar mapear un marco asignando memoria highmem y manipulando tablas de páginas.



En ambos casos, el mapeo de líneas y el mapeo de alta memoria se realizan con protección. PAGE_KERNEL que permite escribir.



copy_to_user_page ()



El último paso es ejecutar la escritura. Esto se hace usando copy_to_user_page()



lo que es esencialmente memcpy. Esto funciona porque el destino es una asignación de escritura kmap()



.



Discusión



Entonces, primero, el kernel, usando la tabla de páginas de memoria que pertenece al programa, convierte la dirección virtual de destino en el espacio del usuario al marco físico correspondiente. A continuación, el núcleo asigna este marco a su propio espacio virtual en el que se puede escribir. Finalmente, escribe con memcpy simple.



Sorprendentemente, CR0.WP no se utiliza aquí. La implementación pasa elegantemente por alto este punto aprovechando el hecho de que no tiene que acceder a la memoria a través de un puntero de espacio de usuario . Dado que el kernel tiene un control completo sobre la memoria virtual, simplemente puede reasignar el marco físico en su propio espacio de direcciones virtuales con resoluciones arbitrarias y hacer lo que quiera con él.



Es importante tener en cuenta que los permisos que protegen una página de memoria están relacionados con la dirección virtual utilizada para acceder a esa página, no con el marco físico asociado con la página . La notación de permiso de memoria se refiere exclusivamente a la memoria virtual, no a la memoria física.



Conclusión



Al examinar los detalles de la semántica contundente en la implementación, /proc/*/mem



podemos reflejar la relación entre el núcleo y el procesador. A primera vista, la capacidad del kernel para escribir en memoria no grabable plantea la pregunta: ¿Hasta qué punto puede afectar el procesador el acceso a la memoria del kernel? El manual describe los mecanismos de control que pueden limitar las acciones del kernel. Pero en una inspección más cercana, las limitaciones son superficiales en el mejor de los casos. Estos son obstáculos simples para sortear.



All Articles