what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New


Posted Jan 26, 2002
Authored by mercenary | Site phreedom.org

Kernel Based Keystroke Loggers for Linux - This paper describes the basic concepts and techniques used for recording keystroke activity under linux. Includes proof of concept LKM which is stealthy, works with recent distributions, and is capable of logging local logins and ssh sessions to and from the host. Tested on Slackware v8.0 with kernel v2.4.5.

tags | kernel, local, proof of concept
systems | linux, unix, slackware
SHA-256 | 09fc0bff73308b65d6613b51aaf2ab2c2e5caf5e344479dd7bcbd5138e4efdec


Change Mirror Download
   Kernel Based Keylogger                                          by mercenary

There is a wide variety of keyloggers for Windows and only a few crippled ones for
Linux. This paper describes some basic concepts, used techniqes and hits. I've
also included proof of concept LKM code which was tested "in the wild". A must
read for every pen tester, system administrator and honeypot freak :)
no comments | post a comment

|=-----------------------=[ Kernel Based Keylogger ]=-----------------------=|
|=---------------------=[ mercenary <bm63p@yahoo.com>]=---------------------=|

----[ Contents

-[1] Introduction

-[2] Crash course in malicious LKM

-[3] Cracking the Nut

-[4] Some details

(1) "Special" characters handling

(2) BackSpace and Ctrl+U issues

(3) Timestamps

-[5] Known bugs, TODO

-[6] Links

-[7] Proof of concept code

----[1] Introduction

Recently, while performing a penetration test for a corporate client, there
was point where I had root access to an external Linux server.One of the system
administrators used to log locally there, and then he ssh'ed to some servers
located on the internal network (which was my goal) of the corporation. I was
trying to get his password (no it wasn't the same as that on the external Linux
box). My first thought was to use a trojaned ssh client (Solars' article), but
there was a tripwire installed and i didn't want to trigger any alarms. Almost
at the same time there was a discussion on Honeypots mail list[6.1] about Linux
keyloggers and their use with honeypots. Three programs were discussed in that
thread, but none ot them seemed to satisfy my needs - stealth little proggie,
working with recent Linux distros[*], capable of loging local logins and ssh
sessions to and from the host. Anyway I decided to write my own tool.
Ultimately the best tools are those written by yourself.
In the course of time I've changed the original concept and besides logging
local logins and ssh ones I've added support for telnet, timestamps, some
"special" characters handling[4.1], UID of logged user and the exact terminal
she writes to. Since we work at kernel level nothing can be hidden from us.

*Note: This LKM is tested _only_ with Slackware 8.0 (2.4.5). Some slight
modifications of timestamp routine is _needed_ so it can work with other
Linux distros. Check 4.3. for details.

----[2] Crash course in malicious LKM

When programming in userspace we use libc functions like open(), mkdir(),
read(), write() etc... In kernelspace there are corresponding functions -
sys_open(), sys_mkdir(), sys_read(), sys_write() and so (you can check
/usr/include/bits/syscall.h for complete list of all). These system calls are
implemented in kernel. Whenever someone uses mkdir() in userspace, mkdir()
"calls" to relevant system call, in this case it's sys_mkdir(). So this is like
a transition between userspace and kernelspace. Then the kernel process the
arguments of the sys_read(), makes some decision and returns the result to
userland. It's simple. More information about how the system calls works,
you can find here - [6.2].
When someone types this in console:

root@merc_lab:~/lkm# mkdir bahur

the mkdir shell command uses libc function mkdir() to create a directory.
The command must know where sys_mkdir() is located in memory. Structure
sys_call_table[] holds pointers to all system calls:

.____________. .__________. ._______. .______________. .________.
| | | | |s c t | | | | |
| user types:| | function:| |y a a | | system call: | | file |
| |--->| |--->|s l b |->| |--->| |
| mkdir bahur| | mkdir() | | l l | | sys_mkdir() | | system |
| | | | | e | | | | |
|____________| |__________| |_______| |______________| |________|

Absolutely all kernel based rootkits use the same technique: in structure
sys_call_table[] the pointer to the legitimate system call (sys_mkdir() in our
case) is changed. Now it points to our own system call (hacked_mkdir()). This
change is possible, because sys_call_table[] is exported by kernel and
read/write access is granted. Then our own system call makes some decision, it
can use the original system call (we saved his pointer) and returns a result to
userland. It acts just like the original system call. User hardly can tell if
there is a fake call or not:

.____________. .__________. ._______. .______________. .________.
| | | | |s c t | | | | |
| user types:| | function:| |y a a | | system call: | | file |
| |--->| |--->|s l b |->| |--->| |
| mkdir bahur| | mkdir() | | l l | |hacked_mkdir()| | system |
| | | | | e | | | | |
|____________| |__________| |_______| |______________| |________|
| ^
| |
| |
| original |
| system call: |
| sys_mkdir() |
| |

I'll demonstrate the idea with a simple example:

**** (C) 2001 by mercenary bm63p@yahoo.com

#define MODULE
#define __KERNEL__

#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/string.h>
#include <sys/syscall.h>

extern void *sys_call_table[]; // export the structure

int (*original_mkdir)(const char *pathname, mode_t mode); // pointer to the
// original
// sys_mkdir()

int hacked_mkdir(const char *pathname, mode_t mode) { // "our" sys_mkdir()

if ( strcmp(pathname, "bahur") == 0 )
return -ENOMEM;

return original_mkdir (pathname, mode); // Let the original
// handle it.

int init_module () {

original_mkdir = sys_call_table[ SYS_mkdir ]; // we save the pointer
// to sys_mkdir()

sys_call_table[ SYS_mkdir ] = hacked_mkdir; // now sys_mkdir()
// points to our call
return 0;

void cleanup_module () {

sys_call_table[ SYS_mkdir ] = original_mkdir; // restore the original
// pointer

When the module is loaded, every time someone tryes to create a directory,
it refers to hacked_mkdir() instead of sys_mkdir(). *pathname is a pointer to
the name of the directory which we want to create:

root@merc_lab:~/lkm# insmod mkdir.o
root@merc_lab:~/lkm# mkdir bahur
mkdir: cannot create directory `bahur': Cannot allocate memory

If the name is 'bahur' the we get back error (it can be any error message -
look at man 2 mkdir). If it's not 'bahur' then the module refers to original
As you can see from this example, we can read (and change) the arguments to
any system call.

You can check this link for more information about malicios LKMs [6.3].

----[3] Cracking The Nut

Every time when a key is being pressed in active shell locally or remotely
(through ssh, telnet, rsh etc...), it uses read() to get the keystroke:

root@merc_lab:~/lkm# strace sh


fstat64(0, 0xbffff8ec) = 0
_llseek(0, 0, 0xbffff8b4, SEEK_CUR) = -1 ESPIPE (Illegal seek)
brk(0x80d0000) = 0x80d0000
rt_sigprocmask(SIG_BLOCK, NULL, [RT_0], 8) = 0
brk(0x80d1000) = 0x80d1000
brk(0x80d2000) = 0x80d2000
read(0, "l", 1) = 1
read(0, "s", 1) = 1
read(0, " ", 1) = 1
read(0, "-", 1) = 1
read(0, "a", 1) = 1
read(0, "l", 1) = 1
read(0, "\n", 1) = 1
stat64(0x80ae40f, 0xbffff71c) = 0
stat64(0x80ce38c, 0xbffff62c) = -1 ENOENT (No such file or directory)
stat64(0x80cdb8c, 0xbffff62c) = -1 ENOENT (No such file or directory)


Obviously read() get every keystrokes one by one as the they are being

read(0, "s", 1) = 1

This means:

0 - file descriptor (fd) - where we read from (0 = stdin)
"s" - what key has been pressed
1 - how many bytes we _want_ to read
=1 - how many bytes has _really_ been read

My idea is simple, as shown in previous example we can read every result
of any system call. So we'll read the buffer from sys_read(), if the fd = 0
(stdin) and if the read byte is 1.Store these bytes in other buffer until Enter
is pressed. After that the second buffer will be written in a logfile.
For better and clean results I've added the name of the terminal where the
keys are pressed (tty, pts), day and month of session, UID of the active shell
(who typed what), as well as the exact time (hour and seconds) of keystroke
(mIRC style).
Here how a real sessions captured by this tool looks like:


--<[vc/4]>-- --[<2002-01-22>]-- --<[UID = 0]>-- // local login as root
// with pass "iamgod"
<22:58:22> root
<22:58:24> iamgod
<22:58:30> ls -al

--<[vc/5]>-- --[<2002-01-22>]-- --<[UID = 0]>-- // local login as user
// silen with pass "l33t"
<22:58:38> silen
<22:58:40> l33t
<22:58:44> ls -al
<22:58:50> netstat -ant
<22:58:53> su -
<22:58:55> iamgod
<22:59:02> ps aux
<22:59:09> kill -9 675

--<[NULL tty]>-- --[<2002-01-22>]-- --<[UID = 0]>-- // ssh login begin

<22:59:26> SSH-1.5-1.0

--<[pts/4]>-- --[<2002-01-22>]-- --<[UID = 1000]>-- // it's just a
// regular user
<22:59:34> cd /root
<22:59:36> ls -al
<22:59:44> su -
<22:59:46> iamgod
<22:59:51> df -h
<23:00:01> du -h /root

--<[vc/5]>-- --[<2002-01-22>]-- --<[UID = 0]>--

<23:00:19> cat /tmp/log
<23:05:05> ssh -l kitaeca aaa.bbb.ccc.ddd // login to remote host
// aaa.bbb.ccc.ddd as user
// "kitaeca"
<23:05:18> SSH-1.5-1.2.27
<23:05:29> kitaeca // with pass "kitaeca"
<23:05:41> su -
<23:05:44> TheGreatWall // roots pass is "TheGreatWall"
<23:05:52> pstree -anp
<23:06:09> exit
<23:06:11> exit
<23:06:17> lsmod // the commands are to our host
<23:06:20> rmmod kkeylogger // unloading this keylogger


When login is local, UID is always 0 because /bin/login runs as root.
When login is remote with ssh to our box, the user pass can't be logged,
since it's encrypted and it's not been transferred to any shell. However we
can still log the whole session.

----[4] Some details

(1) "Special" characters handing

"Special" characters - I mean keys like F1-F12, Arrows, Tab, PgUp, PgDn,
Home, End... Linux reads the "usual" keys as their corresponding ASCII (one
byte) codes: a = 0x61, b = 0x62 etc. The "Special" characters however are being
read by Linux as 3,4 or 5 bytes codes, and the first two bytes are always same-
0x1B and then 0x5B. For example:

root@merc_lab:~/lkm# strace -xx sh


brk(0x80d1000) = 0x80d1000
brk(0x80d2000) = 0x80d2000
read(0, "\x1b", 1) = 1 // pressed key is F1
read(0, "\x5b", 1) = 1
read(0, "\x5b", 1) = 1
read(0, "\x41", 1) = 1
read(0, "\x1b", 1) = 1 // pressed key is F1
read(0, "\x5b", 1) = 1
read(0, "\x5b", 1) = 1
read(0, "\x42", 1) = 1
read(0, "\x0a", 1) = 1 // pressed key is Enter
stat64(0x80ae40f, 0xbffff71c) = 0
stat64(0x80ce3cc, 0xbffff62c) = -1 ENOENT (No such file or directory)
stat64(0x80ce3cc, 0xbffff62c) = -1 ENOENT (No such file or directory)


Here is how all of these are read by read():

Three byte keys:

UpArrow: 0x1B 0x5B 0X41
DownArrow: 0x1B 0x5B 0X42
RightArrow: 0x1B 0x5B 0x43
LeftArrow: 0x1b 0x5B 0x44
Beak(Pause): 0x1b 0x5B 0x50

Four byte keys:

F1: 0x1b 0x5B 0x5B 0x41
F2: 0x1b 0x5B 0x5B 0x42
F3: 0x1b 0x5B 0x5B 0x43
F4: 0x1b 0x5B 0x5B 0x44
F5: 0x1b 0x5B 0x5B 0x45
Ins: 0x1b 0x5B 0x32 0x7E
Home: 0x1b 0x5B 0x31 0x7E
PgUp: 0x1b 0x5B 0x35 0x7E
Del: 0x1b 0x5B 0x33 0x7E
End: 0x1b 0x5B 0x34 0x7E
PgDn: 0x1b 0x5B 0x36 0x7E

Five byte keys:

F6: 0x1b 0x5B 0x31 0x37 0x7E
F7: 0x1b 0x5B 0x31 0x38 0x7E
F8: 0x1b 0x5B 0x31 0x39 0x7E
F9: 0x1b 0x5B 0x32 0x30 0x7E
F10: 0x1b 0x5B 0x32 0x31 0x7E
F11: 0x1b 0x5B 0x32 0x33 0x7E
F12: 0x1b 0x5B 0x32 0x34 0x7E

The LKM is coded to capture all of these keys[*]:


--<[vc/5]>-- --[<2002-01-22>]-- --<[UID = 0]>--

<23:56:24> netst[Tab]-ant
<23:56:29> cat /tmp/log
<23:57:05> [Up.Arrow][Up.Arrow]
<23:57:18> [F5][F4][F3]
<23:57:19> [Up.Arrow][Down.Arrow]
<23:57:23> rmmod kkeylogger


*Note: This has been tested only with local logins!

(2) BackSpace and Ctrl+U issues

This tool watches for keystroke "BackSpace" (if local login 0x7F, if remote
0x08) and erases the last recorded character in second buffer. It also watches
for Ctrl+U (0x15 - both local and remote) which removes the whole row.

(3) Timestamps

This is done by reading /proc/driver/rtc (rtc stands for Real Time Clock).
Well you'll agree that this is dirty, but hey, it's quick and it works for me
(Slackware Linux). There is no such file in Redhat Linux for example, so if you
try to port this tool to some other Linux distro, you've got three options:
remove the timestamp routine, rewrite the tool so it reads timestamps from
/etc/localtime (it's standart for every Linux distro), or write your function
that will calculate current time from the number of seconds elapsed since
00:00:00 on January 1, 1970 since there is a pointer to these seconds -
current->xtime.tv_sec (linux/sched.h).

----[5] Known Bugs, Todo

There is a nasty bug which causes segfault in current shell, when unloading
the LKM (rmmod kkeylogger). However it only occurs when someone logged in,
presses a key on a terminal which is different than the one, the LKM has been
unloaded from.
Currently this tool can't log properly "X" sessions. Well it logs them, but
besides the actual pressed keys, a lot of garbage is being logged too.

There may be other bugs sinse this tool is considered as early alpha

----[6] Links

[1] http://www.securityfocus.com/archive/119/250274
[2] http://www.linuxdoc.org/HOWTO/Module-HOWTO/index.html
[3] http://www.packetst0rm.net/lkm.html[*]

*Note: This link is dead because www.packetst0rm.net is private right now, but
you can still find the paper in googles cache.

----[7] Proof of concept code

Hash: SHA1

begin 644 kkeylogger.tgz

Version: PGPfreeware 7.0.3 for non-commercial use <http://www.pgp.com>


(C) 2002 by mercenary

Security Consultant


Login or Register to add favorites

File Archive:

September 2022

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Sep 1st
    23 Files
  • 2
    Sep 2nd
    12 Files
  • 3
    Sep 3rd
    0 Files
  • 4
    Sep 4th
    0 Files
  • 5
    Sep 5th
    10 Files
  • 6
    Sep 6th
    8 Files
  • 7
    Sep 7th
    30 Files
  • 8
    Sep 8th
    14 Files
  • 9
    Sep 9th
    26 Files
  • 10
    Sep 10th
    0 Files
  • 11
    Sep 11th
    0 Files
  • 12
    Sep 12th
    5 Files
  • 13
    Sep 13th
    28 Files
  • 14
    Sep 14th
    15 Files
  • 15
    Sep 15th
    17 Files
  • 16
    Sep 16th
    9 Files
  • 17
    Sep 17th
    0 Files
  • 18
    Sep 18th
    0 Files
  • 19
    Sep 19th
    12 Files
  • 20
    Sep 20th
    15 Files
  • 21
    Sep 21st
    20 Files
  • 22
    Sep 22nd
    13 Files
  • 23
    Sep 23rd
    12 Files
  • 24
    Sep 24th
    0 Files
  • 25
    Sep 25th
    0 Files
  • 26
    Sep 26th
    30 Files
  • 27
    Sep 27th
    0 Files
  • 28
    Sep 28th
    0 Files
  • 29
    Sep 29th
    0 Files
  • 30
    Sep 30th
    0 Files

Top Authors In Last 30 Days

File Tags


packet storm

© 2022 Packet Storm. All rights reserved.

Hosting By