
طابِق الدوال في الملفات الثنائية وفقًا لما تفعله، وليس وفقًا لشكل وحدات البايت فيها. بصمة سلوكية للدوال عبر التنفيذ المصغّر.
يطابق fnprint الدوال في الملفات التنفيذية بناءً على ما تفعله، وليس بناءً على شكل بايتاتها أو رسومها البيانية للتحكم في التدفق. يشغّل كل دالة في محاكي صغير بمدخلات مُختلقة، ويسجّل الآثار الجانبية التي تنتجها، ويحوّل هذا السلوك إلى بصمة عبر التجزئة. دالتان تتصرفان بالطريقة نفسها تحصلان على بصمات متشابهة، حتى لو بُنيتا بواسطة مترجم مختلف أو عند مستوى تحسين مختلف.
الهدف من فعل ذلك بهذه الطريقة: تواقيع البايتات (FLIRT، FunctionID) تنكسر
في اللحظة التي يُعاد فيها ترجمة الكود، ومطابقات CFG (BinDiff، Diaphora) تصبح هشة عبر
-O0 مقابل -O3. السلوك يصمد أمام كليهما بشكل أفضل بكثير.
x86-64 ELF فقط في الوقت الحالي. راجع الحدود قبل أن تثق به.
وجّهه إلى ملف تنفيذي مجرّد من الرموز وإلى مجموعة من الأشياء التي لديك أسماء لها بالفعل:
$ strip --strip-all mystery.so
$ nm mystery.so
nm: mystery.so: no symbols
$ fnprint index libz.so -o corpus.db # a build you have symbols for
$ fnprint query mystery.so --corpus corpus.db
named 9 function(s):
0x000022f9 100.0% adler32_z
0x00002a8d 100.0% compress2
0x0000ad5d 100.0% inflateBackEnd
0x0000adc4 86.7% inflate_fast
0x00002e67 80.5% crc32_z
...
هذا التشغيل عبارة عن بناء -O0 مجرّد بالكامل من الرموز، تم تسميته من مجموعة -O2. مستوى
تحسين مختلف، صفر رموز متبقية، والأسماء تعود صحيحة. يسمّي ما يثق به ويصمت عن الباقي.
الشيء الآخر الذي يفعله هو مقارنة بناءين وإخبارك بالدوال التي تغيّر سلوكها، وهذا مفيد عندما يطرح مورّد برنامجًا ثابتًا جديدًا وتريد معرفة ما الذي تحرّك فعلاً:
$ fnprint match old.so new.so
compared 84 functions present in both
unchanged: 53
changed: 1
low-signal: 31 (too small to judge)
changed behavior (lowest similarity first):
61.7% deflate_stored
للعمل الفعلي على الثغرات المعروفة (n-day) هناك triage. ابنِ مجموعة من النسخة
المعروفة بأنها قابلة للاستغلال لدالة ما وأخرى من النسخة المُرقّعة، ثم رتّب بناءً
مجهولاً مقابل كليهما. الدالة القريبة من الجانب القابل للاستغلال والمنفصلة بوضوح عن
الجانب المُرقّع هي ما تريد وضعه أمام إنسان، وليس مجرد درجة تطابق واحدة عليك تفسيرها:
$ fnprint index vuln.so -o vuln.db
$ fnprint index patched.so -o patched.db
$ fnprint triage mystery.so --vuln vuln.db --patched patched.db
43 functions triaged: 1 look vulnerable, 0 patched, 42 inconclusive
review queue (vuln-leaning, strongest first):
addr vuln% patched% margin matches
0x000022f9 100.0 27.3 +72.7 adler32_z vs crc32_z
الدوال الـ 42 المتطابقة في كلا النسختين تعود بنتيجة غير حاسمة عن قصد، فلا يمكن نسبها
إلى أي من الجانبين ولا ينبغي الإشارة إليها. يتحكم --margin و--min-sim في مدى
وجوب انفصال الجانبين قبل أن يبتّ.
لكل دالة:
لا بيانات تدريب، لا نموذج. الفكرة نفسها تظهر في الأدبيات باسم Blanket Execution (Egele وآخرون، USENIX Security 2014)؛ وfnprint هو تطبيق عملي ومُصان لها مع واجهة سطر أوامر يمكنك استخدامها فعلاً.
يحتاج إلى سلسلة أدوات rust ومكتبتي unicorn وcapstone.
# debian/ubuntu/kali
sudo apt install libunicorn-dev libcapstone-dev
cargo install --path cli
# or just
cargo build --release # binary at target/release/fnprint
fnprint index <binary> [-o out.db] fingerprint every function, optionally to a db
fnprint match <a> <b> diff two binaries (or .db files) by behavior
fnprint query <target> --corpus <db> name unknown functions from a corpus
fnprint triage <t> --vuln <db> --patched <db> rank a build against vuln vs patched corpora
fnprint eval <a> <b> accuracy metrics using symbol names as truth
fnprint dump <binary> <func> print the recorded effect trace (debugging)
يأخذ match وquery إما ملف ELF أو ملف .db بنيته بـ index، بحيث يمكنك
أخذ بصمة مجموعة مرة واحدة وإعادة استخدامها.
كل أمر يأخذ --format عامًا:
fnprint query mystery.so --corpus corpus.db --format json > names.json
fnprint query mystery.so --corpus corpus.db --format r2 > fnprint.r2
json مخطط مستقر للسكربتات (ونموذج الاستيراد Ghidra في contrib/)،
وr2 يصدر أوامر إعادة تسمية afn تشغّلها داخل rizin/radare2 عبر . fnprint.r2.
تُنقّى أسماء الرموز من الهدف قبل وصولها إلى أي منهما، بحيث لا يمكن لاسم مُصمّم
أن يحقن أوامر r2. المخططات والإعداد في
docs/integrations.md.
الفهرسة أحادية العملية افتراضيًا. FNPRINT_SHARDS=N fnprint index ... يوزّع
فهرسًا كبيرًا على N من العمال المعزولين؛ وتكون المجموعة متطابقة بايتًا بأي قيمة لـ N.
وهو يساعد فقط على الملفات التنفيذية الكبيرة الغنية بالدوال، لذا فهو معطّل ما لم تطلبه.
دقة المرتبة الأولى هي: بالنسبة لدالة في البناء A، رتّب كل دالة في البناء B حسب
التشابه، هل أفضل نتيجة هي الصحيحة. هذه بالضبط مهمة التسمية بعد تجريد الرموز.
مقيسة على zlib 1.3.1 (84 دالة)، قابلة لإعادة الإنتاج عبر bench/run.sh:
| pair | rank-1 | precision |
|---|---|---|
| gcc O0 -> O1 | 97.1% | 93.8% |
| gcc O0 -> O2 | 93.1% | 83.3% |
| gcc O0 -> O3 | 91.3% | 100.0% |
| gcc/clang O0 | 97.7% | 97.1% |
| gcc O2 -> O3 | 56.5% | 66.7% |
| gcc/clang O2 | 59.1% | 50.0% |
الجدول الكامل بالإضافة إلى مكتبة ثانية (lua) في bench/NUMBERS.md.
يُبلّغ eval أيضًا عن recall@3 / recall@5 ومعدل الامتناع، لأن المرتبة الأولى
وحدها تخفي الكثير. على gcc O0 -> O2 تكون أفضل نتيجة صحيحة 93% من الوقت لكن
الدالة الصحيحة تكون ضمن أفضل 5 بنسبة 96.6% من الوقت، لذا تُغلق ميزانية مراجعة
صغيرة معظم الفجوة. كما يمتنع (يرفض إصدار حكم "متطابق" واثق)
عن الأزواج التي ليس متأكدًا منها بدلاً من التخمين، ولهذا تبقى الدقة
عالية بينما يكون الاستدعاء عند العتبة نفسها منخفضًا.
القراءة الصادقة: عندما يكون لدى أحد الجانبين على الأقل بعض الثراء السلوكي (أي شيء
بـ -O0/-O1، أو زوج عبر مترجمين عند -O0) فإنه يقع في نطاق 80-98%.
عندما يكون الجانبان محسّنين بكثافة يصبح السلوك الذي يمكننا ملاحظته
رقيقًا ويهبط نحو رمي العملة. هذه هي الحدود الصعبة لمطابق واحد
بلا تدريب، وهذا لا يدّعي خلاف ذلك.
match. يلتقط التغييرات البنيوية وتغييرات المسار المبكر،
وليس كل تعديل عميق.fnprint هو الخيار بلا تدريب، السلوك أولاً. نموذج الآثار محايد تجاه المعمارية بالفعل، وهو الأساس للمطابقة عبر وحدات المعالجة المركزية.
explore_depth،
معطّل افتراضيًا) لكنه يضيف ضوضاء خاصة بالبناء على مسارات مستحيلة وأضرّ
بدقة المطابقة عبر البناء في الاختبار، لذا يحتاج إلى مرشّح اتساق مسارات قبل
أن يستحق مكانه.--format r2)؛ وإضافة ghidra حقيقية هي التالية.
يوجد نموذج استيراد jython تجريبي في contrib/ في هذه الأثناء.MIT. راجع LICENSE.