앞에서 System V의 Message Queue의 예제와 사용법에 대해 알아보았습니다.
이제 Kernel에서 어떤 방식으로 실행되는지 알아보겠습니다.
kernel version은 v6.10.12 버전으로 분석을 진행합니다.
msq_obtain_object 분석
아래의 글을 읽고 진행하면 이해가 더 쉽습니다.
msgget libc code
libc는 아래와 같이 syscall로 msgget을 호출합니다.
int
msgget (key_t key, int msgflg)
{
#ifdef __ASSUME_DIRECT_SYSVIPC_SYSCALLS
return INLINE_SYSCALL_CALL (msgget, key, msgflg);
#else
return INLINE_SYSCALL_CALL (ipc, IPCOP_msgget, key, msgflg, 0, NULL);
#endif
}
C
복사
msgget kernel code
msgget가 호출되면, 실제로는 ksys_msgget가 호출됩니다.
SYSCALL_DEFINE2(msgget, key_t, key, int, msgflg)
{
return ksys_msgget(key, msgflg);
}
C
복사
ksys_msget
일단 사용하는 구조체부터 하나씩 보겠습니다.
long ksys_msgget(key_t key, int msgflg)
{
struct ipc_namespace *ns;
static const struct ipc_ops msg_ops = {
.getnew = newque,
.associate = security_msg_queue_associate,
};
struct ipc_params msg_params;
ns = current->nsproxy->ipc_ns;
msg_params.key = key;
msg_params.flg = msgflg;
return ipcget(ns, &msg_ids(ns), &msg_ops, &msg_params);
}
SYSCALL_DEFINE2(msgget, key_t, key, int, msgflg)
{
return ksys_msgget(key, msgflg);
}
C
복사
•
key_t key
부호있는 32bit 정수형입니다.
typedef __kernel_key_t key_t;
typedef int __kernel_key_t;
C
복사
•
struct ipc_params
key와 flg를 가지고 있는 ipc에 파라미터를 전달하는 구조체입니다.
struct ipc_params {
key_t key;
int flg;
union {
size_t size; /* for shared memories */
int nsems; /* for semaphores */
} u; /* holds the getnew() specific param */
};
C
복사
ipcget
params→key를 기준으로 새롭게 IPC를 생성할 지 기존 IPC를 사용할 지 선택합니다.
지금 분석에서는 ipcget_new 함수로 분석해보겠습니다.
/**
* ipcget - Common sys_*get() code
* @ns: namespace
* @ids: ipc identifier set
* @ops: operations to be called on ipc object creation, permission checks
* and further checks
* @params: the parameters needed by the previous operations.
*
* Common routine called by sys_msgget(), sys_semget() and sys_shmget().
*/
int ipcget(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
if (params->key == IPC_PRIVATE)
return ipcget_new(ns, ids, ops, params);
else
return ipcget_public(ns, ids, ops, params);
}
C
복사
ipcget_new
/**
* ipcget_new - create a new ipc object
* @ns: ipc namespace
* @ids: ipc identifier set
* @ops: the actual creation routine to call
* @params: its parameters
*
* This routine is called by sys_msgget, sys_semget() and sys_shmget()
* when the key is IPC_PRIVATE.
*/
static int ipcget_new(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
int err;
down_write(&ids->rwsem);
err = ops->getnew(ns, params);
up_write(&ids->rwsem);
return err;
}
C
복사
해당 코드를 이해하기 전에 세마포어 관련해 알아야하는 함수가 존재합니다.
1.
down_read
read lock 획득
2.
up_read
read lock 해제
3.
down_write
write lock 획득
4.
up_write
write lock 해제
즉 해당 코드를 요약하면 write lock 획득을 한 뒤, 연산을 수행하고 write lock을 해제합니다.
ops→getnew를 분석하기 위해서, 먼저 ksys_msget 함수를 보면 아래와 같이 ops→getnew를 newque로 설정한 것을 볼 수 있습니다.
long ksys_msgget(key_t key, int msgflg)
{
...
static const struct ipc_ops msg_ops = {
.getnew = newque,
.associate = security_msg_queue_associate,
};
...
return ipcget(ns, &msg_ids(ns), &msg_ops, &msg_params);
}
int ipcget(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
...
return ipcget_new(ns, ids, ops, params);
...
}
static int ipcget_new(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
...
err = ops->getnew(ns, params);
...
}
C
복사
newque
newque 함수를 보면 kmalloc 함수를 이용하여 msg_queue를 할당합니다.
•
struct msg_queue
struct msg_queue {
struct kern_ipc_perm q_perm;
time64_t q_stime; /* last msgsnd time */
time64_t q_rtime; /* last msgrcv time */
time64_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
struct pid *q_lspid; /* pid of last msgsnd */
struct pid *q_lrpid; /* last receive pid */
struct list_head q_messages;
struct list_head q_receivers;
struct list_head q_senders;
} __randomize_layout;
struct kern_ipc_perm {
spinlock_t lock;
bool deleted;
int id;
key_t key;
kuid_t uid;
kgid_t gid;
kuid_t cuid;
kgid_t cgid;
umode_t mode;
unsigned long seq;
void *security;
struct rhash_head khtnode;
struct rcu_head rcu;
refcount_t refcount;
} ____cacheline_aligned_in_smp __randomize_layout;
C
복사
msq를 할당한 뒤 해당 큐에 대한 초기화를 진행하고, 해당 Message Queue의 ID를 return 합니다.
/**
* newque - Create a new msg queue
* @ns: namespace
* @params: ptr to the structure that contains the key and msgflg
*
* Called with msg_ids.rwsem held (writer)
*/
static int newque(struct ipc_namespace *ns, struct ipc_params *params)
{
struct msg_queue *msq;
int retval;
key_t key = params->key;
int msgflg = params->flg;
msq = kmalloc(sizeof(*msq), GFP_KERNEL_ACCOUNT);
if (unlikely(!msq))
return -ENOMEM;
msq->q_perm.mode = msgflg & S_IRWXUGO;
msq->q_perm.key = key;
msq->q_perm.security = NULL;
retval = security_msg_queue_alloc(&msq->q_perm);
if (retval) {
kfree(msq);
return retval;
}
msq->q_stime = msq->q_rtime = 0;
msq->q_ctime = ktime_get_real_seconds();
msq->q_cbytes = msq->q_qnum = 0;
msq->q_qbytes = ns->msg_ctlmnb;
msq->q_lspid = msq->q_lrpid = NULL;
INIT_LIST_HEAD(&msq->q_messages);
INIT_LIST_HEAD(&msq->q_receivers);
INIT_LIST_HEAD(&msq->q_senders);
/* ipc_addid() locks msq upon success. */
retval = ipc_addid(&msg_ids(ns), &msq->q_perm, ns->msg_ctlmni);
if (retval < 0) {
ipc_rcu_putref(&msq->q_perm, msg_rcu_free);
return retval;
}
ipc_unlock_object(&msq->q_perm);
rcu_read_unlock();
return msq->q_perm.id;
}
C
복사
즉 여기까지 정리를 해보면 msgget 함수를 통해서 syscall을 호출하면, kernel은 kmalloc을 통해서 Message Queue를 할당하고 초기화를 진행합니다.
securify_msg_queue_alloc 함수를 통해 struct kern_ipc_perm msg→q_perm→security를 할당받고 ipc_addid함수를 통해 Message Queue의 IPC radix tree에 등록합니다.
이렇게 되면 Message Queue id를 통해서 Message Queue IPC에서 radix tree를 탐색하여 msg_queue의 객체를 찾아올 수 있기 때문입니다.
그 후 정상적으로 설정이 되었다면 해당 Message Queue의 id를 반환하거나 그렇지 않을 경우 오류를 반환합니다.
전체적인 msgget call stadck
1. ksys_msgget()
2. ipcget()
3. ipcget_new()
4. newque()
C
복사
msgsnd libc code
Message Queue에 메시지 전송
int
__libc_msgsnd (int msqid, const void *msgp, size_t msgsz, int msgflg)
{
#ifdef __ASSUME_DIRECT_SYSVIPC_SYSCALLS
return SYSCALL_CANCEL (msgsnd, msqid, msgp, msgsz, msgflg);
#else
return SYSCALL_CANCEL (ipc, IPCOP_msgsnd, msqid, msgsz, msgflg,
msgp);
#endif
}
weak_alias (__libc_msgsnd, msgsnd)
C
복사
msgsnd kernel code
msgsnd가 호출되면, ksys_msgsnd가 호출됩니다.
SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz,
int, msgflg)
{
return ksys_msgsnd(msqid, msgp, msgsz, msgflg);
}
C
복사
ksys_msgsnd
mtype의 값을 get_user 함수를 통해서 user → kernel 공간으로 복사하고 do_msgsnd 함수를 호출합니다.
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);
}
C
복사
struct msgbuf
실제 msgbuf의 mtext의 들어올 데이터 크기를 알 수 없어 배열형태로 선언되어 있습니다.
struct msgbuf {
__kernel_long_t mtype; /* type of message */
char mtext[1]; /* message text */
};
C
복사
do_msgsnd
아래의 함수 전체를 보기는 너무 길어 조금씩 끊어서 보겠습니다.
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;
}
}
ipc_update_pid(&msq->q_lspid, task_tgid(current));
msq->q_stime = ktime_get_real_seconds();
if (!pipelined_send(msq, msg, &wake_q)) {
/* no one is waiting for this message, enqueue it */
list_add_tail(&msg->m_list, &msq->q_messages);
msq->q_cbytes += msgsz;
msq->q_qnum++;
percpu_counter_add_local(&ns->percpu_msg_bytes, msgsz);
percpu_counter_add_local(&ns->percpu_msg_hdrs, 1);
}
err = 0;
msg = NULL;
out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
if (msg != NULL)
free_msg(msg);
return err;
}
C
복사
struct msg_msg를 load_msg 함수를 통해서 받아옵니다.
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
...
struct msg_msg *msg;
msg = load_msg(mtext, msgsz);
...
}
C
복사
struct msg_msg
먼저 msg_msg 구조체를 보면 아래와 같습니다.
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
struct list_head {
struct list_head *next, *prev;
};
struct msg_msgseg {
struct msg_msgseg *next;
/* the next part of the message follows immediately */
};
C
복사
load_msg 일부
일단 먼저 alloc_msg를 먼저 보겠습니다.
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);
}
C
복사
alloc_msg
코드를 먼저 한번 보겠습니다.
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
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;
}
C
복사
프로세스를 정리하면 아래와 같습니다.
1.
alen은 len과 DATALEN_MSG 중 더 작은 값을 반환합니다
버퍼 크기(DATALEN_MSG)보다 큰 데이터가 올 수 있으니, min 함수를 통해서 처리할 수 있는 최소 단위로 쪼갭니다.
2.
sizeof(*msg) + alen의 크기를 kmalloc으로 할당받습니다.
최소 sizeof(*msg) = 0x30, 최대 PAGE_SIZE - 0x30의 크기를 할당받을 수 있습니다.
3.
alen으로 자른 뒤 남은 데이터가 있다면, msg→next를 설정합니다.
최소 sizeof(*seg) = 0x8, 최대 PAGE_SIZE - 0x8의 크기를 할당받을 수 있습니다.
4.
최종적으로 kmalloc으로 할당한 msg를 return 합니다.
Exploit 관점에서 볼 경우 Heap 위치를 가지고 있어, heap leak을 하는데 사용할 수 있습니다.
load_msg
alloc_msg로 부터 할당받은 msg 객체에 user space에서 kernel space로 데이터를 복사합니다.
DATALEN_MSG를 넘어가는 경우 남은 데이터는 seg를 참조해서 데이터를 복사합니다.
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);
}저
C
복사
여기서 copy_from_user에서 msg + 1, seg + 1 부분에 데이터를 복사합니다.
실제 위에서 본 msg_msg 구조체의 경우 아래와 같습니다.
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
C
복사
하지만 kernel에서 msg_msg 구조체를 사용할 때는 아래와 같이 사용합니다.
data는 page의 최대 크기만큼 들어갈 수 있고 위에서 보았듯 page의 최대 크기를 넘어가는 경우 msg_msgseg *next에 data가 추가적으로 들어갑니다.
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
char data[]
};
C
복사
마지막으로 정리하면 load_msg 함수는 user space로 부터 데이터를 받아 msg_msg 구조체에 데이터를 chain 해서 넣어준 뒤 heap에 할당된 msg를 return 합니다.
do_msgsnd
다시 do_msgsnd 함수로 돌아와 load_msg 이후 return되는 값을 정리하면, kmalloc으로 user space로 부터 받아온 데이터가 msg 구조체에 맞게 들어가 있습니다.
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
...
msg = load_msg(mtext, msgsz);
if (IS_ERR(msg))
return PTR_ERR(msg);
msg->m_type = mtype;
msg->m_ts = msgsz;
...
}
C
복사
이후 id로 Message Queue를 찾고 권한을 확인 한 뒤 Queue Lock을 설정합니다.
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
...
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);
...
}
C
복사
struct msg_sender
ipc 권한 및 유효성 점검을 진행한 뒤 최종적으로 Message를 Queue에 삽입합니다. ipc와 radix tree에 insert 하는 부분은 다음에 시간이 되면 글로 다시 한번 정리해보겠습니다.
struct msg_sender {
struct list_head list;
struct task_struct *tsk;
size_t msgsz;
};
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
...
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;
}
...
}
C
복사
do_msgsnd 전체 코드
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;
}
}
ipc_update_pid(&msq->q_lspid, task_tgid(current));
msq->q_stime = ktime_get_real_seconds();
if (!pipelined_send(msq, msg, &wake_q)) {
/* no one is waiting for this message, enqueue it */
list_add_tail(&msg->m_list, &msq->q_messages);
msq->q_cbytes += msgsz;
msq->q_qnum++;
percpu_counter_add_local(&ns->percpu_msg_bytes, msgsz);
percpu_counter_add_local(&ns->percpu_msg_hdrs, 1);
}
err = 0;
msg = NULL;
out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
if (msg != NULL)
free_msg(msg);
return err;
}
C
복사
전체적인 msgsnd call stack
1. ksys_msgsnd()
2. do_msgsnd()
3. load_msg()
4. alloc_msg()
C
복사
즉 msgsnd를 호출하면 user space의 데이터를 복사해서 kmalloc으로 할당받은 heap에 복사합니다.
여기서 원하는 크기의 heap을 할당받을 수 있으며, heap 주소도 구조체에 포함된다는 것을 알 수 있습니다.
msgrcv libc code
Message Queue에 존재하는 메시지 수신
ssize_t
__libc_msgrcv (int msqid, void *msgp, size_t msgsz, long int msgtyp,
int msgflg)
{
#ifdef __ASSUME_DIRECT_SYSVIPC_SYSCALLS
return SYSCALL_CANCEL (msgrcv, msqid, msgp, msgsz, msgtyp, msgflg);
#else
return SYSCALL_CANCEL (ipc, IPCOP_msgrcv, msqid, msgsz, msgflg,
MSGRCV_ARGS (msgp, msgtyp));
#endif
}
weak_alias (__libc_msgrcv, msgrcv)
C
복사
msgrcv kernel code
msgrcv는 ksys_msgrcv를 호출합니다.
SYSCALL_DEFINE5(msgrcv, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz,
long, msgtyp, int, msgflg)
{
return ksys_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg);
}
C
복사
ksys_msgrcv
do_msgrcv로 전달됩니다. do_msgrcv 함수를 보겠습니다.
long ksys_msgrcv(int msqid, struct msgbuf __user *msgp, size_t msgsz,
long msgtyp, int msgflg)
{
return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill);
}
C
복사
do_msgrcv
함수가 길지만 하나하나 분석해보겠습니다.
Code
do_msgrcv preview
ns에 해당 프로세스에서 사용하는 ipc_namespace를 받아 온 뒤, msqid와 bufsz에 대한 valid check를 수행합니다.
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
int mode;
struct msg_queue *msq;
struct ipc_namespace *ns;
struct msg_msg *msg, *copy = NULL;
DEFINE_WAKE_Q(wake_q);
ns = current->nsproxy->ipc_ns;
if (msqid < 0 || (long) bufsz < 0)
return -EINVAL;
...
}
C
복사
다음 msgflg에 MSG_COPY flag가 설정되어 있다면, prepare_copy로 struct msg_msg copy즉 msg의 복사본을 반환합니다.
if (msgflg & MSG_COPY) {
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
if (IS_ERR(copy))
return PTR_ERR(copy);
}
C
복사
prepare_copy
load_msg 함수를 이용하여 msg를 복사한 뒤 반환합니다.
static inline struct msg_msg *prepare_copy(void __user *buf, size_t bufsz)
{
struct msg_msg *copy;
/*
* Create dummy message to copy real message to.
*/
copy = load_msg(buf, bufsz);
if (!IS_ERR(copy))
copy->m_ts = bufsz;
return copy;
}
C
복사
여기서 왜 굳이 load_msg로 buf에 있는 user space 데이터를 kernel space 데이터에 복사해서 넣어주나 했는데 kernel에 있는 데이터를 유출할 수 있기 때문에 load_msg로 buf에 있는 데이터를 kernel로 다 덮어서 쓴 뒤 msg_msg 객체를 return 한다고 한다.
convert_mode
다음으로 do_msgrcv 함수에서 convert_mode 함수를 호출합니다.
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
...
mode = convert_mode(&msgtyp, msgflg);
...
}
C
복사
해당 함수는 msgtyp과 msgflag를 해석해, 어떻게 msg를 찾을지를 설정합니다.
static inline int convert_mode(long *msgtyp, int msgflg)
{
if (msgflg & MSG_COPY)
return SEARCH_NUMBER;
/*
* find message of correct type.
* msgtyp = 0 => get first.
* msgtyp > 0 => get first message of matching type.
* msgtyp < 0 => get message with least type must be < abs(msgtype).
*/
if (*msgtyp == 0)
return SEARCH_ANY;
if (*msgtyp < 0) {
if (*msgtyp == LONG_MIN) /* -LONG_MIN is undefined */
*msgtyp = LONG_MAX;
else
*msgtyp = -*msgtyp;
return SEARCH_LESSEQUAL;
}
if (msgflg & MSG_EXCEPT)
return SEARCH_NOTEQUAL;
return SEARCH_EQUAL;
}
C
복사
msq_obtain_object_check 함수를 통해 msg_queue를 가져옵니다.
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
...
msq = msq_obtain_object_check(ns, msqid);
if (IS_ERR(msq)) {
rcu_read_unlock();
free_copy(copy);
return PTR_ERR(msq);
...
}
C
복사
해당 for문의 길이가 길어 일부분만 분석을 진행하겠습니다.
for (;;) {
struct msg_receiver msr_d;
...
msg = find_msg(msq, &msgtyp, mode);
}
...
C
복사
find_msg 함수를 통해 msg를 가져옵니다.
find_msg
static struct msg_msg *find_msg(struct msg_queue *msq, long *msgtyp, int mode)
{
struct msg_msg *msg, *found = NULL;
long count = 0;
list_for_each_entry(msg, &msq->q_messages, m_list) {
if (testmsg(msg, *msgtyp, mode) &&
!security_msg_queue_msgrcv(&msq->q_perm, msg, current,
*msgtyp, mode)) {
if (mode == SEARCH_LESSEQUAL && msg->m_type != 1) {
*msgtyp = msg->m_type - 1;
found = msg;
} else if (mode == SEARCH_NUMBER) {
if (*msgtyp == count)
return msg;
} else
return msg;
count++;
}
}
return found ?: ERR_PTR(-EAGAIN);
}
C
복사
1.
list_for_each_entry로 Message Queue에 저장된 모든 msg를 순회합니다.
2.
testmsg로 msg→m_type가 mode를 기반으로 조건에 맞는지 검사합니다.
3.
조건에 맞는 msg를 찾았다면 return 합니다
다음으로 찾은 msg를 인자로 msg_handler 함수를 호출합니다.
for (;;) {
struct msg_receiver msr_d;
...
msg = find_msg(msq, &msgtyp, mode);
}
...
bufsz = msg_handler(buf, msg, bufsz);
free_msg(msg);
return bufsz;
C
복사
msg_handler는 do_msgrcv 함수가 인자로 받는 함수 포인터입니다.
long ksys_msgrcv(int msqid, struct msgbuf __user *msgp, size_t msgsz,
long msgtyp, int msgflg)
{
return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill);
}
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
...
}
C
복사
do_msg_fill preview
일단 간단하게 보고 넘어가면 kernel에서 수신한 msg를 user space으로 복사하는 함수입니다.
store_msg
함수를 이해하기 위해서 store_msg 함수 먼저 보겠습니다.
user space의 dest으로 msg의 data를 복사합니다.
int store_msg(void __user *dest, struct msg_msg *msg, size_t len)
{
size_t alen;
struct msg_msgseg *seg;
alen = min(len, DATALEN_MSG);
if (copy_to_user(dest, msg + 1, alen))
return -1;
for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
dest = (char __user *)dest + alen;
alen = min(len, DATALEN_SEG);
if (copy_to_user(dest, seg + 1, alen))
return -1;
}
return 0;
}
C
복사
static long do_msg_fill(void __user *dest, struct msg_msg *msg, size_t bufsz)
{
struct msgbuf __user *msgp = dest;
size_t msgsz;
if (put_user(msg->m_type, &msgp->mtype))
return -EFAULT;
msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz;
if (store_msg(msgp->mtext, msg, msgsz))
return -EFAULT;
return msgsz;
}
C
복사
1.
put_user 함수로 msg→m_type을 dest→mtype에 복사합니다.
2.
store_msg로 kernel space에서 user space로 데이터를 복사합니다.
3.
msgsz에 최종적으로 전달한 msg의 길이를 담아서 return합니다.
이제 해당 코드를 이해해보면 msg를 찾고 user space로 msg를 복사해서 넣은 뒤 bufsz에 최종적으로 복사한 msg size가 들어가게 됩니다.
for (;;) {
struct msg_receiver msr_d;
...
msg = find_msg(msq, &msgtyp, mode);
}
...
bufsz = msg_handler(buf, msg, bufsz);
free_msg(msg);
return bufsz;
C
복사
최종적으로 free_msg를 보면 kernel에서 할당한 msg를 user space로 전달했기 때문에 kernel에서 전송(할당)받은 msg는 삭제(free)합니다.
void free_msg(struct msg_msg *msg)
{
struct msg_msgseg *seg;
security_msg_msg_free(msg);
seg = msg->next;
kfree(msg);
while (seg != NULL) {
struct msg_msgseg *tmp = seg->next;
cond_resched();
kfree(seg);
seg = tmp;
}
}
void security_msg_msg_free(struct msg_msg *msg)
{
call_void_hook(msg_msg_free_security, msg);
kfree(msg->security);
msg->security = NULL;
}
C
복사
전체적인 msgrcv call stack
1. ksys_msgrcv()
2. do_msgrcv()
3. find_msg()
4. do_msg_fill()
5. free_msg()
C
복사
msgrcv 함수 호출 시 user space로 kernel에 존재하는 msg를 복사한 뒤 msg를 free합니다.
호출 시 전달하는 인자에 따라서 free하지 않을 수 도 있습니다.
후일담
처음하는 kernel 코드 분석이라 틀린부분이 있다면 이해해주시고 payload.jang@gmail.com으로 말씀해주시면 감사하겠습니다.