
استغلال لثغرة تجاوز سعة المخزن المؤقت في المكدس الموجودة في ثنائي 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 في كل شيء باستثناء نفسه: