tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1184Shell 70Rust 62Markdown 501503 Definiciones

⎇ 534014c3ec69daea789a1d0523e1d00a69c6ed65 scripts/harkaq/q1-audit.c C · 247 líneas · 10.5 KB Historia Quién tocó qué crudo

harkaq: la granja VPS ya puede ser el compilador continuo — golden bumpeada a 6.17 (ABI 7) cba59e829 · sergio ·

1// Q1 de harkaq (SDD 16 §8): ¿llegan los registros AUDIT_LANDLOCK_ACCESS a un lector?
2//
3// Prueba las tres cosas de las que cuelga el proyecto:
4//   1. ¿El audit del kernel emite el registro? (audit_enabled, kauditd)
5//   2. ¿Un lector multicast (AUDIT_NLGRP_READLOG) los recibe? (CAP_AUDIT_READ)
6//   3. ¿SOBREVIVEN AL execve? — harkaq restringe y DESPUÉS ejecuta el builder.
7//      El default del kernel NO loguea tras exec (linux/landlock.h:80-83).
8//      Sin LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, harkaq vería denials=[] SIEMPRE.
9//
10// Uso (como root):  ./q1-audit same-exec | new-exec | new-exec-logon
11//
12// El "víctima" se autoencierra con una política que sólo permite leer /tmp, y
13// después intenta leer /etc/passwd (fuera de la clausura) → EACCES → debe generar
14// un AUDIT_LANDLOCK_ACCESS. El padre escucha el netlink y lo imprime.
15
16#define _GNU_SOURCE
17#include <errno.h>
18#include <fcntl.h>
19#include "harkaq-uapi.h"
20#include <linux/netlink.h>
21#include <stdio.h>
22#include <stdlib.h>
23#include <string.h>
24#include <sys/prctl.h>
25#include <sys/socket.h>
26#include <sys/syscall.h>
27#include <sys/wait.h>
28#include <time.h>
29#include <unistd.h>
30
31static int ll_create(const struct landlock_ruleset_attr *a, size_t n, __u32 f) {
32    return syscall(SYS_landlock_create_ruleset, a, n, f);
33}
34static int ll_add(int fd, enum landlock_rule_type t, const void *a, __u32 f) {
35    return syscall(SYS_landlock_add_rule, fd, t, a, f);
36}
37static int ll_restrict(int fd, __u32 f) {
38    return syscall(SYS_landlock_restrict_self, fd, f);
39}
40
41#define ACCESS_FS_ROUGHLY_READ                                    \
42    (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | \
43     LANDLOCK_ACCESS_FS_EXECUTE)
44
45// ---------------------------------------------------------------- netlink audit
46
47static int nl_open(void) {
48    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
49    if (fd < 0) { perror("socket(NETLINK_AUDIT)"); return -1; }
50    struct sockaddr_nl sa = {0};
51    sa.nl_family = AF_NETLINK;
52    sa.nl_pid = 0;
53    // Grupo multicast de sólo-lectura: exactamente para lectores como harkaq-audit.
54    sa.nl_groups = 1 << (AUDIT_NLGRP_READLOG - 1);
55    if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
56        perror("bind(AUDIT_NLGRP_READLOG)  <-- ¿falta CAP_AUDIT_READ?");
57        close(fd);
58        return -1;
59    }
60    return fd;
61}
62
63// Manda un AUDIT_GET/AUDIT_SET. `payload` NULL ⇒ GET.
64static int nl_send(int fd, int type, const void *payload, size_t len) {
65    struct {
66        struct nlmsghdr h;
67        char data[256];
68    } req = {0};
69    req.h.nlmsg_len = NLMSG_LENGTH(len);
70    req.h.nlmsg_type = type;
71    // SIN NLM_F_ACK: la respuesta de AUDIT_GET la manda el kernel de forma ASÍNCRONA
72    // (audit_send_reply usa un kthread), así que el ACK llegaba ANTES que el AUDIT_GET
73    // y el lector lo tomaba por "fallo". Pedir sólo REQUEST y filtrar por tipo al leer.
74    req.h.nlmsg_flags = NLM_F_REQUEST;
75    req.h.nlmsg_seq = 1;
76    req.h.nlmsg_pid = getpid();
77    if (payload) memcpy(req.data, payload, len);
78    struct sockaddr_nl sa = {0};
79    sa.nl_family = AF_NETLINK;
80    return sendto(fd, &req, req.h.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa));
81}
82
83// Lee el estado del audit por un socket dedicado (unicast). Devuelve enabled o -1.
84static int audit_status(int *enabled, int *pid) {
85    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
86    if (fd < 0) return -1;
87    if (nl_send(fd, AUDIT_GET, NULL, 0) < 0) { close(fd); return -1; }
88    struct timeval tv = {.tv_sec = 1};
89    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
90    // Drenamos hasta encontrar el AUDIT_GET: puede venir detrás de otros mensajes.
91    char buf[8192];
92    for (int i = 0; i < 8; i++) {
93        ssize_t n = recv(fd, buf, sizeof(buf), 0);
94        if (n <= 0) break;
95        for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n);
96             h = NLMSG_NEXT(h, n)) {
97            if (h->nlmsg_type == AUDIT_GET) {
98                struct audit_status *st = (struct audit_status *)NLMSG_DATA(h);
99                *enabled = st->enabled;
100                *pid = st->pid;
101                close(fd);
102                return 0;
103            }
104            if (h->nlmsg_type == NLMSG_ERROR) {
105                struct nlmsgerr *e = (struct nlmsgerr *)NLMSG_DATA(h);
106                if (e->error)
107                    fprintf(stderr, "[q1] AUDIT_GET rechazado: %s\n", strerror(-e->error));
108            }
109        }
110    }
111    close(fd);
112    return -1;
113}
114
115static int audit_enable(void) {
116    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
117    if (fd < 0) return -1;
118    struct audit_status st = {0};
119    st.mask = AUDIT_STATUS_ENABLED;
120    st.enabled = 1;
121    int r = nl_send(fd, AUDIT_SET, &st, sizeof(st));
122    close(fd);
123    return r < 0 ? -1 : 0;
124}
125
126// ---------------------------------------------------------------- la víctima
127
128// Se autoencierra: sólo /tmp legible. Todo lo demás → EACCES + registro de audit.
129static void jail(__u32 restrict_flags) {
130    struct landlock_ruleset_attr attr = {.handled_access_fs = ACCESS_FS_ROUGHLY_READ};
131    int rs = ll_create(&attr, sizeof(attr), 0);
132    if (rs < 0) { perror("landlock_create_ruleset"); exit(2); }
133
134    // La "clausura declarada": /tmp y el intérprete. Nada más.
135    const char *allowed[] = {"/tmp", "/usr/lib", "/lib", "/bin", "/usr/bin", NULL};
136    for (int i = 0; allowed[i]; i++) {
137        int dfd = open(allowed[i], O_PATH | O_CLOEXEC);
138        if (dfd < 0) continue;
139        struct landlock_path_beneath_attr pb = {.allowed_access = ACCESS_FS_ROUGHLY_READ,
140                                                .parent_fd = dfd};
141        if (ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0) < 0) perror("add_rule");
142        close(dfd);
143    }
144    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("no_new_privs"); exit(2); }
145    if (ll_restrict(rs, restrict_flags) < 0) { perror("landlock_restrict_self"); exit(2); }
146    close(rs);
147}
148
149int main(int argc, char **argv) {
150    const char *scenario = argc > 1 ? argv[1] : "new-exec";
151
152    int enabled = -1, apid = -1;
153    if (audit_status(&enabled, &apid) == 0)
154        fprintf(stderr, "[q1] audit: enabled=%d auditd_pid=%d\n", enabled, apid);
155    else
156        fprintf(stderr, "[q1] audit: no pude leer el estado (¿sin CAP_AUDIT_CONTROL?)\n");
157
158    // `!= 1` y no `== 0`: si el GET falló (enabled=-1) igual hay que intentar encenderlo.
159    // El bug de la primera corrida fue justo este: GET falló → enabled=-1 → no entró acá →
160    // audit_enabled siguió en 0 → CERO registros en los 3 escenarios → un "hallazgo" que
161    // era el instrumento roto.
162    if (enabled != 1) {
163        fprintf(stderr, "[q1] audit no habilitado (enabled=%d) → AUDIT_SET\n", enabled);
164        if (audit_enable() < 0) perror("[q1] AUDIT_SET");
165        usleep(100000);
166        if (audit_status(&enabled, &apid) == 0)
167            fprintf(stderr, "[q1] audit: enabled=%d (tras AUDIT_SET)\n", enabled);
168    }
169    // GATE: sin audit_enabled=1 el kernel no emite NADA y el test no mide lo que cree medir.
170    // Abortar es obligatorio: un 0 acá significaría "instrumento roto", no "sin denegaciones".
171    if (enabled != 1) {
172        fprintf(stderr,
173                "[q1] ABORTO: no pude confirmar audit_enabled=1. Cualquier resultado sería\n"
174                "     un falso negativo del test, no una medición. (¿root? ¿audit=1?)\n");
175        return 4;
176    }
177
178    // Sin CAP_AUDIT_READ no hay lector, pero la víctima corre igual: así una corrida
179    // sin privilegio valida al menos que la JAULA deniega (la mitad barata del test).
180    int nl = nl_open();
181    if (nl < 0)
182        fprintf(stderr, "[q1] sin lector (correr como root para la prueba completa)\n");
183    else
184        fprintf(stderr, "[q1] lector multicast OK. escenario=%s\n", scenario);
185
186    __u32 flags = 0;
187    if (!strcmp(scenario, "new-exec-logon")) flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
188
189    pid_t pid = fork();
190    if (pid == 0) {
191        usleep(200000);  // dejamos que el padre esté escuchando
192        jail(flags);
193        if (!strcmp(scenario, "same-exec")) {
194            // Denegación SIN execve: el default del kernel SÍ la loguea.
195            int fd = open("/etc/passwd", O_RDONLY);
196            fprintf(stderr, "[victima] open(/etc/passwd) = %d (%s)\n", fd, strerror(errno));
197        } else {
198            // Denegación TRAS execve: el caso REAL de harkaq (bwrap → sh -c → make).
199            fprintf(stderr, "[victima] execve(/bin/cat /etc/passwd)\n");
200            execl("/bin/cat", "cat", "/etc/passwd", (char *)NULL);
201            perror("execl");
202        }
203        _exit(0);
204    }
205
206    // El pid del hijo permite atribuir los registros DOMAIN (traen pid=) a NUESTRO dominio y
207    // no al de una corrida anterior que se libera tarde — el error que casi me como.
208    fprintf(stderr, "[q1] hijo pid=%d (atribuir los DOMAIN por este pid)\n", pid);
209    char buf[16384];
210    int landlock_records = 0, total = 0;
211    if (nl < 0) {
212        waitpid(pid, NULL, 0);
213        fprintf(stderr, "\n[q1] jaula validada; evidencia NO probada (hace falta root).\n");
214        return 3;
215    }
216    // Deadline de pared, NO "cortar al primer timeout": el registro `status=deallocated
217    // denials=N` se emite cuando el dominio se libera, DESPUÉS de que el hijo muere, y con
218    // retraso (RCU/workqueue). Con la ventana de 2s se perdía y aparecía en la corrida
219    // siguiente, haciéndolo pasar por el dominio equivocado. Escuchamos 6s pasado el exit.
220    struct timeval tv = {.tv_sec = 1};
221    setsockopt(nl, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
222    time_t deadline = time(NULL) + 6;
223    while (time(NULL) < deadline) {
224        ssize_t n = recv(nl, buf, sizeof(buf), 0);
225        if (n <= 0) continue;  // timeout de 1s: seguimos hasta el deadline
226        for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n);
227             h = NLMSG_NEXT(h, n)) {
228            total++;
229            if (h->nlmsg_type == AUDIT_LANDLOCK_ACCESS ||
230                h->nlmsg_type == AUDIT_LANDLOCK_DOMAIN) {
231                landlock_records++;
232                printf("[REGISTRO %s] %.*s\n",
233                       h->nlmsg_type == AUDIT_LANDLOCK_ACCESS ? "ACCESS" : "DOMAIN",
234                       (int)(NLMSG_PAYLOAD(h, 0)), (char *)NLMSG_DATA(h));
235            }
236        }
237    }
238    waitpid(pid, NULL, 0);
239
240    fprintf(stderr, "\n[q1] VEREDICTO escenario=%s: %d registros landlock (%d msgs audit total)\n",
241            scenario, landlock_records, total);
242    if (landlock_records == 0)
243        fprintf(stderr, "[q1] ⇒ SIN EVIDENCIA. harkaq reportaría denials=[] (falso 'hermético').\n");
244    else
245        fprintf(stderr, "[q1] ⇒ EVIDENCIA ENTREGADA. Q1 viable por esta vía.\n");
246    return 0;
247}