procfs module의 read() 함수 호출 과정

Merry Berry·2024년 12월 12일
0

Linux Kernel

목록 보기
5/5

커널 익스플로잇 문제를 풀 때 도움이 되기 위해, Proc 파일시스템에 등록된 모듈의 read() 함수 호출 과정을 분석해보았다.
write() 함수는 read() 함수와 호출 흐름이 유사하므로 본 글에 포함하지 않았다.
Kernel version: 5.15.0

1. Test code

1.1. Test Driver (test_module.c)

/*
 * test_module.c
 * reference: https://wikidocs.net/196798
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>

#define MODULE_NAME "testmodule"

static struct proc_dir_entry *proc_dir;

static ssize_t test_read(struct file *filep, char __user *buffer, size_t length, loff_t *offset)
{
        char s[0x10] = "CopiedFromTest\n";
        int len = sizeof(s);
        ssize_t ret = len;

        if (*offset >= len || copy_to_user(buffer, s, len)) {
                pr_info("copy_to_user() failed\n");
                ret = 0;
        } else {
                pr_info("testmodule read %s\n", filep->f_path.dentry->d_name.name);
                *offset += len;
        }

        return ret;
}

static const struct proc_ops test_fops = {
        .proc_read = test_read,
};

static int __init test_init(void)
{
        proc_dir = proc_create(test_module_name, S_IRUGO | S_IWUGO, NULL, &test_fops);
        if (proc_dir == NULL) {
                proc_remove(proc_dir);
                pr_alert("proc_create() failed\n");
                return -ENOMEM;
        }

        pr_info("/proc/%s created\n", MODULE_NAME);
        return 0;
}

static void __exit test_exit(void)
{
        proc_remove(proc_dir);
        pr_info("/proc/%s removed\n", test_module_name);
}

module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Merry Berry");

1.2. Test Code (test_read_write.c)

// gcc -o test_read_write test_read_write.c --static

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

const char *str = "TestWrite\n";

int main()
{
        int fd;
        char buf[0x10];

        if ((fd = open("/proc/testmodule", O_RDWR)) == -1) {
                perror("open()");
                return -1;
        }

		read(fd, buf, 0x10);

        write(fd, str, 0x10);

        close(fd);

        return 0;
}

2. read() 함수 호출 과정

2.1. sys_read()

ksys_read() 함수를 호출한다.

2.2. ksys_read()

fdget_pos() 함수로 파일 디스크립터 fd에 해당하는 struct fd 구조체를 만든다. struct fd 구조체에는 fd에 대응되는 struct file 구조체 포인터가 있다.

file_ppos() 함수로 현재 파일 오프셋(file.f_pos) 필드의 값을 가져와 pos 변수에 저장하고, 그 변수의 주소를 ppos에 저장한다. 그리고 vfs_read() 함수를 호출한 후 파일 오프셋 필드를 업데이트한다.

2.3. vfs_read()

먼저 struct file 구조체의 플래그(f_mode)와 유저 레벨로부터 전달받은 메모리 주소 영역과 읽기 권한을 검사한다.

count 변수가 최대 값(MAX_RW_COUNT)을 넘을 경우 최대 값으로 맞추어주며, struct file_operations 구조체에 등록된 함수가 호출된다.

실제로 struct file 구조체 주소에 0x28을 더해 접근한 후, 0x10을 더해 struct file_operations.read 필드를 rax 레지스터에 넣는다. 그리고 rax 레지스터를 함수 포인터로 호출(__x86_indirect_thunk_rax)한다.

struct file.f_op 필드는 proc_reg_file_ops 변수의 주소가, struct file.f_op.read에는 proc_reg_read() 함수의 주소가 저장되어 있다.

2.4. proc_reg_read()

먼저 struct file.f_inode 필드를 이용해 struct inode 구조체의 주소를 얻고, container_of() 매크로를 이용해 struct proc_inode의 주소를 얻는다. 그리고 struct proc_dir_entry 구조체 포인터인 struct proc_inode.pde 필드의 값을 가져와 pde에 저장한다.

어셈블리 코드를 보면, struct file 구조체 주소(rdi)에 0x20을 더해 struct inode 구조체 포인터를 얻은 후, 0x30을 더해 struct proc_inode.pde 필드의 값을 rbp에 저장한다.


struct proc_inode 구조체에서 vfs_inode 필드 주소와 pde 필드 주소의 차는 0x30이다.

struct proc_dir_entry.flagPROC_ENTRY_PERMANENT가 활성화되어 있는지 확인한다. 필요하다면 struct proc_dir_entry.in_use 값을 1 증가한다. 그리고 pde_read() 함수를 호출한다.

2.5. pde_read()

sturct proc_dir_entry.proc_opsstruct proc_ops 구조체이다. 해당 구조체에서 proc_read 필드의 값을 가져온다.

struct proc_ops 구조체에는 test_module.c에서 작성한 것과 같이 read() 함수만 등록되어 있다.

실제로 지금까지의 과정은 모두 proc_reg_read() 함수 하나로 구현되어 있다. 위 어셈블리 코드를 보면, pde_read() 함수가 별도로 호출되지 않고 proc_reg_read() 함수 내에서 실행됨을 확인할 수 있다.

0개의 댓글