Sahm Academy · Advanced Exploit Development


لفة شاملة في معمارية x86 و x64 لمطورين الـ Exploits

الـ CPU هو الشيء الوحيد اللي فعلياً يشغل الـ exploit حقك. كل register، وكل flag bit، وكل تفصيلة بالـ encoding هي اللي تحدد وش يقدر يسوي الـ payload ووش ما يقدر. هذا هو الأساس المعماري — ومكتوب من منظور هجومي (Offensive).

Series: Advanced Exploit Development · Lesson 01–02 Level: Intermediate → Advanced Read: ~30 minutes

0x00 ليه الـ CPU يعتبر ساحة المعركة؟

الـ CPU هو الشيء الوحيد اللي ينفذ الـ exploit حقك فعلياً. كل شيء ثاني — الـ fuzzer، الـ debugger، إطار عمل الـ ROP، أو الـ shellcode encoder — ما هي إلا مجرد أدوات عشان تحط الـ bytes في المكان الصح بحيث، لا طالع المعالج في الـ RIP المرة الجاية، يشوف بالضبط اللي تبيه يشوفه.

عشان كذا، مطورين الـ exploits مهووسين بمعمارية المعالج (Architecture). مو عشاننا نحب نحفظ معلومات سطحية عن أحجام الـ registers أو تنسيقات الـ descriptor tables، بس لأن كل تقنية هجومية (Offensive technique) نستخدمها تتشكل وتتأثر بالقيود والخصائص الغريبة للآلة الأساسية. مثلاً، الـ buffer overflow يشتغل بس لأن الـ saved return address موجود في offset متوقع على الـ stack. وسلسلة الـ ROP chain تشتغل لأن الـ decoder ما يهتم بحدود الـ instructions الأصلية. وعملية الـ SYSTEM-token swap تضبط لأن الـ kernel والـ user mode يتشاركون نفس الهاردوير الفعلي مع مستويات صلاحيات (privilege levels) مفروضة برمجياً.

هالمقال عبارة عن لفة كاملة في معمارية x86 و x86-64، مكتوبة خصيصاً مع وضع الأمن الهجومي (Offensive security) في البال. بنغطي الـ register file، الـ flag bits، الـ address space layout، الـ pipeline، الـ segmentation، الـ paging، الـ privilege rings، وكيف تدخل على الـ system calls، وكل تعقيدات الـ encoding اللي تطلع لك في الـ payloads الحقيقية. الهدف مو إننا نستبدل أدلة Intel الرسمية — الهدف نعطيك نموذج ذهني عملي بحيث لا جلست قدام GDB أو WinDbg، يكون لكل byte معنى منطقي براسك.

ترا ترقيم الأقسام يستخدم النظام السداسي عشر (Hex)، لأن هذا هو الطبيعي بمجالنا.

0x01 من أيام الـ 16-bit إلين الـ 64-bit

تاريخ الـ x86 طويل. بدأت السالفة سنة 1978 مع معالج Intel 8086 — شريحة 16-bit فيها عشرين address pin (يعني 1 ميجابايت من الـ physical memory) ونموذج ذاكرة مجزأ (segmented memory model) لليوم وحنا نعتذر عن وجوده. بعدها شريحة 80386 سنة 1985 خلتها 32-bit وقدمت الـ protected mode، والـ paging، وأربع مستويات من الصلاحيات (privilege rings). بعدين AMD وسعتها لـ 64-bit سنة 2003 مع معالج Opteron، وسموا الوضع الجديد long mode. وبعدها Intel تبنت نفس المعمارية وسمتها Intel 64.

عشان شغل الـ exploit، فيه ثلاث فترات تهمنا:

  • 16-bit real mode — كيف يقلع (boots) الـ CPU. هالوضع يهمنا في شغل الـ bootkits، والـ UEFI، والـ BIOS.
  • 32-bit protected mode (IA-32) — الهدف اللي كان مسيطر طوال الألفينات. ولليوم مهم للبرامج القديمة (legacy software)، والأنظمة المدمجة (embedded systems)، وعمليات 32-bit WoW64 على الويندوز.
  • 64-bit long mode (AMD64 / Intel 64 / x86-64) — هذا هو الوضع الافتراضي اليوم. كل جهاز مكتبي، لابتوب، أو سيرفر جديد يشتغل عليه.

هالمعمارية معروفة إنها متوافقة مع الإصدارات القديمة (backward-compatible). معالج Ryzen أو Xeon حديث يقدر لليوم ينفذ تعليمات 16-bit code، ونظام التشغيل 64-bit OS يشغل ملفات 32-bit binaries جنباً إلى جنب مع ملفات 64-bit بشكل روتيني. هالتوافق بحد ذاته يعتبر Attack surface — وتقنية Heaven's Gate، اللي بنشرحها بعدين، هي أفضل مثال على هالشي.

0x02 الـ General-Purpose Registers (x86)

يحتوي IA-32 على ثمانية مسجلات (registers) للأغراض العامة بحجم 32-bit. كلمة عامة هنا مجازية شوي؛ لأن فيه تعليمات كثير تطلب مسجلات معينة بشكل ضمني (implicitly)، والـ calling convention يحدد الأدوار المتعارف عليها لباقي المسجلات.

31 15 7 0 +------------------------------------+ | EAX | AX | AH | AL | Accumulator | EBX | BX | BH | BL | Base | ECX | CX | CH | CL | Counter | EDX | DX | DH | DL | Data | ESI | SI | | Source Index | EDI | DI | | Destination Index | ESP | SP | | Stack Pointer | EBP | BP | | Base / Frame Pointer +------------------------------------+
  • EAX (Accumulator) — يُستخدم كـ operand ضمني لتعليمات MUL, DIV, IMUL, IDIV. القيم المرجعة (Return values) من دوال cdecl و stdcall ترجع هنا. يستخدم الـ shellcode مسجل EAX عشان يحفظ رقم الـ syscall وقت استدعاء int 0x80.
  • EBX (Base) — بالـ position-independent code على نظام Linux، يحمل EBX عنوان الـ Global Offset Table. عدا كذا، يعتبر مسجل عام (scratch).
  • ECX (Counter) — العداد الضمني لتعليمات LOOP، وتعليمات الـ string المسبوقة بـ REP، وعمليات الإزاحة (CL يعطيك مقدار الـ shift). الـ thiscall convention حق MSVC يمرر مؤشر (pointer) الـ this حق C++ في ECX.
  • EDX (Data) — الـ accumulator الممتد. تعليمة MUL تطلع نتيجة 64-bit تتوزع على EDX:EAX. تعليمة DIV تستخدم EDX:EAX كـ dividend. تعليمة RDTSC ترجع عداد الـ timestamp مع الجزء العلوي (high 32 bits) في EDX.
  • ESI / EDI — المصدر (Source) والوجهة (Destination) لتعليمات النصوص والبيانات (MOVS, CMPS, LODS, STOS, SCAS). لا اشتغلت تعليمة rep movs، لازم ESI يأشر على ذاكرة قابلة للقراءة (readable memory) و EDI يأشر على ذاكرة قابلة للكتابة (writable memory).
  • ESP (Stack Pointer) — قمة الـ stack. يتعدل ضمنياً مع تعليمات PUSH, POP, CALL, RET, INT, IRET. مطورين الـ exploits يتبعون الـ ESP بايت ببايت، لأن الـ stack هو المكان اللي تصير فيه كل الأكشن.
  • EBP (Base / Frame Pointer) — يثبت الـ stack frame الحالي. البداية الكلاسيكية (prologue) push ebp; mov ebp, esp تبني سلسلة من الـ frame pointers اللي تمشي عليها برامج الـ debuggers عشان تطلع الـ backtraces. الـ flag البرمجي -fomit-frame-pointer يكسر هالسلسلة عشان يكسب register زيادة.

0x03 الـ Sub-register Aliasing

كل register بحجم 32-bit داخله أجزاء فرعية أصغر لها أسماء: EAX يعطيك AX (الـ 16 bits اللي تحت)، و AH (الـ high byte من AX)، و AL (الـ low byte). الكتابة على مسجل فرعي (sub-register) تعدل بس البتات المحددة — وباقي الـ register يحتفظ بقيمته القديمة.

xor eax, eax     ; EAX = 0x00000000
mov al, 0x0B     ; EAX = 0x0000000B  (only AL written)
mov ah, 0xFF     ; EAX = 0x0000FF0B  (AH written, AL unchanged)
mov ax, 0x1234   ; EAX = 0x00001234  (AX written, upper 16 unchanged)

ترا هالشي مو مجرد معلومة عابرة. هذا هو حجر الأساس لكتابة shellcode مضغوط ونظيف من الـ NULL-bytes. رقم الـ execve syscall في Linux هو 11 (يعني 0x0B)، وكتابة mov eax, 0x0B تترجم كـ B8 0B 00 00 00 — ثلاثة NULL bytes بتنهي أي عملية نسخ تستخدم strcpy وتخرب لك الـ payload. التريكة هنا إنك تصفر الـ register بالكامل أول، بعدين تكتب بس الـ byte اللي تحتاجه:

xor eax, eax    ; 31 C0      (clean, no nulls)
mov al, 0x0B    ; B0 0B      (clean, no nulls)

بايتين بس مقابل خمسة، وبدون NULL bytes. كل دليل كلاسيكي يعلمك الـ shellcode بيشرح لك هالحركة بأول عشر صفحات، والسبب يرجع بالضبط لقاعدة الـ aliasing هذي.

0x04 الـ General-Purpose Registers (x64)

معمارية x86-64 وسعت كل register موجود لـ 64 bits وأضافت ثمانية مسجلات جديدة، من R8 إلين R15. طريقة التسمية ماشية على نفس النمط:

  • 64-bit: RAX, RBX, …, R8, R9, …, R15
  • 32-bit: EAX, …, R8D, …, R15D
  • 16-bit: AX, …, R8W, …, R15W
  • 8-bit: AL, …, R8B, …, R15B

وبرضو x64 أتاحت إنك توصل للـ low-byte حق الـ index والـ base والـ stack pointer اللي هي: SIL, DIL, SPL, BPL. هذي تطلب تستخدم REX prefix (بنشرحه بعدين)، وبمجرد ما تستخدم REX prefix، الترميزات القديمة حقت AH/BH/CH/DH تصير ما تقدر توصل لها — النمط الثنائي (bit pattern) اللي كان معناه AH صار الحين معناه SPL.

قاعدة الـ zero-extension

هذي أهم قاعدة بالـ encoding في x64، ودايم تلخبط الواحد أول مرة يشوفها:

القاعدة (Rule)

الكتابة على 32-bit register تسوي zero-extends (تمديد بالصفر) للنتيجة وتخليها 64 bits. بينما الكتابة على مسجلات 16-bit أو 8-bit فرعية تحافظ (preserves) على البتات اللي فوق (upper bits).

mov rax, 0xFFFFFFFFFFFFFFFF
mov eax, 1                    ; RAX = 0x0000000000000001  ← upper 32 bits zeroed!

mov rax, 0xFFFFFFFFFFFFFFFF
mov ax, 1                     ; RAX = 0xFFFFFFFFFFFF0001  ← upper 48 bits preserved

mov rax, 0xFFFFFFFFFFFFFFFF
mov al, 1                     ; RAX = 0xFFFFFFFFFFFFFF01  ← upper 56 bits preserved

هالشي موجود لأن مصممين الهاردوير كانوا يبون طريقة رخيصة يوسعون فيها قيم الـ 32-bit لـ 64-bit بدون ما يحتاجون تعليمة movzx صريحة. بالنسبة لمطورين الـ exploit هذي كنز: تعليمة mov edi, 0x12345678 عبارة عن instruction من خمسة بايتات تحمل pointer بحجم 64-bit جوا RDI وتضمن لك إن الـ 32 bits اللي فوق كلها أصفار. لو حاولت تسوي نفس الشي باستخدام mov rdi, 0x12345678، بتطلع لك instruction من عشرة بايتات مليانة NULL bytes.

0x05 الـ Segment Registers — مسجلات FS و GS، والطريق للـ PEB

معمارية x86 فيها ستة مسجلات مقاطع (segment registers) بحجم 16-bit وهي: CS, DS, SS, ES, FS, GS. في الـ real mode هالمسجلات تشيل عناوين أساسية (base addresses) (مزاحة لليسار أربع بتات عشان تسوي physical address بحجم 20-bit). أما في الـ protected mode والـ long mode فهي تشيل selectors — مؤشرات (indices) داخل الـ GDT أو الـ LDT، مع bit للصلاحيات و bit لمصدر الجدول.

بأنظمة التشغيل الحديثة، كل من CS و DS و SS و ES توصف مساحة عنونة مسطحة (flat address space) بحجم 4 جيجابايت (أو 64-bit) تبدا من الصفر. يعني فعلياً هي no-ops (ما تسوي شي يذكر). المسجلات اللي تهمنا صدق هي FS و GS.

على الويندوز: FS:[0x30] و GS:[0x60]

على 32-bit Windows، يشير FS للـ Thread Environment Block (TEB). ومؤشر الـ PEB (Process Environment Block) موجود في الـ offset 0x30 جوا الـ TEB. من خلال الـ PEB تقدر تمشي على قائمة الـ modules المحملة (PEB_LDR_DATA) وتلقى الـ base address لـ kernel32.dll، و ntdll.dll، وأي شي تبيه.

هذي هي الطريقة اللي يحل فيها الـ stage-1 shellcode عناوين الـ API بدون import table:

  1. تقرا fs:[0x30] → يجيب لك الـ PEB
  2. تقرا PEB+0x0C → يجيب لك الـ Ldr (PEB_LDR_DATA)
  3. تمشي على Ldr->InMemoryOrderModuleList عشان تلقى الـ base حق kernel32.dll
  4. تحلل (Parse) الـ PE headers حقته عشان تلقى الـ export table
  5. تستخرج عناوين LoadLibraryA و GetProcAddress
  6. الحين تقدر تحمل وتشغل اللي تبيه

على 64-bit Windows نفس السالفة تصير، بس الـ TEB موجود في gs:[0] ومؤشر الـ PEB موجود في gs:[0x60].

على لينكس: FS للـ TLS، و GS لبيانات الـ kernel لكل CPU

على 64-bit Linux، الـ C library في الـ user-space تستخدم مسجل FS لتخزين البيانات المحلية للـ thread (thread-local storage) — زي errno، والـ stack canary، وأي شي متعلم بـ __thread بـ C. أما الـ kernel يستخدم المسجل GS لبيانات الـ per-CPU: أول ما يوصل syscall، أول شي يسويه الـ kernel هو إنه ينفذ swapgs عشان يبدل GS من قيمة الـ user لمؤشر الـ per-CPU حق الـ kernel. الغلط في التعامل مع swapgs كان يمثل فئة ثغرات (vulnerability class) لحالها.

0x06 الـ EFLAGS / RFLAGS — حالة الآلة (The State of the Machine)

الـ EFLAGS هو register بحجم 32-bit (يسمونه RFLAGS في الـ long mode، بس الـ 32 bits اللي فوق محجوزة) يشيل ثلاث فئات من الـ bits: status flags اللي تتغير مع العمليات الحسابية، و control flags اللي تعدل سلوك الـ instruction، و system flags اللي تتحكم بميزات الهاردوير.

الـ Status flags

تتغير بواسطة العمليات الحسابية والمنطقية؛ ويتم اختبارها بالـ conditional jumps (القفزات الشرطية).

FlagBitالمعنى (Meaning)
CF0Carry — unsigned overflow / borrow
PF2Parity — low byte has even number of set bits
AF4Aux carry — used by BCD instructions
ZF6Zero — result was zero
SF7Sign — MSB of result was 1
OF11Overflow — signed overflow

النمط الكلاسيكي test eax, eax / jz target هو أرخص وأسرع طريقة تسأل فيها "هل الـ EAX يساوي صفر؟" تعليمة TEST تحسب عملية الـ AND للـ operands حقتها وتتجاهل النتيجة، وبس تعدل الـ flags. تعليمة test eax, eax تضبط الـ ZF إذا كان EAX صفر، وتقوم تعليمة الـ conditional jump تختبر هالشي مباشرة.

الـ Control flags

DF (Direction Flag, bit 10) — يتحكم إذا تعليمات زي MOVS, LODS, STOS وغيرها بتزود (increment إذا كانت DF=0) أو تنقص (decrement إذا كانت DF=1) الـ ESI/EDI بعد كل عملية. الـ System V ABI يفرض إن DF=0 وقت دخول أي function. إذا خربت الـ return ووصلت لـ gadget ما ينفذ cld أول وكان الـ DF مضبوط (set) بالصدفة، تعليمة rep movs حقتك بتنسخ البيانات بالعكس — وغالباً بتطيرها للهاوية.

الـ System flags

  • IF (Interrupt Flag, bit 9) — إذا كان مقفل (clear)، تتقفل الـ maskable interrupts. وما يقدر يعدله إلا Ring 0 باستخدام CLI / STI. أي kernel exploit يقفل الـ interrupts عشان يحافظ على عدم المقاطعة (atomicity)، الأحسن يرجع يشغلها، ولا بيعلق الجهاز باللي فيه.
  • TF (Trap Flag, bit 8) — إذا كان شغال، الـ CPU يرفع استثناء #DB بعد كل instruction. الـ debuggers تستخدم هالشي عشان تطبق الـ single-stepping (خطوة بخطوة). كود الـ Anti-debug دايم يشيك على TF أو يحاول يمسحه.
  • IOPL (bits 12-13) — الحد الأدنى لمستوى الصلاحيات المطلوب عشان تستخدم تعليمات IN / OUT. وكانت تستخدم زمان كـ primitive في سلاسل رفع الصلاحيات (local-privilege-escalation).

0x07 الـ EIP / RIP — العرش اللي تتقاتل عشانه

الـ EIP (و RIP في الـ long mode) يشيل عنوان الـ instruction الجاية اللي بيجيبها الـ CPU (fetch). هو أهم register بالشريحة من منظور هجومي، وفيه ميزة وحدة غريبة: ما تقدر تقراه أو تكتب عليه بشكل مباشر. يعني ما فيه تعليمة زي mov eax, eip.

الـ EIP يتعدل بس عن طريق الـ control-flow (مسار التحكم):

  • CALL تدف (pushes) عنوان التعليمة الجاية وتقفز (jumps) للهدف.
  • RET تسحب (pops) القيمة من قمة الـ stack وتحطها في الـ EIP.
  • JMP / Jcc تحمل قيمة في الـ EIP بشكل غير مشروط / مشروط.
  • INT / IRET تروح عبر الـ IDT للـ handler وترجع.
اللعبة بكبرها

إذا قدرت تخلي تعليمة RET تحمل قيمة أنت تتحكم فيها (attacker-controlled value)، مبروك، أنت كذا تحكمت في التنفيذ. هالجملة تختصر لك تاريخ الـ memory corruption exploitation كله. الـ Stack overflows تكتب فوق الـ saved return address. والـ Heap overflows تخرب مؤشرات الـ vtable اللي تنادى. والـ Use-after-free تستغل الـ function pointers المحررة. تختلف الطريقة؛ بس الهدف واحد.

كيف تقرا الـ RIP بشكل غير مباشر

دايم الـ position-independent shellcode يحتاج يعرف عنوانه الخاص — مثلاً، عشان يحل (resolve) سلسلة نصية زي /bin/sh. بأكواد الـ 32-bit، الخدعة المتعارف عليها هي الـ CALL/POP:

call next
next:
    pop eax     ; EAX = address of 'next'

تعليمة CALL تدف عنوان التعليمة الجاية (اللي هي pop eax) في الـ stack. وبعدين نسوي لها pop في EAX. الحين EAX صار يأشر على كودنا. بأكواد الـ 64-bit هالحركة لسا شغالة، بس استخدام الـ RIP-relative addressing عادة يغنيك عنها.

0x08 الـ RIP-Relative Addressing

هذا هو التغيير الأهم على الإطلاق في طريقة العنونة بالـ x64. في الـ 64-bit mode، الـ memory operand اللي ينكتب بصيغة [disp32] يترجم على أساس إنه [RIP + disp32]، مو كـ absolute address (عنوان مطلق).

; x86 (32-bit) — absolute
mov eax, [0x00401000]

; x64 — RIP-relative
mov eax, [rip + 0x1234]

برامج التفكيك (Disassemblers) عادة تطلع لك الهدف المطلق اللي تم حله (resolved absolute target) عشان تسهل عليك القراءة، بس الصيغة المشفرة (encoded form) تكون نسبية (relative) بالنسبة للـ instruction pointer الحالي. النتيجة إن كود x64 بطبيعته position-independent: يعني يأشر للبيانات والدوال القريبة منه بالنسبة لموقعه، وبالتالي الـ loader يقدر يحطه بأي مكان وبيشتغل زي الحلاوة.

بالنسبة لمطورين الـ exploit، هالشي مفيد لسببين:

  1. الـ position-independent shellcode صار أقصر. إذا كان الـ 32-bit shellcode يحتاج رقصة الـ CALL/POP عشان يلقى نفسه، الـ 64-bit shellcode يكتب ببساطة lea rax, [rip + offset_to_data] وخلصنا.
  2. قراءة الـ Disassembly صارت مختلفة. كل [rip + 0x...] تشوفه هو نسبي بالنسبة للكود. عشان تلقى العنوان الفعلي اللي قاعدين نأشر عليه، لازم تجمع الـ displacement مع عنوان الـ instruction الجاية.

0x0B الـ Instruction Pipeline والـ Speculative Execution

معالجات x86 الحديثة ما تنفذ تعليمة ورا الثانية بالدور. بل تشتغل كمحرك عميق التوجيه (deeply pipelined)، متفوق (superscalar)، وخارج الترتيب (out-of-order) يقدر يعالج عشرات التعليمات في نفس الوقت (in flight).

شوف هالمخطط الديناميكي يوضح لك تدفق البيانات بين مراحل الـ Pipeline وكيف تمشي (Data Flow):

Fetch
Decode
Rename
Execute
Retire

الـ Fetch (الجلب)

الواجهة الأمامية (front end) تقرا الـ bytes من الـ L1 instruction cache اللي يشير له RIP. وحدات التوقع (Branch predictors) تخمن اتجاه وهدف كل قفزة (jump) عشان تخلي الـ pipeline شغال وما يوقف.

الـ Decode (فك التشفير)

تعليمات x86 طولها يتغير (من 1 إلين 15 بايت)، وترميزها معقد (baroque encoding) وفيه optional prefixes، و escape bytes، و ModRM، و SIB، و displacement، و immediate. وحدات الـ decoders الحديثة تترجم هالحوسة لعمليات داخلية صغيرة ثابتة العرض (micro-operations أو uops) للواجهة الخلفية (back end).

الـ decoder ياخذ أي بايتات يشير لها الـ RIP ويفك تشفيرها من هناك. ما يعرف ولا يهمه وش كان يقصد الـ compiler. هذا هو أساس اكتشاف الـ ROP gadget: أي تسلسل بايتات ينتهي بـ 0xC3 (RET) ممكن يكون gadget، حتى لو كانت البايتات هذي الـ compiler يقصد فيها نص instruction ثانية مالها دخل.

الـ Execute والـ retire (التنفيذ والتقاعد)

وحدات التنفيذ (ALU, AGU, FPU, vector) تعالج الـ uops خارج الترتيب (out of order)، مع احترام تبعيات البيانات (data dependencies). وتجلس النتايج في reorder buffer إلين تتقاعد (retire) بترتيب البرنامج (program order). عملية التقاعد هذي هي اللي تخلي حالة المعمارية مرئية (architectural state visible).

0x0C الـ Segmentation والـ Descriptor Tables

على إن أنظمة التشغيل الحديثة تشتغل في بيئة الـ flat memory model، إلا أن آليات الـ descriptor table لسا موجودة ولسا تهمنا في استغلالات الـ kernel.

الـ Interrupt Descriptor Table (IDT)

يربط نواقل المقاطعة (interrupt vectors من 0-255) بعناوين الـ handler. كل مدخل عبارة عن gate descriptor يحدد الـ offset حق الـ handler، والـ code segment selector، ونوع البوابة (interrupt، أو trap، أو task)، والحد الأدنى للـ CPL المطلوب عشان تستدعي البوابة من السوفتوير.

كل من INT 3 (one-byte breakpoint, 0xCC)، و INT 1 (debug)، و INT 0E (page fault)، و INT 80 (legacy Linux syscall) تمر عبر الـ IDT. الـ Kernel exploits اللي تسوي hook للـ IDT، أو تكشف عناوين الـ IDT، هي نمط متكرر جداً.

0x0D الـ Privilege Rings — الـ User Mode مجرد Sandbox

الـ x86 تطبق أربع مستويات من الصلاحيات (privilege levels)، يسمونها حلقات (rings)، مرقمة من 0 لـ 3. مستوى الصلاحية الحالي (CPL) هو أول بتتين (low two bits) من الـ CS selector. كل ما قل الرقم = زادت الصلاحيات.

عملياً، كل نظام تشغيل رئيسي يستخدم بس Ring 0 (kernel) و Ring 3 (user). بينما تعتبر Rings 1 و 2 أثرية ومالها استخدام. الـ Hypervisors تشغل الأنظمة الضيفة (guests) في Ring 0 والمضيف (host) في "Ring -1" (وضع VMX root على Intel، أو SVM على AMD).

وش دخل هذا بالـ Exploit؟

الهدف من الـ kernel exploit إنك تنفذ كود بصلاحية CPL=0. النمط الدارج هو إنك تخرب مؤشر دالة بالنواة (kernel function pointer) (زي مدخل بجدول الـ syscall، أو hook callback) وتشغله (trigger). الكود اللي اخترقته (hijacked code) يشتغل وقتها في Ring 0 ويقدر يسوي اللي يبي: يكتب فوق رمز الحماية (security token)، يخلي الصفحة قابلة للتنفيذ (executable)، يعطل الـ SMEP، أو يثبت rootkit.

0x0F الـ System Calls — الـ INT 0x80 مقابل SYSCALL

هذي من الأماكن اللي تختلف فيها بنية 32-bit و 64-bit اختلاف جذري، والـ shellcode حق وحدة ماراح يشتغل على الثانية أبد.

على 32-bit Linux: INT 0x80

mov eax, 1     ; syscall number: exit
mov ebx, 0     ; arg 1: exit code
int 0x80       ; invoke kernel

في 64-bit: SYSCALL

mov rax, 60    ; syscall number: exit (different from 32-bit!)
mov rdi, 0     ; arg 1: exit code
syscall

ثلاث أشياء لازم تعرفها إذا كنت تنقل (porting) 32-bit shellcode لـ 64-bit:

  1. أرقام الـ Syscall تغيرت. execve هو 11 في الـ 32-bit بس صار 59 في الـ 64-bit.
  2. تغيرت مسجلات الـ Arguments. مو بس تغيرت أساميها — ترتبت من جديد.
  3. تعليمة syscall ما تبدل الـ stack. الـ kernel هو اللي يسوي هالشي يدوياً.

0x14 الزبدة (Key Takeaways)

أهم الأفكار اللي لازم تطلع فيها:

  • عمليات الكتابة على الـ Sub-register تمشي على قواعد غير متماثلة. الكتابة بحجم 32-bit تسوي zero-extend لـ 64-bit؛ بينما الكتابة بحجم 16-bit و 8-bit تحافظ على البتات اللي فوق. هذي زبدة الـ shellcode المضغوط والنظيف من الـ NULL.
  • التحكم في الـ EIP/RIP هو الهدف الأكبر. تقريباً كل تقنيات الـ memory-corruption تختزل بإنك تخلي الـ RET أو الـ indirect CALL تحمل قيمة أنت تتحكم فيها.
  • الـ FS و GS هي اللي تدلي الـ shellcode طريقه. TEB على Windows، TLS على Linux، وبيانات kernel per-CPU بعد swapgs. كل stage-1 payload يحتاج يوصل لواحد منهم.
  • نظام الـ NX، و الـ ASLR، ومساحة الـ 48-bit خلت استغلال الـ memory-corruption صعب. وردة الفعل كانت بهجمات ROP، وتسريب المعلومات (info leaks)، والقنوات الجانبية (side channels).

0x15 وش الجاي؟

الدروس الجاية بالسلسلة بتاخذ كل اللي فوق ونبدأ نطبقه بشكل هجومي (offensively):

  • نفهم الـ Linux و Windows ABI calling conventions.
  • نستغل الـ Stack overflows، القديمة والجديدة، مع تخطي كامل لـ ASLR و NX.
  • نفصفص الـ heap allocator من جوا على glibc، و Windows LFH، و macOS.
  • ثغرات الـ Format string و الـ arbitrary read/write primitives.
  • بناء ROP / JOP / COP باستخدام ropper و ROPgadget.