// Q1 de harkaq (SDD 16 §8): ¿llegan los registros AUDIT_LANDLOCK_ACCESS a un lector? // // Prueba las tres cosas de las que cuelga el proyecto: // 1. ¿El audit del kernel emite el registro? (audit_enabled, kauditd) // 2. ¿Un lector multicast (AUDIT_NLGRP_READLOG) los recibe? (CAP_AUDIT_READ) // 3. ¿SOBREVIVEN AL execve? — harkaq restringe y DESPUÉS ejecuta el builder. // El default del kernel NO loguea tras exec (linux/landlock.h:80-83). // Sin LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, harkaq vería denials=[] SIEMPRE. // // Uso (como root): ./q1-audit same-exec | new-exec | new-exec-logon // // El "víctima" se autoencierra con una política que sólo permite leer /tmp, y // después intenta leer /etc/passwd (fuera de la clausura) → EACCES → debe generar // un AUDIT_LANDLOCK_ACCESS. El padre escucha el netlink y lo imprime. #define _GNU_SOURCE #include #include #include "harkaq-uapi.h" #include #include #include #include #include #include #include #include #include #include static int ll_create(const struct landlock_ruleset_attr *a, size_t n, __u32 f) { return syscall(SYS_landlock_create_ruleset, a, n, f); } static int ll_add(int fd, enum landlock_rule_type t, const void *a, __u32 f) { return syscall(SYS_landlock_add_rule, fd, t, a, f); } static int ll_restrict(int fd, __u32 f) { return syscall(SYS_landlock_restrict_self, fd, f); } #define ACCESS_FS_ROUGHLY_READ \ (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | \ LANDLOCK_ACCESS_FS_EXECUTE) // ---------------------------------------------------------------- netlink audit static int nl_open(void) { int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT); if (fd < 0) { perror("socket(NETLINK_AUDIT)"); return -1; } struct sockaddr_nl sa = {0}; sa.nl_family = AF_NETLINK; sa.nl_pid = 0; // Grupo multicast de sólo-lectura: exactamente para lectores como harkaq-audit. sa.nl_groups = 1 << (AUDIT_NLGRP_READLOG - 1); if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { perror("bind(AUDIT_NLGRP_READLOG) <-- ¿falta CAP_AUDIT_READ?"); close(fd); return -1; } return fd; } // Manda un AUDIT_GET/AUDIT_SET. `payload` NULL ⇒ GET. static int nl_send(int fd, int type, const void *payload, size_t len) { struct { struct nlmsghdr h; char data[256]; } req = {0}; req.h.nlmsg_len = NLMSG_LENGTH(len); req.h.nlmsg_type = type; // SIN NLM_F_ACK: la respuesta de AUDIT_GET la manda el kernel de forma ASÍNCRONA // (audit_send_reply usa un kthread), así que el ACK llegaba ANTES que el AUDIT_GET // y el lector lo tomaba por "fallo". Pedir sólo REQUEST y filtrar por tipo al leer. req.h.nlmsg_flags = NLM_F_REQUEST; req.h.nlmsg_seq = 1; req.h.nlmsg_pid = getpid(); if (payload) memcpy(req.data, payload, len); struct sockaddr_nl sa = {0}; sa.nl_family = AF_NETLINK; return sendto(fd, &req, req.h.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)); } // Lee el estado del audit por un socket dedicado (unicast). Devuelve enabled o -1. static int audit_status(int *enabled, int *pid) { int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT); if (fd < 0) return -1; if (nl_send(fd, AUDIT_GET, NULL, 0) < 0) { close(fd); return -1; } struct timeval tv = {.tv_sec = 1}; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); // Drenamos hasta encontrar el AUDIT_GET: puede venir detrás de otros mensajes. char buf[8192]; for (int i = 0; i < 8; i++) { ssize_t n = recv(fd, buf, sizeof(buf), 0); if (n <= 0) break; for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n); h = NLMSG_NEXT(h, n)) { if (h->nlmsg_type == AUDIT_GET) { struct audit_status *st = (struct audit_status *)NLMSG_DATA(h); *enabled = st->enabled; *pid = st->pid; close(fd); return 0; } if (h->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *e = (struct nlmsgerr *)NLMSG_DATA(h); if (e->error) fprintf(stderr, "[q1] AUDIT_GET rechazado: %s\n", strerror(-e->error)); } } } close(fd); return -1; } static int audit_enable(void) { int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT); if (fd < 0) return -1; struct audit_status st = {0}; st.mask = AUDIT_STATUS_ENABLED; st.enabled = 1; int r = nl_send(fd, AUDIT_SET, &st, sizeof(st)); close(fd); return r < 0 ? -1 : 0; } // ---------------------------------------------------------------- la víctima // Se autoencierra: sólo /tmp legible. Todo lo demás → EACCES + registro de audit. static void jail(__u32 restrict_flags) { struct landlock_ruleset_attr attr = {.handled_access_fs = ACCESS_FS_ROUGHLY_READ}; int rs = ll_create(&attr, sizeof(attr), 0); if (rs < 0) { perror("landlock_create_ruleset"); exit(2); } // La "clausura declarada": /tmp y el intérprete. Nada más. const char *allowed[] = {"/tmp", "/usr/lib", "/lib", "/bin", "/usr/bin", NULL}; for (int i = 0; allowed[i]; i++) { int dfd = open(allowed[i], O_PATH | O_CLOEXEC); if (dfd < 0) continue; struct landlock_path_beneath_attr pb = {.allowed_access = ACCESS_FS_ROUGHLY_READ, .parent_fd = dfd}; if (ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0) < 0) perror("add_rule"); close(dfd); } if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("no_new_privs"); exit(2); } if (ll_restrict(rs, restrict_flags) < 0) { perror("landlock_restrict_self"); exit(2); } close(rs); } int main(int argc, char **argv) { const char *scenario = argc > 1 ? argv[1] : "new-exec"; int enabled = -1, apid = -1; if (audit_status(&enabled, &apid) == 0) fprintf(stderr, "[q1] audit: enabled=%d auditd_pid=%d\n", enabled, apid); else fprintf(stderr, "[q1] audit: no pude leer el estado (¿sin CAP_AUDIT_CONTROL?)\n"); // `!= 1` y no `== 0`: si el GET falló (enabled=-1) igual hay que intentar encenderlo. // El bug de la primera corrida fue justo este: GET falló → enabled=-1 → no entró acá → // audit_enabled siguió en 0 → CERO registros en los 3 escenarios → un "hallazgo" que // era el instrumento roto. if (enabled != 1) { fprintf(stderr, "[q1] audit no habilitado (enabled=%d) → AUDIT_SET\n", enabled); if (audit_enable() < 0) perror("[q1] AUDIT_SET"); usleep(100000); if (audit_status(&enabled, &apid) == 0) fprintf(stderr, "[q1] audit: enabled=%d (tras AUDIT_SET)\n", enabled); } // GATE: sin audit_enabled=1 el kernel no emite NADA y el test no mide lo que cree medir. // Abortar es obligatorio: un 0 acá significaría "instrumento roto", no "sin denegaciones". if (enabled != 1) { fprintf(stderr, "[q1] ABORTO: no pude confirmar audit_enabled=1. Cualquier resultado sería\n" " un falso negativo del test, no una medición. (¿root? ¿audit=1?)\n"); return 4; } // Sin CAP_AUDIT_READ no hay lector, pero la víctima corre igual: así una corrida // sin privilegio valida al menos que la JAULA deniega (la mitad barata del test). int nl = nl_open(); if (nl < 0) fprintf(stderr, "[q1] sin lector (correr como root para la prueba completa)\n"); else fprintf(stderr, "[q1] lector multicast OK. escenario=%s\n", scenario); __u32 flags = 0; if (!strcmp(scenario, "new-exec-logon")) flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON; pid_t pid = fork(); if (pid == 0) { usleep(200000); // dejamos que el padre esté escuchando jail(flags); if (!strcmp(scenario, "same-exec")) { // Denegación SIN execve: el default del kernel SÍ la loguea. int fd = open("/etc/passwd", O_RDONLY); fprintf(stderr, "[victima] open(/etc/passwd) = %d (%s)\n", fd, strerror(errno)); } else { // Denegación TRAS execve: el caso REAL de harkaq (bwrap → sh -c → make). fprintf(stderr, "[victima] execve(/bin/cat /etc/passwd)\n"); execl("/bin/cat", "cat", "/etc/passwd", (char *)NULL); perror("execl"); } _exit(0); } // El pid del hijo permite atribuir los registros DOMAIN (traen pid=) a NUESTRO dominio y // no al de una corrida anterior que se libera tarde — el error que casi me como. fprintf(stderr, "[q1] hijo pid=%d (atribuir los DOMAIN por este pid)\n", pid); char buf[16384]; int landlock_records = 0, total = 0; if (nl < 0) { waitpid(pid, NULL, 0); fprintf(stderr, "\n[q1] jaula validada; evidencia NO probada (hace falta root).\n"); return 3; } // Deadline de pared, NO "cortar al primer timeout": el registro `status=deallocated // denials=N` se emite cuando el dominio se libera, DESPUÉS de que el hijo muere, y con // retraso (RCU/workqueue). Con la ventana de 2s se perdía y aparecía en la corrida // siguiente, haciéndolo pasar por el dominio equivocado. Escuchamos 6s pasado el exit. struct timeval tv = {.tv_sec = 1}; setsockopt(nl, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); time_t deadline = time(NULL) + 6; while (time(NULL) < deadline) { ssize_t n = recv(nl, buf, sizeof(buf), 0); if (n <= 0) continue; // timeout de 1s: seguimos hasta el deadline for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n); h = NLMSG_NEXT(h, n)) { total++; if (h->nlmsg_type == AUDIT_LANDLOCK_ACCESS || h->nlmsg_type == AUDIT_LANDLOCK_DOMAIN) { landlock_records++; printf("[REGISTRO %s] %.*s\n", h->nlmsg_type == AUDIT_LANDLOCK_ACCESS ? "ACCESS" : "DOMAIN", (int)(NLMSG_PAYLOAD(h, 0)), (char *)NLMSG_DATA(h)); } } } waitpid(pid, NULL, 0); fprintf(stderr, "\n[q1] VEREDICTO escenario=%s: %d registros landlock (%d msgs audit total)\n", scenario, landlock_records, total); if (landlock_records == 0) fprintf(stderr, "[q1] ⇒ SIN EVIDENCIA. harkaq reportaría denials=[] (falso 'hermético').\n"); else fprintf(stderr, "[q1] ⇒ EVIDENCIA ENTREGADA. Q1 viable por esta vía.\n"); return 0; }