커널 익스플로잇 문제를 풀 때 도움이 되기 위해, Proc 파일시스템에 등록된 모듈의 read() 함수 호출 과정을 분석해보았다.
write() 함수는 read() 함수와 호출 흐름이 유사하므로 본 글에 포함하지 않았다.
Kernel version: 5.15.0
/*
* 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");
// 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;
}
ksys_read()
함수를 호출한다.
fdget_pos()
함수로 파일 디스크립터 fd
에 해당하는 struct fd
구조체를 만든다. struct fd
구조체에는 fd
에 대응되는 struct file
구조체 포인터가 있다.
file_ppos()
함수로 현재 파일 오프셋(file.f_pos
) 필드의 값을 가져와 pos
변수에 저장하고, 그 변수의 주소를 ppos
에 저장한다. 그리고 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()
함수의 주소가 저장되어 있다.
먼저 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.flag
에 PROC_ENTRY_PERMANENT
가 활성화되어 있는지 확인한다. 필요하다면 struct proc_dir_entry.in_use
값을 1
증가한다. 그리고 pde_read()
함수를 호출한다.
sturct proc_dir_entry.proc_ops
는 struct proc_ops
구조체이다. 해당 구조체에서 proc_read
필드의 값을 가져온다.
struct proc_ops
구조체에는 test_module.c
에서 작성한 것과 같이 read()
함수만 등록되어 있다.
실제로 지금까지의 과정은 모두 proc_reg_read()
함수 하나로 구현되어 있다. 위 어셈블리 코드를 보면, pde_read()
함수가 별도로 호출되지 않고 proc_reg_read()
함수 내에서 실행됨을 확인할 수 있다.