3. Búsqueda de patrones

Bitácoras - Uso del sistema

Es muy importante para un administrador de sistemas analizar los datos generados por las bitácoras de su sistema. De ellas podemos, entre otras cosas, sacar posibles errores que estemos cometiendo con nuestra configuración del sistema o encontrar posibles intentos de intrusión. Desafortunadamente, leer bitácoras puede ser tedioso.
Presento aquí un script que analiza la salida de last buscando cuántas veces se ha conectado un usuario y por cuánto tiempo en el último mes.

Consideraciones de seguridad

Las bitácoras del sistema típicamente seran legibles únicamente para root. En este caso decidí utilizar un programa que funciona en el espacio de usuario y nos presenta la bitácora binaria wtmp para evitar hacer otra demostración que requiera correr como superusuario.

Funcionamiento general

El programa itera sobre cada línea resultante de correr el programa indicado en $lastProg, ignorando las líneas que no van asociadas a un login verdadero. Utilizando el hash %usedTime registra en una sencilla estructura cuántas veces entró un usuario, y por cuánto tiempo lo hizo. Por último, nos lo reporta utilizando un formato de printf.

Funcionamiento: Sumando minutos y horas

En la función toMinutes primero que nada verificamos que el formato de la hora sea el correcto: Dos dígitos representando la hora y otros dos representando los minutos, separados por dos puntos. Nos es más fácil guardar un valor fácil de sumar, como lo son los minutos, por lo que multiplicamos las horas por 60 y sumamos el resultado a los minutos. Sumamos esto al valor ya existente en $usedTime{$user}[0].

Funcionamiento: Separando horas de minutos por el camino fácil

Al principio del programa tenemos la directiva use integer;. Esta indica al compilador que use aritmética entera, en vez de trabajar con puntos flotantes, el comportamiento normal de Perl. Esto, además de darnos un poco de velocidad al hacer operaciones, nos permite dar menos rodeos en la función printResults. Para separar horas y minutos basta con que dividamos los minutos entre 60 - El entero resultante son las horas, el residuo los minutos, y no tendremos que pelearnos con fracciones decimales.

  1. #!/usr/bin/perl -w
  2. use integer;
  3. use strict;
  4. my (%usedTime, $lastProg);
  5. $lastProg = '/usr/bin/last';
  6. foreach my $line (`$lastProg`) {
  7. next if ($line =~ /^(reboot|ftp|wtmp begins)/ || $line =~ /^\s*$/);
  8. $line =~ s/\s+$//g;
  9. my $user = substr($line,0,10);
  10. $user =~ s/\s.+//g;
  11. my $time = &toMinutes(substr($line,length($line)-6,5));
  12. $usedTime{$user} = [0,0] unless defined($usedTime{$user});
  13. $usedTime{$user}[0] += $time;
  14. $usedTime{$user}[1] += 1;
  15. }
  16. &printResults(\%usedTime);
  17. exit 0;
  18. sub toMinutes {
  19. my ($in,$hr,$min);
  20. $in = shift;
  21. return 0 unless ($in =~ /^\d\d\:\d\d$/);
  22. $hr = substr($in,0,2);
  23. $min = substr($in,3,2);
  24. return ($hr*60+$min);
  25. }
  26. sub printResults {
  27. my ($totMin,$hr,$min,$numLogins);
  28. print "Login Time used Logins\n";
  29. print "============================\n";
  30. foreach my $user (sort(keys(%usedTime))) {
  31. $totMin=$usedTime{$user}[0];
  32. $numLogins=$usedTime{$user}[1];
  33. $hr=$totMin / 60;
  34. $min=$totMin % 60;
  35. printf("%-10s %02s:%02s %-2s\n",$user,$hr,$min,$numLogins);
  36. }
  37. }

Bitácoras - Errores del servidor HTTP

Muchos programas analizan, de alguna u otra manera, el contenido de las bitácoras del sistema (como ejemplos, Logcheck (tutorial en español disponible aquí) y Swatch). Hay programas también dedicados a monitorear y reaccionar en tiempo real a los errores producidos por Apache, como el Apache Guardian.
Sin embargo, un programa que recorra un archivo de bitácora de Apache y reporte, de una manera limpia y fácil de entender los errores ocurridos tenía que ser ideado a mano por los administradores. Es eso lo que intento aquí cubrir.

Consideraciones de seguridad

Este script debe correr con un usuario que tenga derecho de ver el archivo de bitácoras de Apache, típicamente localizado en /var/www/logs/error_log/var/log/httpd/error_log. Para evitar que este script requiera privilegios de root para correr, sugiero que dicho archivo pertenezca a un grupo de administración (daemon o wheel, por ejemplo), permitiendo a dicho grupo acceso únicamente de lectura, de la siguiente manera:
-rw-r-----  1 root  daemon  2188152 Dec  7 10:03 /var/www/logs/error_log

Funcionamiento general

El script procesa una tras otra todas las líneas del archivo indicado en la variable $file. Reporta un resúmen de lo que encuentre en dicho archivo con el siguiente formato:

  1. Directory index forbidden by rule
  2. 1
  3. 1 /var/www/htdocs/comun/
  4. 7 /var/www/htdocs/data/Icons/
  5. 2 /var/www/htdocs/images/
  6. File does not exist
  7. 1
  8. 5 /var/www/htdocs/biblio/
  9. 2 /var/www/htdocs/CURSO
  10. 1 /var/www/htdocs/adm_list/approve.cgi
  11. (...)

Funcionamiento: Organizando los datos

El formato de las bitácoras de error de Apache es el siguiente:
[<i>fecha</i>] [<i>categoría</i>] [client <i>w.x.y.z</i>] <i>tipo de error</i>: <i>archivo que lo ocasionó</i>
por ejemplo,
[Thu Dec  7 10:03:27 2000] [error] [client 192.168.2.45] File does not exist: /var/www/htdocs/esta/pagina/no/existe
Lo dividimos, entonces, utilizando el caracter ] como separador de campo. Descartamos todas las líneas que no lleven error como categoría, y de las líneas que nos interesen, el último campo lo dividimos en tipo de error y archivo que lo ocasiona utilizando el
caracter :.
En el hash %errores guardamos los datos resultantes, utilizando como llave el tipo de error, y agregando el archivo que lo ocasionó a la lista guardada por referencia relacionada a esa llave.

Funcionamiento: Reportando

Al reportar, recorremos los tipos de error y, para cada uno de ellos, recorremos el arreglo, guardando ahora en el hash %sumado como llave el nombre de cada uno de los causantes de error, y sumándole uno al valor de esta llave cada que lo encontremos. Una vez procesado el arreglo completo, reportamos primero el tipo de error, y después (indentando por claridad) cada uno de los archivos que lo ocasionaron, con el número de veces que fue llamado.

  1. #!/usr/bin/perl -Tw
  2. use integer;
  3. use strict;
  4. my (%errores, $file);
  5. $file = '/var/www/logs/error_log';
  6. open(IN,&quot;&lt;$file&quot;) or die &quot;Could not open $file - $@ $!&quot;;
  7. foreach my $line (&lt;IN&gt;) {
  8. chomp($line);
  9. my (@datos,@detalles);
  10. @datos = split(/\]/,$line);
  11. next if ($datos[1] ne ' [error');
  12. @detalles = split(/: /,$datos[3]);
  13. $errores{$detalles[0]} = [] if (not defined $errores{$detalles[0]});
  14. my ($tipoErr,$archErr) = @detalles;
  15. push(@{$errores{$tipoErr}},$archErr);
  16. }
  17. &amp;printResults(\%errores);
  18. sub printResults {
  19. my ($llave,%hash,%sumado);
  20. %hash = %{$_[0]};
  21. foreach $llave (sort(keys(%hash))) {
  22. print &quot;$llave\n&quot;;
  23. while (@{$hash{$llave}}) {
  24. my ($tmp);
  25. $tmp=shift(@{$hash{$llave}});
  26. $sumado{$tmp}++;
  27. }
  28. foreach my $file (sort(keys(%sumado))) {
  29. printf (&quot; %3d %25s\n&quot;,$sumado{$file},$file);
  30. }
  31. }
  32. }

Formato

Hay varios archivos fundamentales para la operación de nuestro sistema que requieren seguir un formato específico - probablemente, el mejor ejemplo de esto sea el archivo de información de los usuarios, /etc/passwd, y los archivos íntimamente relacionados con él, /etc/groups y /etc/shells. Además de esto, no es difícil para un atacante esconder su presencia utilizando trucos muy sencillos, algunos de los cuales encontraremos con este programa.

Consideraciones de seguridad

No me cansaré de repetirlo: Si no hay una razón que nos obligue a correr como root determinado programa, no tenemos por qué hacerlo
Este script todavía puede ser muy mejorado - No porque este programa no nos reporte una situación riesgosa en estos archivos significa que están seguros. Podríamos agregar verificaciones para cuentas 0 que no pertenecieran única y exclusivamente a root, cuentas con login root que no tuvieran UID 0, cuentas de usuario con UID 65536 (equivalente a root), nombres de usuario pertenecientes a cuentas del sistema (kmem, mail, adm, wheel, etc.) y otras muchas posibles situaciones.

Funcionamiento general

Primero que nada, creamos dos arreglos, uno con el contenido relevante de /etc/group y otro con el de /etc/shells. Al hacer esto, aprovechamos para verificar su correcto formato - En /etc/group revisamos que tenga el número correcto de campos y que no haya ni nombres de grupo ni GIDs repetidos. Después de esto, revisamos que cada uno de los shells listados en /etc/shells exista y sea ejecutable.
Utilizando las mismas estrategias, finalmente abrimos y analizamos línea por línea /etc/passwd. Revisamos número de campos, usernames/UIDs duplicados, directorio home existente y shell listado en /etc/shells.

  1. #!/usr/bin/perl -Tw
  2.  
  3. use strict;
  4.  
  5. my ($grpFileName,$shellFileName,$pwdFileName,%groups,%shells,@pwdData);
  6.  
  7. $grpFileName = '/etc/group';
  8. $shellFileName = '/etc/shells';
  9. $pwdFileName = '/etc/passwd';
  10.  
  11. # First, load all the needed groups into memory. Check, by the way, for
  12. # repeated groups and incorrect line length.
  13. %groups = &getGroups($grpFileName);
  14. %shells = &getShells($shellFileName);
  15. &checkPasswd($pwdFileName,\%groups,\%shells);
  16.  
  17. sub getGroups {
  18. my ($filename,@groups,%groupids,%groupnames);
  19. $filename = shift;
  20. open(IN,"<$filename") or die "Could not open $filename";
  21. while (my $line = &lt;IN&gt;) {
  22. # We do not chomp($line), because even an empty members field
  23. # is valid, and if we chopped it, we would get false positives.
  24. next if $line =~ /^\s*#/;
  25. my @data = split(/:/,$line);
  26. print "WARNING: Line in $filename with incorrect number of fields (should have four):\n $line\n" if ($#data != 3);
  27. print "WARNING: Duplicate GID: $data[2] refers to $data[0] and $groupids{$data[2]}\n" if (defined $groupids{$data[2]});
  28. $groupids{$data[2]} = $data[0];
  29. print "WARNING: Duplicate group name: $data[0] refers to $data[2] and $groupnames{$data[0]}\n" if (defined $groupnames{$data[0]});
  30. $groupnames{$data[0]} = $data[2];
  31. }
  32. return(%groupids);
  33. }
  34.  
  35. sub getShells {
  36. my ($filename,%shells);
  37. $filename = shift;
  38. open(IN,"<$filename") or die "Could not open $filename";
  39. while (my $line = &lt;IN&gt;) {
  40. next if ($line =~ /^\s*#/);
  41. chomp($line);
  42. print "WARNING: Shell $line does not exist\n" unless (-e $line);
  43. print "WARNING: Shell $line appears twice\n" if (defined $shells{$line} );
  44. $shells{$line} = 1;
  45. }
  46. return(%shells);
  47. }
  48.  
  49. sub checkPasswd {
  50. my (%userid,%username,$filename,%groups,%shells);
  51. $filename = $_[0];
  52. %groups = %{$_[1]};
  53. %shells = %{$_[2]};
  54. open(IN,"<$filename");
  55. while (my $line = &lt;IN&gt;) {
  56. chomp($line);
  57. next if ($line =~/^\s*#/);
  58. my @data = split(/:/,$line);
  59. print "WARNING: Line in $filename with incorrect number of fields (should have 7):\n $line\n" if ($#data != 6);
  60. print "WARNING: Duplicate UID: $data[2] refers to $data[0] and $userid{$data[2]}\n" if (defined $userid{$data[2]});
  61. $userid{$data[2]} = $data[0];
  62. print "WARNING: Dulpicate username: $data[0] refers to $data[2] and $username{$data[0]}\n"if (defined $username{$data[0]});
  63. $username{$data[0]} = $data[2];
  64.  
  65. print "WARNING: Home directory non-existant for user $data[0] ($data[5])\n" unless (-e $data[5]);
  66. print "WARNING: Shell non-existant for user $data[0] ($data[6])\n" unless (-e $data[6]);
  67. }
  68. }