Interception des appels système sur Android sans accès root via un cadre applicatif de bas niveau

Le cadre d'interception au niveau applicatif pour les plateformes Android — interception des appels système (SVC) sans nécessiter de privilèges root, de déverrouillage bootloader ou de flash du firmware.

GitHub : https://github.com/iofomo/abyss

Pour répondre aux exigences de notre produit de virtualisation fonctionnant sur des appareils Android standard, il a été nécessaire de construire un cadre d'interception couvrant toutes les couches applicatives. Le projet Abyss, responsable de l'interception des appels système au niveau SVC, constitue notre solution finale en profondeur.

Code source : https://github.com/iofomo/abyss/tree/main/svcer

  1. Concepts fondamentaux

Seccomp (Secure Computing Mode) : Une fonctionnalité de sécurité du noyau Linux visant à restreindre les appels système qu'un processus peut effectuer. Elle agit comme un filtre pour empêcher l'exécution d'opérations potentiellement dangereuses, souvent combinée avec BPF pour des règles de filtrage plus flexibles.

BPF (Berkeley Packet Filter) : Une technologie noyau initialement conçue pour le filtrage de paquets réseau, étendue depuis à d'autres usages comme le filtrage des appels système. Les programmes BPF s'exécutent dans le noyau pour inspecter et contrôler ces appels.

  1. Processus d'implémentation

Premièrement, on configure les règles BPF pour cibler spécifiquement les numéros d'appels système à intercepter. L'approche consiste à n'autoriser que les appels listés, améliorant ainsi l'efficacité et la stabilité du filtrage.

static void configurerFiltrageAppels(struct sock_filter* regles, unsigned short& index) {
    // Charge le numéro d'appel système dans l'accumulateur
    regles[index++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr)));
    // Configuration de l'appel système cible
    // Ajouter d'autres appels ici si nécessaire
    regles[index++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 1, 0);
    regles[index++] = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
}

Deuxièmement, il convient d'exclure certaines bibliothèques système et la bibliothèque propre au cadre pour éviter les boucles infinies. Les exclusions obligatoires comprennent la bibliothèque du cadre lui-même et la région vdso. L'exclusion des bibliothèques linker et libc est optionnelle mais recommandée pour les performances. L'analyse des zones mémoire du processus (fichier maps) permet de définir les plages d'adresses à ignorer.

static void configurerFiltrageBibliotheque(struct sock_filter* regles, unsigned short& index, const uintptr_t& debut, const uintptr_t& fin) {
    // Charge l'adresse de l'instruction courante
#if defined(__arm__)
    regles[index++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, instruction_pointer));
    regles[index++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, debut, 0, 2);
    regles[index++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, fin, 1, 0);
    regles[index++] = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
#else // __aarch64__
    regles[index++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, instruction_pointer) + 4));
    regles[index++] = BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (uint32_t)(debut >> 32), 0, 4);
    regles[index++] = BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, instruction_pointer)));
    regles[index++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (uint32_t)debut, 0, 2);
    regles[index++] = BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (uint32_t)fin, 1, 0);
    regles[index++] = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
#endif
}

Ensuite, on applique la configuration au processus courant.

struct sigaction gestionSignal = { 0 };
gestionSignal.sa_flags = SA_SIGINFO | SA_NODEFER;
gestionSignal.sa_sigaction = traiterSignalInterception;
struct sigaction ancienneConfig = {};

ret = sigaction(SIGSYS, &gestionSignal, &ancienneConfig);
if (0 != ret) {
    LOGSVCE("sigaction: %d, %d, %s", ret, errno, strerror(errno))
    ::free(regles);
    return -11;
}

// Débloquer le signal SIGSYS
sigset_t masque;
if (sigemptyset(&masque) || sigaddset(&masque, SIGSYS) || sigprocmask(SIG_UNBLOCK, &masque, nullptr)) {
    LOGSVCE("sigprocmask: %d, %d, %s", ret, errno, strerror(errno))
    ::free(regles);
    return -12;
}

struct sock_fprog programme = {
    .len = nombreRegles,
    .filter = regles,
};

ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (0 != ret) {
    LOGSVCE("PR_SET_NO_NEW_PRIVS: %d, %d, %s", ret, errno, strerror(errno))
    ::free(regles);
    return -13;
}

ret = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &programme);
if (0 != ret) {
    LOGSVCE("PR_SET_SECCOMP: %d, %d, %s", ret, errno, strerror(errno))
    ::free(regles);
    return -14;
}

Enfin, on implémente la routine de traitement des appels interceptés.

static void traiterSignalInterception(int signal, siginfo_t* info, void* contexte) {
    if (!info || !contexte || signal != SIGSYS || info->si_code != SYS_SECCOMP) {
        LOGSVCW("signal: %d, code=%d, errno=%d, addr=%p, arch=0x%x, syscall=0x%x,%s",
              info->si_signo, info->si_code, info->si_errno, info->si_call_addr, info->si_arch,
              info->si_syscall, SvcerDumper::nomDepuisIndice(info->si_syscall))
        return;
    }

    ucontext_t *uc = reinterpret_cast<ucontext_t>(contexte);
    intptr_t resultat = SvcerSyscall::Executer(SECCOMP_SYSCALL(uc),
                                   SECCOMP_PARM1(uc),
                                   SECCOMP_PARM2(uc),
                                   SECCOMP_PARM3(uc),
                                   SECCOMP_PARM4(uc),
                                   SECCOMP_PARM5(uc),
                                   SECCOMP_PARM6(uc)
    );
    SvcerSyscall::RetournerValeur(resultat, uc);
}</ucontext_t>
  1. Utilisation simplifiée

Le cadre fournit une interface simplifiée pour les tâches courantes :

1) Ajout d'appels système au journal de diagnostic.

SvcerDumper::enregistrer(SVCER_SYSCALL_execve);
SvcerDumper::enregistrer(SVCER_SYSCALL_execveat);
SvcerDumper::enregistrer(SVCER_SYSCALL_open);
SvcerDumper::enregistrer(SVCER_SYSCALL_openat);
SvcerDumper::toutEnregistrer();

SvcerHooker::initialiser(ESvcerHookerMode_IgnorerTout, "libbibliotheque.so");

2) Enregistrement des gestionnaires d'interception.

for (int i=SVCER_SYSCALL_Aucun; i<svcer_syscall_max __nr_chdir:="" __nr_statfs:="" __nr_truncate:="" arguments="" case="" char="" chemin="(const" const="" gererinterception="" numappel="" static="" svcerhooker::enregistrecallback="" svcerhookerargument="" switch="" void="">obtenirArgument1();
        char bufferChemin[512];
        if (bufferChemin == CorrigerChemin(chemin, bufferChemin)) {
            LOGSVCI("correction, %s: %s", SvcerDumper::nomDepuisIndice(numAppel), __PRINTSTR(chemin))
            arguments->definirArgument1((intptr_t)bufferChemin);
        }
        arguments->executerAppelSysteme();
        return;
    }
    default:
        LOGSVCI("ignoré, %s", SvcerDumper::nomDepuisIndice(numAppel))
        break;
    }
    arguments->executerAppelSysteme();
}</svcer_syscall_max>

3) Initialisation avec configuration des exclusions.

SvcerHooker::initialiser(ESvcerHookerMode_IgnorerVdso|ESvcerHookerMode_IgnorerLibc|ESvcerHookerMode_IgnorerLinker, "libdemonstration.so");
  1. Notes complémentaires

Fonctionnalités additionnelles : Le cadre inclut une détection basique d'émulation, par exemple en restaurant les valeurs de retour lors des appels à \_\_NR\_rt\_sigaction et \_\_NR\_prctl durant la configuration.

Projets de référence :

Étiquettes: Android Seccomp bpf appels système Intercepteurs

Publié le 28 juillet à 05h28