¿Es una buena idea llamar a comandos de shell desde C?

50

Hay un comando de shell de Unix ( udevadm info -q path -n /dev/ttyUSB2) al que quiero llamar desde un programa en C. Probablemente con una semana de lucha, podría volver a implementarlo yo mismo, pero no quiero hacer eso.

¿Es una buena práctica ampliamente aceptada para mí simplemente llamar popen("my_command", "r");, o eso introducirá problemas de seguridad inaceptables y remitirá problemas de compatibilidad? Me parece mal hacer algo como esto, pero no puedo señalar por qué sería malo.

johnny_boy
fuente
8
Una cosa a considerar son los ataques de inyección.
MetaFight
8
Estaba pensando principalmente en el caso en el que proporcionó la entrada del usuario como argumentos de comando de shell.
MetaFight
8
¿Alguien podría poner un udevadmejecutable malicioso en el mismo directorio que su programa y usarlo para escalar privilegios? No sé mucho sobre la seguridad de Unix, pero ¿se ejecutará su programa setuid root?
Andrew dice que reinstale a Mónica el
77
@AndrewPiliser Si el directorio actual no está en el PATH, entonces no, tendría que ejecutarse con él ./udevadm. Sin embargo, IIRC Windows ejecutaría ejecutables del mismo directorio.
Izkata
44
Siempre que sea posible, invoque el programa deseado directamente sin pasar por el shell.
CodesInChaos

Respuestas:

59

No es particularmente malo, pero hay algunas advertencias.

  1. ¿Qué tan portátil será su solución? ¿El binario elegido funcionará igual en todas partes, generará los resultados en el mismo formato, etc.? ¿Saldrá de manera diferente en configuraciones de LANGetc.?
  2. ¿Cuánta carga adicional agrega esto en su proceso? Bifurcar un archivo binario genera mucha más carga y requiere más recursos que ejecutar llamadas a la biblioteca (en general). ¿Es esto aceptable en tu escenario?
  3. ¿Hay problemas de seguridad? ¿Alguien puede sustituir su binario elegido por otro y realizar acciones nefastas a partir de entonces? ¿Utiliza args proporcionados por el usuario para su binario, y podrían proporcionar ;rm -rf /(por ejemplo) (tenga en cuenta que algunas API le permitirán especificar args de manera más segura que solo proporcionarlos en la línea de comandos)

En general, estoy contento de ejecutar archivos binarios cuando estoy en un entorno conocido que puedo predecir, cuando la salida binaria es fácil de analizar (si es necesario, es posible que solo necesite un código de salida) y no necesito hacerlo también a menudo.

Como has notado, el otro problema es ¿cuánto trabajo es replicar lo que hace el binario? ¿Utiliza una biblioteca que también puede aprovechar?

Brian Agnew
fuente
13
Forking a binary results in a lot more load and requires more resources than executing library calls (generally speaking). Si esto fuera en Windows, estaría de acuerdo. Sin embargo, el OP especifica Unix donde la bifurcación tiene un costo lo suficientemente bajo como para que sea difícil decir si cargar dinámicamente una biblioteca es peor que la bifurcación.
wallyk
1
Normalmente esperaría que una biblioteca se cargue una vez , mientras que el binario se puede ejecutar varias veces. Como siempre, depende de los escenarios de uso, pero debe tenerse en cuenta (si posteriormente se descarta)
Brian Agnew
3
No hay una "bifurcación" en Windows, por lo que la cita tiene que ser específico de UNIX.
Cody Gray
55
NT kernel admite bifurcación, aunque no está expuesto a través de Win32. De todos modos, creo que el costo de ejecutar un programa externo vendría más del execque del fork. Cargando secciones, vinculando objetos compartidos, ejecutando código de inicio para cada uno. Y luego tener que convertir todos los datos en texto para analizarlos en el otro lado, tanto para las entradas como para las salidas.
espectras
8
Para ser justos, udevadm info -q path -n /dev/ttyUSB2no va a funcionar en Windows de todos modos, por lo que toda la discusión es un poco teórica.
MSalters
37

Se debe tener mucho cuidado para evitar vulnerabilidades de inyección una vez que haya introducido un vector potencial. Ahora está en la vanguardia de su mente, pero más tarde puede necesitar la capacidad de seleccionar ttyUSB0-3, luego esa lista se usará en otros lugares, por lo que se tendrá en cuenta para seguir el principio de responsabilidad única, luego un cliente tendrá que poner un dispositivo arbitrario en la lista, y el desarrollador que realiza ese cambio no tendrá idea de que la lista eventualmente se utilizará de manera insegura.

En otras palabras, codifique como si el desarrollador más descuidado que conoce esté haciendo un cambio inseguro en una parte aparentemente no relacionada del código.

En segundo lugar, la salida de las herramientas CLI generalmente no se consideran interfaces estables a menos que la documentación las marque específicamente como tales. Puede que esté bien contar con ellos para un script que ejecute y que pueda solucionarlo usted mismo, pero no para algo que implemente en un cliente.

En tercer lugar, si desea una manera fácil de extraer un valor de su software, es probable que alguien más también lo desee. Busque una biblioteca que ya haga lo que quiere. libudevya estaba instalado en mi sistema:

#include <libudev.h>
#include <sys/stat.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
    struct stat statbuf;

    if (stat("dev/ttyUSB2", &statbuf) < 0)
        return -1;
    struct udev* udev = udev_new();
    struct udev_device *dev = udev_device_new_from_devnum(udev, 'c', statbuf.st_rdev);

    printf("%s\n", udev_device_get_devpath(dev));

    udev_device_unref(dev);
    udev_unref(udev);
    return 0;
}

Hay otra funcionalidad útil en esa biblioteca. Supongo que si necesitabas esto, algunas de las funciones relacionadas también podrían ser útiles.

Karl Bielefeldt
fuente
2
GRACIAS. Pasé tanto tiempo buscando libudev. ¡Simplemente no pude encontrarlo!
johnny_boy
2
No veo cómo las "vulnerabilidades de inyección" son un problema aquí a menos que se invoque el programa de OP cuando otro usuario tiene control sobre su entorno, que es principalmente el caso en el caso de suid (también posiblemente, pero en menor medida, cosas como comando forzado de cgi y ssh). No hay razón para que los programas normales deban ser reforzados contra el usuario con el que se ejecutan.
R ..
2
@R ..: La "vulnerabilidad de inyección" es cuando generalizas ttyUSB2y usas sprintf(buf, "udevadm info -q path -n /dev/%s", argv[1]). Ahora eso parece bastante seguro, pero ¿y si lo argv[1]es "nul || echo OOPS"?
MSalters
3
@R ..: solo necesitas más imaginación. Primero es una cadena fija, luego es un argumento proporcionado por el usuario, luego es un argumento proporcionado por algún otro programa ejecutado por el usuario pero que no entiende, luego es un argumento que su navegador ha extraído de una página web. Si las cosas siguen "cuesta abajo", eventualmente alguien tiene que asumir la responsabilidad de desinfectar la información, y Karl dice que es extremadamente cuidadoso identificar ese punto, donde sea que llegue.
Steve Jessop
66
Aunque si el principio de precaución era realmente "asumir que el desarrollador más descuidado que conoces está haciendo un cambio inseguro", y ahora tenía que escribir el código para garantizar que eso no pueda resultar en una explotación, entonces personalmente no podría salir de la cama. Los desarrolladores más descuidados que conozco hacen que Machievelli parezca que ni siquiera lo está intentando .
Steve Jessop
16

En su caso específico, donde desea invocar udevadm, sospecho que podría ingresar udevcomo una biblioteca y realizar las llamadas a funciones apropiadas como alternativa.

por ejemplo, podría echar un vistazo a lo que está haciendo udevadm cuando se invoca en modo "info": https://github.com/gentoo/eudev/blob/master/src/udev/udevadm-info.c y hacer llamadas equiv. en cuanto a esos udevadm está haciendo.

Esto evitaría muchas de las desventajas enumeradas en la excelente respuesta de Brian Agnew, por ejemplo, no depender del binario existente en un camino determinado, evitar el gasto de bifurcación, etc.

Adam Krouskop
fuente
Gracias, encontré ese repositorio exacto, y terminé buscando un poco.
johnny_boy
1
... y libudev no es particularmente difícil de usar. Falta un poco de manejo de errores, pero enumerar, consultar y monitorear las API es bastante sencillo. La lucha que tu miedo puede llegar a ser menos de 2 horas si lo pruebas.
espectras
7

Su pregunta parecía requerir una respuesta de bosque, y las respuestas aquí parecen respuestas de árbol, así que pensé en darle una respuesta de bosque.

Raramente es así como se escriben los programas en C. Siempre es cómo se escriben los scripts de shell y, a veces, cómo se escriben los programas Python, perl o Ruby.

Las personas suelen escribir en C para facilitar el uso de las bibliotecas del sistema y el acceso directo de bajo nivel a las llamadas al sistema operativo, así como para la velocidad. Y C es un lenguaje difícil de escribir, por lo que si las personas no necesitan esas cosas, entonces no usan C. También se espera que los programas C solo dependan de bibliotecas compartidas y archivos de configuración.

Desplazarse a un subproceso no es particularmente rápido, y no requiere un acceso controlado y detallado a las instalaciones del sistema de bajo nivel, e introduce una dependencia posiblemente sorprendente en un ejecutable externo, por lo que es poco común ver en los programas C.

Hay algunas preocupaciones adicionales. Las preocupaciones de seguridad y portabilidad que mencionan las personas son completamente válidas. Por supuesto, son igualmente válidos para los scripts de shell, pero la gente espera ese tipo de problemas en los scripts de shell. Pero, por lo general, no se espera que los programas C tengan este tipo de preocupación de seguridad, lo que lo hace más peligroso.

Pero, en mi opinión, las mayores preocupaciones tienen que ver con la forma en popenque interactuará con el resto de su programa. popentiene que crear un proceso hijo, leer su salida y recopilar su estado de salida. Mientras tanto, el proceso 'stderr estará conectado al mismo stderr que su programa, lo que puede causar resultados confusos, y su stdin será el mismo que su programa, lo que podría causar otros problemas interesantes. Puede resolver eso al incluir </dev/null 2>/dev/nullen la cadena a la que pasa, popenya que es interpretada por el shell.

Y popencrea un proceso hijo. Si hace algo con el manejo de la señal o los procesos de bifurcación, puede terminar recibiendo SIGCHLDseñales extrañas . Sus llamadas a waitpueden interactuar de manera extraña popeny posiblemente crear condiciones de carrera extrañas.

Las preocupaciones de seguridad y portabilidad están ahí, por supuesto. Como son para scripts de shell o cualquier cosa que inicie otros ejecutables en el sistema. Y debe tener cuidado de que las personas que usan su programa no puedan obtener metacaracteres de shell en la cadena que ingresa popenporque esa cadena se le da directamente shcon sh -c <string from popen as a single argument>.

Pero no creo que sea por eso que es extraño ver un programa en C usando popen. La razón por la que es extraño es porque C es típicamente un lenguaje de bajo nivel y popenno es de bajo nivel. Y debido al uso de popenrestricciones de diseño de lugares en su programa porque interactuará de manera extraña con la entrada y salida estándar de su programa y hará que sea difícil hacer su propia gestión de procesos o manejo de señales. Y debido a que normalmente no se espera que los programas C tengan dependencias de ejecutables externos.

De todo género
fuente
0

Su programa puede estar sujeto a piratería, etc. Una forma de protegerse de este tipo de actividad es crear una copia del entorno actual de su máquina y ejecutar su programa utilizando un sistema llamado chroot.

Desde el punto de vista de su programa, se está ejecutando en un entorno normal, desde un aspecto de seguridad, si alguien interrumpe su programa, solo tiene acceso a los elementos que proporcionó cuando realizó la copia.

Tal configuración se llama chroot jail. Para más detalles, vea chroot jail .

Normalmente se usa para configurar servidores de acceso público, etc.

Dave
fuente
3
El OP está escribiendo un programa para que otras personas lo ejecuten, no jugando con binarios o fuentes no confiables en su propio sistema.
Peter Cordes