본문으로 바로가기

Kernel (5) - Exploit Core Functions

Contents

  • prepare_kernel_cred()

  • commit_creds()

  • copy_to_user

  • copy_from_user


prepare_kernel_cred()

struct cred *prepare_kernel_cred(struct task_struct *daemon)
/* https://elixir.bootlin.com/linux/v4.18/source/kernel/cred.c#L595 */

/**
* prepare_creds - Prepare a new set of credentials for modification
*
* Prepare a new set of task credentials for modification. A task's creds
* shouldn't generally be modified directly, therefore this function is used to
* prepare a new copy, which the caller then modifies and then commits by
* calling commit_creds().
*
* Preparation involves making a copy of the objective creds for modification.
*
* Returns a pointer to the new creds-to-be if successful, NULL otherwise.
*
* Call commit_creds() or abort_creds() to clean up.
*/

struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
   const struct cred *old;
   struct cred *new;

   new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
   if (!new)
       return NULL;

   kdebug("prepare_kernel_cred() alloc %p", new);

   if (daemon)
       old = get_task_cred(daemon);
   else
       old = get_cred(&init_cred);

   validate_creds(old);

   *new = *old;
   atomic_set(&new->usage, 1);
   set_cred_subscribers(new, 0);
   get_uid(new->user);
   get_user_ns(new->user_ns);
   get_group_info(new->group_info);

#ifdef CONFIG_KEYS
   new->session_keyring = NULL;
   new->process_keyring = NULL;
   new->thread_keyring = NULL;
   new->request_key_auth = NULL;
   new->jit_keyring = KEY_REQKEY_DEFL_THREAD_KEYRING;
#endif

#ifdef CONFIG_SECURITY
   new->security = NULL;
#endif
   if (security_prepare_creds(new, old, GFP_KERNEL) < 0)
       goto error;

   put_cred(old);
   validate_creds(new);
   return new;

error:
   put_cred(new);
   put_cred(old);
   return NULL;
}
EXPORT_SYMBOL(prepare_kernel_cred);

일단 위에 주석처리된 부분을 번역해보겠다.

credentials는 인증,권한의 의미를 가지고 modification는 수정의 의미를 가진다.

즉, 위 주석에 나와있는 설명대로 prepare_creds - Prepare a new set of credentials for modification자격 수정에 대한 새로운 세트를 준비한다라고 의역할 수 있다.

그리고 아랫 부분을 계속 읽어보면, commit_creds()이라는 함수를 통하여 권한을 수정 한 다음, prepare_creds()에서 사용하고 싶은 권한을 넣어주면 된다고 설명되어있다.

함수의 호출에 성공하면 새로운 권한(자격)에 대한 pointer를, 아니면 NULL을 반환한다.


코드를 보며 정리해보면, prepare_kernel_cred()는 아래와 같이 진행된다.

  • new = kmem_cache_alloc(cred_jar, GFP_KERNEL);으로 객체 할당

  • if (daemon)으로 인자의 값에 따라서 동작이 달라짐

    • 해당 조건문을 만족하면 get_task_cred()함수를 호출하여 전달된 프로세스의 자격 증명(credentials)을 old 변수에 저장함.

    • 해당 조건문을 만족하지 않으면 get_cred()함수를 호출하여 init_cred 의 자격 증명(credentials)을 old 변수에 저장함.

  • validate_creds(old);으로 old(권한)가 올바른지 확인함.

  • atomic_set()함수에 의해 "&new→usage" 영역에 1이 설정됨.

  • set_cred_subscribers() 함수를 이용하여 "&cred→subscribers" 영역에 0이 설정됨.

  • get_uid(), get_user_ns(), get_group_info() 새 자격 증명의 uid, user namespace, group info를 조회함.

  • security_prepare_creds()함수를 이용하여 현재 프로세스의 자격 증명을 변경.

  • put_cred()함수를 이용하여 현재 프로세스가 이전에 참조한 자격 증명을 해제함.

  • validate_creds() 함수에 의해 전달된 자격 증명(new)의 유효성을 검사함.

여기서 자격 증명은 struct cred init_cred에 의해 관리된다.


#define GLOBAL_ROOT_UID KUIDT_INIT(0)
#define GLOBAL_ROOT_GID KGIDT_INIT(0)

struct cred init_cred = {
  .usage          = ATOMIC_INIT(4),
#ifdef CONFIG_DEBUG_CREDENTIALS
  .subscribers        = ATOMIC_INIT(2),
  .magic          = CRED_MAGIC,
#endif
  .uid            = GLOBAL_ROOT_UID,
  .gid            = GLOBAL_ROOT_GID,
  .suid           = GLOBAL_ROOT_UID,
  .sgid           = GLOBAL_ROOT_GID,
  .euid           = GLOBAL_ROOT_UID,
  .egid           = GLOBAL_ROOT_GID,
  .fsuid          = GLOBAL_ROOT_UID,
  .fsgid          = GLOBAL_ROOT_GID,
  .securebits     = SECUREBITS_DEFAULT,
  .cap_inheritable    = CAP_EMPTY_SET,
  .cap_permitted      = CAP_FULL_SET,
  .cap_effective      = CAP_FULL_SET,
  .cap_bset       = CAP_FULL_SET,
  .user           = INIT_USER,
  .user_ns        = &init_user_ns,
  .group_info     = &init_groups,
};


typedef struct {
   uid_t val;
} kuid_t;


typedef struct {
   gid_t val;
} kgid_t;

#define KUIDT_INIT(value) (kuid_t){ value }
#define KGIDT_INIT(value) (kgid_t){ value }

이 구조체를 보면 GLOBAL_ROOT_UID등의 코드를 통해 uid,gid,suid,sgid의 권한이 root로 설정 되어있음을 볼 수 있다.

따라서 prepare_kernel_cred()함수 호출 시 인자값으로 root를 의미하는 NULL(0)을 전달하면 root권한의 자격 증명을 준비할 수 있다.


최종적으로 정리하면, prepare_kernel_cred()를 호출하면 new cred를 만드는데, 0을 인자로 주면 uid, gid 등이 0으로 설정된 cred가 생성이 된다. 즉 prepare_kernel_cred(0)credential structure를 0으로 초기화해서 생성해주는 역할을 한다.

commit_cred()credential structure를 인자로 받아서 해당 cred로 재설정을 해주는 역할을 하기에, 0으로 초기화된 cred를 넘겨주면 uid, gid 등이 결국 0으로 재설정되어 lpe가 가능한 것이다.


juntae@ubuntu:~$ id
uid=1000(juntae) gid=1000(juntae) groups=1000(juntae),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),129(docker)

juntae@ubuntu:~$ sudo su
root@ubuntu:/home/juntae# id
uid=0(root) gid=0(root) groups=0(root)

추가로, 위와 같이 root권한의 uid등은 0임을 확인할 수 있다.


commit_creds()

int commit_creds(struct cred *new)
/**
* commit_creds - Install new credentials upon the current task
* @new: The credentials to be assigned
*
* Install a new set of credentials to the current task, using RCU to replace
* the old set. Both the objective and the subjective credentials pointers are
* updated. This function may not be called if the subjective credentials are
* in an overridden state.
*
* This function eats the caller's reference to the new credentials.
*
* Always returns 0 thus allowing this function to be tail-called at the end
* of, say, sys_setgid().
*/
int commit_creds(struct cred *new)
{
struct task_struct *task = current;
const struct cred *old = task->real_cred;

kdebug("commit_creds(%p{%d,%d})", new,
      atomic_read(&new->usage),
      read_cred_subscribers(new));

BUG_ON(task->cred != old);
#ifdef CONFIG_DEBUG_CREDENTIALS
BUG_ON(read_cred_subscribers(old) < 2);
validate_creds(old);
validate_creds(new);
#endif
BUG_ON(atomic_read(&new->usage) < 1);

get_cred(new); /* we will require a ref for the subj creds too */

/* dumpability changes */
if (!uid_eq(old->euid, new->euid) ||
   !gid_eq(old->egid, new->egid) ||
   !uid_eq(old->fsuid, new->fsuid) ||
   !gid_eq(old->fsgid, new->fsgid) ||
   !cred_cap_issubset(old, new)) {
if (task->mm)
set_dumpable(task->mm, suid_dumpable);
task->pdeath_signal = 0;
smp_wmb();
}

/* alter the thread keyring */
if (!uid_eq(new->fsuid, old->fsuid))
key_fsuid_changed(task);
if (!gid_eq(new->fsgid, old->fsgid))
key_fsgid_changed(task);

/* do it
* RLIMIT_NPROC limits on user->processes have already been checked
* in set_user().
*/
alter_cred_subscribers(new, 2);
if (new->user != old->user)
atomic_inc(&new->user->processes);
rcu_assign_pointer(task->real_cred, new);
rcu_assign_pointer(task->cred, new);
if (new->user != old->user)
atomic_dec(&old->user->processes);
alter_cred_subscribers(old, -2);

/* send notifications */
if (!uid_eq(new->uid,   old->uid)  ||
   !uid_eq(new->euid,  old->euid) ||
   !uid_eq(new->suid,  old->suid) ||
   !uid_eq(new->fsuid, old->fsuid))
proc_id_connector(task, PROC_EVENT_UID);

if (!gid_eq(new->gid,   old->gid)  ||
   !gid_eq(new->egid,  old->egid) ||
   !gid_eq(new->sgid,  old->sgid) ||
   !gid_eq(new->fsgid, old->fsgid))
proc_id_connector(task, PROC_EVENT_GID);

/* release the old obj and subj refs both */
put_cred(old);
put_cred(old);
return 0;
}

commit_cred함수는 prepare_kernel_cred함수에 의해 준비된 권한 증명을 실제로 적용하여 현재 프로세스에 새 자격 증명을 설치한다.

아래와 같이 동작한다.

  • current가 가지고 있는 현재 프로세스의 정보를 task에 저장한다.

  • task 구조체를 이용하여 현재 프로세스가 사용중인 자격 증명 정보를 old 변수에 저장한다.

  • BUG_ON() 을 이용하여 다음과 같은 사항을 확인한다.

    • "task->cred"과 "old"의 자격증명이 다른지 확인한다.

    • "&new→usage"에 저장된 값이 1 보다 작은지 확인한다.

      • 위의 prepare_kernel_cred함수에서 &new->uasge에 1이라는 값을 저장하였기 때문에, 해당 부분은 무시된다.

  • get_cred()를 이용하여 new 변수에 저장된 자격 증명에 참조됨 정보를 가져온다.

  • uid_eq(), gid_eq()를 이용하여 다음과 같은 구조체 내에 저장된 변수의 값을 확인한다.

    • euid,egid는 유효 사용자 식별자(effective user ID, euid)이라는 의미로 프로세스가 파일에 대해 가지는 권한을 뜻한다.

    • fsuid는 리눅스에는 파일 시스템 접근 제어 용도로 사용되는 파일 시스템 사용자 ID(file system user ID, fsuid)를 뜻한다.

      • old->euid, new→euid

      • old->egid, new→egid

      • old->fsuid, new→fsuid

      • old->fsgid, new→fsgid

  • cred_cap_issubset()함수를 이용하여 두 자격 증명이 동일한 사용자공간에 있는지 확인한다.

  • 또다시 uid_eq(), gid_eq() 함수를 이용하여 다음과 같은 구조체 내에 저장된 변수의 값을 확인한다.

    • new→fsuid, old→fsuid

    • new→fsgid, old→fsgid

    • 비교 값이 다를 경우 key_fsuid_changed(), key_fsgid_changed() 를 이용하여 현재 프로세스의 fsuid, fsgid으로 값을 갱신한다.

  • alter_cred_subscribers() 를 이용하여 new 구조체에서 subscribers 변수에 2를 더한다.

  • rcu_assign_pointer() 를 이용하여 현재 프로세스의 "task->real_cred", "task→cred" 영역에 새로운 자격 증명을 등록한다.

  • alter_cred_subscribers() 를 이용하여 old 구조체에서 subscribers 변수에 -2를 더한다.

  • put_cred() 함수를 이용하여 이전에 사용된 자격 증명을 모두(old obj and subj)해제 한다.


copy_from_user(), _copy_from_user()

/* include/linux/uaccess.h */
copy_from_user(void *to, const void __user *from, unsigned long n)
{
       if (likely(check_copy_size(to, n, false)))
               n = _copy_from_user(to, from, n);
       return n;
}

user address(from)에서 kernel address(to)nbytes(n)만큼 복사한다.

/* include/linux/asm/uaccess.h */
#ifdef INLINE_COPY_FROM_USER
static inline unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
       unsigned long res = n;
       might_fault();
       if (likely(access_ok(from, n))) {
               kasan_check_write(to, n);
               res = raw_copy_from_user(to, from, n);
      }
       if (unlikely(res))
               memset(to + (n - res), 0, res);
       return res;
}
#endif

user application에서 사용될때는 INLINE함수로 사용된다.

여기서 INLINE함수란, 함수를 라이브러리를 통하여 가져오는게 아닌 코드를 그자리에서 즉시 실행한다는 점이 특징이다. 즉, 함수 호출과정이 없으므로 속도가 빠르다.

따라서 인라인함수는 자주 호출되면서 속도가 중요한 부분에 사용된다.

kernel application 에서 사용될 때에는 그냥 라이브러리 에서 가져온다.


return : 복사되지 않은 바이트 수. 정상적으로 수행됐다면 0return한다.


copy_to_user(), _copy_to_user()

/* include/linux/asm/uaccess.h */
unsigned long copy_to_user(void __user* to, const void* from, unsigned long n)
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(check_copy_size(from, n, true)))
n = _copy_to_user(to, from, n);
return n;
}

kernel address(from)에서 user address(to)nbytes만큼 값을 써넣는다.

return : 복사되지 않은 바이트 수. 정상적으로 수행됐다면 0return한다.


#ifdef INLINE_COPY_TO_USER
static inline unsigned long
_copy_to_user(void __user *to, const void *from, unsigned long n)
{
might_fault();
if (access_ok(VERIFY_WRITE, to, n)) {
kasan_check_read(from, n);
n = raw_copy_to_user(to, from, n);
}
return n;
}

마찬가지로 INLINE함수가 존재한다.


Reference