Android 驅動程式Demo及流程
很久前就想了解驅動程式的想法,這裡現做一個簡單的開始,從demo做起,看到Android驅動程式的基本執行流程,這對漏洞分析、檢測和挖掘都是必要的。同樣,本篇基本也是自己學習過程的記錄,無干貨。本篇大多數內容來自Linux裝置驅動之Ioctl控制。
一、使用者層
不管是漏洞檢測,還是poc中,我們見到最多的函式就是ioctl()函式,這個函式就是使用者層呼叫核心程式的介面。
/*
fd:檔案描述符
cmd:控制命令
...:可選引數:插入*argp,具體內容依賴於cmd
*/
int ioctl(int fd,unsigned long cmd,...);
函式第一個引數檔案控制代碼,可以通過open()獲得,第二個引數是指令的值,和驅動程式裡的switch()裡的case值是對應的,第三個是可選引數,通常是一個指標,指向某個變數或者結構體。函式執行成功,返回0,出錯返回-1。
下面來看一個使用者層demo程式(程式碼取自Linux裝置驅動之Ioctl控制,如有冒犯請聯絡刪除),和後面驅動程式是對應的,首先定義標頭檔案。
#ifndef _MEMDEV_H_
#define _MEMDEV_H_
#include <linux/ioctl.h>
#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 0 /*預設的mem的主裝置號*/
#endif
#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2 /*裝置數*/
#endif
/*mem裝置描述結構體*/
struct mem_dev
{
char *data;
unsigned long size;
};
/* 定義幻數 */
#define MEMDEV_IOC_MAGIC 'k'
/* 定義命令 */
#define MEMDEV_IOCPRINT _IO(MEMDEV_IOC_MAGIC, 1)
#define MEMDEV_IOCGETDATA _IOR(MEMDEV_IOC_MAGIC, 2, int)
#define MEMDEV_IOCSETDATA _IOW(MEMDEV_IOC_MAGIC, 3, int)
#define MEMDEV_IOC_MAXNR 3
#endif /* _MEMDEV_H_ */
#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif
使用者層程式碼:
#include <stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include "memdev.h" /* 包含命令定義 */
int main()
{
int fd = 0;
int cmd;
int arg = 0;
char Buf[4096];
/*開啟裝置檔案*/
fd = open("/dev/memdev0",O_RDWR);
if (fd < 0)
{
printf("Open Dev Mem0 Error!\n");
return -1;
}
/* 呼叫命令MEMDEV_IOCPRINT */
printf("<--- Call MEMDEV_IOCPRINT --->\n");
cmd = MEMDEV_IOCPRINT;
if (ioctl(fd, cmd, &arg) < 0)
{
printf("Call cmd MEMDEV_IOCPRINT fail\n");
return -1;
}
/* 呼叫命令MEMDEV_IOCSETDATA */
printf("<--- Call MEMDEV_IOCSETDATA --->\n");
cmd = MEMDEV_IOCSETDATA;
arg = 2007;
if (ioctl(fd, cmd, &arg) < 0)
{
printf("Call cmd MEMDEV_IOCSETDATA fail\n");
return -1;
}
/* 呼叫命令MEMDEV_IOCGETDATA */
printf("<--- Call MEMDEV_IOCGETDATA --->\n");
cmd = MEMDEV_IOCGETDATA;
if (ioctl(fd, cmd, &arg) < 0)
{
printf("Call cmd MEMDEV_IOCGETDATA fail\n");
return -1;
}
printf("<--- In User Space MEMDEV_IOCGETDATA Get Data is %d --->\n\n",arg);
close(fd);
return 0;
}
可以看到,程式呼叫了3次ioctl()函式,分別傳遞了3個cmd值,所以,我們後面可以看到,在驅動程式對應的ioctl函式裡應該至少有這3個值對應的case。
據此,可以看到在使用者層呼叫ioctl()函式十分簡單,大致流程其實就是引數構造過程,fd引數構造很簡單,直接獲取open()對應的裝置檔案即可獲得控制代碼。剩下第二個第三個引數的構造,實際上比較麻煩。首先,我學習這個不是為了開發,而是和漏洞相關的工作。那麼,這裡就有幾個問題:
1) 我們需要呼叫的驅動功能可能沒有原始碼,那麼如何構造原始碼引數2?
2)即使存在原始碼,我們如果要做批量的fuzz,如何比較通用的構造引數2?
引數3是個指標,可能指向一個結構體,那麼引數3構造同樣存在上面的兩個問題,而且更加不好解決。即使是寫漏洞檢測程式碼或者漏洞poc,找到這些結構體依賴也是一件體力活。
二、驅動層
在使用者層呼叫了ioctl函式之後,核心裡面對應的ioctl函式會被呼叫,我們暫時不去關心這中間的呼叫鏈。我們把重點放在核心層的ioctl函式以及驅動程式的執行流程上。
首先,驅動程式不想我們常見的c語言函式,不是以main()作為函式的入口的。取而代之,在驅動程式中兩個特殊的函式:
module_init(initFunc);
module_exit(exitFunc);
module_init定義驅動被載入時的行為,在此函式應該完成一些初始化操作,通過“insmod 模組檔名”命令安裝時,次函式會被呼叫。module_exit定義驅動被解除安裝時的行為,次函式函式中完成一些“善後工作”,通過“rmmod 模組檔名”來解除安裝模組時,函式會被呼叫。所以一個最簡單的helloworld類似這樣:
int text_init(void){
printk("<0>Hello World!");
return 0;
}
void text_cleanup(void){
printk("<0>Goodbye World!");
}
module_init(text_init); //註冊載入時執行的函式
module_exit(text_cleanup); //註冊解除安裝時執行的函式
顯然,這樣的helloworld定義行為太少,還有很多工作沒做,比如使用者層如何呼叫此驅動。前面我們知道,我們呼叫驅動程式是通過裝置檔案的形式的,接下來先介紹一些字元裝置驅動程式的知識,linux裝置驅動第三篇:如何寫一個簡單的字元裝置驅動?這篇文章內容不錯。
1. 主裝置號與此裝置號
主裝置號表示具體的驅動程式,次裝置號由核心使用,表示具體的裝置檔案。例如,虛擬控制檯和串列埠終端有驅動程式4管理,而不同的終端分別有不同的次裝置號。
1.1 裝置號資料結構
核心中,dev_t用於描述裝置標號,為32位的int型別,前12位表示主裝置號,後20位表示此裝置號。dev_t和主裝置號、次裝置號可以通過linux中一些巨集方面的轉換。
//獲取主裝置號
MAJOR(dev_t dev);
//獲取次裝置號
MINOR(dev_t dev);
//轉換為dev_t
MKDEV(int major, int minor);
1.2 分配和釋放裝置號
int register_chrdev_region(dev_t first, unsigned int count, const char name);
first是要分配的裝置編號範圍的起始值。count是連續裝置的編號的個數。name是和該裝置編號範圍關聯的裝置名稱,他將出現在/proc/devices和sysfs中。此函式成功返回0,失敗返回負的錯誤碼。
此函式是在已知主裝置號的情況下使用,在未知主裝置號的情況下,我們使用下面的函式:
int alloc_chrdev_region(dev_t dev, unsigned int firstminor, unsigned int count, const char *name);
dev用於輸出申請到的裝置編號,firstminor要使用的第一個次裝置編號。
在不使用時需要釋放這些裝置編號,已提供其他裝置程式使用:
void unregister_chrdev_region(dev_t dev, unsigned int count);
此函式多在模組的清除函式中呼叫。
以上完成裝置編號註冊。
2. 重要資料結構
2.1 檔案操作file_operations
file_operations資料結構定義在 <linux/fs.h>, 是一個函式指標的集合,裝置所能提供的功能大部分都由此結構提供。這個資料結構裡有大量指標,對應到檔案的操作,部分實現即可。
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.open = mem_open,
.release = mem_release,
.ioctl = memdev_ioctl,
};
2.2 檔案結構file
file資料介面定義於<linux/fs.h>,其中幾個重要的結構體成員如下:
struct file_operations *f_op:就是上面剛剛介紹的檔案操作的集合結構。
mode_t f_mode:檔案模式確定檔案是可讀的或者是可寫的(或者都是), 通過位 FMODE_READ 和 FMODE_WRITE.
loff_t f_pos:當前讀寫位置. loff_t 在所有平臺都是 64 位。驅動可以讀這個值, 如果它需要知道檔案中的當前位置, 但是正常地不應該改變它。
unsigned int f_flags:這些是檔案標誌, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驅動應當檢查 O_NONBLOCK 標誌來看是否是請求非阻塞操作。
void *private_data:open 系統呼叫設定這個指標為 NULL, 在為驅動呼叫 open 方法之前. 你可自由使用這個成員或者忽略它; 你可以使用這個成員來指向分配的資料, 但是接著你必須記住在核心銷燬檔案結構之前, 在 release 方法中釋放那個記憶體. private_data 是一個有用的資源, 在系統呼叫間保留狀態資訊, 我們大部分例子模組都使用它。
2.3 node結構
inode 結構由核心在內部用來表示檔案. 因此, 它和代表開啟檔案描述符的檔案結構是不同的。可能有代表單個檔案的多個開啟描述符的許多檔案結構, 但是它們都指向一個單個 inode 結構。
3. 字元裝置的註冊
核心中使用結構體 struct cdev來描述字元裝置。
有 2 種方法來分配和初始化一個這些結構. 如果你想在執行時獲得一個獨立的 cdev 結構, 你可以為此使用這樣的程式碼:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
更常使用的方法來分配和初始化:
void cdev_init(struct cdev cdev, struct file_operations fops);
一旦 cdev 結構建立, 最後的步驟是把它告訴核心:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count)
這裡, dev 是 cdev 結構, num 是這個裝置響應的第一個裝置號, count 是應當關聯到裝置的裝置號的數目. 常常 count 是 1。
從系統去除一個字元裝置, 呼叫:
void cdev_del(struct cdev *dev);
具備了基本的知識後,再看驅動程式碼就簡單多了:
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include "memdev.h"
static int mem_major = MEMDEV_MAJOR;//=0
module_param(mem_major, int, S_IRUGO);
struct mem_dev *mem_devp; /*裝置結構體指標*/
struct cdev cdev;
/*struct mem_dev
{
char *data;
unsigned long size;
};
*/
/*檔案開啟函式*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
/*獲取次裝置號*/
int num = MINOR(inode->i_rdev);
if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];
/*將裝置描述結構指標賦值給檔案私有資料指標*/
filp->private_data = dev;
return 0;
}
/*檔案釋放函式*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*IO操作*/
int memdev_ioctl(struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg)
{
int err = 0;
int ret = 0;
int ioarg = 0;
/* 檢測命令的有效性 */
if (_IOC_TYPE(cmd) != MEMDEV_IOC_MAGIC)
return -EINVAL;
if (_IOC_NR(cmd) > MEMDEV_IOC_MAXNR)
return -EINVAL;
/* 根據命令型別,檢測引數空間是否可以訪問 */
if (_IOC_DIR(cmd) & _IOC_READ)
err = !access_ok(VERIFY_WRITE, (void *)arg, _IOC_SIZE(cmd));
else if (_IOC_DIR(cmd) & _IOC_WRITE)
err = !access_ok(VERIFY_READ, (void *)arg, _IOC_SIZE(cmd));
if (err)
return -EFAULT;
/* 根據命令,執行相應的操作 */
switch(cmd) {
/* 列印當前裝置資訊 */
case MEMDEV_IOCPRINT:
printk("<--- CMD MEMDEV_IOCPRINT Done--->\n\n");
break;
/* 獲取引數 */
case MEMDEV_IOCGETDATA:
ioarg = 1101;
ret = __put_user(ioarg, (int *)arg);
break;
/* 設定引數 */
case MEMDEV_IOCSETDATA:
ret = __get_user(ioarg, (int *)arg);
printk("<--- In Kernel MEMDEV_IOCSETDATA ioarg = %d --->\n\n",ioarg);
break;
default:
return -EINVAL;
}
return ret;
}
/*檔案操作結構體*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.open = mem_open,
.release = mem_release,
.ioctl = memdev_ioctl,
};
/*裝置驅動模組載入函式*/
static int memdev_init(void)
{
int result;
int i;
dev_t devno = MKDEV(mem_major, 0);
/* 靜態申請裝置號*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /* 動態分配裝置號 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno);
}
if (result < 0)
return result;
/*初始化cdev結構*/
cdev_init(&cdev, &mem_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &mem_fops;
/* 註冊字元裝置 */
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
/* 為裝置描述結構分配記憶體*/
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!mem_devp) /*申請失敗*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev));
/*為裝置分配記憶體*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
}
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}
/*模組解除安裝函式*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*登出裝置*/
kfree(mem_devp); /*釋放裝置結構體記憶體*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*釋放裝置號*/
}
MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);