
جولة تفاعلية في GDB لتقنية House of Apple 2 FSOP على glibc 2.43، مع بيئة اختبار قابلة لإعادة الإنتاج تغطي تجاوز vtable، وتحويل المكدس، وROP.
هذا المستودع عبارة عن ساحة تجريبية مكتفية ذاتيًا مستوحاة من وحدة استغلال بنية الملفات في pwn.college. لا يقدّم هذا المستودع تنويعًا جديدًا لـ House of Apple 2، بل يجيب فقط عن فضولي حول كيفية صمود هذه التقنية على الإصدارات الحديثة من glibc وما إذا كانت لا تزال مسارًا قابلًا للاستغلال. يوفّر المستند جولة تفاعلية في GDB يمكن للقراء متابعتها جنبًا إلى جنب مع البيئة المعزولة لتطوير فهم أكثر حدسية للبدائية (primitive). تستخدم جميع التجارب glibc 2.43، كما هي مُحزَّمة في Ubuntu 26.04 وFedora 44 وقت كتابة هذا النص.
البرمجة الموجّهة بتدفقات الملفات (FSOP). يتعلق الأمر بالتلاعب ببنى تدفقات ملفات glibc لاختطاف مسار التحكم. إحدى طرق فعل ذلك هي إفساد آلية إرسال vtable الخاصة بـ _IO_FILE_plus. تتحقق glibc الحديثة من صحة هذا vtable، لذا فإن النهج البديهي المتمثل في استبداله بعنوان عشوائي لا ينجح.
House of Apple 2، التي قدّمها في الأصل Roderick، تتحايل على هذا القيد باستخدام vtable صالح لـ _IO_FILE_plus للوصول إلى آلية تدفقات المحارف العريضة (wide-character)، حيث يتم إرسال vtable ثانوي مباشرةً دون التحقق من النطاق. يوفّر هذا بدائية arbitrary call يمكننا تصعيدها إلى pivot للمكدس وسلسلة ROP.
يفترض هذا الاستكشاف أننا قادرون على الكتابة فوق بنية FILE وأن لدينا تسريبًا لكلٍّ من الـ heap والـ libc. يوفّر الملف التنفيذي الهدف هذا بالفعل.
تعمل الساحة التجريبية على Ubuntu 26.04 LTS، مما يمنحنا بيئة حديثة لاستكشاف التقنية.
تتضمن الصورة GDB وpwndbg وpwntools وropper وtmux. كما تحتوي على ملف تنفيذي هدف بقائمة تفاعلية لاستدعاء عمليات تدفقات الملفات مثل fopen وfread وfwrite وfclose. يمنحنا هذا طريقة ملائمة للتلاعب بالتدفقات أثناء التنقيح واختبار الأفكار.
ابنِ الساحة التجريبية وشغّلها باستخدام:``` ./build.sh ./run.sh
## الاستكشاف
لنبدأ بفحص البنيتين `_IO_FILE` و `_IO_FILE_plus`:```c
pwndbg> ptype struct _IO_FILE
type = struct _IO_FILE {
int _flags;
char *_IO_read_ptr;
char *_IO_read_end;
char *_IO_read_base;
char *_IO_write_base;
char *_IO_write_ptr;
char *_IO_write_end;
char *_IO_buf_base;
char *_IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2 : 24;
char _short_backupbuf[1];
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
struct _IO_FILE **_prevchain;
int _mode;
int _unused3;
__uint64_t _total_written;
char _unused2[8];
}
pwndbg> ptype struct _IO_FILE_plus
type = struct _IO_FILE_plus {
FILE file;
const struct _IO_jump_t *vtable;
}
من الناحية العملية، _IO_FILE_plus هو _IO_FILE مع مؤشر vtable. يبدو ذلك مثيرًا للاهتمام على الفور: إذا تمكّنّا من التحكم في هذا المؤشر، فقد نتمكن من إعادة توجيه استدعاء غير مباشر واختطاف مسار التنفيذ.
لفحص vtable، دعنا نفحص مؤشر FILE الذي يُرجعه fopen.```c
pwndbg> p *(struct _IO_FILE_plus *)0x37ecf010
$4 = {
file = {
_flags = 0xfbad2480,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7f58a7f4b4a0 <IO_2_1_stderr>,
_fileno = 0x3,
_flags2 = 0x0,
_short_backupbuf = "",
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x37ecf0f0,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x37ecf100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
_prevchain = 0x7f58a7f4b480 <_IO_list_all>,
_mode = 0x0,
_unused3 = 0x0,
_total_written = 0x0,
_unused2 = "\000\000\000\000\000\000\000"
},
vtable = 0x7f58a7f49030 <_IO_file_jumps>
}
المؤشر يستهدف جدول `_IO_file_jumps`.

هذه مجموعة من 21 مؤشر دالة. تُوزَّع عمليات تدفق الملفات عبر مدخلات مختلفة اعتمادًا على مسار التنفيذ.
### تتبع مسار `fwrite`
في هذا الاستكشاف سأركّز على مسار `fwrite`. بعد وضع نقاط التوقف على كل دالة واستدعاء `fwrite`، أول نقطة توقف نصادفها هي `_IO_file_xsputn`.

يحدث الاستدعاء عند `fwrite+216`. وهذا يتطابق مع [مصدر glibc](https://elixir.bootlin.com/glibc/glibc-2.43/source/libio/iofwrite.c#L44): `_IO_sputn` هو ماكرو يُوزِّع عبر vtable، ويُحَلّ إلى `_IO_file_xsputn` لهذا التدفق.```asm
0x00007fd5181d362a <+202>: mov rdx,rcx
0x00007fd5181d362d <+205>: mov rdi,rbx
0x00007fd5181d3630 <+208>: mov QWORD PTR [rbp-0x30],r8
0x00007fd5181d3634 <+212>: mov QWORD PTR [rbp-0x28],rcx
0x00007fd5181d3638 <+216>: call QWORD PTR [rax+0x38]
كمحاولة أولى، دعنا نستبدل مؤشر جدول الدوال الافتراضية بـ desired_func - 0x38 ونضع نقطة توقف عند fwrite+216.```c
pwndbg> p &win
$3 = (<text variable, no debug info> *) 0x4019e1
pwndbg> p/x &win - 0x38
$4 = 0x4019a9
pwndbg> set ((struct _IO_FILE_plus *)0x5334010)->vtable = (void *)0x4019a9
pwndbg> b *fwrite+216
Breakpoint 4 at 0x7fd5181d3638: file ./libio/libioP.h, line 1042.

يتوقف التنفيذ قبل الوصول إلى نقطة التوقف. يشير الخطأ إلى أن glibc تتحقق من صحة مؤشر vtable قبل إجراء الاستدعاء غير المباشر. لنفحص التتبع العكسي ونرى أين يحدث ذلك.
تصل `fwrite` إلى `_IO_vtable_check` التي ترفض مؤشر vtable المزوّر.

يحتوي التنفيذ على آلية لقبول vtables الأجنبية، لكنها ليست تحت سيطرتنا. الكود ذو الصلة متاح في [`vtables.c`](https://elixir.bootlin.com/glibc/glibc-2.43/source/libio/vtables.c#L504).
### فهم التحقق من صحة vtable