يعد اعتراض حركة مرور الشبكة المشفرة عبر بروتوكول HTTPS بين تطبيق الهاتف المحمول والواجهات البرمجية الخلفية (Backend APIs) الخطوة الأولى والأساسية في أي اختبار اختراق أمني لتطبيقات أندرويد. وسواء كان الهدف هو تقييم تطبيق مصرفي، أو فحص آليات التوثيق، أو التحقق من أمان البيانات الحساسة، يحتاج مختبرو الاختراق إلى معاينة وتعديل الطلبات الشبكية أثناء انتقالها. غير أن فرق التطوير تلجأ بصورة متزايدة إلى فرض تقنية تثبيت شهادات SSL/TLS (Certificate Pinning) لمنع هجمات اعتراض البيانات (Man-in-the-Middle - MITM).
وعلى الرغم من أن تثبيت الشهادات يمثل خط دفاع مهم ضد استغلال الشهادات المزيفة أو الشبكات اللاسلكية العامة غير الآمنة، إلا أنه لا يضمن سلامة التطبيق أمام باحث أو مهاجم يتحكم مادياً في جهاز الاختبار. نظراً لأن نظام أندرويد مبني على بيئة تشغيل مفتوحة، فإن أي فحص أمني يتم تنفيذه في جانب العميل (Client-Side) يمكن مراقبته والتلاعب به وإبطال مفعوله برمجياً.
في هذا الدليل التقني المتقدم، يشرح فريق أمن الهواتف المحمولة في كاي فورا (Cayvora) كيفية عمل تثبيت الشهادات في بيئة أندرويد، ثم يستعرض أربع منهجيات عملية لتجاوزه: التعديل الثابت على حزمة APK، وأتمتة التجاوز أثناء التشغيل باستخدام Objection، وكتابة سكربتات فريدا (Frida) مخصصة لمكتبة OkHttp3، والاعتراض البرمجي المباشر على مستوى التجميع الأصلي (Native Hooking) لتطبيقات فلاتر (Flutter) ومكتبة BoringSSL، وصولاً إلى استراتيجيات التحصين الهندسي الموصى بها للمطورين.
1. ما هو تثبيت شهادات SSL/TLS (Certificate Pinning)؟
في اتصالات HTTPS القياسية على نظام أندرويد، يتحقق نظام التشغيل من هوية الخادم البعيد عبر التسلسل الهرمي للبنية التحتية للمفاتيح العامة (PKI). يبدأ التطبيق جلسة TLS، ويقدم الخادم سلسلة شهاداته (X.509)، ويتأكد نظام أندرويد من أن السلسلة تنتهي عند جهة إصدار شهادات جذرية (Root CA) موثوقة ومثبتة مسبقاً في مخزن النظام (/system/etc/security/cacerts/).
إذا قام المختبر بتثبيت شهادة وساطة مخصصة (مثل شهادة Burp Suite أو OWASP ZAP) في مخزن شهادات المستخدم على الهاتف، يصبح بالإمكان اعتراض وفك تشفير الاتصالات المشفرة بسهولة. ولمنع ذلك، يطبق المطورون تقنية تثبيت الشهادات (Certificate Pinning) أو تثبيت المفاتيح العامة (Public Key Pinning).
بدلاً من الوثوق التلقائي بأي جهة إصدار في مخزن النظام، يتحقق كود التطبيق من أن الشهادة المقدمة أو مفتاحها العام يطابق تماماً بصمة تشفير مشفرة مسبقاً (SHA-256 Hash) ومثبتة داخل كود التطبيق أو ملفات إعداده. وإذا لم تتطابق الشهادة أثناء مصافحة TLS، يُقطع الاتصال فوراً وتظهر أخطاء مثل javax.net.ssl.SSLHandshakeException: CertPathValidatorException.
2. أساليب تثبيت الشهادات الشائعة في أندرويد
تعتمد تطبيقات أندرويد على طبقات متعددة لتطبيق تثبيت الشهادات:
أ. إعدادات الأمان الشبكي (Network Security Config)
منذ إطلاق إصدار Android 7.0 (API 24)، قدمت جوجل ملف network_security_config.xml لتحديد سياسات الأمان الشبكي بشكل تصريحي. افتراضياً، تتجاهل التطبيقات شهادات المستخدمين المضافة يدوياً، كما تتيح للمطورين تثبيت بصمات المفاتيح العامة بصورة صارمة:
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.cayvora.com</domain>
<pin-set expiration="2027-01-01">
<pin digest="SHA-256">k2v657xBsOVe1PQR/JU7tVu5prsS400NZP1IwTUqlMY=</pin>
</pin-set>
</domain-config>
</network-security-config>
ب. فئة CertificatePinner في مكتبة OkHttp3
تعد مكتبة OkHttp من شركة Square العمود الفقري للاتصالات الشبكية في أغلب تطبيقات أندرويد الحديثة. وتوفر المكتبة فئة جاهزة لبناء سياسات التثبيت:
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("api.cayvora.com", "sha256/k2v657xBsOVe1PQR/JU7tVu5prsS400NZP1IwTUqlMY=")
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build();
أثناء الاتصال، يقوم التابع CertificatePinner.check(hostname, peerCertificates) بفحص سلسلة شهادات السيرفر، ويطلق استثناء SSLPeerUnverifiedException عند عدم العثور على البصمة المطلوبة.
ج. مكتبة TrustKit وواجهات X509TrustManager المخصصة
تعتمد بعض التطبيقات على مكتبة TrustKit لإدارة التثبيت والتقارير تلقائياً، بينما تبني تطبيقات أخرى واجهات X509TrustManager مخصصة في لغة جافا، حيث يعاد تعريف التابع checkServerTrusted() للتحقق اليدوي من شهادات الخادم ومقارنتها مع الأصول المخزنة في التطبيق.
3. المنهجية الأولى: التجاوز الثابت عبر تعديل حزمة APK
في التطبيقات التي لا تحتوي على آليات دفاعية ضد التلاعب أو فحص سلامة التوقيع الرقمي، يمثل التعديل الثابت على ملف APK أسلوباً دائماً وموثوقاً، حيث يتم تفكيك الحزمة وتعديل ملفات الإعداد ثم إعادة بنائها وتوقيعها.
الخطوة 1: تفكيك الحزمة باستخدام apktool
يتم استخراج ملفات الموارد وشيفرة Smali التجميعية عبر الأمر:
apktool d target_app.apk -o app_src/
الخطوة 2: تعديل ملف network_security_config.xml
انتقل إلى المجلد app_src/res/xml/. إذا كان الملف network_security_config.xml موجوداً، استبدل محتواه لتمكين الثقة في شهادات المستخدم وحذف أي وسوم <pin-set>:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
وإذا لم يكن الملف موجوداً، أنشئه في نفس المسار وقم بربطه في ملف app_src/AndroidManifest.xml داخل وسم <application>:
<application
android:name="com.example.app.MyApplication"
android:networkSecurityConfig="@xml/network_security_config" ... >
الخطوة 3: إعادة بناء وتوقيع حزمة APK
أعد تجميع المجلد إلى حزمة APK جديدة:
apktool b app_src/ -o patched_unsigned.apk
يرفض نظام أندرويد تثبيت التطبيقات غير الموقعة، لذا نستخدم أداة uber-apk-signer لمحاذاة الملف وتوقيعه بتوقيعات رقمية صالحة (v1 وv2 وv3):
java -jar uber-apk-signer.jar --apks patched_unsigned.apk
الآن يمكن تثبيت الملف الناتج patched_unsigned-aligned-debugSigned.apk عبر أداة ADB والتقاط كافة الاتصالات بوضوح عبر برنامج Burp Suite.
4. المنهجية الثانية: التجاوز الديناميكي باستخدام أداة Objection
إذا كان التطبيق محمياً بأنظمة فحص سلامة التوقيع الرقمي أو تعمية الموارد، فإن التطبيق المعدل سيتوقف عن العمل فوراً بعد إعادة بنائه. وهنا يأتي دور الاعتراض الديناميكي (Dynamic Instrumentation) للتحكم في ذاكرة التطبيق أثناء التشغيل على هاتف مروّت دون المساس بالملفات المخزنة على القرص.
تعتبر أداة Objection منصة استكشاف أمنية متنقلة مبنية على إطار العمل الشهير فريدا (Frida)، وتوفر أوامر فورية لإلغاء القيود الأمنية أثناء تشغيل التطبيق.
تشغيل خادم فريدا وتشغيل التطبيق عبر Objection
تأكد من تشغيل خادم frida-server بصلاحيات الجذر على جهاز الاختبار:
adb root
adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server && /data/local/tmp/frida-server &"
ثم أطلق التطبيق تحت إدارة Objection عبر وضع التشغيل اللحظي (Spawn) لضمان اعتراض الاتصالات من اللحظة الأولى لتشغيل التطبيق:
objection --gadget com.example.app explore
تعطيل تثبيت الشهادات بأمر واحد
داخل طرفية Objection التفاعلية، نفذ الأمر التالي:
android sslpinning disable
تقوم أداة أوبجيكشن بحقن خطافات جافا سكريبت جاهزة لاعتراض عشرات فئات التحقق من الشهادات في ذاكرة جافا في وقت واحد:
- اعتراض التابع
TrustManagerImpl.verifyChain()وجعله يقر بصحة الشهادات دون إطلاق أي استثناء. - اعتراض التابع
OkHttpClient.Builder.certificatePinner()واستبدال كائن التثبيت بكائن فارغ. - تعطيل التحقق من أسماء النطاقات في
HttpsURLConnection. - إبطال آليات التثبيت في مكاتب مثل Apache HttpClient وWebViews وحزم SDK الخارجية.
5. المنهجية الثالثة: كتابة سكربتات فريدا مخصصة لمكتبة OkHttp3
في التطبيقات المعقدة، يقوم المطورون أحياناً بتعديل مسارات حزم OkHttp3 أو تغيير أسماء الفئات، مما يتسبب في فشل الأدوات الآلية كأداة Objection. في هذه الحالة، تمنحنا سكربتات فريدا المكتوبة يدوياً دقة فائقة للسيطرة على مسار التنفيذ.
اعتراض التابع OkHttp3 CertificatePinner.check()
نقطة التفتيش الرئيسية في OkHttp3 هي التابع check() ضمن الفئة okhttp3.CertificatePinner. يقبل هذا التابع اسم النطاق وقائمة الشهادات، ويطلق استثناء SSLPeerUnverifiedException إذا فشل الفحص، بينما لا يُرجع شيئاً (void) في حال النجاح.
من خلال استبدال هذا التابع بخطاف مخصص يعود فوراً دون فحص أو إطلاق استثناءات، يتم إبطال التثبيت بالكامل:
Java.perform(function() {
try {
var CertificatePinner = Java.use("okhttp3.CertificatePinner");
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(hostname, peerCertificates) {
console.log('[+] Bypassed OkHttp3 CertificatePinner for: ' + hostname);
return; // العودة بسلام دون رمي أي استثناء
};
console.log('[*] OkHttp3 CertificatePinner hook installed successfully.');
} catch (err) {
console.log('[-] OkHttp3 CertificatePinner not found or already obfuscated: ' + err);
}
});
التعامل مع التعمية البرمجية (ProGuard / R8)
عند تعمية الكود عبر ProGuard أو R8، يتم تغيير اسم الفئة إلى اسم عشوائي مثل a.b.c.d. هنا نستخدم واجهة الانعكاس (Reflection API) في فريدا للبحث عن أي فئة تحتوي على تابع يحمل نفس التوقيع البرمجي (Ljava/lang/String;Ljava/util/List;)V:
Java.perform(function() {
Java.enumerateLoadedClasses({
onMatch: function(className) {
if (className.indexOf("android.") === -1 && className.indexOf("java.") === -1) {
try {
var clazz = Java.use(className);
var methods = clazz.class.getDeclaredMethods();
methods.forEach(function(m) {
if (m.getName() === "check" && m.getParameterTypes().length === 2) {
clazz.check.overload('java.lang.String', 'java.util.List').implementation = function(h, p) {
console.log('[+] Obfuscated pinner bypassed in: ' + className + ' for ' + h);
return;
};
}
});
} catch(e) {}
}
},
onComplete: function() { console.log('[*] Class enumeration complete.'); }
});
});
يتم حقن السكربت لحظة بدء التطبيق عبر أداة فريدا في سطر الأوامر:
frida -U -f com.example.app -l bypass_okhttp.js
6. المنهجية الرابعة: التثبيت على المستوى الأصلي (BoringSSL وتطبيقات Flutter)
تشهد تطبيقات الهواتف الحديثة انتشاراً واسعاً لأطر العمل العابرة للمنصات، وفي مقدمتها Google Flutter. تكمن خصوصية تطبيقات Flutter في أنها تُترجم مباشرة إلى كود آلي مجمع للأجهزة (ملفات libapp.so و libflutter.so). وبما أن محرك Flutter لا يمرر اتصالات الشبكة عبر بيئة جافا (ART)، فإن جميع خطافات جافا (بما فيها network_security_config.xml ومكتبات جافا) تصبح غير مجدية على الإطلاق.
يقوم محرك فلاتر بدمج نسخته الخاصة المترجمة من مكتبة BoringSSL المكتوبة بلغة C/C++ مباشرة داخل libflutter.so. وتجري كافة عمليات التحقق من صحة شهادات SSL في الكود الأصلي.
تحديد دالة التحقق من الشهادات داخل BoringSSL
في مكتبة BoringSSL، تسمى الدالة المسؤولة عن التحقق من سلسلة الشهادات عادة ssl_crypto_x509_session_verify_cert_chain (أو session_verify_cert_chain). في كود لغة C، تُرجع هذه الدالة قيمة عددية: 1 في حال نجاح التحقق، و 0 (أو قيمة سالبة) في حال الفشل.
من خلال فحص وتفكيك مكتبة libflutter.so باستخدام أدوات مثل Ghidra أو IDA Pro، يستطيع الباحثون تحديد موقع الدالة عبر البحث عن نصوص مثل "ssl_client" أو "x509_ctx"، أو عبر مطابقة بصمات تعليمات معمارية ARM64.
كتابة خطاف أصلي بمعمارية ARM64 باستخدام فريدا
بمجرد تحديد عنوان الدالة أو بصمتها الثنائية في الذاكرة، نستخدم واجهة Interceptor الأصلية في فريدا لاعتراض استدعاء الدالة على مستوى أوامر المعالج:
// خطاف أصلي بمعمارية ARM64 لمكتبة BoringSSL في تطبيقات Flutter
function hookFlutterPinning() {
var m = Process.findModuleByName("libflutter.so");
if (!m) {
console.log("[-] libflutter.so not loaded yet, retrying...");
setTimeout(hookFlutterPinning, 500);
return;
}
// البحث عن رمز الدالة أو العنوان النسبي في مكتبة libflutter.so
var targetSymbol = "ssl_crypto_x509_session_verify_cert_chain";
var verifyAddr = Module.findExportByName("libflutter.so", targetSymbol);
if (verifyAddr) {
Interceptor.attach(verifyAddr, {
onEnter: function(args) {
console.log("[+] Intercepted native BoringSSL certificate verification call");
},
onLeave: function(retval) {
console.log("[+] Original verification returned: " + retval + ", overriding to 1 (SUCCESS)");
retval.replace(ptr(1)); // فرض القيمة 1 للإشارة إلى نجاح التحقق
}
});
console.log("[*] Native hook active on " + targetSymbol);
} else {
console.log("[-] Symbol not exported, falling back to signature pattern scan...");
}
}
hookFlutterPinning();
من خلال إجبار مسجل المعالج (Register X0 في معمارية ARM64) على إرجاع القيمة 1 عند خروج الدالة، يعتبر اتصال TLS أن الشهادة موثوقة تماماً، مما يتيح فك تشفير واعتراض بيانات تطبيقات فلاتر عبر Burp Suite بسلاسة.
7. التحصين الدفاعي: بناء معماريات وتطبيقات قوية
تثبت المنهجيات الأربع السابقة أن تثبيت الشهادات ليس حصناً منيعاً لا يمكن اختراقه. فعندما يكون الجهاز مروتاً أو تحت بيئة محاكاة يسيطر عليها الباحث، لا يمكن الوثوق بالبيئة المنفذة في جانب العميل. ومع ذلك، فإن تطبيق استراتيجية دفاعية متعددة الطبقات يرفع صعوبة وتكلفة الاختراق بشكل هائل أمام المهاجمين.
1. استراتيجية التثبيت متعدد الطبقات
لا تعتمد على طبقة واحدة فقط لحماية اتصالاتك:
- ابدأ بضبط ملف
network_security_config.xmlكأساس تنظيمي للسياسات الشبكية. - طبق ميزة
CertificatePinnerفي مكتبة OkHttp مع الحرص على تثبيت مفاتيح عامة احتياطية (Backup Pins) لضمان تجديد الشهادات دون إيقاف التطبيق لدى المستخدمين. - استخدم مكتبات C/C++ أصلية عبر واجهة JNI للتحقق المتقاطع وغير المتزامن من معايير اتصال TLS أثناء التشغيل.
2. فحص بيئة التشغيل وكشف الروت والتلاعب
تعتمد هجمات التجاوز الديناميكي على تشغيل خوادم فريدا وأدوات التعديل، لذا يجب أن يقوم التطبيق بفحص بيئة الهاتف قبل فتح القنوات الشبكية الحساسة:
- كشف الروت: التحقق من وجود ملفات su (مثل
/system/bin/suو/data/local/tmp/su) ونقاط ربط Magisk و KernelSU. - كشف فريدا وأدوات التنقيح: مراقبة ملف
/proc/self/mapsللتأكد من عدم وجود مكتبةfrida-agent.so، وفحص المنافذ المحلية الشائعة (مثل المنفذ27042)، وقراءة حقلTracerPidفي/proc/self/status. - التصديق المعتمد على العتاد: تفعيل واجهة Google Play Integrity API للتحقق من أن التطبيق لم يُعد تجميعه وأنه يعمل على جهاز أصلي موثق ومعتمد.
3. التعمية البرمجية المتقدمة (R8 وتسطيح مسار التحكم)
قم بضبط أداة R8 أو ProGuard لإزالة أسماء الفئات وتواقيع الدوال وسمات التصحيح. كما يساهم استخدام أدوات الحماية التجارية المتقدمة (مثل DexGuard) في تشفير النصوص البرمجية وتسطيح تدفق التحكم (Control Flow Flattening)، مما يجعل الهندسة العكسية وتحليل الكود اليدوي أمراً مكلفاً ومعقداً للغاية.
خاتمة
يمثل تثبيت شهادات SSL خط دفاع حيوي لمنع التلصص على بيانات المستخدمين واعتراضها عبر الشبكات العامة. ومع ذلك، ينبغي على مهندسي الأمن والمطورين إدراك أن الفحوصات المنفذة على أجهزة المستخدمين لا تشكل حاجز تفويض نهائي. ومن خلال الجمع بين التعديل الثابت، والأدوات الآلية كـ Objection، وسكربتات فريدا، والاعتراض الأصلي لمكتبة BoringSSL، يمتلك خبراء الأمن القدرة على تحليل حركة المرور بدقة لتقييم أمان الواجهات الخلفية وحمايتها من أشد الثغرات خطورة.