
طابِق الدوال في الملفات الثنائية وفقًا لما تفعله، وليس وفقًا لشكل وحدات البايت فيها. بصمة سلوكية للدوال عبر التنفيذ المصغّر.
fnprint يطابق الدوال في الملفات التنفيذية بناءً على ما تفعله، وليس بناءً على شكل بايتاتها أو رسومها البيانية للتحكم في التدفق. يشغّل كل دالة في محاكي صغير بمدخلات مُختلقة، ويسجّل الآثار الجانبية التي تنتجها، ويحوّل هذا السلوك إلى بصمة. دالتان تتصرفان بالطريقة نفسها تحصلان على بصمات متشابهة، حتى لو بُنيتا بمترجم مختلف، أو بمستوى تحسين مختلف، أو لمعالج مختلف.
الهدف من فعل ذلك بهذه الطريقة: بصمات البايتات (FLIRT، FunctionID) تنكسر
في اللحظة التي يُعاد فيها ترجمة الكود، ومطابقات CFG (BinDiff، Diaphora) تصبح متزعزعة بين
-O0 و -O3 وتستسلم بين المعماريات. السلوك يصمد أمام كل ذلك
بشكل أفضل بكثير. لا بيانات تدريب، لا نموذج، ملف تنفيذي ثابت واحد.
ثلاثة أشياء يفعلها لا يفعلها الآخرون:
-O0 مقابل aarch64 -O0: 97.7% من الدوال سُمّيت بشكل صحيح.triage يمنحك حكماً، لا فرقاً. مجموعتان (معروفة الثغرات،
مُرقّعة)، وهامش، وقائمة مراجعة للدوال التي تميل نحو الثغرة.
هذا هو سير عمل n-day كأمر واحد.x86-64 و aarch64 ELF. انظر limits قبل أن تثق به.
وجّهه نحو ملف تنفيذي مجرّد ومجموعة من الأشياء التي تملك أسماءها بالفعل:
$ 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 --threshold 0.6
named 27 function(s):
addr sim graph name
0x000022f9 100.0% 0% adler32_z (libz.so)
0x00002a8d 100.0% 0% compress2 (libz.so)
0x00004500 100.0% 0% deflateStateCheck (libz.so)
0x0000ad5d 100.0% 0% inflateBackEnd (libz.so)
0x0000b69c 100.0% 0% inflateStateCheck (libz.so)
0x00012f8c 100.0% 0% _tr_tally (libz.so)
...
هذا التشغيل هو بناء -O0 مجرّد بالكامل سُمّي من مجموعة -O2. مستوى تحسين
مختلف، صفر رموز متبقية، 27 اسماً تعود وكل الـ 27 صحيحة. يسمّي ما هو واثق
منه ويصمت عن الباقي. (الـ gif أعلاه من 0.5، الذي وجد 9؛ 0.6 يستعيد أيضاً
الدوال الثابتة التي يخفيها ملف .so مجرّد خلف صادراته.)
الشيء نفسه عبر المعالجات. مجموعة من بناء x86-64، والهدف هو بناء aarch64 من المصدر نفسه، والرموز ذهبت:
$ fnprint index libz_x86_64.so -o corpus.db
$ fnprint query libz_arm64_stripped.so --corpus corpus.db
named 42 function(s):
addr sim graph name
0x00001af4 100.0% 0% adler32_z (libz_x86_64.so)
0x0000263c 100.0% 0% compress2 (libz_x86_64.so)
0x00002958 100.0% 100% x2nmodp (libz_x86_64.so)
0x00002a98 100.0% 0% crc32_z (libz_x86_64.so)
0x000034c4 100.0% 100% crc32_combine64 (libz_x86_64.so)
...
42 سُمّيت، 42 صحيحة، من مجموعة بُنيت لمعالج مختلف.
الشيء الآخر الذي يفعله هو مقارنة بناءين وإخبارك أي الدوال غيّرت سلوكها، وهو مفيد عندما يطلق مورّد برنامجاً ثابتاً جديداً وتريد أن تعرف ما الذي تحرّك فعلاً. يعمل مع أو بدون أسماء الرموز: معها يحاذي بالاسم، وبدونها يحاذي البناءين على السلوك بالإضافة إلى رسم الاستدعاءات، بحيث يمكن مقارنة صورتين ثابتتين مجرّدتين بالكامل:
$ fnprint match old.so new.so
aligned 38 function pairs by behavior + call graph (no symbol names needed)
unchanged: 31
changed: 7
low-signal: 57 (too small to judge)
changed behavior (lowest similarity first):
30.5% compress_block
43.0% deflate_rle -> deflate_huff
68.8% deflate_slow
لعمل 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 في مدى صرامة انفصال الجانبين قبل أن يلتزم.
لكل دالة:
strip. الاستدعاءات الداخلية مجهولة في
المخرجات وتصبح حواف رسم استدعاءات بدلاً من ذلك، من مسح ثابت للجسم.-O2 كلها تُضمّن
فحص الحالة نفسه من المطالبة به جميعاً.لا بيانات تدريب، لا نموذج. الفكرة نفسها تظهر في الأدبيات باسم Blanket Execution (Egele وآخرون، USENIX Security 2014)؛ fnprint هو تناول عملي ومُصان لها مع CLI يمكنك استخدامه فعلاً. نموذج التأثير محايد تجاه المعمارية بالبناء، ولهذا تصطف بصمات x86-64 و aarch64 من المصدر نفسه دون أي شيء مُتعلَّم.
يحتاج سلسلة أدوات rust، و cmake، وسلسلة أدوات C. unicorn (التفرع بدون JIT) و capstone يُبنيان من المصدر بواسطة cargo، لا شيء لتثبيته عبر apt.
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)
fnprint completions <shell> print a shell completion script to stdout
يأخذ match و query إما ملف ELF أو ملف .db بنيته بـ index، بحيث
يمكنك أخذ بصمة مجموعة مرة وإعادة استخدامها. index -o يُلحق، بحيث يمكن للمجموعة
أن تحمل عدة ملفات تنفيذية.