Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

175162
807
85243


[라즈베리파이] 시스템 콜 - 매개 변수 점검(시스템 콜 핸들러) 11장. 시스템 콜

리눅스 커널 시스템 콜 핸들러 인터페이스 함수를 열어 보면 수 많은 예외 처리 코드를 볼 수 있습니다. 유저 모드에서 시스템 콜을 호출할 때 잘못된 인자(스트링 갯수, 메모리 주소)를 전달할 수 있기 때문입니다.
 
먼저 write() 이라는 리눅스 시스템 저수준 함수를 살펴보겠습니다.

write() 함수를 호출할 때는 다음 6번 째 줄 코드와 같이 세 가지 인자를 전달해야 합니다.
[https://android.googlesource.com/platform/system/core/+/master/init/uevent_listener.cpp]
1 ListenerAction UeventListener::RegenerateUeventsForDir(DIR* d,
2                                                       const ListenerCallback& callback) const {
3    int dfd = dirfd(d);
4    int fd = openat(dfd, "uevent", O_WRONLY);
5    if (fd >= 0) {
6        write(fd, "add\n", 4);
7        close(fd);
...

위 코드는 안드로이드 디바이스에서 uevent를 처리하는 동작입니다.

첫 번째 인자인 fd는 파일 디스크립터, 두 번째 인자는 쓰려고 하는 스트링, 세 번째는 스트링 사이즈입니다.

다음 경로에 있는 unistd.h 해더 파일을 열어서 write() 함수 선언부를 보면 3가지 인자가 어떤 유형인지 확인할 수 있습니다.
[/usr/include/unistd.h]
extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur;
__fd: 파일 디스크립터
__buf: 파일 쓰기 버퍼
__n: 쓰려는 파일 사이즈

유저 공간에서 write() 함수를 호출하면 시스템 콜이 발생한 다음에 시스템 콜 핸들러인 sys_write() 함수를 호출합니다.

이번에 sys_write() 함수 선언부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/syscalls.h]
 long sys_write(unsigned int fd, const char *buf,
      size_t count);

그럼 두 함수 선언부를 같이 보겠습니다.
유저 공간: extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur;
커널 공간: long sys_write(unsigned int fd, const char *buf, size_t count);

유저 공간에서 write() 함수를 통해 전달한 3개 인자가 커널 공간에서 실행하는 sys_write() 함수로 그대로 호출합니다.
다른 파일 입출력 함수인 read() 함수도 마찬가지입니다.
유저 공간: extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur;
커널 공간: long sys_read(unsigned int fd, char *buf, size_t count);

대부분 리눅스 저수준 표준 함수를 통해 전달된 인자들은 시스템 콜 핸들러 함수 인자로 그대로 전달됩니다. 

그런데 유저 어플리케이션은 누가 어떻게 작성할지 예상할 수 없습니다. 누구나 리눅스 표준 함수 매뉴얼을 보고 코드를 작성한 후 리눅스 시스템에서 컴파일 후 실행할 수 있습닏. 

만약 유저 어플리케이션에서 다음과 같이 write() 함수로 인자를 잘못 전달했다고 가정합시다.
정상 코드: write(fd, “add\n”, 4);
오류 코드: write(fd, “add\n”, 4000);

쓰려고 하는 문자열 크기가 4이어야 하는데 실수로 4000을 입력한 것입니다. 위와 같이 오류 코드를 작성해서 실행하면 write() 함수에 대응하는 시스템 콜 핸들러인 sys_write() 함수에서 이 인자를 그래도 받아서 처리를 하면 리눅스 커널은 오동작을 할 가능성이 높습니다.

그래서 시스템 콜 핸들러는 함수로 전달된 매개 변수 인자를 먼저 확인합니다.
유저 공간에서 리눅스 저수준 표준 함수를 호출할 때 어떤 인자를 전달할지 모르기 때문입니다.

먼저 sys_write() 시스템 콜 핸들러 함수를 보면서 매개 인자를 어떻게 처리하는지 살펴 봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/read_write.c]
1 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
2 size_t, count)
3 {
4 struct fd f = fdget_pos(fd);
5 ssize_t ret = -EBADF;
6
7 if (f.file) {
8 loff_t pos = file_pos_read(f.file);
9 ret = vfs_write(f.file, buf, count, &pos);

4번째 줄 코드에서 fdget_pos() 함수를 호출해서 sys_write() 함수 첫 번째 인자인 fd 번호를 통해 커널 내부 파일 디스크립터를 읽은 다음에 가상 파일 시스템 인퍼페이스 계층에 접근해 vfs_write() 함수를 호출합니다. 

다음 vfs_write() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/read_write.c]
1 ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
2 {
3 ssize_t ret;
4
5 if (!(file->f_mode & FMODE_WRITE))
6 return -EBADF;
7 if (!(file->f_mode & FMODE_CAN_WRITE))
8 return -EINVAL;
9 if (unlikely(!access_ok(VERIFY_READ, buf, count)))
10 return -EFAULT;

함수 첫 부분부터 예외 처리 코드를 볼 수 있습니다. 유저 어플리케이션에서 open() 함수를 호출해서 반환된 fd(파일 디스크립터) 정수값이 아닌 유효하지 않은 쓰레기 fd 정수값으로 파일을 쓰려고 시도할 수 있습니다. 이 때 실행하는 예외처리 코드입니다.

5~8번째 코드를 보겠습니다.
파일 디스크립터 f_mode 멤버가 FMODE_WRITE나 FMODE_CAN_WRITE가 아니면 오류 타입 매크로 숫자를 반환하고 함수 실행을 멈춥니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/uapi/asm-generic/errno-base.h]
#define EBADF  9 /* Bad file number */
#define EFAULT 14 /* Bad address */
#define EINVAL 22 /* Invalid argument */

9번째 줄 코드는 sys_write() 함수로 전달된 버퍼에 오류가 있는지 점검합니다. 
9 if (unlikely(!access_ok(VERIFY_READ, buf, count)))
10 return -EFAULT;

유저 공간에서 잘못된 메모리 포인터 주소로 버퍼를 할당했을 수도 있기 때문입니다.


다음과 같이 ftrace 이벤트를 설정하고 ftrace 로그를 받으면 sys_write() 호출 시 어떤 인자가 전달되는지 알 수 있습니다.
"echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/enable"

1 RPi_signal-1218  [003] ....  3557.394288: sys_enter: NR 4 (1, 17ee008, 12, 0, 12, 17ee008)
2 RPi_signal-1218  [003] .n..  3557.394348: sys_exit: NR 4 = 18

1번째 줄 로그는 fd: 1, buf: 17ee008, size: 0x12(18 십진수) 로 sys_write() 함수 실행 시작을 알 수 있습니다. “NR 4” 로그에서 4는 시스템 콜 번호를 의미합니다.

다음 2번째 줄 로그를 보면 sys_write() 함수가 어떤 조건으로 실행을 마무리했는지 알 수 있습니다. 반환값은 문자열 스트링을 쓴 크기인데 18을 반환하고 있습니다.

만약 시스템 콜 핸들러에서 예외 처리 루틴이 실행되면 ftrace 로그에서 어떤 패턴을 확인할 수 있을까요?
1 RPi_signal-1105  [000] ....  3557.402524: sys_enter: NR 4 (1, 1c6e008, 12, 0, 12, 1c6e008)
2 RPi_signal-1105  [000] ....  3557.402528: sys_exit: NR 4 = -5  // #define SIGTRAP

2번째 로그 -5와 같이 마이너스 정수를 반환합니다.

이번에는 다른 시스템 콜 핸들러 코드를 봅시다.
유저 공간에서 reboot() 이란 함수를 호출하면 sys_reboot() 이란 시스템 콜 핸들러 함수가 실행합니다.

reboot() 함수 선언부는 다음과 같으며 cmd 인자로 시스템 리부팅 타입을 전달합니다.
int reboot(int cmd);

sys_reboot() 함수 앞 부분 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/reboot.c]
1 SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
2 void __user *, arg)
3 {
4 struct pid_namespace *pid_ns = task_active_pid_ns(current);
5 char buffer[256];
6 int ret = 0;
7
8 /* We only trust the superuser with rebooting the system. */
9 if (!ns_capable(pid_ns->user_ns, CAP_SYS_BOOT))
10 return -EPERM;
11
12 /* For safety, we require "magic" arguments. */
13 if (magic1 != LINUX_REBOOT_MAGIC1 ||
14 (magic2 != LINUX_REBOOT_MAGIC2 &&
15 magic2 != LINUX_REBOOT_MAGIC2A &&
16 magic2 != LINUX_REBOOT_MAGIC2B &&
17 magic2 != LINUX_REBOOT_MAGIC2C))
18 return -EINVAL;

9번째 줄 코드는 reboot() 함수를 호출한 프로세스가 슈퍼 유저 권한이 있는지 점검합니다. 리눅스 시스템에서는 슈퍼 유저만이 시스템을 종료할 수 있는 권한을 갖고 있기 때문입니다. 만약 이 조건을 만족 못할 경우 EPERM(1) 음수 값을 반환하고 함수 실행을 중단합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/uapi/asm-generic/errno-base.h]
#define EPERM  1 /* Operation not permitted */

EPERM 매크로는 권한 없이 어떤 코드를 실행했다는 정보입니다.

13~18번째 줄 코드는 magic2 인자로 전달된 매직 값을 점검합니다.
reboot() 함수는 리눅스 시스템 전원을 내리거나 리부팅을 하는 중요한 역할을 수행하므로 위와 같은 예외 처리 조건을 추가한 것입니다.

시스템 콜 핸들러 함수나 시스템 콜 핸들러에서 호출하는 함수에서 위와 같이 매개 변수에 오류가 있는지 점검하는 코드를 많이 볼 수 있습니다. 이는 유저 공간에서 저수준 함수로 잘못된 인자가 시스템 콜 핸들러 함수로 전달됐을 경우 오동작을 막기 위한 예외 처리 코드입니다.
리눅스 해커들은 여러 가지 인자 조합으로 시스템 콜 핸들러 예외 처리 루틴의 헛점을 노립니다.
이런 코드를 Security Hole이라고 하며 이를 방지하기 위한 Security 패치들이 리눅스 커널 코드에 반영되고 있습니다.

리눅스 해커들은 보통 상용 리눅스 시스템의 루트 권한 획득을 노립니다. 루트 권한으로 시스템을 마음대로 제어할 수 있기 때문입니다. 리눅스 해커들은 ARM, x86 아키텍처에서 시스템 콜을 어떤 흐름으로 처리하는지 어셈블리 코드를 외울 정도로 숙지하고 있습니다.

#Reference 시스템 콜


Reference(워크큐)
워크큐(Workqueue) Overview

.

핑백

덧글

댓글 입력 영역