SECCOMP(2)

dandb3·2023년 5월 22일
0

pwnable

목록 보기
3/17

앞서 SECCOMP가 무엇이고, 어떤 식으로 동작하는지에 대해서 간단히 알아보았다.
그러면 이 SECCOMP가 실제로 어떻게 적용되어 있고, 이를 우회하는 방법에 대해서 알아보자.

  • seccomp-tools를 사용해서 확인한다. 다음은 그 예시이다 :

     seccomp-tools dump ./bypass_seccomp 
     line  CODE  JT   JF      K
    =================================
     0000: 0x20 0x00 0x00 0x00000004  A = arch
     0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
     0002: 0x20 0x00 0x00 0x00000000  A = sys_number
     0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
     0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
     0005: 0x15 0x04 0x00 0x00000001  if (A == write) goto 0010
     0006: 0x15 0x03 0x00 0x00000002  if (A == open) goto 0010
     0007: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0010
     0008: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0010
     0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
     0010: 0x06 0x00 0x00 0x00000000  return KILL
    • struct sock_filter구조체 하나가 위의 명령어 한 줄에 해당한다.
    • 이 경우에는 write, open, execve, execveat syscall을 DENY하는 SECCOMP이다.
    • 0003에서는 A < 0x40000000을 비교하는데, 이 부분은 왜 있을까???
  • do_syscall_64함수의 내부를 볼 필요가 있다.

    __visible noinstr void do_syscall_64(struct pt_regs *regs, int nr)
    {
        add_random_kstack_offset();
        nr = syscall_enter_from_user_mode(regs, nr);
    
        instrumentation_begin();
    
        if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) {
            /* Invalid system call, but still a system call. */
            regs->ax = __x64_sys_ni_syscall(regs);
        }
    
        instrumentation_end();
        syscall_exit_to_user_mode(regs);
    }
    • x86_64 아키텍쳐에서는 64비트 뿐만 아니라 32비트도 호환하기 때문에, 둘 모두의 syscall을 실행할 수 있게 설계되어 있다.
    • 그래서 자세히 보면 do_syscall_x64 실행 후 실패하면 do_syscall_x32를 실행하는 코드가 있는 것을 확인할 수 있다.
  • do_syscall_x64

    static __always_inline bool do_syscall_x64(struct pt_regs *regs, int nr)
    {
        /*
         * Convert negative numbers to very high and thus out of range
         * numbers for comparisons.
         */
        unsigned int unr = nr;
    
        if (likely(unr < NR_syscalls)) {
            unr = array_index_nospec(unr, NR_syscalls);
            regs->ax = sys_call_table[unr](regs);
            return true;
        }
        return false;
    }
  • do_syscall_x32

    static __always_inline bool do_syscall_x32(struct pt_regs *regs, int nr)
    {
        /*
         * Adjust the starting offset of the table, and convert numbers
         * < __X32_SYSCALL_BIT to very high and thus out of range
         * numbers for comparisons.
         */
        unsigned int xnr = nr - __X32_SYSCALL_BIT;
    
        if (IS_ENABLED(CONFIG_X86_X32_ABI) && likely(xnr < X32_NR_syscalls)) {
            xnr = array_index_nospec(xnr, X32_NR_syscalls);
            regs->ax = x32_sys_call_table[xnr](regs);
            return true;
        }
        return false;
    }
    • x32의 경우 x64와 다른 부분이 하나 존재하는데, 바로 nr에서 __X32_SYSCALL_BIT을 빼주는 부분이다.
    #define __X32_SYSCALL_BIT	0x40000000
    • 헉 앞에 나왔던 0x40000000랑 완전히 일치하는 값이네??
    • 그렇다면, 32비트의 syscall 인자로 들어오는 값은 syscall number + 0x40000000이었다는 것임을 알 수 있다.
    • 앞의 SECCOMP에서의 0x40000000보다 크거나 같으면 KILL하는 부분을 다시 보면, 32비트의 system call들의 경우 모두 다 차단한다는 것을 알 수 있다.
    • 즉, write, open, execve, execve 시스템 콜 뿐만 아니라 32비트 시스템 콜을 모두 DENY한다는 것!
    • 뒤늦게 하나 말하자면, 앞선 SECCOMP는 seccomp library를 이용해서 설정한 값이다. 그냥 아무생각 없이 직접 구조체를 만들어서 SECCOMP를 적용시킨다면, 0x40000000과 비교시켜주는 명령어를 하나를 직접 추가해 주어야 한다.
  • 그래서 0x40000000이 어떤 문제를 일으킬 수 있을까?
    - 위의 SECCOMP에 따르면, execve를 실행할 수 없기 때문에 쉘을 따는 것은 어려워 보인다. 하지만, 0x40000000을 비교하는 부분이 없다면?
    - syscall을 호출할 때에 32비트 execve syscall번호 + 0x40000000한 값을 인자로 넘겨주게 되면 SECCOMP에 걸리지 않고 성공적으로 실행될 수 있게 된다.
    요기까지..

  • 출처

profile
공부 내용 저장소

0개의 댓글