دليل على عكس خادم العميل apk على مثال المهمة NeoQUEST-2020


اليوم لدينا برنامج غني (سيكون هناك الكثير من مجالات الأمن السيبراني في وقت واحد!): فكّر في فك ترميز تطبيق Android ، واعتراض حركة المرور للحصول على عناوين URL ، وإعادة بناء apk بدون شفرة المصدر ، والعمل مع محللي التشفير والمزيد :)

وفقًا للأسطورة NeoQUEST-2020 ، عثر البطل على أجزاء روبوت قديمة يجب استخدامها للحصول على المفتاح. دعنا نبدأ!

1. Reversim apk


لذا ، قبلنا قليلاً تمكنا من استخلاصه من روبوت شبه مفكك - تطبيق apk يجب أن يساعدنا بطريقة أو بأخرى في الحصول على المفتاح. لنقم بأكثر وضوح: تشغيل apk وإلقاء نظرة على وظائفه. لا تترك واجهة التطبيق أكثر من أضيق الحدود - هذا عميل ملف FileDroid مخصص يسمح لك بتنزيل ملف من خادم بعيد. حسنًا ، يبدو الأمر سهلاً. نقوم بتوصيل الهاتف بالإنترنت ، ونقوم بمحاولة تجريبية للتنزيل (على الفور key.txt - حسنًا ، ماذا لو؟) - غير ناجح ، الملف مفقود على الخادم.



ننتقل إلى الحدث التالي من حيث التعقيد - نقوم بفك ملف apk باستخدام JADXوتحليل شفرة المصدر للتطبيق ، والتي ، لحسن الحظ ، ليست غامضة على الإطلاق. مهمتنا الحالية هي فهم الملفات التي يوفرها الخادم البعيد للتنزيل ، وتحديد الملف الذي يحتوي على المفتاح منه.

نبدأ بفئة com.ctf.filedroid.MainActivity ، التي تحتوي على طريقة onClick () الأكثر إثارة للاهتمام بالنسبة لنا ، والتي تتم فيها معالجة النقر على الزر "تنزيل". داخل هذه الطريقة ، يتم استدعاء فئة ConnectionHandler مرتين: أولاً ، يتم استدعاء أسلوب ConnectionHandler.getToken () ، وعندها فقط ، يتم استدعاء ConnectionHandler.getEncryptedFile () ، الذي يمرر اسم الملف الذي يطلبه المستخدم.



نعم ، هذا أولاً ، نحن بحاجة إلى رمز مميز! سوف ندرس أكثر بقليل مع عملية الحصول عليها.
تأخذ طريقة ConnectionHandler.getToken () سطرين من المدخلات ، ثم ترسل طلب GET ، لتمرير هذه السطور كمعلمات "crc" و "sign". ورداً على ذلك ، يرسل الخادم البيانات بتنسيق JSON ، الذي يستخرج منه تطبيقنا رمز الدخول ويستخدمه لتنزيل الملف. هذا كله ، بالطبع ، جيد ، لكن ما هي "crc" و "sign"؟



لفهم هذا ، ننتقل أكثر نحو فئة الشيكات ، يرجى تقديم أساليب badHash () و badSign (). الأول يحسب المجموع الاختباري من classes.dex و resources.arsc ، يربط هاتين القيمتين ويلفها في Base64 (انتبه للعلم 10 = NO_WRAP | URL_SAFE ، سيكون مفيدًا). وماذا عن الطريقة الثانية؟ ويفعل الشيء نفسه مع بصمة SHA-256 لتوقيع التطبيق. إيه ، يبدو أن FileDroid ليس حريصًا حقًا على إعادة بنائه :(



حسنًا ، لنفترض أننا تلقينا الرمز المميز. ماذا بعد؟ نقوم بتمريره إلى إدخال أسلوب ConnectionHandler.getEncryptedFile () ، الذي يلحق اسم الملف المطلوب بالرمز ويولد طلب GET آخر ، هذه المرة مع معلمات "token" و "file". يرسل الخادم استجابة (بناءً على اسم الطريقة) ملفًا مشفرًا ، يتم تخزينه على / sdcard /.

لذا ، لتلخيص المجموع الفرعي الصغير: لدينا خبران ، و ... كلاهما سيئان. أولاً ، لا يدعم FileDroid بالفعل حماستنا لتعديل apk (يتم التحقق من المجموع الاختباري والتوقيع) ، وثانيًا ، يعد الملف المستلم من الخادم بتشفيره.

حسنًا ، سنقوم بحل المشكلات بمجرد توفرها ، والآن مشكلتنا الرئيسية هي أننا ما زلنا لا نعرف الملف الذي نحتاج لتنزيله. ومع ذلك ، في عملية دراسة فئة ConnectionHandler ، لا يسعنا إلا أن نلاحظ هذا الحق بين طرق getToken () و getEncryptedFile () ، نسي مطورو FileDroid طريقة أخرى جذابة جدًا تسمى تحدث getListing (). لذا ، يدعم الخادم مثل هذه الوظائف ... يبدو أن هذا ما تحتاجه!



للحصول على القائمة ، سنحتاج إلى "crc" و "sign" المعروفين - ليست مشكلة ، فنحن نعرف بالفعل من أين أتوا. نقرأ القيم ونرسل طلب GET و ... لذا توقف. إلى أين سنرسل طلب GET؟ سيكون من الجيد الحصول على عنوان URL للخادم البعيد أولاً. إيه ، نعود إلى MainActivity.onClick () ونرى كيف يتم إنشاء وسيطات netPath لاستدعاء طريقتين getToken () و getEncryptedFile ():

Method getSecureMethod = 
wat.class.getDeclaredMethod("getSecure", new Class[]{String.class});

// . . .

// netPath --> ConnectionHandler.getToken()
(String) getSecureMethod.invoke((Object) null, new Object[]{"fnks"})

// netPath --> ConnectionHandler. getEncryptedFile()
(String) getSecureMethod.invoke((Object) null, new Object[]{"qdkm"})

تجبرنا تركيبات الأحرف الغريبة "fnks" و "qdmk" على التحول إلى نتيجة فك طريقة wat.getSecure (). المفسد: JADX لها هذه النتيجة.



عند الفحص الدقيق ، يصبح من الواضح أن كل هذا المحتوى غير اللطيف جدًا للطريقة يمكن استبداله بحالة التبديل المعتادة من هذا النوع:

// . . .
switch(CODE)
{
    case «qdkm»: 
        r.2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(2);
        break;
    case «tkog»: 
        r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(1);
        break;
    case «fnks»: 
        String r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(0);
	break;
}
java.lang.StringBuilder r1 = new java.lang.StringBuilder 
r1.<init>(r2) 
java.lang.String r0 = r1.toString() 
java.lang.String r1 = radon(r0)
return r1 

نظرًا لأن "fnks" و "qdmk" يُستخدمان بالفعل للحصول على رمز مميز وتنزيل ملف ، يجب أن يعطي "tkog" عنوان URL المطلوب لطلب قائمة بالملفات المتوفرة على الخادم. يبدو أن هناك أمل في الحصول على المسار المطلوب بثمن بخس ... أولاً وقبل كل شيء ، دعنا نرى كيف يتم تخزين عناوين URL في التطبيق. نفتح وظيفة com.ctf.filedroid.x37AtsW8g.rlieh786d () ونرى أن كل عنوان URL يتم حفظه كصفيف بايت مشفر ، وأن الوظيفة نفسها تشكل سلسلة من هذه البايتات وتعيدها.



حسن. ولكن بعد ذلك يتم تمرير الخط إلى دالة com.ctf.filedroid.wat.radon () ، والتي يتم تقديم تنفيذها إلى المكتبة الأصلية libae3d8oe1.so. عكس الذراع 64؟ محاولة جيدة ، FileDroid ، ولكن يأتي في وقت آخر؟

2. احصل على عناوين url للخادم


دعنا نحاول الاقتراب من الجانب الآخر: لاعتراض حركة المرور والحصول على عناوين URL بنص واضح (وكمكافأة أيضًا ، وقيم المجموع الاختباري والتوقيع!) ، ومطابقتها مع صفائف البايت من com.ctf.filedroid.x37AtsW8g.rlieh786d () - يمكن هل اتضح أن التشفير هو تشفير قيصر أو XOR المعتاد؟ .. فلن يكون من الصعب استعادة عنوان URL الثالث وتنفيذ القائمة.

لإعادة توجيه حركة المرور ، يمكنك استخدام أي وكيل مناسب ( Charles ، عازف الكمان ، BURPإلخ.). نقوم بتكوين إعادة التوجيه على جهاز محمول ، وتثبيت الشهادة المناسبة ، والتحقق من نجاح الاعتراض ، وإطلاق FileFroid. نحن نحاول تنزيل ملف تعسفي و ... انظر "NetworkError". حدث هذا الخطأ بسبب وجود تثبيت الشهادة (راجع أسلوب com.ctf.filedroid.ConnectionHandler.sendRequest): يتحقق عميل الملف من أن الشهادة "سلكية" في التطبيق تتوافق مع الخادم الذي تتفاعل معه. أصبح من الواضح الآن سبب التحكم في سلامة موارد التطبيق!



ومع ذلك ، في حركة المرور التي تم اعتراضها ، يمكننا أن نرى على الأقل اسم نطاق الخادم الذي تم الوصول إليه من قبل عميل الملف ، مما يعني أن الأمل في فك تشفير عناوين URL لا يزال!



دعنا نعود إلى وظيفة com.ctf.filedroid.x37AtsW8g.rlieh786d () ونلاحظ أن العشرات القليلة الأولى من البايت تتزامن في جميع المصفوفات:

cArr[0] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'i', . . .};

cArr[1] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . .};

cArr[2] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . ., '='};

بالإضافة إلى ذلك ، يشير البايت الأخير من المصفوفة الثالثة إلى أنه لم يكن خاليًا من base64. دعنا نحاول فك تشفير البايت الناتج مع جزء معروف من عنوان URL:



يبدو أنه لم يكن أي شخص سعيدًا على الإطلاق باستخدام ARMag3dd0n! الشيء صغير: فك تشفير عناوين URL base64 بالتسلسل و xorim باستخدام المفتاح الذي تم العثور عليه. ولكن ... وإذا لم يكن XOR ، ولكن تشفير تبادلي ذاتي ، لا يمكنك التقاطه حتى مع مائة محاولة؟

3. إعادة بناء apk مع فريدا


كجزء من هذه الكتابة ، سننظر في طريقة حل أكثر إيلامًا (وفي رأينا ، أكثر جمالًا) - باستخدام إطار Frida ، والذي سيسمح لنا بتنفيذ طرق تطبيق apk تعسفية بالحجج التي نحتاجها في وقت التشغيل. للقيام بذلك ، تحتاج إلى هاتف مع حقوق الجذر أو محاكي. نفترض خطة العمل التالية:

  1. تركيب مكونات فريدا على جهاز كمبيوتر وهاتف تجريبي.
  2. استعد عناوين URL المطابقة لرمز أو طلبات الإدراج وقم بتنزيل الملف (باستخدام Frida).
  3. استرجاع قيم المجموع الاختباري والتوقيع للتطبيق الأصلي.
  4. الحصول على قائمة بالملفات المخزنة على الخادم وتحديد الملف المطلوب.
  5. قم بتنزيل الملف وفك تشفيره.

أولاً ، سنوضح العلاقة بين الهاتف الجذر و apk. نقوم بتثبيت التطبيق وتشغيله ، ولكن عميل الملفات لا يريد التمهيد بالكامل ، فهو يومض فقط ويغلق. نتحقق من الرسائل من خلال logcat - نعم ، يشعر FileDroid بالفعل أن شيئًا ما خاطئ ويقاوم قدر الإمكان.



ننتقل مرة أخرى إلى فئة MainActivity ونجد أن طريقة doChecks () تسمى onCreate () ، وتعرض الأخطاء التالية في السجل:



بالإضافة إلى ذلك ، يتحقق onResume () أيضًا لمعرفة ما إذا كان المنفذ النموذجي لـ Frida مفتوحًا:



عميل الملفات الخاص بنا غير متسامح قليلاً إلى التصحيح والجذر وفريدا نفسها. لا يتم تضمين هذه المعارضة على الإطلاق في خططنا ، لذلك نحصل على رمز smali للتطبيق باستخدام أداة apktool، افتح ملف MainActivity.smali في أي محرر نصوص ، وابحث عن طريقة onCreate () وقم بتحويل استدعاء doChecks () إلى تعليق غير ضار:



ثم نحرم طريقة الانتحار () من الفرصة لإغلاق التطبيق حقًا:



بعد ذلك ، دعونا نبني تطبيقنا المحسن قليلاً مرة أخرى باستخدام apktool وتوقيع له عن طريق تنفيذ الأوامر التالية (قد تحتاج إلى حقوق المسؤول):

cd "C:\Program Files\Java\jdk-14\bin"
.\keytool -genkey -v -keystore filedroid.keystore -alias filedroid_alias -keyalg RSA -keysize 2048 -validity 10000
.\jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore  filedroid.keystore filedroid_patched.apk filedroid_alias
.\jarsigner -verify -verbose -certs filedroid_patched.apk

نحن نعيد تثبيت التطبيق على الهاتف ، ونشغله - على عجل ، التنزيل بدون حوادث ، السجل نظيف!



ننتقل إلى تثبيت إطار عمل Frida على جهاز كمبيوتر وجهاز محمول:

$ sudo pip3 install frida-tools
$ wget https://github.com/frida/frida/releases/download/$(frida --version)/frida-server-$(frida --version)-android-arm.xz
$ unxz frida-server-$(frida --version)-android-arm.xz
$ adb push frida-server-$(frida --version)-android-arm /data/local/tmp/frida-server

قم بتشغيل خادم إطار فريدا على جهاز محمول:

$ adb shell su - "chmod 755 /data/local/tmp/frida-server"
$ adb shell su - "/data/local/tmp/frida-server &"  

نحن بصدد إعداد نص برمجي get-urls.js بسيط يستدعي wat.getSecure () لجميع خوادم الطلبات المدعومة:

Java.perform(function () 
{
     const wat = Java.use('com.ctf.filedroid.wat');
	console.log(wat.getSecure("fnks"));
	console.log(wat.getSecure("qdmk"));
	console.log(wat.getSecure("tkog"));
});

أطلقنا FileDroid على الجهاز المحمول و "التشبث" مع نصنا للعملية المقابلة:



4. الحصول على قائمة الملفات الموجودة على الخادم


وأخيرًا ، أصبح الخادم البعيد أقرب إلينا قليلاً! نعلم الآن أن الخادم يدعم الطلبات بالطرق التالية:

  1. مجتمعة في androidroid.neoquest.ru/api/verifyme؟crc= {crc} & sign = {sign}
  2. chedroid.neoquest.ru/api/list_post_apocalyptic_collection؟crc= {crc} & sign = {sign}
  3. لم يتم حفظه في ملف androidroid.neoquest.ru/api/file؟file= {file} & token = {token}

من أجل الحصول على قائمة بالملفات المتوفرة ، يبقى حساب قيم المجموع الاختباري والتوقيع للتطبيق الأصلي ، ثم ترميزها في base64.

سيسمح لك مثل هذا البرنامج النصي في python3 بالقيام بذلك:

المفسد
import hashlib
import binascii
import base64
from asn1crypto import cms, x509
from zipfile import ZipFile


def get_info(apk):
    with ZipFile(apk, 'r') as zipObj:
        classes = zipObj.read("classes.dex")
        resources = zipObj.read("resources.arsc")
        cert = zipObj.read("META-INF/CERT.RSA")
        crc = "%s%s" % (get_crc(classes), get_crc(resources))
        return get_full_crc(classes, resources).decode("utf-8"), get_sign(cert).decode("utf-8")


def get_crc(file):
    crc = binascii.crc32(file) & 0xffffffff
    return crc


def get_full_crc(classes, resources):
    crc = "%s%s" % (get_crc(classes), get_crc(resources))
    return base64.urlsafe_b64encode(bytes(crc, "utf-8"))


def get_sign(file):
    pkcs7 = cms.ContentInfo.load(file)
    data = pkcs7['content']['certificates'][0].chosen.dump()
    sha256 = hashlib.sha256()
    sha256.update(data)
    return base64.urlsafe_b64encode(sha256.digest())  

get_info('filedroid.apk')


يمكنك أيضًا يدويًا. نعتبر CRC32 من classes.dex و resources.arsc أي أداة مناسبة (على سبيل المثال ، لنظام Linux - أداة crc32 القياسية) ، نحصل على القيم 1276945813 و 2814166583 على التوالي ، وقم بربطها (12769458132814166583 ستخرج) وترميز في base64 ، على سبيل المثال ، هنا :



من أجل تنفيذ إجراء مماثل لتوقيع التطبيق ، في نافذة JADX ، انتقل إلى قسم "توقيع APK" ، انسخ قيمة "SHA-256 Fingerprint" وقم بترميزها في base64 كمجموعة بايت:



هام: هام:في ملف apk الأصلي ، يتم تنفيذ ترميز base64 بعلامة URL_SAFE ، أي بدلاً من استخدام الأحرف "+" و "/" و "-" و "_" على التوالي. من الضروري التأكد من أنه سيتم ملاحظة ذلك أيضًا مع الترميز الذاتي. للقيام بذلك ، عند الترميز عبر الإنترنت ، يمكنك استبدال الأبجدية المستخدمة بـ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcde fghijklmnopqrstuvwxyz0123456789 + /" بـ "ABCDEFGHIJKLMNOPQRSTUVWxvppc6464.cp.cp.cp64.cp.cp.cp64.cp.cp64.cp

أخيرًا ، لدينا جميع المكونات لإدراج الملفات بنجاح:

  1. chedroid.neoquest.ru/api/list_post_apocalyptic_collection؟crc= {crc} & sign = {sign}
  2. crc: MTI3Njk0NTgxMzI4MTQxNjY1ODM =
  3. تسجيل الدخول: HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY =

ننفذ طلب GET - ونهتف ، قائمتنا! علاوة على ذلك ، فإن اسم أحد الملفات يتحدث عن نفسه - "مفتوح إذا أردت أن تهرب" - يبدو أننا بحاجة إليه.



بعد ذلك ، نطلب رمز وصول لمرة واحدة ونقوم بتنزيل الملف:

import requests

response = requests.get('https://filedroid.neoquest.ru/api/verifyme', 
    		params={    'crc': 'MTI3Njk0NTgxMzI4MTQxNjY1ODM=', 
'sign': HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY=},
verify=False)
    
token = response.json()['token']
print(token)
response = requests.get('https://filedroid.neoquest.ru/api/file', 
params={'token': token, 'file': '0p3n1fuw4nt2esk4p3.jpg'}, verify=False)

with open("0p3n1fuw4nt2esk4p3.jpg", 'wb') as fd:
        fd.write(response.content)

نفتح الملف الذي تم تنزيله ونتذكر ظرفًا صغيرًا تركناه لوقت لاحق:



5. إضافة قليل من التشفير ...


إيه ، من المبكر جدًا تأجيل FileDroid. دعنا نعود إلى JADX ونرى ما إذا كان مطورو عملاء الملف قد تركوا أي شيء مفيد لنا. نعم ، هذا هو الحال عندما يكون تنظيف التعليمات البرمجية غير شائع بشكل واضح: تنتظر طريقة decryptFile () غير المستخدمة بهدوء انتباهنا في فئة ConnectionHandler. ما لدينا؟

تشفير AES وضع CBC ، sinhroposylka تحتل أول 16 بايت ... الكسل - محرك التقدم ، فمن الأفضل مرة أخرى استخدام Frida وفك شفرة 0p3n1fuw4nt2esk4p3.jpg جهدنا. ولكن مجرد تمرير كمفتاح التشفير؟ لا يوجد العديد من الخيارات ، ولكن نظرًا لوجود طريقة أخرى "منسية" savePlainFile (ملف String ، String token) ، فإن الاختيار واضح.

تحضير البرنامج النصي decrypt.js التالي ( كرمز أشر إلى القيمة الفعلية ، على سبيل المثال ، "HoHknc572mVpZESSQN1Xa7S9zOidxX1PMbykdoM1EXI = '):

Java.perform(function () {
    const JavaString = Java.use('java.lang.String');
    const file_name = JavaString.$new('0p3n1fuw4nt2esk4p3.jpg');
    const ConnectionHandler = Java.use('com.ctf.filedroid.ConnectionHandler');
    const result = ConnectionHandler.savePlainFile(file_name, <token>);
    console.log(result);
});

وضعنا الملف المشفر 0p3n1fuw4nt2esk4p3.jpg على / sdcard / ، وقم بتشغيل FileDroid وحقن البرنامج النصي decrypt.js باستخدام Frida. بعد تشغيل البرنامج النصي ، سيظهر ملف plainfile.jpg على / sdcard /. نفتحه و ... حل فقط!



تتطلب هذه المهمة الصعبة أن يكون لدى المشاركين المعرفة والمهارات في العديد من مجالات أمن المعلومات في وقت واحد ، ويسعدنا أن معظم المنافسين تعاملوا معها بنجاح!

نأمل أن أولئك الذين لم يكن لديهم الوقت أو المعرفة الكافية قبل استلام المفتاح سوف يجتازون الآن مهام مماثلة بنجاح في أي CTF :)

All Articles