1 - 介绍
2 - linux的keyboard驱动是如何工作的
3 - 基于内核的键盘纪录的原理
3.1 - 中断句柄
3.2 - 函数劫持
3.2.1 - 劫持handle_scancode
3.2.2 - 劫持put_queue
3.2.3 - 劫持receive_buf
3.2.4 - 劫持tty_read
3.2.5 - 劫持syserials_read/syserials_write
4 - vlogger
4.1 - 工作原理
4.2 - 功能及特点
4.3 - 如何使用
5 - 感谢
6 - 参考资料
7 - Keylogger源代码
--[ 1 - 介绍
本文分成两个部分。第一部分给出了linux键盘驱动的工作原理,并且讨论了建立一个基于
内核的键盘纪录器的方法。这部分内容对那些想写一个基于内核的键盘纪录器,或者写一个
自己键盘驱动的朋友会有帮助。
第二部分详细描述了vlogger的每个细节,vlogger是一个强大的基于内核的linux键盘纪录器,
以及如何来使用它。这向技术可以运用在蜜罐系统中,也可以做成一些很有意思的hacker game,
主要用来分析和采集hacker的攻击手法。我们都知道,一些大家熟知的键盘纪录器,如iob,
uberkey,unixkeylogger等,它们是基于用户层的。这里介绍的是基于内核层的键盘纪录器。
最早期的基于内核的键盘纪录器是linspy,它发表在phrack杂志第50期。而现代的kkeylogger(
后面我们将用kkeylogger来表示基于内核的键盘纪录器)广泛采用的手法是中断syserials_read或者
syserials_write系统调用来对用户的击键进行记录。
显然,这种方法是很不稳定的并且会明显的降低系统的速度,因为我们中断的恰恰是系统使用最
频繁的两个系统调用syserials_read,syserials_write;syserials_read在每个进程需要读写设备的时候都会用到。
在vlogger里,我用了一个更好的方法,就是劫持tty buffer进程函数,下面会介绍到。
我假定读者熟悉linux的可加载模块的原理和运作过程,如果不熟悉,推荐大家首先阅读我以前写
过的linux kernel simple hacking,或者linux tty hijack,(在http://e4gle.org有下载),
参阅《linux驱动程序设计》来获得相关的理论基础知识。
--[ 2 - linux键盘驱动的工作原理
首先让我们通过以下的结构图来了解一下用户从终端的击键是如何工作的:
_____________ _________ _________
/ \ put_queue| |receive_buf| |tty_read
/handle_scancode\--------|tty_queue|----------|tty_ldisc|-------
\ / | | |buffer |
\_____________/ |_________| |_________|
_________ ____________
| |syserials_read| |
---|/dev/ttyX|-------|user process|
| | | |
|_________| |____________|
Figure 1
首先,当你输入一个键盘值的时候,键盘将会发送相应的scancodes给键盘驱动。一个独立的
击键可以产生一个六个scancodes的队列。
键盘驱动中的handle_scancode()函数解析scancodes流并通过kdb_translate()函数里的
转换表(translation-table)将击键事件和键的释放事件(key release events)转换成连
续的keycode。
比如,'a'的keycode是30。击键’a'的时候便会产生keycode 30。释放a键的时候会产生
keycode 158(128+30)。
然后,这些keycode通过对keymap的查询被转换成相应key符号。这步是一个相当
复杂的过程。
以上操作之后,获得的字符被送入raw tty队列--tty_flip_buffer。
receive_buf()函数周期性的从tty_flip_buffer中获得字符,然后把这些字符送入
tty read队列。
当用户进程需要得到用户的输入的时候,它会在进程的标准输入(stdin)调用read()函数。
syserials_read()函数调用定义在相应的tty设备(如/dev/tty0)的file_operations结构
中指向tty_read的read()函数来读取字符并且返回给用户进程。
/*e4gle add
file_operations是文件操作结构,定义了文件操作行为的成员,结构如下,很容易理解:
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 *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
};
我们直到unix系统中设备也是文件,所以tty设备我们也可以进行文件操作。
*/
键盘驱动器可以有如下4种模式:
- scancode(RAW模式):应用程序取得输入的scancode。这种模式通常
用于应用程序实现自己的键盘驱动器,比如X11程序。
- keycode(MEDIUMRAW模式):应用程序取得key的击键和释放行为(通过
keycode来鉴别这两种行为)信息。
- ASCII(XLATE模式):应用程序取得keymap定义的字符,该字符是
8位编码的。
- Unicode(UNICODE模式):此模式唯一和ASCII模式不同之处就是UNICODE模式
允许用户将自己的10进制值编写成UTF8的unicode字符,如十进制的数可以编写成
Ascii_0到Ascii_9,或者用户16进制的值可以用Hex_0到Hex_9来代表。一个keymap
可以产生出一系列UTF8的序列。
以上这些驱动器的工作模式决定了应用程序所取得的键盘输入的数据类型。大家如果需要详细了解scancode,
keycode和keymaps的相关信息,参看read[3]。
--[ 3 - 基于内核的键盘纪录器的实现步骤
我们论述两种实现方法,一个是书写我们自己的键盘中断句柄,另一个是劫持输入进程函数.
----[ 3.1 - 中断句柄
要纪录击键信息,我们就要利用我们自己的键盘中断。在Intel体系下,控制键盘的IRQ值是1。
当接受到一个键盘中断时,我们的键盘中断器会读取scancode和键盘的状态。读写键盘事件
都是通过0x60端口(键盘数据注册器)和0x64(键盘状态注册器)来实现的。
/* 以下代码都是intel格式 */
#define KEYBOARD_IRQ 1
#define KBD_STATUS_REG 0x64
#define KBD_CNTL_REG 0x64
#define KBD_DATA_REG 0x60
#define kbd_read_input() inb(KBD_DATA_REG)
#define kbd_read_status() inb(KBD_STATUS_REG)
#define kbd_write_output(val) outb(val, KBD_DATA_REG)
#define kbd_write_command(val) outb(val, KBD_CNTL_REG)
/* 注册我们的IRQ句柄*/
request_irq(KEYBOARD_IRQ, my_keyboard_irq_handler, 0, "my keyboard", NULL);
在my_keyboard_irq_handler()函数中定义如下:
scancode = kbd_read_input();
key_status = kbd_read_status();
log_scancode(scancode);
这种方法不方便跨平台操作。而且很容易crash系统,所以必须小心操作你的终端句柄。
----[ 3.2 - 函数劫持
在第一种思路的基础上,我们还可以通过劫持handle_scancode(),put_queue(),receive_buf(),
tty_read()或者syserials_read()等函数来实现我们自己的键盘纪录器。注意,我们不能劫持
tty_insert_flip_char()函数,因为它是一个内联函数。
------[ 3.2.1 - handle_scancode函数
它是键盘驱动程序中的一个入口函数(有兴趣可以看内核代码keynoard.c)。
# /usr/src/linux/drives/char/keyboard.c
void handle_scancode(unsigned char scancode, int down);
我们可以这样,通过替换原始的handle_scancode()函数来实现纪录所有的scancode。这就我们
在lkm后门中劫持系统调用是一个道理,保存原来的,把新的注册进去,实现我们要的功能,再调用
回原来的,就这么简单。就是一个内核函数劫持技术。
/* below is a code snippet written by Plasmoid */
static struct semaphore hserials_sem, log_sem;
stati