An Exhibition of A Hunger Artist 1nzag (graypanda) Security researcher

struct seq_operations

Intro

seq_operations 구조체는 seq_read() 함수에서 사용하는 구조체이다. 만약 UAF 를 kmalloc-32 에서 수행 할 수 있을 때, 해당 구조체가 유용하게 쓰일 수 있다.

Simple dive in to seq_read()

seq_read() 함수는 single_open 을 사용하는 파일에 read 를 하면서 발생하는 함수이다. 대표적으로 /proc/self/stat 함수를 open 하고, 거기에 read 할 때 트리거 된다.

seq_read() 함수가 어떤 함수인지 한번 파해쳐 보자.

  • fs/seq_file.c
/**
 *	seq_read -	->read() method for sequential files.
 *	@file: the file to read from
 *	@buf: the buffer to read to
 *	@size: the maximum number of bytes to read
 *	@ppos: the current position in the file
 *
 *	Ready-made ->f_op->read()
 */
ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	struct iovec iov = { .iov_base = buf, .iov_len = size};
	struct kiocb kiocb;
	struct iov_iter iter;
	ssize_t ret;

	init_sync_kiocb(&kiocb, file);
	iov_iter_init(&iter, READ, &iov, 1, size);

	kiocb.ki_pos = *ppos;
	ret = seq_read_iter(&kiocb, &iter);
	*ppos = kiocb.ki_pos;
	return ret;
}
EXPORT_SYMBOL(seq_read);

먼저 init_sync_kiocb() 함수를 통해 struct kiocb 구조체로 선언된 kiocb 변수를 초기화 한다.

  • include/linux/fs.h
static inline void init_sync_kiocb(struct kiocb *kiocb, struct file *filp)
{
	*kiocb = (struct kiocb) {
		.ki_filp = filp,
		.ki_flags = iocb_flags(filp),
		.ki_hint = ki_hint_validate(file_write_hint(filp)),
		.ki_ioprio = get_current_ioprio(),
	};
}

kiocb 구조체를 다음과 같이 초기화 하는 것을 볼 수 있는데, 다른건 제치고 우리는 ki_filp 만 볼 것이다.

ki_filp 필드는 인자로 주어진 filp 인 것을 알 수 있는데, 이는 seq_read() 함수의 인자로 주어졌던 struct file 구조체인 file이다.

struct file 구조체를 살펴보자.

  • include/linux/fs.h
struct file {
	union {
		struct llist_node	fu_llist;
		struct rcu_head 	fu_rcuhead;
	} f_u;
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	/*
	 * Protects f_ep, f_flags.
	 * Must not be taken from IRQ context.
	 */
	spinlock_t		f_lock;
	enum rw_hint		f_write_hint;
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	fmode_t			f_mode;
	struct mutex		f_pos_lock;
	loff_t			f_pos;
	struct fown_struct	f_owner;
	const struct cred	*f_cred;
	struct file_ra_state	f_ra;

	u64			f_version;
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data;

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct hlist_head	*f_ep;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
	errseq_t		f_wb_err;
	errseq_t		f_sb_err; /* for syncfs */
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

우리는 여기서 private_data 필드를 볼 것이다. 그 이유는 아래 다음 코드에서 볼 수 있다.

seq_read() 함수는 seq_read_iter() 함수를 통해 실질적인 read 를 수행한다.

  • fs/seq_file.c
/*
 * Ready-made ->f_op->read_iter()
 */
ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
	struct seq_file *m = iocb->ki_filp->private_data;
	size_t copied = 0;
	size_t n;
	void *p;
	int err = 0;

	if (!iov_iter_count(iter))
		return 0;

	mutex_lock(&m->lock);

	/*
	 * if request is to read from zero offset, reset iterator to first
	 * record as it might have been already advanced by previous requests
	 */
	if (iocb->ki_pos == 0) {
		m->index = 0;
		m->count = 0;
	}

	/* Don't assume ki_pos is where we left it */
	if (unlikely(iocb->ki_pos != m->read_pos)) {
		while ((err = traverse(m, iocb->ki_pos)) == -EAGAIN)
			;
		if (err) {
			/* With prejudice... */
			m->read_pos = 0;
			m->index = 0;
			m->count = 0;
			goto Done;
		} else {
			m->read_pos = iocb->ki_pos;
		}
	}

	/* grab buffer if we didn't have one */
	if (!m->buf) {
		m->buf = seq_buf_alloc(m->size = PAGE_SIZE);
		if (!m->buf)
			goto Enomem;
	}
	// something left in the buffer - copy it out first
	if (m->count) {
		n = copy_to_iter(m->buf + m->from, m->count, iter);
		m->count -= n;
		m->from += n;
		copied += n;
		if (m->count)	// hadn't managed to copy everything
			goto Done;
	}
	// get a non-empty record in the buffer
	m->from = 0;
	p = m->op->start(m, &m->index);
	while (1) {
		err = PTR_ERR(p);
		if (!p || IS_ERR(p))	// EOF or an error
			break;
		err = m->op->show(m, p);
		if (err < 0)		// hard error
			break;
		if (unlikely(err))	// ->show() says "skip it"
			m->count = 0;
		if (unlikely(!m->count)) { // empty record
			p = m->op->next(m, p, &m->index);
			continue;
		}
		if (!seq_has_overflowed(m)) // got it
			goto Fill;
		// need a bigger buffer
		m->op->stop(m, p);
		kvfree(m->buf);
		m->count = 0;
		m->buf = seq_buf_alloc(m->size <<= 1);
		if (!m->buf)
			goto Enomem;
		p = m->op->start(m, &m->index);
	}
	// EOF or an error
	m->op->stop(m, p);
	m->count = 0;
	goto Done;
Fill:
	// one non-empty record is in the buffer; if they want more,
	// try to fit more in, but in any case we need to advance
	// the iterator once for every record shown.
	while (1) {
		size_t offs = m->count;
		loff_t pos = m->index;

		p = m->op->next(m, p, &m->index);
		if (pos == m->index) {
			pr_info_ratelimited("buggy .next function %ps did not update position index\n",
					    m->op->next);
			m->index++;
		}
		if (!p || IS_ERR(p))	// no next record for us
			break;
		if (m->count >= iov_iter_count(iter))
			break;
		err = m->op->show(m, p);
		if (err > 0) {		// ->show() says "skip it"
			m->count = offs;
		} else if (err || seq_has_overflowed(m)) {
			m->count = offs;
			break;
		}
	}
	m->op->stop(m, p);
	n = copy_to_iter(m->buf, m->count, iter);
	copied += n;
	m->count -= n;
	m->from = n;
Done:
	if (unlikely(!copied)) {
		copied = m->count ? -EFAULT : err;
	} else {
		iocb->ki_pos += copied;
		m->read_pos += copied;
	}
	mutex_unlock(&m->lock);
	return copied;
Enomem:
	err = -ENOMEM;
	goto Done;
}
EXPORT_SYMBOL(seq_read_iter);

코드를 보면 struct seq_operations 의 구조체를 m 이라는 이름의 변수로 할당 하고 iocb->ki_filp->private_data 로 초기화를 하는 것을 알 수 있다.

struct seq_file 의 구조는 다음과 같다.

  • include/linux/seq_file.h
struct seq_file {
	char *buf;
	size_t size;
	size_t from;
	size_t count;
	size_t pad_until;
	loff_t index;
	loff_t read_pos;
	struct mutex lock;
	const struct seq_operations *op;
	int poll_event;
	const struct file *file;
	void *private;
};

보다시피 우리가 봐야 할 struct seq_operaitons 구조체가 있는 필드가 op 라는 이름으로 지정되어 있고, 아래부분에 파일 읽기를 수행 할 때 op 의 맴버 함수를 호출 하는 것을 알 수 있다.

그러면 해당 private_data는 어디서 초기화가 되는 것일까?

그것은 바로 read 를 하기 전 파일을 open 할 떄 초기화가 된다.

  • fs/seq_file.c
int single_open(struct file *file, int (*show)(struct seq_file *, void *),
		void *data)
{
	struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);
	int res = -ENOMEM;

	if (op) {
		op->start = single_start;
		op->next = single_next;
		op->stop = single_stop;
		op->show = show;
		res = seq_open(file, op);
		if (!res)
			((struct seq_file *)file->private_data)->private = data;
		else
			kfree(op);
	}
	return res;
}
EXPORT_SYMBOL(single_open);

/*...*/

int seq_open(struct file *file, const struct seq_operations *op)
{
	struct seq_file *p;

	WARN_ON(file->private_data);

	p = kmem_cache_zalloc(seq_file_cache, GFP_KERNEL);
	if (!p)
		return -ENOMEM;

	file->private_data = p;

	mutex_init(&p->lock);
	p->op = op;

	// No refcounting: the lifetime of 'p' is constrained
	// to the lifetime of the file.
	p->file = file;

	/*
	 * seq_files support lseek() and pread().  They do not implement
	 * write() at all, but we clear FMODE_PWRITE here for historical
	 * reasons.
	 *
	 * If a client of seq_files a) implements file.write() and b) wishes to
	 * support pwrite() then that client will need to implement its own
	 * file.open() which calls seq_open() and then sets FMODE_PWRITE.
	 */
	file->f_mode &= ~FMODE_PWRITE;
	return 0;
}
EXPORT_SYMBOL(seq_open);

앞서 설명한 대로 single_open() 을 통해 파일을 open 할 때 opstruct seq_operations 구조체로 할당이 되고, kmalloc을 통해 힙영역에 해당 구조체를 할당 하는 것을 알 수 있다. 그 후 해당 op 의 맴버를 여러 함수 포인터로 초기화 하는 것을 알 수 있다.

그리고 seq_open() 함수를 호출 하는데, 해당 함수는 struct fileprivate_data 필드를 해당 op로 초기화 하는 것을 볼 수 있다.

그럼 이제 다시 seq_read()의 코드를 보자.

/* ... */
	p = m->op->start(m, &m->index);
	while (1) {
		err = PTR_ERR(p);
		if (!p || IS_ERR(p))	// EOF or an error
			break;
		err = m->op->show(m, p);
		if (err < 0)		// hard error
			break;
		if (unlikely(err))	// ->show() says "skip it"
			m->count = 0;
		if (unlikely(!m->count)) { // empty record
			p = m->op->next(m, p, &m->index);
			continue;
		}
		if (!seq_has_overflowed(m)) // got it
			goto Fill;
		// need a bigger buffer
		m->op->stop(m, p);
		kvfree(m->buf);
		m->count = 0;
		m->buf = seq_buf_alloc(m->size <<= 1);
		if (!m->buf)
			goto Enomem;
		p = m->op->start(m, &m->index);
	}
	// EOF or an error
	m->op->stop(m, p);
	m->count = 0;
	goto Done;
/* ... */

seq_read() 함수에서는 해당 구조체의 함수 포인터를 호출하는 것을 볼 수 있다.

Structure of seq_operations

그럼 이런 함수 포인터를 담고 있는 struct seq_opertaions 구조체의 구조를 보자.

  • include/linux/seq_file.h
struct seq_operations {
	void * (*start) (struct seq_file *m, loff_t *pos);
	void (*stop) (struct seq_file *m, void *v);
	void * (*next) (struct seq_file *m, void *v, loff_t *pos);
	int (*show) (struct seq_file *m, void *v);
};

총 4개의 함수포인터가 있고, 크기는 64비트에서 32 바이트라는 것을 알 수 있다. 즉, 이 구조체는 kmalloc-32에 할당이 되는 것을 알 수 있다.

Conclusion

만약 우리가 kmalloc-32 에서 OOB 나 UAF를 일으킬 수 있다고 가정하면, 우리는 single_open() 을 통해 kmalloc-32seq_operations 데이터를 스프레이 하고, 취약점을 트리거 하여 해당 함수 포인터를 변조한 뒤, seq_read() 함수를 호출하여 RIP를 변조 할 수 있을것이다.

즉 이 포스트에서 소개하는 구조체는 kmalloc-32 익스플로잇에 활용할 수 있는 유용한 구조체중 하나이다.

연습문제를 푸는데, kmalloc-32에서 익스하는 방법을 찾아보다 알게 된 구조체이다. 하지만, 내가 푸는 문제에는 쓸모가 없었다 ;;

나중에 쓸 기회가 되면 이 포스트에 사용하는 방법까지 자세하게 포스트할 생각이다.

References