리눅스에서 대부분의 버전에 영향을 미치는 로컬 권한 상승 취약점(CVE-2009-2692)이 발견되었습니다. 이 글은 Full disclosure의 권고문인 [Full-disclosure] Linux NULL pointer dereference due to incorrect proto_ops initializations 글을 번역한 것입니다.

이번 취약점은 proto_ops 구조체의 초기화가 제대로 되지 않은 상태에서 sock_sendpage() 함수에서 널 포인터를 참조하기 때문에 발생하였습니다. proto_ops 구조체는 accept, bind, shutdown 등 다양한 함수를 구현하는데 필요한 함수 포인터들을 담고 있습니다.

특정 소켓 작업이 구현되지 않은 경우에는 미리 정의된 스텁을 가리키도록 되어있습니다. 가령 accept를 구현하지 않는다면 accept_no_accept를 사용해야 합니다. 그런데 이렇게 하지 않고 초기화 되지 않은 상태로 남는 경우가 있습니다.

보통의 경우에는 호출하는 쪽에서 먼저 포인터를 검증하기 때문에 문제가 되지 않습니다. 가령 sock_splice_read()의 경우 아래와 같이 구현됩니다.

static ssize_t sock_splice_read(struct file *file, loff_t *ppos,
                    struct pipe_inode_info *pipe, size_t len,
                unsigned int flags)
{
    struct socket *sock = file->private_data;
    if (unlikely(!sock->ops->splice_read))
        return -EINVAL;
    return sock->ops->splice_read(sock, ppos, pipe, len, flags);
}

그런데 sock_sendpage()의 경우에는 이런 식으로 함수 포인터를 검증하지 않기 때문에, 위와 같이대로 초기화가 되지 않은 상태에서는 보안 문제가 발생할 수 있습니다.

지금까지 조사된 바로는 초기화가 제대로 이루어지지 않는 부분은 아래와 같습니다.

  • include/linux/net.h에 정의된 SOCKOPS_WRAP 매크로는 딱 보기엔 괜찮아 보이지만 사실 문제가습니다. PF_APPLETALK, PK_IPX, PF_IRDA, PF_X25, PF_AX25와 관련되어 있습니다.
  • PF_BLUETOOTH, PF_IUCV, PF_INET6 (과 IPPROTO_SCTP), PF_PPPOX, PF_ISDN 프로토콜에서도 초기화가 빠진 부분이 있습니다.

2001년 5월 이후에 나온 모든 리눅스 2.4/2.6 버전이 취약합니다.

  • 2.4의 경우 2.4.4부터 2.4.37.4까지
  • 2.6의 경우 2.6.0부터 2.6.30.4까지

공격자는 0번 주소에 커널 권한으로 실행될 코드를 매핑해놓고 아래와 같이 취약한 작업을 실행하여 로컬 권한 상승이 가능합니다.

/* ... */
    int fdin = mkstemp(template);
    int fdout = socket(PF_PPPOX, SOCK_DGRAM, 0);
    unlink(template);
    ftruncate(fdin, PAGE_SIZE);
    sendfile(fdout, fdin, NULL, PAGE_SIZE);
/* ... */

sendfile()은 sendpage를 호출하게 만드는 여러가지 방법 중 하나일 뿐이라는 사실을 기억해주시기 바랍니다. mmap_min_addr 기능을 가지고 있는 최근의 커널은 sysctl vm.mmap_min_addr을 0 이상으로 설정한 경우 위와 같은 공격을 막을 수 있습니다.

리누스는 2009년 8월 13일에 이 문제를 해결하는 패치를 올렸습니다.