Meta-programación (I): "Reflexiones sobre confiar en la confianza"

Hace unas semanas recibí un email de un investigador del CERT en México preguntándome sobre cómo Ken Thompson (padre de C y de UNIX, ahí es nada 😉 ) comentó en un discurso tras la entrega de los premios de la ACM cómo pudo haber troyanizado todo sistema existente derivado de UNIX, como comenté en mi artículo sobre Virus en GNU/Linux. Mucha otra gente lo ha preguntado y siempre los he remitido al documento original, pero me voy a permitir la licencia de hacer una traducción informal para quienes no se defiendan bien con la lengua de Shakespeare… (las correcciones a la traducción son MUY bienvenidas ;-).

 

Reflexiones sobre confiar en la confianza

por Ken Thompson

Reimpreso de «Communication of the ACM», Vol. 27, No. 8, August 1984, pp. 761-763. Copyright © 1984, Association for Computing Machinery, Inc. También aparece en «ACM Turing Award Lectures: The First Twenty Years 1965-1985», Copyright © 1987 por ACM press y «Computers Under Attack: Intruders, Worms, and Viruses», Copyright © 1990 por ACM press.

Esto es una copia convertida a digital derivada de un trabajo de la ACM bajo copyright. No está garantizado que sea una copia exacta del trabajo original del autor.

Traducción por Pablo Garaizar Sagarminaga (2006), sin ningún tipo de restricción de copia (salvo las ya existentes en el original).

Introducción

Doy gracias a la ACM por esta concesión. No puedo dejar de sentir que estoy recibiendo este honor por la sincronización y la casualidad tanto como por el mérito técnico. UNIX consiguió renombre con un cambio a nivel industrial, de mainframes centrales a los minicomputadores autónomos. Sospecho que Daniel Bobrow (1) estaría aquí en vez de mí si él no hubiera podido producir un PDP-10 y no tuviera que “colocar” un anuncio de un PDP-11. Por otra parte, el estado actual de UNIX es el resultado del trabajo de una gran cantidad de gente.

Hay un viejo adagio, “baila con la que te trajo,” que significa que debo hablar de UNIX. No he trabajado en el UNIX común en muchos años, sin embargo continúo consiguiendo crédito no merecido por el trabajo de otros. Por lo tanto, no voy a hablar de UNIX, sino que deseo dar las gracias a cada uno de los que ha contribuido.

Esto me lleva hasta Dennis Ritchie. Nuestra colaboración ha sido preciosa. En los diez años que hemos trabajado juntos, solamente puedo recordar un caso de descoordinación en el trabajo. En esa ocasión, descubrí que ambos habíamos escrito el mismo programa de 20 lineas en ensamblador. Comparé el código fuente y me asombré al encontrar que coincidían carácter a carácter. El resultado de nuestro trabajo conjunto ha sido mucho mayor que el trabajo que cada uno de nosotros contribuyó.

Soy programador. En mi formulario 1040, eso es lo que pongo como mi ocupación. Como programador, escribo programas. Quisiera presentarles el programa más lindo que escribí nunca. Haré esto en tres etapas e intentaré juntarlo al final.

Etapa I

En la universidad, antes de los videojuegos, nos divertíamos planteando ejercicios de programación. Uno de los favoritos era escribir el programa de auto-reproducción más corto. Dado que este era un ejercicio totalmente alejado de la realidad, el lenguaje de programación generalmente era FORTRAN. Realmente, FORTRAN era el lenguaje por el que optábamos por la misma razón que las carreras de tres piernas son populares.

Explicado de forma más precisa, el problema es escribir un programa que, cuando es compilado y ejecutado, produzca como salida una copia exacta de su código fuente. Si nunca has hecho esto, te animo a intentarlo por ti mismo. El descubrimiento de cómo hacerlo es una revelación que sobrepasa de lejos cualquier ventaja obtenida de haber escuchado cómo se hace. La parte sobre «el más corto» era solamente un incentivo para demostrar habilidad y para determinar a un ganador.

char s[] = {
	'\t',
	'0',
	'\n',
	'}',
	';',
	'\n',
	'\n',
	'/',
	'*',
       (213 lines deleted),
 	0
};

/*
 * This string s is a
 * representation of the body
 * of this program from '0'
 * to the end.
 */

main() {

  int i;

  printf("char \ts[] = {\n");
        for(i=0;s[i];i++)
                printf("\r%d,\n",s[i]);
        printf("%s",s);

}

CUADRO 1

El cuadro I muestra un programa que se auto-reproduce en el lenguaje de programación C (el purista observará que el programa no es exactamente un programa que se auto-reproduce, pero producirá un programa que se auto-reproduce). Este ejemplo es demasiado grande como para ganar un premio, pero demuestra la técnica y tiene dos características importantes que necesito para terminar mi historia:

  1. Este programa se puede escribir fácilmente por otro programa.
  2. Este programa puede contener una cantidad arbitraria de exceso de equipaje que será reproducida junto con el algoritmo principal. En el ejemplo, incluso se reproduce el comentario.

Etapa II

El compilador de C se escribe en C. Lo que estoy a punto de describir es uno de los muchos problemas del «huevo y la gallina» que se presentan cuando los compiladores se escriben en su propio lenguaje de programación. En este caso, utilizaré el ejemplo específico del compilador de C.

C permite construir una matriz (array) de caracteres inicializada indicando una cadena de caracteres (string). Los caracteres individuales en la cadena se pueden escapar para representar caracteres no imprimibles. Por ejemplo,

   "Hola mundo \ n"

representa una secuencia con el carácter «\n», representando el carácter nueva línea.

...
c = next();
if( c != '\\' )
c = next();
if( c == '\\' )
  return ('\\');
if( c == 'n' )
  return ('\n');
...

CUADRO 2

El cuadro 2 es una idealización del código en el compilador de C que interpreta las secuencias de escape de caracteres. Éste es un fragmento asombroso de código. «Sabe» de una manera totalmente portable qué código de carácter se compila para una nueva línea en cualquier juego de caracteres. El acto de saber esto le permite recompilarse a sí mismo, perpetuando así el conocimiento.

...
c = next();
if( c != '\\' )
c = next();
if( c == '\\' )
  return ('\\');
if( c == 'n' )
  return ('\n');
if( c == 'v' )
  return ('\v');
...

CUADRO 3

Supongamos que deseamos modificar el compilador de C para incluir la secuencia «\v» para representar el carácter de tabulación vertical. La extensión al cuadro 2 es obvia y se presenta en el cuadro 3. Recompilamos el compilador de C, pero conseguimos un aviso. Dado que la versión binaria del compilador no sabe nada sobre «\v», el código fuente no es obviamente C legal. Debemos “entrenar” al compilador. Después de que “sepa” qué significa «\v», nuestro nuevo cambio se convertirá en C legal. Miramos en una tabla ASCII que un tabulador vertical es el decimal 11. Modificamos nuestro código fuente para convertirlo en el cuadro 4. Ahora el viejo compilador acepta el nuevo código fuente. Instalamos el binario resultante como el nuevo compilador de C oficial y ahora podemos escribir la versión portable de la manera en la que la teníamos en el cuadro 3.

...
c = next();
if( c != '\\' )
c = next();
if( c == '\\' )
  return ('\\');
if( c == 'n' )
  return ('\n');
if( c == 'v' )
  return (11);
...

CUADRO 4

Este es un concepto profundo. Es lo más cercano a un programa “que aprende” que he visto. Simplemente se lo dices una vez y puedes utilizar esta definición auto-referente.

Etapa III

compile(s)
char s;
{
  ...
}

CUADRO 5

Una vez más, en el compilador de C, el cuadro 5 representa el control de alto nivel del compilador de C donde la rutina “compile” se llama para compilar la siguiente línea de código fuente. El cuadro 6 muestra una pequeña modificación para que el compilador deliberadamente compile erroneamente siempre que se encuentre un patrón particular. Si esto no fuera deliberado, esto sería un fallo en el compilador. Dado que es deliberado, debe llamarse “Caballo de Troya”.

compile(s)
char s;
{
    if(match(s,"pattern") {
        compile("bug");
        return;
    }
    ...
}

CUADRO 6

El fallo real que implanté en el compilador buscaría el patrón en el comando de «login» de UNIX. El código de reemplazo compilaría erroneamente el comando «login» de modo que aceptara una contraseña cifrada prevista o una contraseña conocida concreta. Así, si este código fue instalado en binario y fueron utilizados binarios para compilar el comando de «login», podría registrarme en ese sistema como cualquier usuario.

Tal código, evidentemente, no pasaría desapercibido por mucho tiempo. Incluso una lectura ocasional más atenta del código fuente del compilador de C levantaría suspicacias.

compile(s)
char s;
{
    if(match(s,"pattern1") {
        compile("bug1");
        return;
    }
    if(match(s,"pattern2") {
        compile("bug2");
        return;
    }
    ...
}

CUADRO 7

El paso final se representa en el cuadro 7. Esto simplemente agrega un segundo Caballo de Troya a ya existente. El segundo patrón está dirigido al compilador de C. El código de reemplazo es un programa que se auto-reproduce, de la etapa I, que inserta ambos Caballos de Troya en el compilador. Esto requiere una fase en la que aprenda, como en el ejemplo de la etapa II. Primero compilamos el código fuente modificado con el compilador de C normal para producir un binario infectado. Instalamos este binario como el compilador oficial de C. Ahora podemos quitar el código troyano del código fuente del compilador y el nuevo binario reinsertará este código troyano siempre que compile. Por supuesto, el comando de «login» seguirá infectado sin rastro en su ninguna parte de su código fuente.

Moral

La moraleja es obvia. No puedes confiar en el código que no creaste totalmente por ti mismo (especialmente código de las empresas que contratan a gente como yo). Ninguna cantidad de verificación o de escrutinio a nivel de código fuente te protegerá contra usar código no confiable. Para demostrar la posibilidad de esta clase de ataque, escogí el compilador de C. Habría podido escoger en cualquier programa de manejo de programas tales como un ensamblador, un cargador, o incluso microcódigo de hardware. Conforme el nivel del programa es más bajo, estos código troyanos son más y más difíciles de detectar. Un troyano bien instalado en microcódigo será casi imposible de detectar.

Después de intentar convencerte de que no puedes confiar en mí, deseo moralizar. Quisiera criticar el manejo que hace la prensa de los «hackers», de la banda 414, de la banda de Dalton, del etc. Los actos realizados por estos chicos son vandalismo en el mejor de los casos y probablemente allanamiento y hurto en el peor. Solamente la insuficiencia del código penal ahorra a estos hackers una pena muy grave. Las empresas que son vulnerables a estas actividades (y la mayoría de las empresas grandes son muy vulnerables) están presionando fuertemente para poner al día el código penal. El acceso desautorizado a los sistemas informáticos es ya un crimen grave en algunos estados y se está tratando actualmente en muchas más legislaturas de estado así como en el congreso.

Hay una situación explosiva en ebullición. Por una parte, la prensa, la televisión, y las películas convierten en héroes a vándalos llamándolos «whiz kids». Por otra parte, los actos realizados por estos chicos pronto serán castigados con varios años en prisión.

He visto testificar a estos chicos antes del congreso. Está claro que son totalmente inconscientes de la seriedad de sus actos. Hay obviamente una brecha cultural. El acto de entrar en un sistema informático tiene que tener el mismo estigma social que entrar en la casa de un vecino. No debe importar que la puerta del vecino esté abierta. La prensa debe aprender que el uso incorrecto de una computadora no es más asombroso que conducir bebido un automóvil.

Reconocimiento

Primero leí de la posibilidad de tal Caballo de Troya en una crítica de las Fuerza Aéreas (4) sobre la seguridad de una puesta en práctica temprana de Multics. No puedo encontrar una referencia más específica a este documento. Apreciaría si alguien pudiera conseguirme esta referencia.

Referencias

  1. Bobrow, D.G., Burchfiel, J.D., Murphy, D.L., and Tomlinson, R.S. TENEX, a paged time-sharing system for the PDP-IO. Commun. ACM 15, 3 (Mar. 1972), 135-143.
  2. Kernighan, B.W., and Ritchie, D.M. The C Programming Language. Prentice-Hall, Englewood Cliffs, N.J., 1978.
  3. Ritchie, D.M., and Thompson, K. The UNIX time-sharing system. Commun. ACM 17, 7(July 1974), 365-375.
  4. Unknown Air Force Document.

8 pensamientos en “Meta-programación (I): "Reflexiones sobre confiar en la confianza"

  1. john

    Estimado, soy de Argentina, me gusta mucho tu blog, hay pocos en español que posean esta calidad. quisiera preguntare que libro me recomiendas para programacion en C, ya tengo conocimientos de C, pero quiesiera algo completo, desde lo mas basico hasta lo mas avanzado, nose si sera bueno conseguir el libro de los creadores de C. Tu que dices? muchas gracias y saludos.

    Responder
  2. txipi

    @john: la verdad es que no soy un gran experto en programación, así que no soy la persona idónea para aconsejarte sobre un libro de C. Yo aprendí con un libro de Anaya ("El libro de C") no especialmente bueno, y completé un poco mi formación con los libros de apuntes de la facultad de Informática y con un libro de Prentice Hall sobre programación en C para UNIX. Supongo que el libro de los propios creadores del lenguaje, si es una edición más o menos reciente, será bastante bueno. Si finalmente lo decides conseguir, comenta qué tal te fue 😉

    Responder
  3. axi

    He encontrado esto por casualidad, y como mola.

    John, yo te recomiendo libros para aprender a programar de verdad, de la experiencia de gente que sabe, y no limitarte a aprender las funciones admitidas en el estándar ANSI C o en POSIX, cómo funciona un puntero y como usar la función fopen. Al final programar, es mitad arte y mitad ciencia, cuando leas un buen libro sentirás un cosquilleo porque no se limitará a ser un manual, te dará ideas, conceptos que sean elegantes y útiles. Normalmente los "Real Programmers" han estado muy unidos a UNIX, ejemplo de lo que te comentaba antes pueden ser estas frases.

    – Simplicity, Clarity, Generality.
    – Keep It Sinple Stupid.

    Entre los libros malos, hay muchos, a mi personalmente no me gusta el libro de Kernighan y Ritchie, no está bien organizado, no relaciona sus partes, quizás ahora no te des cuenta, pero cuando compres varios, o cuando hayas alcanzado el nivel de decir, sé usar los punteros, sé manejar la memoria, y sabes leerte la ayuda, necesitarás algo más, algo que te ayude a decidir entre si usar un define o una enumeración, entender los problemas de portabilidad o decidir entre utilizar una tabla hash y un árbol splay, consejos buenísimos son el leerte el código de los demás y preguntarte porqué es asi, está claro que leyendo código de gente reconocida.
    Te recomiendo "The practice of programming" de Brian Kernighan y Pike, y "The art of UNIX programming" de Eric S. Raymond.
    Geniales ambos.

    Un Saludo,

    Responder
  4. Vr0s4nKH

    Te felicito, son pocas las personas que tratan de decir las cosas como son, yo estoy volviendo al mundo de la programación y me gustó leer tu blog, tiene una gran calidad, y esto me ayuda bastante a profesar la teoría de la conspiración jejeje, saludos. Por cierto, siempre hay algo nuevo que aprender…

    Responder
  5. Pingback: ¿Quines o Virus?

  6. LuLu

    Bueno, no hace falta saber mucho C para ver
    que ese código, sean cuales sean las 213 líneas que
    faltan no se auto-reproduce en el sentido de que
    tras compilarlo y ejecutarlo ofrezca una salida que
    sea igual a su código «fuente».

    Responder
  7. txipi

    @LuLu: como dice en el texto… «el purista observará que el programa no es exactamente un programa que se auto-reproduce, pero producirá un programa que se auto-reproduce».

    Responder

Responder a txipi Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *