
مؤلف المقال https://github.com/Nalen98
مساء الخير
كان موضوع بحثي كجزء من التدريب الصيفي الصيفي لـ Summer of Hack 2019 في Digital Security هو "إلغاء ترجمة eBPF في Ghidra". كان من الضروري تطوير نظام eBPF bytecode للترجمة بلغة Sleigh في PCode Ghidra حتى يكون قادرًا على تفكيك برامج eBPF وإلغاء ترجمتها. نتيجة الدراسة هي امتداد مطور لـ Ghidra يضيف دعمًا لمعالج eBPF. يمكن اعتبار الدراسة ، مثلها مثل دراسة المتدربين الآخرين ، "بحق لأول مرة" ، لأنه في وقت سابق لم يكن من الممكن فك شفرة eBPF في أدوات الهندسة العكسية الأخرى.
قبل التاريخ
ذهب هذا الموضوع إلي بسخرية كبيرة من المصير ، لأنني لم أكن على دراية بـ eBPF من قبل ، ولم يستخدمه غدر من قبل ، لأن هناك بعض العقائد التي تقول "IDA Pro أفضل". كما اتضح ، هذا ليس صحيحا تماما.
اتضح أن عملية التعارف مع Ghidra كانت سريعة جدًا ، حيث قام مطوروها بوضع وثائق ذات كفاءة عالية ويمكن الوصول إليها. أيضا ، كان لا بد لي من إتقان لغة مواصفات معالج Sleigh ، والتي تم تنفيذ التطوير بها. بذل المطورون قصارى جهدهم وأنشأوا وثائق مفصلة للغاية لكل من الأداة نفسها و Sleigh ، والتي بفضلها جزيل الشكر لهم.
على الجانب الآخر من الحاجز كان هناك مرشح Berkeley Packet Filter. eBPF عبارة عن جهاز افتراضي في Linux kernel يسمح لك بتحميل كود مستخدم اعتباطي يمكن استخدامه لتتبع العمليات وتصفية الحزم في مساحة kernel. الهندسة المعمارية هي آلة تسجيل RISC مع 11 تسجيل 64 بت ، عداد برامج ومكدس 512 بايت. هناك عدد من القيود على eBPF:
- دورات محظورة ؛
- الوصول إلى الذاكرة ممكن فقط من خلال المكدس (ستكون هناك قصة منفصلة حول هذا الموضوع) ؛
- تتوفر وظائف kernel فقط من خلال وظائف مجمّع خاصة (مساعدين eBPF).

هيكل eBPF التكنولوجيا. مصدر الصورة: http://www.brendangregg.com/ebpf.html .
بشكل أساسي ، يتم استخدام هذه التقنية في مهام الشبكة - تصحيح الأخطاء ، تصفية الحزمة ، وما إلى ذلك على مستوى kernel. تمت إضافة دعم eBPF منذ الإصدار 3.15 من النواة ؛ تم تخصيص عدد قليل من التقارير لهذه التكنولوجيا في مؤتمر سباكين Linux 2019. لكن في eBPF ، بخلاف Ghidra ، الوثائق غير مكتملة ولا تحتوي على الكثير. لذلك ، يجب البحث عن التوضيحات والمعلومات المفقودة على الإنترنت. استغرق الأمر بعض الوقت للعثور على الإجابات ، وكل ما تبقى هو الأمل في أن يتم الانتهاء من هذه التكنولوجيا وسيتم إنشاء الوثائق العادية.
وثائق سيئة
لتطوير مواصفات لـ Sleigh ، تحتاج أولاً إلى فهم كيفية عمل بنية المعالج الهدف. وهنا ننتقل إلى الوثائق الرسمية.
يحتوي على عدد من العيوب:
لم يتم وصف بنية تعليمات eBPF بشكل كامل.
تشير معظم المواصفات ، مثل Intel x86 ، عادةً إلى ما يذهب إليه كل جزء من التعليمات ، أي الكتلة التي تنتمي إليها. لسوء الحظ ، في مواصفات eBPF ، هذه التفاصيل إما مبعثرة في جميع أنحاء الوثيقة أو غائبة تمامًا ، ونتيجة لذلك يتعين علينا استخلاص الحبوب المفقودة من تفاصيل التنفيذ في نواة Linux.
على سبيل المثال ، في بنية التعليمة op:8, dst_reg:4, src_reg:4, off:16, imm:32
لا توجد كلمة تقول أن الإزاحة (off) و فوري (imm) موقّعتان ، وهذا مهم للغاية ، لأنه يؤثر على للعمل من التعليمات الحسابية ليقفز. ساعد التعليمات البرمجية المصدر لنظام Linux kernel.
لا توجد صورة كاملة لكل فن العمارة ممكن.
في بعض الوثائق ، وليس فقط جميع التعليمات ، تتم الإشارة إلى معاملاتها ، ولكن أيضًا دلالاتها في C ، وحالات التطبيق ، وميزات المعامل ، وما إلى ذلك. تحتوي وثائق eBPF على فصول التعليمات ، ولكن هذا لا يكفي للمطور. لننظر فيها بمزيد من التفصيل.
جميع تعليمات eBPF هي 64 بت ، باستثناء LDDW
(تحميل الكلمة المزدوجة) ، ويبلغ حجمها 128 بت ، وتسلسل اثنين من imm مع 32 بت لكل منهما. تحتوي تعليمات eBPF على البنية التالية.

eBPF ترميز التعليمات
يعتمد هيكل حقل OPAQUE
على فئة التعليمات (ALU / JMP ، Load / Store).
على سبيل المثال ، فئة تعليمات ALU
:

تعليمات ALU الترميز
وفئة JMP
لها بنية حقلية خاصة بها:

فرع تعليمات الترميز
بالنسبة لتعليمات التحميل / المتجر ، يختلف الهيكل:

تحميل / تخزين تعليمات الترميز
ساعدت وثائق eBPF غير الرسمية على حل هذه المشكلة .
لا توجد معلومات حول المساعدين للاتصال ، حيث تم بناء معظم برامج eBPF لمنصة Linux kernel.
وهذا غريب للغاية ، لأن المساعدين هم أهم شيء في برامج eBPF ، فهم يؤدون المهام التي تركز عليها التكنولوجيا.

إمكانية التشغيل التفاعلي EBPF مع الوظائف النووية
يسحب البرنامج هذه الوظائف من kernel ، ويعملون فقط مع العمليات ، ويتعاملون مع حزم الشبكة ، ويعملون مع خرائط eBPF ، ومآخذ الوصول ، ويتفاعلون مع المستخدمين. على الرغم من حقيقة أن الوظائف لا تزال نووية ، إلا أنه في الوثائق الرسمية ، يجدر الكتابة بمزيد من التفاصيل عنها. تم العثور على التفاصيل الكاملة في مصدر Linux.
- ليست كلمة عن دعوات الذيل.

المكالمات الذيل EBPF. مصدر الصورة: https://cilium.readthedocs.io/ar/latest/bpf/#tail-calls .
المكالمات الخلفية هي آلية تسمح لأحد برامج eBPF باستدعاء برنامج آخر دون الرجوع إلى البرنامج السابق ، أي القفز بين برامج eBPF المختلفة. لا يتم تنفيذها في الملحق المطور ، ويمكن الاطلاع على معلومات مفصلة في وثائق Cilium .
كانت التوثيق السيئ وعدد من السمات المعمارية لـ eBPF هي "الشظايا" الرئيسية في التطوير ، لأنها تسببت في مشاكل أخرى. لحسن الحظ ، تم حل معظمهم بنجاح.
حول بيئة التطوير

لا يعلم جميع المطورين أنه لإنشاء وتحرير رمز Sleigh وعمومًا كافة ملفات الامتداد / البرنامج المساعد لـ Ghidra ، توجد أداة ملائمة إلى حد ما - Eclipse IDE مع دعم الإضافات GhidraDev و GhidraSleighEditor . عند إنشاء الامتداد ، سيتم تأطيره على الفور في شكل مسودة عمل ، وهناك تسليط الضوء مناسب إلى حد كبير على رمز Sleigh ، وكذلك مدقق للأخطاء الرئيسية في بناء جملة اللغة.
في Eclipse ، يمكنك تشغيل Ghidra (بالفعل مع التمديد قيد التشغيل) ، debug ، وهو أمر مريح للغاية. ولكن ربما تكون أروع فرصة لدعم وضع "Ghidra Headless" ، لا تحتاج إلى إعادة تشغيل Ghidr من واجهة المستخدم الرسومية 100500 مرة للعثور على خطأ في الكود ، يتم تنفيذ جميع العمليات في الخلفية.
المفكرة يمكن أن تكون مغلقة! ويمكنك تنزيل Eclipse من الموقع الرسمي . لتثبيت المكوّن الإضافي ، حدد Ecplise في تعليمات Ecplise → ، ثم انقر فوق Add (إضافة) وحدد أرشيف zip plugin.
تطوير التمديد
بالنسبة للامتداد ، تم تطوير ملفات مواصفات المعالج ، وهي أداة تحميل ترث من أداة التحميل الرئيسية ELF وتوسع قدراتها من حيث التعرف على برامج eBPF ، ومعالج نقل لتطبيق خرائط eBPF في أداة إزالة تفكيك Ghidra و decompiler ، وكذلك محلل لتحديد توقيعات مساعد eBPF.


ملفات التمديد كمشروع في Eclipse IDE
الآن عن الملفات الرئيسية:
.cspec
- يشير إلى أنواع البيانات المستخدمة ، ومقدار الذاكرة المخصص لها في eBPF ، ويتم تعيين حجم المكدس ، ويتم تعيين الملصق "stackpointer" على تسجيل R10
، ويتم توقيع اتفاقية الاتصال. تم تنفيذ الاتفاقية (مثل البقية) وفقًا للوثائق:
لذلك ، يتم تعريف اصطلاح النداء eBPF على النحو التالي:
- R0 - قيمة الإرجاع من وظيفة in-kernel ، وقيمة الخروج لبرنامج eBPF
- R1 - R5 - الوسائط من برنامج eBPF إلى وظيفة في النواة
- R6 - R9 - السجلات المحفوظة التي سيتم حفظها في وظيفة النواة
- R10 - مؤشر إطار للقراءة فقط للوصول إلى المكدس
eBPF.cspec <?xml version="1.0" encoding="UTF-8"?> <compiler_spec> <data_organization> <absolute_max_alignment value="0" /> <machine_alignment value="2" /> <default_alignment value="1" /> <default_pointer_alignment value="4" /> <pointer_size value="4" /> <wchar_size value="4" /> <short_size value="2" /> <integer_size value="4" /> <long_size value="4" /> <long_long_size value="8" /> <float_size value="4" /> <double_size value="8" /> <long_double_size value="8" /> <size_alignment_map> <entry size="1" alignment="1" /> <entry size="2" alignment="2" /> <entry size="4" alignment="4" /> <entry size="8" alignment="8" /> </size_alignment_map> </data_organization> <global> <range space="ram"/> <range space="syscall"/> </global> <stackpointer register="R10" space="ram"/> <default_proto> <prototype name="__fastcall" extrapop="0" stackshift="0"> <input> <pentry minsize="1" maxsize="8"> <register name="R1"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R2"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R3"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R4"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R5"/> </pentry> </input> <output killedbycall="true"> <pentry minsize="1" maxsize="8"> <register name="R0"/> </pentry> </output> <unaffected> <varnode space="ram" offset="8" size="8"/> <register name="R6"/> <register name="R7"/> <register name="R8"/> <register name="R9"/> <register name="R10"/> </unaffected> </prototype> </default_proto> </compiler_spec>
قبل المتابعة لوصف ملفات التطوير ، سوف .cspec
إلى سطر صغير من ملف .cspec
.
<stackpointer register="R10" space="ram"/>
إنه المصدر الرئيسي للشر عند إلغاء ترجمة eBPF في Ghidra ، وقد بدأ رحلة مثيرة إلى كومة eBPF ، التي شهدت عددًا من اللحظات غير السارة ، والتي جلبت معظم الألم إلى التطور.
كل ما نحتاجه هو ... كومة
لنلقِ نظرة على وثائق النواة الرسمية :
س: هل يمكن لبرامج BPF الوصول إلى مؤشر التعليمات أو إرجاع العنوان؟
ج: لا.
س: هل يمكن لبرامج BPF الوصول إلى مؤشر المكدس؟
ج: لا. يمكن الوصول إلى مؤشر الإطار فقط (تسجيل R10). من وجهة نظر برنامج التحويل البرمجي ، من الضروري أن يكون لديك مؤشر مكدس. على سبيل المثال ، يعرّف LLVM السجل R11 كمؤشر مكدس في الواجهة الخلفية لـ BPF الخاصة به ، لكنه يتأكد من أن الكود الذي تم إنشاؤه لا يستخدمه أبدًا.
لا يحتوي المعالج على مؤشر تعليمات (IP) أو مؤشر مكدس (SP) ، والأخير مهم للغاية بالنسبة لـ Ghidra ، وتعتمد جودة إلغاء ترجمته على ذلك. في ملف cspec
، تحتاج إلى تحديد السجل الذي هو مؤشر المكدس (كما هو موضح أعلاه). R10
هو سجل eBPF الوحيد الذي يسمح بالوصول إلى حزمة البرامج ، وهو مؤشر فرعي ، وهو ثابت ودائمًا صفريًا. تعليق التسمية "stackpointer" على R10
في ملف cspec
خطأ جوهري ، لكن لا توجد خيارات أخرى ، لأن Ghidra لن يعمل مع مكدس البرنامج. وفقًا لذلك ، فإن SP الأصلي غائب ، ولا شيء يحل محله في بنية eBPF.
تنشأ عدة مشاكل من هذا:
سيتم ضمان أن يكون حقل "Stack Depth" في Ghidra صفراً ، حيث يتعين علينا ببساطة تعيين R10
كمكدس في هذه الظروف المعمارية ، وهو في جوهره دائمًا صفري ، وقد تمت مناقشته سابقًا. سيعكس "عمق المكدس" السجل مع التسمية "stackpointer".
وعليك أن تتحملها ، فهذه هي ميزات العمارة.
التعليمات التي تعمل على R10
(أي ، تلك التي تتعامل مع المكدس) لا يتم فك تشفيرها في الغالب. Ghidra بشكل عام لا يقوم بفك تشفير ما يعتبره الكود الميت (أي المقتطفات التي لا يتم تنفيذها مطلقًا). ونظرًا لأن R10
قابل للتغيير ، يتم التعرف على العديد من إرشادات المتجر / التحميل من قِبل Ghidr باعتبارها رمزًا نهائيًا وتختفي من أداة فك التشفير.
لحسن الحظ ، تم حل هذه المشكلة عن طريق كتابة محلل مخصص ، وكذلك الإعلان عن مساحة عنوان إضافية مع مساعدي eBPF في ملف pspec
، والذي تم طلبه من أحد مطوري Ghidra في مشروع Issue .
تطوير الإرشاد (تابع)
.ldefs
يصف ميزات المعالج ، ويحدد ملفات المواصفات.
eBPF.ldefs <?xml version="1.0" encoding="UTF-8"?> <language_definitions> <language processor="eBPF" endian="little" size="64" variant="default" version="1.0" slafile="eBPF.sla" processorspec="eBPF.pspec" id="eBPF:LE:64:default"> <description>eBPF processor 64-bit little-endian</description> <compiler name="default" spec="eBPF.cspec" id="default"/> <external_name tool="DWARF.register.mapping.file" name="eBPF.dwarf"/> </language> </language_definitions>
.opinion
ملف .opinion
التحميل إلى المعالج.
eBPF.opinion <opinions> <constraint loader="Executable and Linking Format (ELF)" compilerSpecID="default"> <constraint primary="247" processor="eBPF" endian="little" size="64" /> </constraint> </opinions>
يتم الإعلان عن عداد البرنامج بالملحق .pspec ، لكن مع eBPF ، يكون ضمنياً ولا يُستخدم في المواصفات بأي شكل من الأشكال ، لذلك فهو مخصص للأغراض الشكلية فقط. بالمناسبة ، يكون PC
الخاص بـ eBPF حسابيًا ، وليس عنوانًا (يشير إلى التعليمات ، وليس البايت المحدد للبرنامج) ، ضع ذلك في الاعتبار عند القفز.
يحتوي الملف أيضًا على مساحة عنوان إضافية لمساعدي eBPF ، حيث يتم الإعلان عنها كأحرف.
eBPF.pspec <?xml version="1.0" encoding="UTF-8"?> <processor_spec> <programcounter register="PC"/> <default_symbols> <symbol name="bpf_unspec" address="syscall:0x0"/> <symbol name="bpf_map_lookup_elem" address="syscall:0x1"/> <symbol name="bpf_map_update_elem" address="syscall:0x2"/> <symbol name="bpf_map_delete_elem" address="syscall:0x3"/> <symbol name="bpf_probe_read" address="syscall:0x4"/> <symbol name="bpf_ktime_get_ns" address="syscall:0x5"/> <symbol name="bpf_trace_printk" address="syscall:0x6"/> <symbol name="bpf_get_prandom_u32" address="syscall:0x7"/> <symbol name="bpf_get_smp_processor_id" address="syscall:0x8"/> <symbol name="bpf_skb_store_bytes" address="syscall:0x9"/> <symbol name="bpf_l3_csum_replace" address="syscall:0xa"/> <symbol name="bpf_l4_csum_replace" address="syscall:0xb"/> <symbol name="bpf_tail_call" address="syscall:0xc"/> <symbol name="bpf_clone_redirect" address="syscall:0xd"/> <symbol name="bpf_get_current_pid_tgid" address="syscall:0xe"/> <symbol name="bpf_get_current_uid_gid" address="syscall:0xf"/> <symbol name="bpf_get_current_comm" address="syscall:0x10"/> <symbol name="bpf_get_cgroup_classid" address="syscall:0x11"/> <symbol name="bpf_skb_vlan_push" address="syscall:0x12"/> <symbol name="bpf_skb_vlan_pop" address="syscall:0x13"/> <symbol name="bpf_skb_get_tunnel_key" address="syscall:0x14"/> <symbol name="bpf_skb_set_tunnel_key" address="syscall:0x15"/> <symbol name="bpf_perf_event_read" address="syscall:0x16"/> <symbol name="bpf_redirect" address="syscall:0x17"/> <symbol name="bpf_get_route_realm" address="syscall:0x18"/> <symbol name="bpf_perf_event_output" address="syscall:0x19"/> <symbol name="bpf_skb_load_bytes" address="syscall:0x1a"/> <symbol name="bpf_get_stackid" address="syscall:0x1b"/> <symbol name="bpf_csum_diff" address="syscall:0x1c"/> <symbol name="bpf_skb_get_tunnel_opt" address="syscall:0x1d"/> <symbol name="bpf_skb_set_tunnel_opt" address="syscall:0x1e"/> <symbol name="bpf_skb_change_proto" address="syscall:0x1f"/> <symbol name="bpf_skb_change_type" address="syscall:0x20"/> <symbol name="bpf_skb_under_cgroup" address="syscall:0x21"/> <symbol name="bpf_get_hash_recalc" address="syscall:0x22"/> <symbol name="bpf_get_current_task" address="syscall:0x23"/> <symbol name="bpf_probe_write_user" address="syscall:0x24"/> </default_symbols> <default_memory_blocks> <memory_block name="eBPFHelper_functions" start_address="syscall:0" length="0x200" initialized="true"/> </default_memory_blocks> </processor_spec>
.sinc
ملف .sinc
هو ملف التمديد الأكثر حجمًا ، حيث يتم تعريف جميع السجلات وهيكل تعليمة eBPF والرموز وأسلوب الاستذكار ودلالات التعليمات في Sleigh هنا.
EBPF.sinc مقتطف صغير define space ram type=ram_space size=8 default; define space register type=register_space size=4; define space syscall type=ram_space size=2; define register offset=0 size=8 [ R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 PC ]; define token instr(64) imm=(32, 63) signed off=(16, 31) signed src=(12, 15) dst=(8, 11) op_alu_jmp_opcode=(4, 7) op_alu_jmp_source=(3, 3) op_ld_st_mode=(5, 7) op_ld_st_size=(3, 4) op_insn_class=(0, 2) ; #We'll need this token to operate with LDDW instruction, which has 64 bit imm value define token immtoken(64) imm2=(32, 63) ; #To operate with registers attach variables [ src dst ] [ R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 _ _ _ _ _ ]; … :ADD dst, src is src & dst & op_alu_jmp_opcode=0x0 & op_alu_jmp_source=1 & op_insn_class=0x7 { dst=dst + src; } :ADD dst, imm is imm & dst & op_alu_jmp_opcode=0x0 & op_alu_jmp_source=0 & op_insn_class=0x7 { dst=dst + imm; } …
يعمل مُحمل eBPF على توسيع القدرات الأساسية لمحمل ELF بحيث يمكنه التعرف على أن البرنامج الذي قمت بتنزيله على Ghidra يحتوي على معالج eBPF. بالنسبة له ، يتم تخصيص ثابت BPF في ElfConstants
Ghidra ، ويحدد المحمل معالج eBPF منه.
eBPF_ElfExtension.java package ghidra.app.util.bin.format.elf.extend; import ghidra.app.util.bin.format.elf.*; import ghidra.program.model.lang.*; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; public class eBPF_ElfExtension extends ElfExtension { @Override public boolean canHandle(ElfHeader elf) { return elf.e_machine() == ElfConstants.EM_BPF && elf.is64Bit(); } @Override public boolean canHandle(ElfLoadHelper elfLoadHelper) { Language language = elfLoadHelper.getProgram().getLanguage(); return canHandle(elfLoadHelper.getElfHeader()) && "eBPF".equals(language.getProcessor().toString()) && language.getLanguageDescription().getSize() == 64; } @Override public String getDataTypeSuffix() { return "eBPF"; } @Override public void processGotPlt(ElfLoadHelper elfLoadHelper, TaskMonitor monitor) throws CancelledException { if (!canHandle(elfLoadHelper)) { return; } super.processGotPlt(elfLoadHelper, monitor); } }
مطلوب معالج الترحيل لتطبيق خرائط eBPF في أداة فك التشفير و decompiler. يتم التفاعل معهم من خلال عدد من المساعدين ، تستخدم الوظائف واصف ملف للإشارة إلى الخرائط. استنادًا إلى جدول النقل ، يمكن ملاحظة أن المُحمل يقوم بتصحيح تعليمة LDDW ، والتي تنشئ Rn
لهؤلاء المساعدين (على سبيل المثال ، bpf_map_lookup_elem(…)
).
لذلك ، يقوم المعالج بتوزيع جدول نقل البرنامج ، والعثور على عناوين النقل (التعليمات) ، وكذلك بجمع معلومات السلسلة حول اسم الخريطة. علاوة على ذلك ، بالرجوع إلى جدول الرموز ، فإنه يحسب العناوين الحقيقية لهذه الخرائط وبقع التعليمات.
eBPF_ElfRelocationHandler.java public class eBPF_ElfRelocationHandler extends ElfRelocationHandler { @Override public boolean canRelocate(ElfHeader elf) { return elf.e_machine() == ElfConstants.EM_BPF; } @Override public void relocate(ElfRelocationContext elfRelocationContext, ElfRelocation relocation, Address relocationAddress) throws MemoryAccessException, NotFoundException { ElfHeader elf = elfRelocationContext.getElfHeader(); if (elf.e_machine() != ElfConstants.EM_BPF) { return; } Program program = elfRelocationContext.getProgram(); Memory memory = program.getMemory(); int type = relocation.getType(); int symbolIndex = relocation.getSymbolIndex(); long value; boolean appliedSymbol = true;

نتيجة تفكيك و decompiling eBPF
وفي النهاية ، نحصل على أداة فك و فك eBPF! استخدام للصحة!
التمديد على GitHub: eBPF لـ Ghidra .
النشرات هنا: هنا .
PS
شكرا جزيلا للأمن الرقمي على فترة تدريب مثيرة للاهتمام ، خاصة للموجهين من قسم الأبحاث (ألكساندر ونيكولاي). أنحني لك!