چالش مهندسی معکوس اول - COVIDVaccine
توضیحات
اخیراً عدهای در ایالات متحده برای نیل به اهداف پلید خود به عوامالناس اعلام میکنند که به زودیِ زود، واکسن کرونا را به دست خواهند آورد!
ثابت کنید که بیرون کشیدن واکسن برای چنین ویروس
نامرد و پیچیدهای
کار دشواری است
قالب پرچم در این سوال به صورت parcham{some_l33t_string} است.
حل چالش
ابتدا دستور file
را بر روی فایل داده شده اجرا میکنیم.
$ file COVIDVaccine_b6b7f82a047962d929ad13694ffe6a52
COVIDVaccine_b6b7f82a047962d929ad13694ffe6a52: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=40302164fe97af0dad2b29318dc71b09dc8edeb8, stripped
بنابراین با یک پروندهی اجرایی روبرو هستیم که آن را strip کردهاند و جدول علائم آن پاک شده است. اگر آن را با استفاده از strace
اجرا کنیم، متوجه میشویم که از ما یک ورودی میخواهد. یک رشتهی بیمعنی میدهیم و میبینیم که به ما خطا میدهد و پیام Wrong
نمایش داده میشود.
این پرونده را با رادار۲ باز میکنیم تا ببینیم اوضاع از چه قرار است!
$ radare2 -AA COVIDVaccine_b6b7f82a047962d929ad13694ffe6a52
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Finding function preludes
[x] Enable constraint types analysis for variables
-- radare2 contributes to the One Byte Per Child foundation.
[0x00001140]>
با دستور s main
سراغ تابع main میرویم و آن را دیکامپایل میکنیم.
[0x000013a4]> pdgo 0x000013a4 |undefined8 0x000013a4 |main(undefined8 placeholder_0, undefined8 placeholder_1, undefined8 placeholder_2, undefined8 placeholder_3, | undefined8 placeholder_4, undefined8 placeholder_5, undefined8 placeholder_6, undefined8 placeholder_7, | int64_t arg_20h) |{ | char cVar1; | int32_t iVar2; | char cVar3; | uint8_t uVar4; | int64_t in_FS_OFFSET; | undefined8 timer; | uint32_t var_c4h; | void *var_c0h; | void *var_b8h; | char *var_b0h; | char *var_a8h; | char *var_a0h; | char *var_98h; | char *format; | undefined s2; | undefined var_85h; | undefined var_84h; | undefined var_83h; | undefined var_82h; | undefined var_81h; | int64_t var_80h; | int64_t var_78h; | undefined auStack120 [104]; | int64_t canary; | 0x000013b3 | canary = *(int64_t *)(in_FS_OFFSET + 0x28); 0x000013c2 | var_c0h = (void *)0x1d; 0x000013cd | timer._0_4_ = 0; 0x000013d7 | var_c4h = 0; 0x000013fe | while ((int32_t)var_c4h < 100) { 0x000013eb | auStack120[(int32_t)var_c4h] = 0x20; 0x000013f0 | var_c4h = var_c4h + 1; | } 0x00001405 | var_b8h = (void *)sym.imp.malloc(0x1d); 0x0000141f | sym.imp.getline(&var_b8h, &var_c0h, _reloc.stdin); 0x00001439 | var_c4h = fcn.00001229((uint64_t)var_c4h); 0x00001458 | var_80h = 0x35696577556e675f; 0x0000145c | var_78h = 0x5f6241745f617361; 0x00001460 | timer._4_4_ = 0; 0x000014ca | while (timer._4_4_ < 0x10) { 0x000014ab | if ((*(char *)((int64_t)&var_80h + (int64_t)timer._4_4_) == 0x00001490 | *(char *)((int64_t)var_b8h + (int64_t)(int32_t)timer + 5)) && 0x000014a1 | ((timer._4_4_ - (timer._4_4_ >> 0x1f) & 1U) + (timer._4_4_ >> 0x1f) == 1)) { 0x000014b3 | timer._0_4_ = (int32_t)timer + 1; | } 0x000014bc | timer._4_4_ = timer._4_4_ + 1; | } 0x000014d7 | if (var_c0h != (void *)0x1d) { 0x000014d9 | timer._0_4_ = 1; | } 0x000014f1 | if (((int32_t)timer + 8U & 0xf) == 0) { 0x000014fe | sym.imp.puts("This proccess may take a while.\nGrab some coffee ;)"); 0x0000150a | var_b0h = "Iran Iran"; 0x00001511 | timer._4_4_ = 0; 0x000015ad | while (timer._4_4_ < 0x192643) { 0x00001525 | sym.imp.usleep(); 0x00001550 | *(uint64_t *)((int64_t)var_b8h + 0xd) = *(uint64_t *)((int64_t)var_b8h + 0xd) ^ 0xc5dd89d072b7137d; 0x00001559 | timer._0_4_ = (int32_t)timer * 2; 0x00001571 | iVar2 = fcn.00001229(*(uint64_t *)((int64_t)var_b8h + 0xd) & 0xffffffff); 0x0000157e | if (iVar2 < (int32_t)timer) { 0x00001591 | timer._0_4_ = fcn.000012b6(*(int64_t *)((int64_t)var_b8h + 0xd)); | } 0x0000159c | timer._4_4_ = timer._4_4_ + 1; | } 0x000015b3 | timer._0_4_ = 1; 0x000015c4 | sym.imp.puts(0x20ce); 0x000015c9 | var_81h = 0x6d; 0x000015d0 | s2 = 0x72; 0x000015d7 | var_82h = 0x34; 0x000015f3 | *(undefined *)((int64_t)var_b8h + (int64_t)var_c0h + -2) = 0; 0x000015fd | // WARNING: Load size is inaccurate 0x000016a6 | if (((((*(uint8_t *)((int64_t)var_b8h + 1) ^ *var_b8h) == 0x5f) && 0x00001631 | ((*(uint8_t *)((int64_t)var_b8h + 2) ^ *(uint8_t *)((int64_t)var_b8h + 1)) == 0x47)) && 0x00001657 | ((*(uint8_t *)((int64_t)var_b8h + 3) ^ *(uint8_t *)((int64_t)var_b8h + 2)) == 0x41)) && 0x0000167d | (((*(uint8_t *)((int64_t)var_b8h + 4) ^ *(uint8_t *)((int64_t)var_b8h + 3)) == 0x6a && 0x0000169f | ((*var_b8h ^ *(uint8_t *)((int64_t)var_b8h + 4)) == 0x33 0x0000169f | // WARNING: Load size is inaccurate)))) { 0x000016b3 | // WARNING: Load size is inaccurate 0x000016b3 | cVar1 = *var_b8h; 0x000016eb | if ((int32_t)timer == 1) { 0x000016ed | cVar3 = '\x11'; | } else { 0x000016f4 | cVar3 = '\x0f'; | } 0x000016fb | if (cVar3 == (char)(cVar1 + ((char)((int16_t)(cVar1 * 0x100b5) >> 0xe) - (cVar1 >> 7)) * -0x5b)) { 0x00001701 | var_85h = 0x30; 0x00001708 | var_83h = 0x72; 0x0000170f | var_84h = 0x47; 0x00001733 | iVar2 = sym.imp.strncmp((int64_t)var_b8h + 0x15, &s2, 6, (int64_t)var_b8h + 0x15); 0x0000173a | if (iVar2 == 0) { 0x0000174a | iVar2 = fcn.0000134e((int64_t)&var_b8h); 0x00001752 | if (iVar2 != 0x61) { 0x0000175b | // WARNING: Load size is inaccurate 0x00001763 | uVar4 = (uint8_t)(*var_b8h >> 7) >> 4; 0x00001770 | timer._0_4_ = (int32_t)(char)((*var_b8h + uVar4 & 0xf) - uVar4) << 8; | } 0x00001794 | if (*(int64_t *)((int64_t)var_b8h + 0xd) == -0x4a7d1343dd25ddf2) { 0x000017c0 | *(uint64_t *)((int64_t)var_b8h + 0xd) = 0x000017a5 | *(uint64_t *)((int64_t)var_b8h + 0xd) ^ 0xc5dd89d072b7137d; 0x000017c3 | sym.imp.time(&timer); 0x000017dd | if ((int32_t)timer < 0x5f820fcc) { 0x000017ea | sym.imp.puts(0x20ce); 0x000017f6 | var_a8h = (char *)0x20cf; 0x00001804 | var_a0h = (char *)0x20d3; 0x00001812 | var_98h = (char *)0x20d7; 0x00001820 | format = (char *)0x20db; 0x0000185f | sym.imp.printf("\nCorrect :)\nFLAG:\tparcham{%s}\n%s%s%s%s\n", var_b8h, 0x20cf, 0x20d3, 0x0000185f | 0x20d7, 0x20db); 0x00001869 | goto code_r0x0000187c; | } | } | } | } | } | } 0x00001872 | sym.imp.puts("Wrong!\n"); |code_r0x0000187c: 0x00001889 | if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) { 0x0000188b | // WARNING: Subroutine does not return 0x0000188b | sym.imp.__stack_chk_fail(); | } 0x00001891 | return 0; |}
در ابتدا باید چند دقیقهای را صرف بررسی کلی کد کنیم. باید تلاش کنیم که متغیرهای اصلی کد را شناسایی کنیم. باید توجه کرد که گاهی اوقات -مثل همین دفعه- شناسایی کردن متغیرهای کلیدی با بررسی خروجی دیکامپایل شده، چندان راحت نیست و نیاز داریم که همزمان کد اسمبلی را هم بررسی کنیم تا به درک درستی از کد برسیم. میتوان با دستور pd
کد اسمبلی را دید و یا از دستور VV
برای رفتن به حالت تصویری استفاده کرد و با بررسی جریان کنترلی برنامه، به این درک رسید که کدام قسمتهای برنامه، بیهوده هستند و برای شلوغکردن سوال قرار داده شدهاند.
بعد از این بررسیها، به این درک خواهیم رسید که اولین قسمت مهم برنامه، این خطها است:
var_c0h = (void *)0x1d;
.
.
.
var_b8h = (void *)sym.imp.malloc(0x1d);
sym.imp.getline(&var_b8h, &var_c0h, _reloc.stdivar_c0h = (void *)0x1d;n)
که بیان میدارد که ورودی ما، بایستی «احتمالا» ۲۹ نویسهای باشد. توجه که کنید در man getline
اشاره شده است که کاربر میتواند ورودیهای بزرگتر از ۲۹ بدهد و در نتیجه تا این لحظه الزامی برای ۲۹ نویسهای بودن پرچم نداریم. فعلا فرض میکنیم که طراح «احتمالا» خواسته است که بدین وسیله، به ما راهنمایی کند که طول ورودی کمتر از ۲۹ است.
بعد از این، یک تابع روی متغیر var_c4h
فراخوانی شده است اما خروجی آن در همان متغیر ریخته شده است و در این حوالی دیگر مورد استفاده قرار نگرفته است. بنابراین فعلا از بررسی آن صرف نظر میکنیم. سپس دو عدد صحیح ۶۴ بیتی مشاهده میکنیم که اگر به حالت VV
برویم، متوجه میشویم که همارز با دو رشته هم هستند. هنوز نمیتوان نظر قطعی داد که باید آنها را به چشم عدد دید یا رشته!
; '_gnUwei5'
movabs rax, 0x35696577556e675f
; 'asa_tAb_'
movabs rdx, 0x5f6241745f617361
mov qword [var_80h], rax
mov qword [var_78h], rdx
سپس با این قسمت روبرو میشویم:
while (var_cch._4_4_ < 0x10) { if ((*(char *)((int64_t)&var_80h + (int64_t)var_cch._4_4_) == *(char *)(var_b8h + (int64_t)(int32_t)var_cch + 5) ) && ((var_cch._4_4_ - (var_cch._4_4_ >> 0x1f) & 1U) + (var_cch._4_4_ >> 0x1f) == 1)) { var_cch._0_4_ = (int32_t)var_cch + 1; } var_cch._4_4_ = var_cch._4_4_ + 1; } if (var_c0h != 0x1d) { var_cch._0_4_ = 1; } if (((int32_t)var_cch + 8U & 0xf) == 0) {
با دقت در خط 0x000014f1
و 0x000014d7
میتوان فهمید که برای ورود به بلوک شرط، لازم است که مقدار var_c0h
برابر با ۲۹ باشد. این جا به نتیجهی قطعی میرسیم که طول ورودی صحیح با احتساب نویسهی \n
کمتر از ۲۹ است. همچنین، میبینیم که مقدار اولیهی timer._0_4_
یا همان timer
صفر است. این متغیر فقط در حلقهی مربوط به خط 0x000014ca
تغییر میکند. با کمی دقت میتوان متوجه شد که مقدار نهایی این متغیر در هنگام رسیدن به خط 0x000014f1
، بایستی برابر با ۸ باشد. خب، چطور به این مقدار میتوان رسید؟
سراغ حلقهی 0x000014ca
میرویم. میبینیم که ورودی داده شده توسط ما که اشارهگر آن var_b8h
است از نویسهی ۵ام به بعد، با بایتهای همان دو عدد ۶۴ بیتی مقایسه میشود. شمارگر مربوط به var_80h
به طور عادی هر بار زیاد میشود اما شمارگر مربوط به ورودی ما، فقط در ازای یک شرط ویژه زیاد میشود. میتوان با کمک کد اسمبلی فهمید که مقدار نهاییِ شمارگر مربوط به ورودی ما، همان چیزی است که در شرط 0x000014f1
لازم است برابر با ۸ باشد. از آنجایی که طول رشتهی موجود در var_80h
و var_78h
برابر با ۱۶ است، میتوان حدس زد که این کد دارد به طور یکی درمیان ورودی ما را چک میکند. فرض کنید که نتوانیم حدس بزنیم. این خط بیشتر از سایر قسمتها غیرقابل فهم است:
((timer._4_4_ - (timer._4_4_ >> 0x1f) & 1U) + (timer._4_4_ >> 0x1f) == 1)
میدانیم که timer._4_4_
عددی مثبت است و «احتمالا» عددی ۴ بایتی است. بنابراین، مقدار عبارت timer._4_4_ >> 0x1f
همیشه برابر با صفر خواهد بود. پس عبارت کلی به این شکل ساده میشود:
((timer._4_4_ - & 1U) == 1)
که معنیاش این است که چک میکند عدد timer._4_4_
در تقسیم بر ۲، باقیماندهی یک دارد یا نه. راههای دیگری هم برای فهمیدن داشتیم. مثلا اسمبلیاش را بخوانیم. یا مثلا روی خط اسمبلی مربوط به این قسمت breakpoint بگذاریم و کد را چند بار اجرا کنیم و با تغییر رجیسترها، ورودیهای مختلف به این قسمت از کد بدهیم و عملکردش را تخمین بزنیم (همان طور که در ویدیوها این کار را کردیم).
نکته:یادتان باشد این کد را برای خودتان با پیمانههای مختلف بنویسید و کامپایل کنید و دیکامپایل کنید. مثلا پیمانه را به ۳ تغییر دهید و ببینید خروجی دیکامپایل چه میشود.
#include <stdio.h>
int main(){
int i;
for (i = 0; i < 1399; i++)
if (i % 2 == 1)
printf("%d\n", i);
}
پس این قسمت از کد به طور سادهشده، به شکل زیر است:
for (i = 0; i < 16; i++)
if (src[i] == input[u + 5] && i % 2 == 1)
u++;
if (strlen(input) != 29)
u = 1;
if ((u + 8) & 16 == 0)
به این ترتیب، ۸ بایت از پرچم را میفهمیم:
?????gUe5s_A_????????????????
اکنون میتوانیم سراغ بلوک داخل 0x000014f1
برویم و آن را بررسی کنیم.اول از همه، یک حلقه در خط
0x000015ad
مشاهده میکنیم. به سرعت میتوان فهمید که دستوراتی که مربوط به متغیر timer._0_4_
هستند، بیهودهاند زیرا این متغیر بلافاصله بعد از حلقه برابر با یک قرار داده میشود. پس آنها را نادیده میگیریم. به جز آن، میتوان دید که در حلقه، ۸ بایت از ورودی ما، یعنی از بایت ۱۳ام تا بایت ۲۰ام، با یک عدد خاص xor میشود. میدانیم که:بنابراین، با توجه به این که حلقه به تعداد دفعات فرد اجرا میشود، نتیجهی محاسبات مانند این است که ورودی ما فقط یک بار با آن عدد خاص xor شود.
در خط
0x000016a6
یک شرط بزرگ میبینیم که حاصل عملیات xor را به طور دو به دو بین ۵ بایت ابتدایی ورودی ما، چک کرده است. اما این معادلات برای به دست آوردن این ۵ بایت کافی نیست. اگر ادامه دهیم، میبینیم که در خط 0x000016fb
شرط دیگری وجود دارد که ما را احتمالا به جواب این معادله راهنمایی کند. میبینیم که متغیر cVar1
یک متغیر یک بایتی است و برابر با نویسهی ابتدایی در ورودی ماست. متغیر timer
کمی بالاتر برابر یک قرار داده شده است، بنابراین مقدار cVar3
برابر با ۱۷ است. اکنون به این معادله میرسیم:
cVar3 == (char)(cVar1 + ((char)((int16_t)(cVar1 * 0x100b5) >> 0xe) - (cVar1 >> 7)) * -0x5b)
خوشبختانه اطلاعاتی داریم که این معادله را برای ما سادهتر میکند. اولا میدانیم که cVar1
باید قابل چاپ باشد و در نتیجه مقدار آن کمتر از ۱۲۸ است. پس عبارت (cVar1 >> 7)
برابر با صفر خواهد بود. از طرفی در عبارت
(char)((int16_t)(cVar1 * 0x100b5) >> 0xe)
میدانیم که بعد از ۱۴ بار جابجایی یک عدد ۱۶ بیتی به سمت راست، مقدار نهایی میتواند بین صفر تا ۳ باشد. پس معادله این شکل ساده میشود:
cVar3 == (char)(cVar1 - (a number from 0,1,2,3) * 91)
سمت چپ معادله را میدانیم که برابر با ۱۷ است. پس عدد cVar1
باید با توجه به قابل چاپ بودنش، عددی بین صفر و ۱۲۷ باشد که بر ۹۱ باقیماندهی ۱۷ داشته باشد. با مراجعه به جدولهای ASCII میفهمیم که خود ۱۷ قابل چاپ نیست و در نتیجه cVar1
بایستی برابر با ۱۰۸ باشد که همان نویسهی l
است. اکنون به خط 0x000016a6
برمیگردیم و اکنون میتوانیم مقدار هر ۵ نویسه را بفهمیم. بعد از محاسبه، میفهمیم که پرچم تا اینجای کار به شکل زیر است:
l3t5_gUe5s_A_??????????????
بعد از این، میبینیم که در خط 0x00001733
یک فراخوانی بر روی تابع strncmp
انجام شده است. در این فراخوانی، ورودی ما از بایت ۲۱ام به اندازهی ۶ بایت، با آرایهای در حافظه که ابتدای آن s2
است، مقایسه شده است. در اینجا لازم داریم که مقادیر موجود در آرایهی s2
را بفهمیم. به تعریف متغیرها در ابتدای تابع رجوع میکنیم. میدانیم که در رادار، نامگذاری به این ترتیب است که متغیر فرضی var_abh
در آدرس rbp-0xab
قرار دارد. همچنین میدانیم که ترتیب تعریف متغیرها در ابتدای توابع در رادار، مشابه ترتیبی است که در حافظه از rsp به rbp دارند. بنابراین، مقادیر مربوط به آرایهی s2
همان مقادیر var_86h
تا var_81h
است. اگر به کمی بالاتر از محل strncmp
نگاه کنیم، مقادیر این ۶ نویسه را میبینیم. به این ترتیب، نویسههای ۲۱ام تا ۲۶ام پرچم، r0Gr4m
است. در این لحظه، فقط مقدار نویسههای ۱۳ام تا ۲۰ام پرچم را نمیدانیم که در قسمتهای قبل، آن را با یک عدد خاص xor کردهایم.در خط
0x00001794
میبینیم که روی همان ۸ بایت شرط گذاشته و در صورتی که شرط درست باشد، دوباره آنها را با همان عدد قبلی xor میکند و چند خط بعدتر آن را چاپ میکند. با این حساب، باید کاری کنیم که شرط درست باشد. یعنی:به این ترتیب این ۸ بایت قابل محاسبه است. با در نظر گرفتن endianness معکوس، متوجه میشویم که این بخش از رشته
s1mPle_p
است.به نظر میرسد که به پرچم رسیدهایم. هنوز یک شرط دیگر داریم که زمان سیستم را چک میکند و آن را میسنجد. این قسمت بیاعتبار است زیرا بر روی ورودی ما تاثیر ندارد و صرفا باعث میشود که حتی اگر پرچم درست را به کد بدهیم، به ما خروجی
Correct
را ندهد. پس به آن توجه نمیکنیم و قسمتهای مختلف پرچم را به یکدیگر میچسبانیم:parcham{l3t5_gUe5s_A_s1mPle_pr0Gr4m}