خلاصه کلیدی
پینینگ گواهی یک لایه است، نه یک دیوار. مسیر واقعی دور زدن آن روی یک APK منتشرشده، از شناسایی کتابخانه تا قلابگذاری تابع بررسی، مشخص و تکرارپذیر است.
تقریباً هر تیم موبایل در سالهای اخیر یکی از این سه کار را انجام داده است: پینینگ گواهی را فعال کرده، از پروکسی شبکه داخلی خودداری کرده، یا هر دو. این تصمیمها ارزشمندند — اما اغلب با این باور همراه میشوند که اپلیکیشن حالا «فقط از طریق کانال امن ارتباط برقرار میکند». این باور، نادرست است.
گام ۱: شناسایی کتابخانه پینینگ
پیش از هر قلابگذاری، باید بدانید با چه چیزی طرفید. سه خانواده رایج وجود دارد و هر کدام راهحل متفاوتی میخواهد:
- OkHttp CertificatePinner — جدیدترین پروژههای اندروید؛ در فراخوانی `CertificatePinner.check()` قابل مشاهده است.
- TrustManager / X509TrustManager سفارشی — کلاسهایی با نامهایی مثل `CustomTrustManager` یا `PinnedTrustManager` در decompile دیده میشوند.
- network_security_config.xml — پیکربندی اعلامشده در مانیفست؛ ضعفی که با تغییر فایل در APK رفع میشود.
# pull and decompile the real store build
apktool d app-release.apk -o out
# look for OkHttp pinning
grep -rn "CertificatePinner\|certificatePinner" out/smali* | head
# look for a custom TrustManager
grep -rln "X509TrustManager\|checkServerTrusted" out/smali* | head
# check the declared security config
grep -rn "networkSecurityConfig" out/AndroidManifest.xml
cat out/res/xml/network_security_config.xmlگام ۲: قلابگذاری فراخوانی بررسی گواهی
در اینجا Frida وارد میشود. ما فراخوانی اعتبارسنجی را قلاب میزنیم و اعتبار یک گواهی تولیدشده محلی را برمیگردانیم. نکته مهم این است که این کار را روی نسخه منتشرشده از فروشگاه انجام میدهیم، نه روی بیلد دیباگ — چون بیلد دیباگ معمولاً متفاوت است و نتیجه قابل اتکا نیست.
Java.perform(function () {
var Pinner = Java.use("okhttp3.CertificatePinner");
Pinner.check.overload("java.lang.String", "java.util.List")
.implementation = function (hostname, pins) {
console.log("[*] pinned host: " + hostname);
// bypass: return without throwing
return;
};
// also cover the varargs form used on some versions
try {
Pinner.check.overload("java.lang.String", "java.util.List[]")
.implementation = function (hostname, pins) {
console.log("[*] pinned host: " + hostname);
return;
};
} catch (e) { /* not present on this version */ }
});# 1. device with USB debugging (or an emulator you control)
adb devices
# 2. spawn the real process so hooks land before startup
frida -U -f com.target.app -l bypass-pinning.js --no-pause
# 3. confirm the hook fired
# [*] pinned host: api.target.comگام ۳: چه چیزی بعد از پینینگ پیدا میشود
پینینگ معمولاً تنها آخرین لایهای نیست که در تستهای ما کنار میرود. پس از باز شدن کانال، معمولاً با این موارد روبهرو میشویم:
- توکن نشست که پس از تغییر شماره تلفن یا ایمیل، بدون احراز هویت مجدد صادر میشود.
- رمزنگاری محلی که کلیدش از یک مقدار ثابت در کد میآید.
- اعتبارسنجی سمت سرور که به یک هدر غیرقابلجعل اتکا میکند.
- دادههای حساس که در SharedPreferences بدون رمزنگاری ذخیره میشوند.
- منابعی که مستقیماً از APK قابل استخراجاند و نباید باشند.
دفاع درست: لایهبندی به جای یک لایه
توصیه ما به تیمهایی که با این نتیجه مواجه میشوند ساده است: پینینگ را نگه دارید، اما آن را بهعنوان تنها دفاع حساب نکنید. یک دفاع لایهبندیشده به این ترتیب ساخته میشود:
- 1اعتبارسنجی سمت سرور، همیشه. هیچ تصمیم امنیتی نباید به دادهای که کلاینت میفرستد تکیه کند.
- 2توکنهای کوتاهعمر با امکان باطلسازی واقعی، نه توکنهای بلندعمر که فقط رمزنگاری دارند.
- 3ذخیرهسازی محلی با کلید مشتقیافته از Keystore سیستمعامل، نه از یک ثابت در کد.
- 4پینینگ گواهی، بهعنوان یک لایه در انتهای زنجیره.
اگر تنها کاری که میتوانید انجام دهید یک لایه است، آن لایه باید جایی باشد که حداکثر ارزش را دارد — نه جایی که پیادهسازیاش آسانتر است.