censar: tres nombres equivocados que llegaban al plan, y los 7 «binario desconocido» eran permisos
El censo nombraba los servicios por `comm`, que viene del kernel. De ahí salieron tres errores de
CLASIFICACIÓN — y no son cosméticos: ese nombre es el que el plan mete en `--in /etc/init.d/<n>` y el
que va de `label` en la tarjeta de arje.
· `comm` está capado a 15 caracteres: `willay-crosscheck` llegaba como `willay-crossche` y con ese
nombre no casaba contra su declaración ⇒ figuraba como `no-declarado` TENIENDO su tarjeta en la
semilla de arje. La línea de comando trae el nombre entero.
· `supervise-daemon` no es un servicio, es un ENVOLTORIO. Los cinco de gioser se fundían en una
entrada `supervise-daemo`; y del otro lado `dbus`, `metalog`, `dhcpcd`, `squid` y `shuma-daemon`
salían como `declarado-muerto` ESTANDO VIVOS — el mismo agujero que este censo existe para tapar,
entrando por la otra puerta. El nombre real es su argv[1] y el comando real es el último token
antes del `--` suelto (comprobado contra los cinco).
· `head -15` con ppid==1 era un resto de tubería reparentado, contado como servicio. Va a
`descartados`, que se imprimen: un huérfano es un hallazgo, no basura.
**Y los 7 «no se pudo leer su binario» eran dos cosas distintas.** Muchos demonios reescriben su
`argv[0]` (`sshd: /usr/bin/sshd [listener]`, `php-fpm: master process (…)`), así que la línea de
comando no dice cuál es el binario — `/proc/<pid>/exe` sí, y necesita ser dueño o root. Medido:
uid 1001 : desconocido 7 · paquete-ajeno 13 · receta-takana 6 · suelto 13
root : desconocido 0 · paquete-ajeno 20 · receta-takana 5 · suelto 14
Ahora el censo DICE cuál de las dos pasó: «repetilo con sudo» y «averigualo a mano» son trabajos muy
distintos, y confundirlos manda a alguien a investigar un permiso.
Desenvolver al supervisor arregló además dos datos que salían falsos: el `exec` de `squid` era
`/usr/bin/supervise-daemon` (⇒ figuraba provisto por el paquete `openrc`), y el `cmdline` guardado
era el del SUPERVISOR — una tarjeta hecha con eso arrancaría `supervise-daemon` dentro de arje, que
ya supervisa. Y se rescata el `--user`: `shuma-daemon` corre como `sergio`, dato que la tarjeta de
arje no puede guardar (no tiene campo de usuario), así que ahora se avisa EN LA REVISIÓN — sin eso a
la vista, un servicio que acá corre sin privilegios termina de root en el destino.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
+134
-14
@@ -94,13 +94,24 @@ def invocaciones(t, pids):
|
||||
# Una sonda que falla es un dato; no puede arrastrarse a las que funcionaron.
|
||||
cmd, _ = t.run(f"tr '\\0' ' ' < /proc/{pid}/cmdline 2>/dev/null")
|
||||
cwd, _ = t.run(f"readlink /proc/{pid}/cwd 2>/dev/null")
|
||||
cmd, cwd = cmd.strip(), cwd.strip()
|
||||
if cmd.startswith("(sonda falló"):
|
||||
cmd = ""
|
||||
if cwd.startswith("(sonda falló"):
|
||||
cwd = ""
|
||||
if cmd or cwd:
|
||||
out[nombre] = {"cmdline": cmd, "cwd": cwd}
|
||||
# ⚠ Tercera sonda, y también separada. Muchos demonios REESCRIBEN su `argv[0]` para decir en
|
||||
# qué andan (`sshd: /usr/bin/sshd [listener]`, `php-fpm: master process (…)`), así que la
|
||||
# línea de comando NO dice cuál es el binario. `/proc/<pid>/exe` sí — pero necesita permiso
|
||||
# de ptrace, o sea ser el dueño o root. Medido en gioser con uid 1001: 7 de 38 servicios
|
||||
# quedaban en «no se pudo leer su binario» por esto, y no por ser realmente desconocidos.
|
||||
exe, _ = t.run(f"readlink /proc/{pid}/exe 2>/dev/null")
|
||||
cmd, cwd, exe = cmd.strip(), cwd.strip(), exe.strip()
|
||||
for v in ("cmd", "cwd", "exe"):
|
||||
if locals()[v].startswith("(sonda falló"):
|
||||
cmd, cwd, exe = (("" if v == "cmd" else cmd), ("" if v == "cwd" else cwd),
|
||||
("" if v == "exe" else exe))
|
||||
sup = SUPERVISADOS.get(pid)
|
||||
if sup:
|
||||
cmd, exe = sup["cmdline"], sup["exec"]
|
||||
if cmd or cwd or exe:
|
||||
out[nombre] = {"cmdline": cmd, "cwd": cwd,
|
||||
"exe": exe if exe.startswith("/") else "",
|
||||
"usuario": (sup or {}).get("usuario", "")}
|
||||
return out
|
||||
|
||||
|
||||
@@ -136,14 +147,98 @@ def duenos(t, ejecutables):
|
||||
return res
|
||||
|
||||
|
||||
# Envoltorios que NO son el servicio: el nombre de verdad está en sus argumentos. Sin desenvolverlos,
|
||||
# los CINCO `supervise-daemon` de gioser se juntaban en UNA entrada llamada `supervise-daemo` — cinco
|
||||
# servicios distintos (shuma-daemon, dbus, metalog…) colapsados en una tarjeta con el nombre del
|
||||
# envoltorio. Un servicio de más es ruido; cinco servicios fundidos en uno es un plan que no arranca.
|
||||
SUPERVISORES = {"supervise-daemon", "start-stop-daemon", "s6-supervise", "runsv", "daemonize"}
|
||||
_COMM_SUP = {s[:15] for s in SUPERVISORES}
|
||||
|
||||
# pid → lo que el supervisor manda de verdad. Se llena al listar los procesos y se consulta al leer
|
||||
# `/proc`: el pid con ppid==1 es el SUPERVISOR, así que su `cmdline` y su `exe` describen al
|
||||
# envoltorio, no al servicio.
|
||||
SUPERVISADOS = {}
|
||||
|
||||
|
||||
def _mandado_por_supervisor(args):
|
||||
"""El comando REAL que hay detrás de `supervise-daemon`, y con qué usuario corre.
|
||||
|
||||
El formato es `supervise-daemon <servicio> --start [opciones] <ejecutable> -- [argumentos]`, así
|
||||
que el ejecutable es el ÚLTIMO token antes del `--` suelto. Comprobado contra los cinco de gioser
|
||||
(dbus, dhcpcd, metalog, squid, shuma-daemon).
|
||||
|
||||
⚠ Sin esto, el censo guardaba como `cmdline` la línea del SUPERVISOR — y una tarjeta de arje
|
||||
hecha con eso arrancaría `supervise-daemon` DENTRO de arje, que ya supervisa. Y el `exec` salía
|
||||
`/usr/bin/supervise-daemon`, o sea que `squid` figuraba como provisto por el paquete `openrc`.
|
||||
|
||||
⚠⚠ Y se rescata el `--user`: `shuma-daemon` corre como `sergio`. Perder ese dato al desenvolver
|
||||
sería la misma elisión que el escritor de arje reporta como SIN-TRADUCIR — un servicio que en el
|
||||
origen corría sin privilegios terminaría de root en el destino, y nadie se enteraría."""
|
||||
p = args.split()
|
||||
if "--start" not in p:
|
||||
return "", "", ""
|
||||
resto = p[p.index("--start") + 1:]
|
||||
usuario = ""
|
||||
for i, x in enumerate(resto):
|
||||
if x in ("--user", "-u") and i + 1 < len(resto):
|
||||
usuario = resto[i + 1]
|
||||
break
|
||||
corte = resto.index("--") if "--" in resto else len(resto)
|
||||
izq, der = resto[:corte], resto[corte + 1:]
|
||||
if not izq or not izq[-1].startswith("/"):
|
||||
return "", "", usuario
|
||||
return izq[-1], " ".join([izq[-1]] + der), usuario
|
||||
|
||||
|
||||
def _desenvolver(comm, args):
|
||||
"""(nombre_real, args_reales) para un proceso que puede ser un envoltorio."""
|
||||
p = args.split()
|
||||
if not p:
|
||||
return comm, args
|
||||
base = os.path.basename(p[0])
|
||||
if base not in SUPERVISORES and comm not in {s[:15] for s in SUPERVISORES}:
|
||||
return comm, args
|
||||
if base in ("supervise-daemon", "s6-supervise", "runsv") and len(p) > 1:
|
||||
# `supervise-daemon <servicio> --start …` · `runsv /etc/service/<servicio>`
|
||||
return os.path.basename(p[1].rstrip("/")), args
|
||||
for i, x in enumerate(p): # start-stop-daemon
|
||||
if x in ("--name", "-n") and i + 1 < len(p):
|
||||
return p[i + 1], args
|
||||
if x in ("--exec", "-x", "--startas", "-a") and i + 1 < len(p):
|
||||
return os.path.basename(p[i + 1]), args
|
||||
return comm, args
|
||||
|
||||
|
||||
def _destruncar(comm, args):
|
||||
"""`comm` viene del kernel y está capado a 15 caracteres: `willay-crosscheck` llega como
|
||||
`willay-crossche`. Ese nombre truncado no es cosmético — es el que el plan mete en
|
||||
`--in /etc/init.d/<nombre>` (ruta que no existe) y el que va de `label` en la tarjeta de arje.
|
||||
La línea de comando sí trae el nombre entero, así que se usa ESA cuando confirma al truncado."""
|
||||
if len(comm) < 15:
|
||||
return comm
|
||||
p = args.split()
|
||||
if p:
|
||||
base = os.path.basename(p[0])
|
||||
if base.startswith(comm):
|
||||
return base
|
||||
return comm
|
||||
|
||||
|
||||
def procesos_vivos(t):
|
||||
"""Lo que REALMENTE corre, colgado de PID 1. La verdad, no la declaración."""
|
||||
out, _ = t.run("ps -eo pid,ppid,comm 2>/dev/null | awk '$2==1 && $1!=1 {print $1\" \"$3}'")
|
||||
out, _ = t.run("ps -eo pid=,ppid=,comm=,args= 2>/dev/null | awk '$2==1 && $1!=1'")
|
||||
vivos = {}
|
||||
for l in out.splitlines():
|
||||
parts = l.split()
|
||||
if len(parts) == 2 and parts[0].isdigit():
|
||||
vivos.setdefault(parts[1], []).append(int(parts[0]))
|
||||
parts = l.split(None, 3)
|
||||
if len(parts) < 3 or not parts[0].isdigit():
|
||||
continue
|
||||
pid, comm, args = int(parts[0]), parts[2], (parts[3] if len(parts) > 3 else "")
|
||||
nombre, args = _desenvolver(comm, args)
|
||||
nombre = _destruncar(nombre, args)
|
||||
exe, linea, usuario = _mandado_por_supervisor(args) if comm[:15] in _COMM_SUP else ("", "", "")
|
||||
if exe:
|
||||
SUPERVISADOS[pid] = {"exec": exe, "cmdline": linea, "usuario": usuario}
|
||||
vivos.setdefault(nombre, []).append(pid)
|
||||
# los kernel threads no son servicios
|
||||
return {k: v for k, v in vivos.items() if not k.startswith(("kworker", "kthread", "ksoftirq",
|
||||
"migration", "rcu_", "irq/", "idle_"))}
|
||||
@@ -320,6 +415,11 @@ def fuera_de_git(t):
|
||||
NO_SERVICIOS = {
|
||||
"agetty", "getty", "login", "bash", "sh", "dash", "zsh", "ssh", "sshd-session", "su", "sudo",
|
||||
"bwrap", "ps", "awk", "sed", "tail", "sleep", "rsync", "scp", "cat", "python3", "perl",
|
||||
# Restos de tubería reparentados a PID 1 cuando su padre muere. Medido: `head -15` figuraba como
|
||||
# servicio `no-declarado` con ppid==1. No se esconden —salen en `descartados`, porque un huérfano
|
||||
# ES un hallazgo— pero no son servicios y contarlos como tales es el ruido que mata un censo.
|
||||
"head", "tr", "cut", "grep", "wc", "find", "xargs", "tee", "sort", "uniq", "env", "nohup",
|
||||
"timeout", "flock", "tar", "gzip", "zstd", "dd", "md5sum", "sha256sum", "less", "more",
|
||||
}
|
||||
|
||||
|
||||
@@ -432,6 +532,10 @@ def emitir_toml(c):
|
||||
L.append(f"cmdline = {toml_escape(s.get('cmdline',''))}")
|
||||
L.append(f"cwd = {toml_escape(s.get('cwd',''))}")
|
||||
L.append(f"exec = {toml_escape(s.get('exec',''))}")
|
||||
if s.get('usuario'):
|
||||
L.append(f"usuario = {toml_escape(s['usuario'])}")
|
||||
if s.get('exec_motivo'):
|
||||
L.append(f"exec_motivo = {toml_escape(s['exec_motivo'])}")
|
||||
L.append(f"paquete = {toml_escape(s.get('paquete',''))}")
|
||||
L.append('decision = "" # muda | muere | (vacío = sin decidir)')
|
||||
L.append("")
|
||||
@@ -521,12 +625,28 @@ def main():
|
||||
i = inv.get(e['name'], {})
|
||||
e['cmdline'] = i.get('cmdline', '')
|
||||
e['cwd'] = i.get('cwd', '')
|
||||
ejes = [ (e['cmdline'] or '').split(' ')[0] for e in svc if (e.get('cmdline') or '').startswith('/') ]
|
||||
e['exe_real'] = i.get('exe', '')
|
||||
if i.get('usuario'):
|
||||
e['usuario'] = i['usuario']
|
||||
# `/proc/<pid>/exe` MANDA sobre `argv[0]`: es el binario de verdad y no lo puede reescribir el
|
||||
# proceso. `argv[0]` queda de respaldo para cuando la sonda no tuvo permiso.
|
||||
def _exe_de(e):
|
||||
return e.get('exe_real') or (
|
||||
(e.get('cmdline') or '').split(' ')[0] if (e.get('cmdline') or '').startswith('/') else '')
|
||||
ejes = [x for x in (_exe_de(e) for e in svc) if x]
|
||||
dn = duenos(t, ejes)
|
||||
raiz = os.geteuid() == 0 if a.local else None
|
||||
for e in svc:
|
||||
exe = (e.get('cmdline') or '').split(' ')[0]
|
||||
e['exec'] = exe if exe.startswith('/') else ''
|
||||
exe = _exe_de(e)
|
||||
e['exec'] = exe
|
||||
e['paquete'] = dn.get(exe, '')
|
||||
if not exe and e.get('pids'):
|
||||
# Decir POR QUÉ no se sabe. «Averigualo a mano» y «volvé a correr el censo con sudo» son
|
||||
# dos trabajos muy distintos, y confundirlos manda a alguien a investigar un permiso.
|
||||
e['exec_motivo'] = ("el censo no corrió como root: `/proc/<pid>/exe` de un proceso de "
|
||||
"otro usuario no se puede leer. Repetilo con sudo"
|
||||
if raiz is False else
|
||||
"el proceso reescribió su argv[0] y no se pudo leer /proc/<pid>/exe")
|
||||
pts = puertos(t)
|
||||
nombres = dominios(t)
|
||||
sondear = (a.probe or a.probe_dns or bool(a.host)) and not a.no_probe
|
||||
|
||||
Reference in New Issue
Block a user