في الجزء السابق ، وصفت تقريبًا كيف يمكنك تحميل وظائف eBPF من ملف ELF. الآن حان الوقت للانتقال من الخيال إلى الرسوم السوفيتية ، واتباع النصائح الحكيمة ، بعد قضاء قدر معين من الجهد مرة واحدة ، اصنع أداة عالمية للأدوات (أو باختصار ، UII !!!) . عند القيام بذلك ، سأستفيد من تصميم Golden Hammer antipattern وأنشئ أداة من QEMU المعروفة نسبيًا. على سبيل المكافأة على ذلك ، نحصل على أجهزة عبر الهندسة المعمارية ، وكذلك أجهزة على مستوى الكمبيوتر الافتراضي بأكمله. ستكون الأجهزة من النوع "ملف أصلي صغير جدًا + ملف .o صغير مع eBPF". في هذه الحالة ، سيتم استبدال وظائف eBPF قبل الإرشادات المقابلة للتمثيل الداخلي لجهاز QEMU قبل التحسين وإنشاء الشفرة.
كنتيجة لذلك ، فإن الجهاز نفسه ، الذي تمت إضافته أثناء إنشاء الكود (أي ، دون حساب بضع كيلو بايت من وقت تشغيل النظام العادي) ، يبدو كهذا ، وهذا ليس رمزًا زائفًا:
#include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_qemu_brcond_i64(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } void inst_qemu_brcond_i32(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; }
حسنا ، لقد حان الوقت لتحميل قزمنا في المصفوفة. حسنا ، كيفية التحميل ، بدلا من ذلك vmazat رذاذ.
كما ذكرنا سابقًا في المقالة حول QEMU.js ، فإن أحد أوضاع تشغيل QEMU هو إنشاء JIT لرمز الجهاز المضيف من الضيف (يحتمل أن يكون لهيكل مختلف تمامًا). إذا كانت آخر مرة قمت فيها بتطبيق الواجهة الخلفية لإنشاء الشفرة ، فسأقوم هذه المرة بمعالجة التمثيل الداخلي من خلال الالتفاف مباشرة أمام المُحسِّن. هل هذا قرار تعسفي؟ لا. هناك أمل في أن يقوم المُحسِّن بقطع الزوايا الزائدة ، ورمي المتغيرات غير الضرورية ، إلخ. بقدر ما أفهم ، فهو ، في الواقع ، يقوم بأشياء بسيطة وقابلة للتنفيذ بسرعة: دفع الثوابت ، وطرد التعبيرات مثل "x: = x + 0" وحذف الكود غير القابل للوصول. ويمكننا الحصول على كمية مناسبة منه.
تكوين البرنامج النصي التجميع
أولاً ، دعنا نضيف ملفاتنا المصدر: tcg/bpf-loader.c
و tcg/instrument.c
إلى Makefiles. بشكل عام ، هناك رغبة في دفع هذا في يوم من الأيام إلى المنبع ، لذلك ستحتاج إلى القيام بذلك في النهاية بحكمة ، لكن الآن سأضيف هذه الملفات دون قيد أو شرط إلى التجميع. وسوف آخذ المعلمات في أفضل تقاليد AFL - من خلال متغيرات البيئة. بالمناسبة ، سأختبر هذا مرة أخرى على أجهزة AFL.
مجرد إلقاء نظرة على ذكر "الجار" - ملف optimize.c
مع grep -R
ولن نجد أي شيء. لأنه كان من الضروري البحث عن optimize.o
:
أولاً ، دعنا نضيف bpf-loader.c
من السلسلة الأخيرة برمز يسحب نقاط الدخول المقابلة لعمليات QEMU. tcg-opc.h
ملف tcg-opc.h
الغامض tcg-opc.h
هذا الأمر. يبدو مثل هذا:
DEF(discard, 1, 0, 0, TCG_OPF_NOT_PRESENT) DEF(set_label, 0, 0, 1, TCG_OPF_BB_END | TCG_OPF_NOT_PRESENT) DEF(call, 0, 0, 3, TCG_OPF_CALL_CLOBBER | TCG_OPF_NOT_PRESENT) DEF(br, 0, 0, 1, TCG_OPF_BB_END)
ما هذا الهراء؟ والأمر ببساطة هو أنه غير متصل في رأس المصدر - تحتاج إلى تعريف الماكرو DEF
، تضمين هذا الملف ، وحذف الماكرو على الفور. انظر ، هو لا يملك حتى الحراسة.
static const char *inst_function_names[] = { #define DEF(name, a, b, c, d) stringify(inst_qemu_##name), #include "tcg-opc.h" #undef DEF NULL };
نتيجة لذلك ، نحصل على مجموعة أنيقة من أسماء الوظائف المستهدفة ، مفهرسة بواسطة رموز التشغيل وتنتهي بـ NULL ، والتي يمكننا تشغيلها لكل حرف في الملف. أنا أفهم أن هذا ليس فعالا. لكن الأمر بسيط ، وهو أمر مهم ، نظرًا لطبيعة هذه العملية لمرة واحدة. بعد ذلك ، نحن فقط نتخطى جميع الشخصيات التي
ELF64_ST_BIND(sym->st_info) == STB_LOCAL || ELF64_ST_TYPE(sym->st_info) != STT_FUNC
يتم فحص الباقي مقابل القائمة.
نحن نعلق على تدفق التنفيذ
أنت الآن بحاجة إلى الاستيقاظ في مكان ما من تدفق آلية إنشاء الشفرة ، والانتظار حتى تمر تعليمات الفائدة. لكن عليك أولاً تحديد tcg_instrument
instrumentation_shutdown
و tcg_instrument
و instrumentation_shutdown
في tcg/tcg.h
وكتابة مكالماتهم: التهيئة - بعد تهيئة الواجهة الخلفية ، والأجهزة - مباشرة قبل استدعاء tcg_optimize
. يبدو أنه قد يتم تعليق instrumentation_init
في instrumentation_init
في atexit
عدم القيام بذلك ولا يمكن atexit
. اعتقدت ذلك أيضًا ، وعلى الأرجح ستعمل في وضع مضاهاة النظام بالكامل ، ولكن في وضع مضاهاة exit_group
تقوم QEMU بترجمة مكالمات نظام exit_group
وأحيانًا exit
إلى استدعاء دالة _exit
، والتي تتجاهل كل معالجات atexit هذه ، لذلك ، linux-user/syscall.c
عنها في linux-user/syscall.c
المكالمة على الكود الخاص بنا أمامه.
تفسير Bytecode
لذا فقد حان الوقت لقراءة ما أنشأه المترجم لنا. يتم ذلك بسهولة باستخدام llvm-objdump
مع الخيار -x
، أو الأفضل ، على الفور -d -t -r
.
مثال الإخراج $ ./compile-bpf.sh test-bpf.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 inst_brcond_i64: 0: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll 0000000000000000: R_BPF_64_64 prev 2: 79 23 00 00 00 00 00 00 r3 = *(u64 *)(r2 + 0) 3: 77 03 00 00 01 00 00 00 r3 >>= 1 4: 7b 32 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r3 5: af 13 00 00 00 00 00 00 r3 ^= r1 6: 57 03 00 00 ff ff 00 00 r3 &= 65535 7: 18 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r4 = 0 ll 0000000000000038: R_BPF_64_64 __afl_area_ptr 9: 79 44 00 00 00 00 00 00 r4 = *(u64 *)(r4 + 0) 10: 0f 34 00 00 00 00 00 00 r4 += r3 11: 71 43 00 00 00 00 00 00 r3 = *(u8 *)(r4 + 0) 12: 07 03 00 00 01 00 00 00 r3 += 1 13: 73 34 00 00 00 00 00 00 *(u8 *)(r4 + 0) = r3 14: 7b 12 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r1 15: 95 00 00 00 00 00 00 00 exit 0000000000000080 inst_brcond_i32: 16: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll 0000000000000080: R_BPF_64_64 prev 18: 79 23 00 00 00 00 00 00 r3 = *(u64 *)(r2 + 0) 19: 77 03 00 00 01 00 00 00 r3 >>= 1 20: 7b 32 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r3 21: af 13 00 00 00 00 00 00 r3 ^= r1 22: 57 03 00 00 ff ff 00 00 r3 &= 65535 23: 18 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r4 = 0 ll 00000000000000b8: R_BPF_64_64 __afl_area_ptr 25: 79 44 00 00 00 00 00 00 r4 = *(u64 *)(r4 + 0) 26: 0f 34 00 00 00 00 00 00 r4 += r3 27: 71 43 00 00 00 00 00 00 r3 = *(u8 *)(r4 + 0) 28: 07 03 00 00 01 00 00 00 r3 += 1 29: 73 34 00 00 00 00 00 00 *(u8 *)(r4 + 0) = r3 30: 7b 12 00 00 00 00 00 00 *(u64 *)(r2 + 0) = r1 31: 95 00 00 00 00 00 00 00 exit SYMBOL TABLE: 0000000000000000 l df *ABS* 00000000 test-bpf.c 0000000000000000 ld .text 00000000 .text 0000000000000000 *UND* 00000000 __afl_area_ptr 0000000000000080 g F .text 00000080 inst_brcond_i32 0000000000000000 g F .text 00000080 inst_brcond_i64 0000000000000008 g O *COM* 00000008 prev
إذا حاولت البحث عن وصف لأكواد شفرة eBPF ، فقد تبيّن أنه في الأماكن الواضحة (صفحات المصدر والصفحات الأساسية لـ Linux kernel) توجد أوصاف لكيفية استخدامها وكيفية التجميع وما إلى ذلك. ثم تأتي إلى صفحة فريق أداة iovisor مع إشارة eBPF غير رسمية مريحة.
يشغل التعليمات كلمة 64 بت واحدة (بعض اثنين) ويحتوي النموذج
struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; };
تلك التي تشغل كلمتين تتكون ببساطة من التعليمة الأولى مع كل المنطق و "مقطورة" مع 32 بت أخرى ذات قيمة فورية وتكون واضحة للعيان على disjembump disassembler.
تشتمل أكواد opcodes نفسها أيضًا على هيكل منتظم: الأجزاء الثلاثة البطيئة هي فئة العملية: 32 بت ALU ، 64 بت ALU ، تحميل / تخزين ، تفرع مشروط. لذلك ، فهي مريحة للغاية للتنفيذ على وحدات الماكرو في أفضل تقاليد QEMU. لن أجري تعليمات مفصلة حول قاعدة الشفرة نحن لسنا على مراجعة التعليمات البرمجية من الأفضل أن أخبركم عن المخاطر.
كانت مشكلتي الأولى هي أنني صنعت مُخصصًا لتسجيل eBPF كسولًا في شكل QEMU- local_temp
، وبدأت في نقل استدعاء هذه الوظيفة دون تفكير إلى الماكرو. اتضح كما هو الحال في ميم الشهيرة: "لقد أدخلنا تجريدًا في تجريد بحيث يمكنك إنشاء تعليمة أثناء قيامك بإنشاء تعليمة." في مرحلة ما بعد الواقع ، أنا لا أفهم جيدًا ما تم كسره بعد ذلك ، لكن يبدو أن شيئًا غريبًا قد حدث مع ترتيب التعليمات التي تم إنشاؤها. بعد ذلك ، قمت بعمل نظائر tcg_gen_...
لدفع الإرشادات الجديدة إلى منتصف القائمة ، مع الأخذ المعاملات كحجج للدالة ، وأصبح الترتيب تلقائيًا كما ينبغي (نظرًا لأن الوسيطات يتم حسابها تمامًا تمامًا قبل الاستدعاء).
المشكلة الثانية كانت محاولة دفع TCG const كأداة لتعليمات تعسفية عند النظر إلى المعامل الفوري في eBPF. عند tcg-opc.h
سبق ذكرها ، فإن تكوين قائمة وسيطة العملية ثابت بشكل صارم: n
وسيطات الإدخال ، و m
output و k
ثابت. بالمناسبة ، عند تصحيح مثل هذا الرمز ، فإنه يساعد على تمرير QEMU وسيطة سطر الأوامر -d op,op_opt
أو حتى -d op,op_opt,out_asm
.
الحجج الممكنة $ ./x86_64-linux-user/qemu-x86_64 -d help Log items (comma separated): out_asm show generated host assembly code for each compiled TB in_asm show target assembly code for each compiled TB op show micro ops for each compiled TB op_opt show micro ops after optimization op_ind show micro ops before indirect lowering int show interrupts/exceptions in short format exec show trace before each executed TB (lots of logs) cpu show CPU registers before entering a TB (lots of logs) fpu include FPU registers in the 'cpu' logging mmu log MMU-related activities pcall x86 only: show protected mode far calls/returns/exceptions cpu_reset show CPU state before CPU resets unimp log unimplemented functionality guest_errors log when the guest OS does something invalid (eg accessing a non-existent register) page dump pages at beginning of user mode emulation nochain do not chain compiled TBs so that "exec" and "cpu" show complete traces trace:PATTERN enable trace events Use "-d trace:help" to get a list of trace events.
حسنًا ، لا تكرر أخطائي: add_i64 loc15,loc15,$554412123213
الداخلية التي تم تفكيكها متقدمة تمامًا ، وإذا رأيت شيئًا مثل add_i64 loc15,loc15,$554412123213
الشيء بعد علامة الدولار ليس مؤشرًا. بتعبير أدق ، هذا ، بطبيعة الحال ، هو مؤشر ، ولكن ربما يتم تعليقه بالأعلام وفي دور القيمة الحرفية للمعامل ، وليس المؤشر. ينطبق كل هذا ، بالطبع ، إذا كنت تعلم أنه يجب أن يكون هناك عدد محدد ، مثل $0
أو $ff
، فلا يجب أن تخاف من المؤشرات على الإطلاق. :) كيفية movi
مع هذا - تحتاج فقط إلى إنشاء وظيفة تقوم بإرجاع temp
جديدة ، والتي من خلالها movi
تضع الثابت المطلوب.
بالمناسبة ، إذا علقت على #define USE_TCG_OPTIMIZATIONS
في #define USE_TCG_OPTIMIZATIONS
tcg/tcg.c
، فجأة ، سيتم إيقاف التحسين وسيصبح تحليل التحويلات البرمجية أسهل.
بالنسبة إلى sim ، سأرسل قارئًا مهتمًا باختيار QEMU في الوثائق ، حتى الرسمية! بالنسبة للباقي ، سأبين الأدوات الموعودة ل AFL.
نفس والأرنب
بالنسبة للنص الكامل لوقت التشغيل ، سأرسل القارئ مرة أخرى إلى المستودع ، لأنه (النص) ليس ذا قيمة فنية ويتم qemu_mode
بصدق من qemu_mode
من تسليم AFL ، وبصفة عامة ، جزء منتظم من الشفرة C. لكن هنا كيف تبدو الأجهزة نفسها :
#include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_qemu_brcond_i64(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } void inst_qemu_brcond_i32(uint64_t tag, uint64_t x, uint64_t y, uint64_t z, uint64_t u) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; }
من المهم أن تحتوي وظائف الخطاف على العديد من الوسائط مثل iargs
لعملية QEMU المقابلة. سيتم ربط جهازي extern
في رأس وقت التشغيل أثناء عملية النقل. من حيث المبدأ ، يمكن تعريف prev
هنا ، ولكن بعد ذلك يجب تعريفه على أنه static
، وإلا فسيقع في قسم COMMON الذي لا أؤيده. في الواقع ، نحن ، في الواقع ، نعيد ببساطة كتابة الشفرة الزائفة من الوثائق ، لكن هنا يمكن قراءتها آلياً!
للتحقق ، قم بإنشاء ملف bug.c
:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char *argv[]) { char buf[16]; int res = read(0, buf, 4); if (buf[0] == 'T' && buf[1] == 'E' && buf[2] == 'S' && buf[3] == 'T') abort(); return res * 0; }
وأيضًا - ملف forksrv
، وهو مناسب لتغذية AFL:
وتشغيل الغمغمة:
AFL_SKIP_BIN_CHECK=1 afl-fuzz -i ../input -o ../output -m none -- ./forksrv
غامض الأمريكية لوب 1234 T234 TE34 TES4 TEST <- crashes, 2200
حتى الآن ، ليست السرعة ساخنة للغاية ، ولكن كذريعة سأقول هنا (الآن) لا qemu_mode
استخدام ميزة مهمة في qemu_mode
الأصلي: إرسال عناوين التعليمات البرمجية القابلة للتنفيذ إلى خادم الشوكة. ولكن لا يوجد أي شيء في قاعدة بيانات QEMU الآن ، وهناك أمل في أن تصبح هذه الأداة المعممة في يوم من الأيام مكتظة.
مشروع جيثب