L'étrange bizarrerie du pseudo fichier
/proc/*/mem
réside dans sa sémantique percutante. Les opérations d'écriture via ce fichier réussiront même si la mémoire virtuelle cible est marquée comme non accessible en écriture. C'est intentionnel et ce comportement est activement utilisé par des projets tels que le compilateur Julia JIT ou le débogueur rr.
Mais la question est: le code privilégié obéit-il aux autorisations de mémoire virtuelle? Dans quelle mesure le matériel peut-il affecter l'accès à la mémoire du noyau?
Nous allons essayer de répondre à ces questions et considérer les nuances de l'interaction entre le système d'exploitation et le matériel sur lequel il est exécuté. Explorons les limites du processeur qui peuvent affecter le noyau et voyons comment le noyau peut les contourner.
Patch libc avec / proc / self / mem
À quoi ressemble cette sémantique percutante? Considérez le code:
#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();
}
Il est
/proc/self/mem
utilisé ici pour écrire sur deux pages de mémoire non inscriptibles. Le premier contient le code lui-même et le second appartient à
libc
(la fonction
getchar
). La dernière partie est plus intéressante: le code écrit l'octet 0xcc (un point d'arrêt dans les applications x86-64), qui, s'il est exécuté, amènera le noyau à fournir à notre processus un SIGTRAP. Cela change littéralement l'exécutable de la libc. Et si au prochain appel
getchar
nous obtenons SIGTRAP, nous saurons que l'enregistrement a réussi.
Voici à quoi cela ressemble lorsque vous exécutez le programme:
Travaux! Au milieu, des expressions sont imprimées qui prouvent que la valeur 0x41414140 a été correctement écrite et lue à partir de la mémoire. La dernière sortie montre qu'après l'application des correctifs, notre processus a reçu un SIGTRAP à la suite de notre appel
getchar
.
Dans la vidéo:
Nous avons vu comment cette fonctionnalité fonctionne du point de vue de l'espace utilisateur. Creusons plus profondément. Pour bien comprendre comment cela fonctionne, vous devez examiner comment le matériel impose des contraintes de mémoire.
Équipement
Sur la plate-forme x86-64, deux paramètres de processeur contrôlent la capacité du noyau à accéder à la mémoire. Ils sont utilisés par l'unité de gestion de la mémoire (MMU).
Le premier paramètre est le bit de protection en écriture (CR0.WP). D'après le manuel Intel (Volume 3, Section 2.5), nous savons:
Protection en écriture (16e bit CR0). Si elle est donnée, elle empêche les procédures de niveau superviseur d'écrire sur des pages protégées en écriture. Si le bit est vide, les procédures de niveau superviseur peuvent écrire sur des pages protégées en écriture (quels que soient les paramètres de bit U / S; voir les sections 4.1.3 et 4.6).
Cela empêche le noyau d'écrire sur des pages protégées en écriture, ce qui est naturellement autorisé par défaut .
Le deuxième paramètre est la prévention d'accès en mode superviseur (SMAP) (CR4.SMAP). La description complète du volume 3, section 4.6, est détaillée. En bref, SMAP prive complètement le noyau de la capacité d'écrire ou de lire à partir de la mémoire de l'espace utilisateur. Cela évite les exploits qui inondent l'espace utilisateur de données malveillantes que le noyau doit lire lors de l'exécution.
Si votre code noyau n'utilise que des canaux approuvés (
copy_to_user
etc.), alors SMAP peut être ignoré en toute sécurité, ces fonctions l'utiliseront automatiquement avant et après l'accès à la mémoire. Qu'en est-il de la protection en écriture?
Si CR0.WP n'est pas spécifié, alors l'implémentation du
/proc/*/mem
noyau peut en effet écrire sans cérémonie dans la mémoire de l'espace utilisateur protégée en écriture.
Cependant, CR0.WP est défini au démarrage et dure généralement toute la durée de fonctionnement des systèmes. Dans ce cas, lors d'une tentative d'écriture, une erreur de page sera émise. Il s'agit plus d'un outil de copie sur écriture que d'un outil de sécurité, il n'impose donc aucune restriction réelle sur le noyau. En d'autres termes, une gestion des défauts peu pratique est nécessaire, ce qui n'est pas nécessaire pour un bit donné.
Voyons maintenant l'implémentation.
Comment fonctionne / proc / * / mem
/proc/*/mem
Il est mis en œuvre dans fs / proc / base.c .
La structure
file_operations
contient les fonctions du gestionnaire et la fonction mem_rw () prend entièrement en charge le gestionnaire d'écriture.
mem_rw()
utilise access_remote_vm () pour les opérations d'écriture . Et
access_remote_vm()
il fait ceci:
- Appelle
get_user_pages_remote()
pour trouver une trame physique qui correspond à l'adresse virtuelle cible. - Appelle
kmap()
pour marquer ce cadre comme inscriptible dans l'espace d'adressage virtuel du noyau. - Appelle l'
copy_to_user_page()
exécution finale des opérations d'écriture.
Cette implémentation contourne complètement le problème de la capacité du noyau à écrire dans un espace utilisateur non inscriptible! Le contrôle du noyau sur le sous-système de mémoire virtuelle permet à la MMU d'être complètement contournée, permettant au noyau d'écrire simplement dans son propre espace d'adressage inscriptible. Ainsi, la discussion de CR0.WP devient hors de propos.
Regardons chacune des étapes:
get_user_pages_remote ()
Pour contourner la MMU, le noyau doit faire manuellement ce que la MMU fait dans le matériel de l'application. Tout d'abord, vous devez convertir l'adresse virtuelle cible en adresse physique. Ceci est fait par la famille de fonctions
get_user_pages()
... Ils parcourent les tables de pages et recherchent des cadres de mémoire physique correspondant à une plage d'adresses virtuelles donnée.
L'appelant fournit le contexte et utilise des indicateurs pour modifier le comportement
get_user_pages()
. Le drapeau
FOLL_FORCE
qui est transmis est particulièrement intéressant
mem_rw()
. L'indicateur déclenche check_vma_flags (logique de contrôle d'accès
get_user_pages()
) pour ignorer les écritures sur des pages non inscriptibles et poursuivre la recherche. La sémantique "punchy" se réfère complètement à
FOLL_FORCE
(mes commentaires):
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()
Il adhère également à la sémantique de copie sur écriture (CoW). Si une écriture dans une table de pages non accessible en écriture est spécifiée, un échec de page est émulé en appelant le
handle_mm_fault
gestionnaire d'erreurs de page principale. Cela lance la routine de traitement de copie sur écriture appropriée
do_wp_page
, qui copie la page selon les besoins. Ainsi, si les entrées via
/proc/*/mem
sont exécutées par mappage partagé privé, par exemple, libc, elles ne sont visibles que dans le processus.
kmap () Une
fois qu'une trame physique est trouvée, elle doit être mappée à l'espace d'adressage virtuel du noyau, qui est accessible en écriture. Ceci est fait avec l'aide de
kmap()
.
Sur une plate-forme x86 64 bits, toute la mémoire physique est mappée via la zone de mappage en ligne de l'espace d'adressage virtuel du noyau. Dans ce cas, cela
kmap()
fonctionne très simplement: il suffit d'ajouter l'adresse de départ du mappage linéaire à l'adresse physique de la trame pour calculer l'adresse virtuelle à laquelle cette trame est mappée.
Sur une plate-forme x86 32 bits, le mappage en ligne contient un sous-ensemble de mémoire physique, de sorte qu'une fonction
kmap()
peut avoir besoin de mapper une trame en allouant de la mémoire highmem et en manipulant des tables de pages.
Dans les deux cas, le mappage de ligne et le mappage haut de gamme sont effectués avec protection. PAGE_KERNEL qui permet l'écriture.
copy_to_user_page ()
La dernière étape consiste à exécuter l'écriture. Ceci est fait en utilisant
copy_to_user_page()
ce qui est essentiellement memcpy. Cela fonctionne car la cible est un mappage inscriptible à partir de
kmap()
.
Discussion
Ainsi, tout d'abord, le noyau, en utilisant la table des pages mémoire appartenant au programme, convertit l'adresse virtuelle cible dans l'espace utilisateur en trame physique correspondante. Le noyau mappe ensuite cette trame à son propre espace virtuel inscriptible. Enfin, il écrit avec un simple memcpy.
De manière frappante, CR0.WP n'est pas utilisé ici. L'implémentation contourne élégamment ce point en tirant parti du fait qu'elle n'a pas besoin d'accéder à la mémoire via un pointeur d'espace utilisateur . Étant donné que le noyau a un contrôle complet sur la mémoire virtuelle, il peut simplement remapper la trame physique dans son propre espace d'adressage virtuel avec des résolutions arbitraires et en faire ce qu'il veut.
Il est important de noter que les autorisations qui protègent une page de mémoire sont liées à l'adresse virtuelle utilisée pour accéder à cette page, et non au cadre physique associé à la page . La notation d'autorisation de mémoire se réfère exclusivement à la mémoire virtuelle, pas à la mémoire physique.
Conclusion
En examinant les détails de la sémantique percutante de l'implémentation,
/proc/*/mem
nous pouvons refléter la relation entre le cœur et le processeur. À première vue, la capacité du noyau à écrire dans une mémoire non inscriptible soulève la question: dans quelle mesure le processeur peut-il affecter l'accès à la mémoire du noyau? Le manuel décrit les mécanismes de contrôle qui peuvent limiter les actions du noyau. Mais à y regarder de plus près, les limites sont au mieux superficielles. Ce sont de simples obstacles à contourner.