이번 포스팅에서는 LKDTM 기능을 활용해 강제 크래시를 유발한 후 얻은 익셉션 클래스 레지스터의 값에 대해 정리한다.
오버뷰
"DABT (current EL)"
익셉션 클래스가 37인 경우 데이터 어보트(Data Abort taken without a change in Exception level)로 인식된다.
아래 노드에 접근할 때 커널 데이터 어보트가 발생한다.
"ACCESS_NULL, ACCESS_USERSPACE, WRITE_KERN"
"IABT (current EL)"
익셉션 클래스가 33인 경우 인스트럭션 어보트(Instruction Abort taken without a change in Exception level)로 인식된다.
아래 노드에 접근할 때 커널 데이터 어보트가 발생한다.
"EXEC_DATA, EXEC_KMALLOC, EXEC_NULL, EXEC_STACK, EXEC_USERSPACE"
이제부터 코드를 분석하자.
"DABT (current EL)"를 유발하는 코드 분석
데이터 어보트가 발생하는 코드는 각각 다음과 같다.
ACCESS_NULL
lkdtm_ACCESS_NULL() 함수의 구현부를 보자.
https://elixir.bootlin.com/linux/v4.9.200/source/drivers/misc/lkdtm_perms.c
void lkdtm_ACCESS_NULL(void)
{
unsigned long tmp;
unsigned long *ptr = (unsigned long *)NULL;
pr_info("attempting bad read at %px\n", ptr);
tmp = *ptr;
tmp += 0xc0dec0de;
pr_info("attempting bad write at %px\n", ptr);
*ptr = tmp;
}
코드의 내용은 0x0(NULL)에 0xc0dec0de를 저장하는 동작이다.
ACCESS_USERSPACE
이어서 lkdtm_ACCESS_USERSPACE() 함수를 보자.
https://elixir.bootlin.com/linux/v4.9.200/source/drivers/misc/lkdtm_perms.c
void lkdtm_ACCESS_USERSPACE(void)
{
unsigned long user_addr, tmp = 0;
unsigned long *ptr;
user_addr = vm_mmap(NULL, 0, PAGE_SIZE,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_ANONYMOUS | MAP_PRIVATE, 0);
if (user_addr >= TASK_SIZE) {
pr_warn("Failed to allocate user memory\n");
return;
}
if (copy_to_user((void __user *)user_addr, &tmp, sizeof(tmp))) {
pr_warn("copy_to_user failed\n");
vm_munmap(user_addr, PAGE_SIZE);
return;
}
ptr = (unsigned long *)user_addr;
pr_info("attempting bad read at %p\n", ptr);
tmp = *ptr;
tmp += 0xc0dec0de;
pr_info("attempting bad write at %p\n", ptr);
*ptr = tmp;
vm_munmap(user_addr, PAGE_SIZE);
}
역시 유저 공간의 주소에 커널이 직접 엑세스하는 동작이다.
WRITE_KERN
lkdtm_WRITE_KERN() 함수를 보자.
https://elixir.bootlin.com/linux/v4.9.200/source/drivers/misc/lkdtm_perms.c
void lkdtm_WRITE_KERN(void)
{
size_t size;
unsigned char *ptr;
size = (unsigned long)do_overwritten - (unsigned long)do_nothing;
ptr = (unsigned char *)do_overwritten;
pr_info("attempting bad %zu byte write at %p\n", size, ptr);
memcpy(ptr, (unsigned char *)do_nothing, size);
flush_icache_range((unsigned long)ptr, (unsigned long)(ptr + size));
do_overwritten();
}
do_nothing() 함수의 코드를 do_overwritten() 함수에 메모리를 복사해 커널 코드 영역을 오염시킨다.
do_overwritten()/do_nothing() 함수의 구현부는 각각 다음과 같다.
/* Must immediately follow do_nothing for size calculuations to work out. */
static void do_overwritten(void)
{
pr_info("do_overwritten wasn't overwritten!\n");
return;
}
static void do_nothing(void)
{
return;
}
"IABT (current EL)"를 유발하는 코드 분석
EXEC_DATA
lkdtm_EXEC_DATA() 함수를 보자.
https://elixir.bootlin.com/linux/v4.9.200/source/drivers/misc/lkdtm_perms.c
void lkdtm_EXEC_DATA(void)
{
execute_location(data_area, CODE_WRITE);
}
data_area[] 배열에 있는 값을 실행시키는 동작이다.
EXEC_STACK
lkdtm_EXEC_STACK() 함수를 보자.
https://elixir.bootlin.com/linux/v4.9.200/source/drivers/misc/lkdtm_perms.c
void lkdtm_EXEC_STACK(void)
{
u8 stack_area[EXEC_SIZE];
execute_location(stack_area, CODE_WRITE);
}
스택에 있는 데이터를 실행하는 코드이다.
EXEC_KMALLOC
lkdtm_EXEC_KMALLOC() 함수를 분석하자.
https://elixir.bootlin.com/linux/v4.9.200/source/drivers/misc/lkdtm_perms.c
void lkdtm_EXEC_KMALLOC(void)
{
u32 *kmalloc_area = kmalloc(EXEC_SIZE, GFP_KERNEL);
execute_location(kmalloc_area, CODE_WRITE);
kfree(kmalloc_area);
}
동적 메모리를 할당받아 이를 프로그램 카운터로 지정한다.
EXEC_NULL
lkdtm_EXEC_NULL() 함수를 보겠다.
https://elixir.bootlin.com/linux/v4.9.200/source/drivers/misc/lkdtm_perms.c
void lkdtm_EXEC_NULL(void)
{
execute_location(NULL, CODE_AS_IS);
}
프로그램 카운터에 0x0을 지정하는 동작이다.
EXEC_USERSPACE
lkdtm_EXEC_USERSPACE() 함수를 보겠다.
https://elixir.bootlin.com/linux/v4.9.200/source/drivers/misc/lkdtm_perms.c
void lkdtm_EXEC_USERSPACE(void)
{
unsigned long user_addr;
user_addr = vm_mmap(NULL, 0, PAGE_SIZE,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_ANONYMOUS | MAP_PRIVATE, 0);
if (user_addr >= TASK_SIZE) {
pr_warn("Failed to allocate user memory\n");
return;
}
execute_user_location((void *)user_addr);
vm_munmap(user_addr, PAGE_SIZE);
}
유저 공간에 위치한 주소를 바로 실행한다.
Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자

최근 덧글