Ir al contenido principal

Desarrollo de controladores de dispositivo (device drivers) en Linux

Continúo la serie de artículos sobre programación del kernel Linux con un artículo sobre el desarrollo de controladores de dispositivos o device drivers. El controlador de ejemplo que voy a mostraros se carga en memoria como un módulo del kernel, así que conviene leer los anteriores posts sobre programación de módulos del kernel Linux y cómo usar /proc desde un módulo del kernel Linux. Para compilar el módulo me he basado en el entorno para desarrollo para kernel Linux que publiqué en un anterior artículo.
Voy a comenzar presentando un ejemplo sencillo que seguidamente pasaré a analizar.



El código de ejemplo hay que situarlo en el directorio ~kerneldev/src/modulo3 y lo llamaremos ramdisk.c.
Este controlador, llamado ramdisk, en realidad no va a ser más que un pequeño buffer donde podremos almacenar hasta 255 bytes. Una especie de mini disco ram, que a pesar de ser muy simple, nos permitirá echar un primer vistazo al funcionamiento interno de los controladores.

Sin más preámbulos, vamos directos al código.

#include <asm/unistd.h>
#include <asm/uaccess.h>
#include <linux/module.h>
#include <linux/proc_fs.h>

#define DRIVER_MAJOR 231
#define DRIVER_NAME "ramdisk"

MODULE_LICENSE ("GPL");
MODULE_DESCRIPTION ("Ram Disk");
MODULE_AUTHOR ("Alberto Garcia");

char buffer[255];

int do_open (struct inode *inode, struct file *filp)
{
  return 0;
}

ssize_t do_read (struct file * filp, char *buf, 
                 size_t count, loff_t * f_pos)
{
  loff_t real_read;
  int k;

  /* El número de bytes a leer ha de ser >= 0 */
  if (count < 0)
    return -EINVAL;


  /* Calcula el número real de caracteres a leer */
  /* para evitar sobrepasar el tamaño del buffer */
  if ((*f_pos + count) < sizeof (buffer))
    real_read = count;
  else
    real_read = sizeof (buffer) - *f_pos;

  /* Se copia buffer al espacio de usuario */
  k = copy_to_user (buf, buffer + *f_pos, real_read);

  /* Actualización del puntero */
  *f_pos += real_read;

  /* Retornamos número de caracteres leídos */
  return real_read;
}

ssize_t do_write (struct file * filp, const char *buf, 
                  size_t count, loff_t * f_pos)
{
  /* comprobamos el número de bytes a escribir */
  /* para evitar desbordamientos del buffer    */
  if (*f_pos + count > sizeof(buffer)) 
    return -EINVAL;

  /* se copian los bytes desde el espacio */
  /* de usuario al buffer                 */
  copy_from_user(buffer + *f_pos, buf, count);

  return 1;
}

/* llamada del sistema close() */
int do_release (struct inode *inode, struct file *filp)
{
  return 0;
}

static long do_ioctl (struct file *filp, u_int cmd, 
                      u_long arg)
{
  return 0;
}

static loff_t do_llseek (struct file *file, 
                         loff_t offset, int orig)
{
  loff_t ret;

  switch (orig)
  {
    case SEEK_SET:
      ret = offset;
      break;
    case SEEK_CUR:
      ret = file->f_pos + offset;
      break;
    case SEEK_END:
      ret = sizeof (buffer) - offset;
      break;
    default:
      ret = -EINVAL;
  }

  if (ret >= 0)
    file->f_pos = ret;
  else
    ret = -EINVAL;

  return ret;
}

struct file_operations ramdisk_op = {
  open:do_open,
  read:do_read,
  write:do_write,
  release:do_release,         
  unlocked_ioctl:do_ioctl,
  llseek:do_llseek
};

static int __init ramdisk_init (void)
{
  int result;

  result = register_chrdev (DRIVER_MAJOR, DRIVER_NAME, &ramdisk_op);
  if (result < 0)
  {
    printk ("No se pudo registrar el módulo\n");
    return result;
  }

  printk (KERN_INFO "Modulo RamDisk instalado\n");
  return (0);
}

static void __exit ramdisk_cleanup (void)
{
  unregister_chrdev (DRIVER_MAJOR, DRIVER_NAME);

  printk (KERN_INFO "Hasta otra\n");
}

module_init (ramdisk_init);
module_exit (ramdisk_cleanup);
Puedes usar el siguiente Makefile para compilar el módulo:
ifneq ($(KERNELRELEASE),)
 obj-m := ramdisk.o 
else
 KERNEL_VERSION = linux-3.5.0
 KERNELDIR := ../$(KERNEL_VERSION)
 PWD := $(shell pwd)
modules:
 $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

clean:
 rm -rf *.[oas] .*.flags *.ko .*.cmd .*.d \
 .*.tmp *.mod.c .tmp_versions Module.symvers
Una vez compilado, para probar el ejemplo, iniciamos la máquina virtual Qemu que hemos usado en los anteriores artículos y copiamos el kernel desde allí.
scp alberto@10.0.2.2:kerneldev/src/modulo3/ramdisk.ko .
Para poder usar un controlador de dispositivo primero hay que crearlo en el directorio /dev de linux con la instrucción mknod. En nuestro caso, vamos a asignar como major number del dispositivo el 231, y será de tipo carácter, así que se crea el descriptor con el siguiente comando:
mknod /dev/ramdisk c 231 0
El módulo se carga en memoria de la forma habitual con insmod en la máquina virtual.
insmod ramdisk.ko
Podemos usar el dispositivo como cualquier otro dispositivo de caracteres y escribir en él con echo o leer con cat. Por supuesto, también podemos usar las llamadas al sistema open(), close(), read(), write(), etc. (por ejemplo a través de libc).
Tras comprobar que todo está en orden y funcionando analicemos el código del ejemplo. No volveré a explicar la parte relacionada con la carga y descarga del módulo, que es similar a los ejemplos que ya presenté en los anteriores artículos. Lo primero que hay que hacer es registrar el dispositivo para que el kernel tenga conocimiento de él y de cuáles son las funciones a las que hay que invocar para realizar operaciones (de lectura, escritura, etc.) sobre él. Esto se hace en la siguiente línea:
result = register_chrdev (DRIVER_MAJOR, DRIVER_NAME, &ramdisk_op);
El primer parámetro es el número mayor asociado al dispositivo (en este caso 231), seguido del nombre del driver. El tercer parámetro es un puntero a una estructura de datos de tipo file_operations, que hemos definido de la siguiente manera:
struct file_operations ramdisk_op = {
  open:do_open,
  read:do_read,
  write:do_write,
  release:do_release,         
  unlocked_ioctl:do_ioctl,
  llseek:do_llseek
};
Como se puede observar, esta estructura le indica al kernel qué funciones van a gestionar las llamadas del kernel open, read, write, close, ioctl y lseek. Por ejemplo, cuando se realice una llamada open() sobre el dispositivo, el kernel ejecutará la función do_open(). En este caso, la función do_open() no hace nada, ya que no estamos accediendo a un dispositivo real. En otro caso sí tendríamos que realizar algunas operaciones, como comprobar si otro proceso ya tiene abierto el dispositivo (en caso de que sea de acceso exclusivo), etc. La estructura file_operations completa tiene la siguiente definición (aunque, como en nuestro caso, no todos los campos de la estructura ha de rellenarse):
struct file_operations {
  struct module *owner;
  loff_t (*llseek) (struct file *, loff_t, int);
  ssize_t (*read) (struct file *, char *, size_t, loff_t *);
  ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
  int (*readdir) (struct file *, void *, filldir_t);
  unsigned int (*poll) (struct file *, struct poll_table_struct *);
  int (*ioctl) (struct inode *, struct file *, unsigned int, 
  unsigned long);
  int (*mmap) (struct file *, struct vm_area_struct *);
  int (*open) (struct inode *, struct file *);
  int (*flush) (struct file *);
  int (*release) (struct inode *, struct file *);
  int (*fsync) (struct file *, struct dentry *, int datasync);
  int (*fasync) (int, struct file *, int);
  int (*lock) (struct file *, int, struct file_lock *);
  ssize_t (*readv) (struct file *, const struct iovec *, unsigned long,
  loff_t *);
  ssize_t (*writev) (struct file *, const struct iovec *, unsigned long,
  loff_t *);
};
En este código se han implementado las funciones do_read(), do_write() y do_llseek(). No entraré en demasiados detalles ya que estas funciones están bien comentadas sobre el propio código y son bastante autoexplicativas. Sólo comentaré los parámetros de la función do_read() y do_write() que pueden ser un poco confusos.
ssize_t do_read (struct file * filp, char *buf, 
                 size_t count, loff_t * f_pos)
*filp es un puntero a una estructura de datos que mantiene información sobre el fichero (por ahora no entraré en más detalle). El puntero *buf apunta al inicio del buffer donde debemos escribir los bytes a devolver para que sean recuperados desde el espacio de usuario (usaremos copy_to_user() para poner ahí los bytes leídos). count es el número de bytes solicitados y *f_pos es un puntero al primer carácter que debe leerse (offset), y además debemos actualizarlo según vayamos transfiriendo bytes al espacio de usuario.
ssize_t do_write (struct file * filp, const char *buf, 
                  size_t count, loff_t * f_pos)
Vemos que en el caso de do_write() los parámetros son los mismos. Las diferencias son que ahora *buf contiene los bytes que han de ser escritos al dispositivo y *f_pos apunta a la posición del fichero donde debemos empezar a escribir. En este caso usamos la función copy_from_user() para transferir datos del espacio de usuario hacia el espacio del kernel.

Huelga decir que para codificar estas funciones hay que tener un buen conocimiento del hardware y acceso a su documentación técnica para poder hacer una correcta implementación. Si en el futuro el tiempo (no el climatológico) me lo permite, os presentaré un ejemplo más realista en el que trataremos de llevar a cabo un driver para una pieza de hardware concreta. Hasta entonces os invito a "jugar" con este ejemplo, a mejorarlo y a ampliarlo.

Comentarios

Entradas populares de este blog

Creando firmas de virus para ClamAV

ClamAv es un antivirus opensource y multiplataforma creado por Tomasz Kojm muy utilizado en los servidores de correo Linux. Este antivirus es desarrollado por la comunidad, y su utilidad práctica depende de que su base de datos de firmas sea lo suficientemente grande y actualizado. Para ello es necesario que voluntarios contribuyan activamente aportando firmas. El presente artículo pretende describir de manera sencilla cómo crear firmas de virus para ClamAV y contribuir con ellas a la comunidad.

Manejo de grafos con NetworkX en Python

El aprendizaje computacional es un área de investigación que en los últimos años ha tenido un auge importante, sobre todo gracias al aprendizaje profundo (Deep Learning). Pero no todo son redes neuronales. Paralelamente a estas técnicas, más bien basadas en el aprendizaje de patrones, también hay un auge de otras técnicas, digamos, más basadas en el aprendizaje simbólico. Si echamos la vista algunos años atrás, podemos considerar que quizá, la promesa de la web semántica como gran base de conocimiento ha fracasado, pero no es tan así. Ha ido transmutándose y evolucionando hacia bases de conocimiento basadas en ontologías a partir de las cuales es posible obtener nuevo conocimiento. Es lo que llamamos razonamiento automático y empresas como Google ya lo utilizan para ofrecerte información adicional sobre tus búsquedas. Ellos lo llaman Grafos de Conocimiento o Knowledge Graphs . Gracias a estos grafos de conocimiento, Google puede ofrecerte información adicional sobre tu búsqueda, ad

Scripts en NMAP

Cuando pensamos en NMAP, pensamos en el escaneo de puertos de un host objetivo al que estamos relizando una prueba de intrusión, pero gracias a las posibilidades que nos ofrecen su Scripting Engine , NMAP es mucho más que eso. Antes de continuar, un aviso: algunas de posibilidades que nos ofrecen los scripts de NMAP son bastante intrusivas, por lo que recomiendo hacerlas contra hosts propios, máquinas virtuales como las de Metasploitable, o contrato de pentesting mediante. Para este artículo voy a usar las máquinas de Metasploitable3 . No voy a entrar en los detalles sobre el uso básico de NMAP, ya que hay miles de tutoriales en Internet que hablan sobre ello. Lo cierto es que NMAP tiene algunas opciones que permiten obtener información extra, además de qué puertos están abiertos y cuales no. Por ejemplo, la opción -sV trata de obtener el servicio concreto, e incluso la versión del servicio que está corriendo en cada puerto. Otro ejemplo es la opción -O, que intenta averiguar el