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

Kernel heap spray with msg_msg

Intro

커널 익스플로잇, 특히 SLUB 을 이용해 익스플로잇을 하게 되면 힙 스프레이를 해야 되는 경우가 생길 것이다.

이 포스트는 msg_msg를 이용해 커널 힙 스프레이를 하는 방법에 대해 설명한다.

What is msg_msg

msg_msg 구조체는 sys_msgsnd syscall 에서 사용하는 메시지 구조체이다. sys_msgsnd 함수는 프로세스 통신간에 사용하는 메시지 큐에 메시지를 보내는데 사용되는 시스템 콜이다.

sys_msgsnd syscall 을 한번 살펴보자.

int sys_msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msqid: 메시지를 보낼 메시지 큐의 id
  • msgp: 메시지 포인터
    • 보통 다음과 같은 구조체 포인터를 사용함.
        struct msgbuf {
            long mtype;
            char mtext[1];
        }
      
  • msgsz: mtext 필드의 길이
  • msgflg: 메시지 전송 옵션

Dive in to sys_msgsnd

직접 sys_msgsnd가 어떻게 msg_msg 구조체를 사용하는지 살펴보자.

sys_msgsnd 시스템 콜을 호출하면, 커널 내부에서는 ksys_msgsnd() 함수가 호출된다.

  • ipc/msg.c
long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz,
		 int msgflg)
{
	long mtype;

	if (get_user(mtype, &msgp->mtype))
		return -EFAULT;
	return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg);
}

/*...*/

static long do_msgsnd(int msqid, long mtype, void __user *mtext,
		size_t msgsz, int msgflg)
{
	struct msg_queue *msq;
	struct msg_msg *msg;
	int err;
	struct ipc_namespace *ns;
	DEFINE_WAKE_Q(wake_q);

	ns = current->nsproxy->ipc_ns;

	if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
		return -EINVAL;
	if (mtype < 1)
		return -EINVAL;

	msg = load_msg(mtext, msgsz);
	if (IS_ERR(msg))
		return PTR_ERR(msg);

	msg->m_type = mtype;
	msg->m_ts = msgsz;

	rcu_read_lock();
	msq = msq_obtain_object_check(ns, msqid);
	if (IS_ERR(msq)) {
		err = PTR_ERR(msq);
		goto out_unlock1;
	}

	ipc_lock_object(&msq->q_perm);

	for (;;) {
		struct msg_sender s;

		err = -EACCES;
		if (ipcperms(ns, &msq->q_perm, S_IWUGO))
			goto out_unlock0;

		/* raced with RMID? */
		if (!ipc_valid_object(&msq->q_perm)) {
			err = -EIDRM;
			goto out_unlock0;
		}

		err = security_msg_queue_msgsnd(&msq->q_perm, msg, msgflg);
		if (err)
			goto out_unlock0;

		if (msg_fits_inqueue(msq, msgsz))
			break;

		/* queue full, wait: */
		if (msgflg & IPC_NOWAIT) {
			err = -EAGAIN;
			goto out_unlock0;
		}

		/* enqueue the sender and prepare to block */
		ss_add(msq, &s, msgsz);

		if (!ipc_rcu_getref(&msq->q_perm)) {
			err = -EIDRM;
			goto out_unlock0;
		}

		ipc_unlock_object(&msq->q_perm);
		rcu_read_unlock();
		schedule();

		rcu_read_lock();
		ipc_lock_object(&msq->q_perm);

		ipc_rcu_putref(&msq->q_perm, msg_rcu_free);
		/* raced with RMID? */
		if (!ipc_valid_object(&msq->q_perm)) {
			err = -EIDRM;
			goto out_unlock0;
		}
		ss_del(&s);

		if (signal_pending(current)) {
			err = -ERESTARTNOHAND;
			goto out_unlock0;
		}

	}
}

ksys_msgsnd()do_msgsnd() 함수를 호출하고, do_msgsnd() 함수는 mtext영역의 데이터와 msgsz 를 받아 load_msg() 함수에 넘겨준다.

  • ipc/msgutil.c
struct msg_msg *load_msg(const void __user *src, size_t len)
{
	struct msg_msg *msg;
	struct msg_msgseg *seg;
	int err = -EFAULT;
	size_t alen;

	msg = alloc_msg(len);
	if (msg == NULL)
		return ERR_PTR(-ENOMEM);

	alen = min(len, DATALEN_MSG);
	if (copy_from_user(msg + 1, src, alen))
		goto out_err;

	for (seg = msg->next; seg != NULL; seg = seg->next) {
		len -= alen;
		src = (char __user *)src + alen;
		alen = min(len, DATALEN_SEG);
		if (copy_from_user(seg + 1, src, alen))
			goto out_err;
	}

	err = security_msg_msg_alloc(msg);
	if (err)
		goto out_err;

	return msg;

out_err:
	free_msg(msg);
	return ERR_PTR(err);
}

load_msg() 함수는 struct msg_msg 구조체를 리턴하는 것을 알 수 있다!

먼저 alloc_msg() 함수를 통해 len 만큼 크기의 msg를 할당받고, copy_from_user() 함수를 통해 sizeof(struct msg_msg) 뒤에 유저가 보낸 src를 복사한다.

  • ipc/msgutil.c
static struct msg_msg *alloc_msg(size_t len)
{
	struct msg_msg *msg;
	struct msg_msgseg **pseg;
	size_t alen;

	alen = min(len, DATALEN_MSG);
	msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
	if (msg == NULL)
		return NULL;

	msg->next = NULL;
	msg->security = NULL;

	len -= alen;
	pseg = &msg->next;
	while (len > 0) {
		struct msg_msgseg *seg;

		cond_resched();

		alen = min(len, DATALEN_SEG);
		seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
		if (seg == NULL)
			goto out_err;
		*pseg = seg;
		seg->next = NULL;
		pseg = &seg->next;
		len -= alen;
	}

	return msg;

out_err:
	free_msg(msg);
	return NULL;
}

alloc_msg() 함수는 kmalloc() 함수를 이용해 sizeof(*msg) + alen 만큼의 공간을 할당 하는 것을 볼 수 있다.

즉, sys_msgsnd 시스템 콜을 사용하여 우리가 지정한 버퍼를 msg_msg 구조체를 이용해 커널의 힙 부분에 쓸 수 있다.

Why Use msg_msg?

유저가 힙 영역에 메모리를 allocate 하는 방법은 간접적으로 이루어져야 한다. 유저영역에서 직접 kmalloc()을 호출 할 수는 없기 때문이다.

이 때 유저가 원하는 데이터를 원하는 크기만큼 힙 영역에 할당시킬 수 있는 방법은 여러가지가 있을것이다.

하지만 대부분은 익스플로잇을 하는 동안 커널 힙 영역에 메모리 할당을 유지하는 조건을 달성하기가 힘들다.

sys_msgsnd를 사용하여 msg_msg 구조체를 메시지 큐에 넣는다면, sys_msgrcv를 통해 메시지 큐에서 해당 메모리를 팝 하지 않는 이상, 커널 힙 메모리에 메모리가 계속 할당된 채로 유지한다!

따라서 sys_msgsnd 를 이용해 msg_msg 구조체를 할당하는 방법은 커널 힙 스프레이에 유용하다.

How to use msg_msg

그럼 직접 msg_msg를 사용해 heap spray를 해보자.

sys_msgsnd 시스템 콜은 msgsnd API 를 통해 할 수 있다.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define SPRAY_SIZE

struct msgbuf {
    long mtype;
    char mtext[1];
}

int msg_msg_spray(int spray_count)
{
    int spray_queue;
    struct *msgbuf msg_buf = (struct msgbuf*)malloc(SPRAY_SIZE);
    int i;
    
    if (spray_queue = msgget(IPC_PRIVATE, 0666 | IPC_CREAT) == -1)
    {
        perror("create queue failed");
        return;
    }

    for(i = 0; i < spray_count; i++)
    {
        if (msgsnd(spray_queue, msg_buf, SPRAY_SIZE - sizeof(msgbuf.mtype), 0) < 0)
        {
            perror("msgsnd failed");
        }
    }
}

int main(void)
{
    msg_msg_spray(0x10000);
}

먼저 msgget() 함수를 이용해 메시지 큐를 생성하고, 해당 큐에 msgsnd() 함수를 통해 원하는 만큼의 횟수만큼 메시지를 보내면 된다.

Limitation of msg_msg

해당 기법에는 단점이 하나 존재한다. 위의 do_msgsnd() 함수의 리뷰에서 봤듯이, 할당되는 메모리가 온전히 유저가 지정한 영역이 아니라 sizeof(msg_msg)의 크기만큼의 헤더를 가진다는 것이다.

---------------------

   <msg_msg header>
        48byte

---------------------

 <user defined data>


---------------------

Avaliable size with msg_msg

msg_msg를 통해 할당을 할 때에는 최대 크기가 있다. 다시 alloc_msg() 함수의 코드를 보자.

  • ipc/msgutil.c
static struct msg_msg *alloc_msg(size_t len)
{
	struct msg_msg *msg;
	struct msg_msgseg **pseg;
	size_t alen;

	alen = min(len, DATALEN_MSG);
	msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
	if (msg == NULL)
		return NULL;

	msg->next = NULL;
	msg->security = NULL;

	len -= alen;
	pseg = &msg->next;
	while (len > 0) {
		struct msg_msgseg *seg;

		cond_resched();

		alen = min(len, DATALEN_SEG);
		seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
		if (seg == NULL)
			goto out_err;
		*pseg = seg;
		seg->next = NULL;
		pseg = &seg->next;
		len -= alen;
	}

	return msg;

out_err:
	free_msg(msg);
	return NULL;
}

유저가 보낸 메시지가 DATALEN_MSG 보다 크면, DATALEN_MSG 길이 단위로 끊어서 할당을 하는 것을 볼 수 있다.

DATALEN_MSG의 크기는 다음과 같다.

  • ipc/msgutil.c
#define DATALEN_MSG	((size_t)PAGE_SIZE-sizeof(struct msg_msg))

페이지 사이즈에 msg_msg 사이즈를 뺀 만큼인 것을 알 수 있다. 보통 커널의 페이지 사이즈는 4096 바이트이므로, 평균적으로 최대 4048 만큼의 유저 지정 데이터를 한번 할당 할 수 있다는 것을 알 수 있다.

Conclusion

원데이를 공부하다 msg_msg를 이용하여 힙 스프레이를 한다는 말이 정말 많이 나와서 공부하게 됐다. 앞으로 다른 원데이들을 공부하면서 PoC 를 짜는데에 이해가 되면서 도움이 될 수 있을거라 생각한다.

다음엔 위의 기법을 쓰는 원데이 익스플로잇을 리뷰할 생각이다.

msg_msg에 대해서 각 구조체 필드의 의미와 자료 저장 원리를 알고 싶다면 이 포스트를 보자

References