چالش مهندسی معکوس اول - 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 می‌شود. می‌دانیم که:

a ⊕ b ⊕ b = a

بنابراین، با توجه به این که حلقه به تعداد دفعات فرد اجرا می‌شود، نتیجه‌ی محاسبات مانند این است که ورودی ما فقط یک بار با آن عدد خاص 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 می‌کند و چند خط بعدتر آن را چاپ می‌کند. با این حساب، باید کاری کنیم که شرط درست باشد. یعنی:

x ⊕ 0xc5dd89d072b7137d = -0x4a7d1343dd25ddf2

به این ترتیب این ۸ بایت قابل محاسبه است. با در نظر گرفتن endianness معکوس، متوجه می‌شویم که این بخش از رشته s1mPle_p است.
به نظر می‌رسد که به پرچم رسیده‌ایم. هنوز یک شرط دیگر داریم که زمان سیستم را چک می‌کند و آن را می‌سنجد. این قسمت بی‌اعتبار است زیرا بر روی ورودی ما تاثیر ندارد و صرفا باعث می‌شود که حتی اگر پرچم درست را به کد بدهیم، به ما خروجی Correct را ندهد. پس به آن توجه نمی‌کنیم و قسمت‌های مختلف پرچم را به یکدیگر می‌چسبانیم:
parcham{l3t5_gUe5s_A_s1mPle_pr0Gr4m}