Linux kernel exploit basic


그렇다. 이것은 Linux kernel exploit에 관한 기본적인 지식들을 까먹지않게 적어놓는 공간인 것이다.

Mitigation


그렇다. 보호기법이다.

  • KASLR : ASLR과 똑같이 Kernel단 address를 randomization함.
  • SMEP : Ring3에서 Ring0 영역의 명령어를 실행할 수 없도록 만듦.
  • SMAP : Ring3에서 Ring0 영역을 접근할 수 없도록 만듦.
  • KADR : /proc/kallsyms을 읽었을 때 보이는 주소를 다 0으로 바꿔버림.
  • KPTI : Ring3에서 Ring0의 정보를 읽어올 수 없게 아예 새로 페이징 하는 보호기법.
  • canary : 그렇다. 그 canary인 것이다.
  • mmap_min_addr : mmap을 할 수 있는 최소 주소를 정해 null pointer dereference 공격 방지

Function that we need to know to exploit


익스를 하기 위해 알아야 할 함수들이다.

prepare_kernel_cred(struct task_struct *daemon)

위 함수는 인자로 넘겨받은 값에 따라 cred 구조체를 생성하고, 그 주소를 반환한다.
NULL을 인자로 넘겨주면, root권한의 cred구조체를 새로 생성한 뒤 반환한다.

commit_creds(struct cred *new)

입력받은 cred 구조체로 유저의 권한을 변환시킨다. return값은 항상 0인듯?

copy_from_user(void *to, const void __user *from, unsigned long n)

void __user은 그냥 charvoid로 봐도 무방할듯하다.
from -> ton bytes만큼 memcpy하는 것과 유사하게 작동한다.
이름값 하듯이 user space에서 kernel space로 값을 적는 것임.

copy_to_user(void *to, const void *from, unsigned long n)

이것도 아까와 같이 from -> ton bytes만큼 memcpy하는 것과 유사하게 작동한다.
다만 반대로 kernel space -> user space로 복사한다.

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 */
    unsigned    securebits; /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;  /* caps we're permitted */
    kernel_cap_t    cap_effective;  /* caps we can actually use */
    kernel_cap_t    cap_bset;   /* capability bounding set */
    kernel_cap_t    cap_ambient;    /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char   jit_keyring;    /* default keyring to attach requested
                     * keys to */
    struct key  *session_keyring; /* keyring inherited over fork */
    struct key  *process_keyring; /* keyring private to this process */
    struct key  *thread_keyring; /* keyring private to this thread */
    struct key  *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void        *security;  /* subjective LSM security */
#endif
    struct user_struct *user;   /* real user ID subscription */
    struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    struct group_info *group_info;  /* supplementary groups for euid/fsgid */
    /* RCU deletion */
    union {
        int non_rcu;            /* Can we skip RCU deletion? */
        struct rcu_head rcu;        /* RCU deletion hook */
    };
} __randomize_layout;

cred구조체의 구현은 이렇게 되어있다.
딱히 설명할 것은 없는듯.

Etc..


커널을 익스할 때는 보통, 커널 유저 쉘에 접근한 후 C언어로 작성한 ELF를 실행시켜서 권한을 획득한다.

하지만 당연하게도 부팅되는 커널에는 gcc가 존재하지 않는다.

그러므로 host의 쉘에서 미리 gccstatic compile을 진행한 후 exploit 할 커널에 넘겨주어야한다.

방법은 여러가지가 있겠지만 나 같은 경우는 ELFbase64 encoding -> kernel -> base64 decoding -> execute순서로 진행하는 것을 선호한다.

저 밑에 스켈레톤 참조.


x86_64같은 경우에는 상관 없는 이야기겠지만, arm혹은 mips아키텍쳐 쪽은 호환성 때문인지 pwndbg, peda보다 gef를 가장 많이 사용하는 것 같다.


커널 보호기법 확인하는 법.

mmap_min_addr : sysctl -a | grep vm.mmap
smep : cat /proc/cpuinfo | grep smep
kaslr : cat /proc/kallsyms | grep _text | head -n 1

fork 함수로 자식 프로세스를 생성하면 현재 사용자의 권한에 맞는 cred 구조체를 힙에 새로 생성한다.

skeleton code.


compile.sh

#!/bin/bash
musl-gcc -o solve solve.c -static
base64 ./solve > solve.b64
cat solve.b64
rm solve solve.b64

solve.c

#include <stdio.h>

int main() {
    puts("Hello, World!");
    return 0;
} 

solve.py

from pwn import *
import subprocess as sub

p = process(argv=['/bin/bash', '-c', './boot.sh'])

def main() :
    res = sub.check_output('./compile.sh').strip()
    p.sendlineafter('$', 'cd /home/ctf')
    p.sendline('echo "{}" > solve.b64'.format(res))
    p.sendline('base64 -d solve.b64 > solve')
    p.sendline('chmod +x solve')
    p.interactive()
if __name__ == '__main__' :
    main()