مهندسی معکوس به همراه یک مثال ساده
در پست “کشف دنیای جذاب مهندسی معکوس” با مهندسی معکوس آشنا شدیم. در این پست هم قصد داریم به یک مثال ساده در این مورد بپردازیم و نحوه انجام کار رو به صورت عملی ببینیم.
برنامه ای که برای این مثال در نظر گرفتم رو در ادامه میتونید ببینید.
ابتدا اطلاعاتی رو در مورد فایل ELF با ابزار readelf می گیریم. همانطور که مشاهده می کنید، کلاس ELF64 هست که یعنی برای پردازنده های 64 بیتی میباشد. مدل داده هم مکمل 2 هست و little endian هم یعنی اعداد منفی از روش معکوس عدد بعلاوه یک به دست میان و همچنین داده ها که به صورت برعکس هستند، یعنی از آخر به اول چیده میشن. اطلاعات دیگه مثل نوع فایل که اجرایی هست و نوع سیستم عامل هایی که بر روی انها میتواند اجرا شود یعنی یونیکسی ها و BSD ها رو میتونید ببینید.
خب، در اینجا با ابزار objdump یه disassembly از تابع main رو میبینیم و اگر پست قبلی را خوانده باشید میتونید از روی آدرس اون متوجه بشید که entry point همون تابع main نیست. ابزار objdump یک ابزار gnu هست و تمام ابزارهای گنو بطور پیشفرض از سینتکس at&t برای اسمبلی استفاده می کنند.
خب در اینجا با ابزار readelf سمبل هایی که تابع هستند را می گیریم و توابع داخلی برنامه مثل save_command و create_tempfile و تابع main رو می بینیم که توی این لیست هستند.
حالا ما یک فایل برای نوشتن شبه کد در سمت دیگر ایجاد می کنیم و بدنه ای برای این توابع داخلی می نویسیم.
حالا ترجیحا با ابزار radare2 فایل رو باز می کنیم. ابتدا با دستور aaa آنالیز و hint فایل را فعال می کنیم، بعد از آن نقطه فعلی را می بریم به آدرس تابع main که البته ضرورتی هم نداره و صرفا برای این هست که هر دفعه در دستورات دیگر آدرس را مشخص نکنیم، بعد از اون با دستور pdf، disassembly از کل تابع میگیریم. سینتکس بطور پیشفرض روی اینتل هست، اگر با سینتکس at&t راحت تر هستید میتوانید با دستور e asm.syntax = att سینتکس را به at&t تغییر دهید.
بذارید یک نگاه به 5 خط اول بیاندازیم. سه خط اول برای تنظیم حافظه stack است، یعنی حافظه ای که متغیرهای لوکال هر تابع در اون ذخیره میشن. خط اول محتویات ثبات rbp هست، یعنی همان base pointer که آدرس پایه رو ذخیره میکنه تا تابع بتونه استک خودش رو بسازه و آدرس استک قبلی یعنی استک تابعی که این تابع را صدا زده، حفظ شود تا در آخر تابع آن را برگرداند.
در خط دوم rsp را بر روی rbp میریزد که یعنی قرار است استک این تابع، پایین استک تابع قبلی قرار بگیرد. در خط سوم از rsp به هگز 0x40 یعنی 64 خانه کم می کند، یعنی استک 64 بایت می باشد.
در خط چهارم و پنجم از ثبات های rdi و rsi به ترتیب آرگومان های تابع را گرفته و در حافظه استک که ساخته است، ذخیره می کند. از آنجایی که ما می دانیم به تابع main چه آرگومان هایی داده می شود و با چه نوعی و به چه منظوری آنها رو در شبه کد می نویسیم. در اینجا فقط دو آرگومان گرفته شده پس فقط آنها را می نویسیم.
خب در اینجا میبینیم که مکانیزم امنیتی پیشگیری از حملات stack smashing به نام stack guard بر روی این باینری صورت گرفته است. برای پیاده سازی این مکانیزم روی خروجیه باینری از gcc، کافیه فقط آپشن -fstack-protector را به آپشن های دیگه اضافه کنید. در مورد نحوه کارکرد این مکانیزم و روش دور زدن آن احتمالا در یک پست دیگر توضیح خواهم داد.
خب در اینجا صدا کردن تابع puts رو می بینیم که یک رشته بعنوان آرگومان به آن داده اند. از آنجایی که با دستور aaa آنالیز انجام داده و hint ها را فعال کردیم بجای آدرس هگز، برنامه radare2 برای رشته ها برچسب گذاشته و از برچسب استفاده می کند.
ما با استفاده از نام برچسب، مقدار نسبی اش را نوشتیم. اما برای اینکه مقدار دقیق را بدست بیاوریم از دستور ps که مخفف print string هست استفاده می کنیم.
در این قطعه کد کاملا مشخص است که یک رشته را در یک آرایه بر روی استک ذخیره می کند.
از آنجایی که byte order داده این فایل little endian است، یعنی داده ها از آخر به اول نوشته می شوند، ما بعد از تبدیل آن کد هگز به رشته کاراکتری آن را معکوس می کنیم.
از آنجایی که عینا چند بار تکرار شده ما حدس میزنیم که آن ماکرو باشد که البته فرق زیادی نمی کند اما در خوانایی کد تاثیر دارد.
در اینجا می بینیم که برنامه تابع داخلی create_tempfile را با یک آرگومان که همان رشته اسم فایل است صدا می کند. از این رو مشخصا نوع آرگومان char* می باشد.
در اینجا خروجی تابع create_tempfile در یک متغیر از نوع int ذخیره شده و با عدد -1 مقایسه می شود. اکثر اوقات و تقریبا همیشه شرط پرش بعد از مقایسه برعکس شرط داخل پرانتز است و دستورات بعد از آن دستورات داخل بلوک ان شرط هستند. از آن دستور پرش در آدرس 0x4009be و نبود هیچ مقایسه ای بعد از آن و همینطور از دستور پرشی که در آدرس 0x400984 دیده ایم مشخص است که اون یک بلوک else هست.
ما در اینجا یک صدا کردن تابع perror با یک آرگومان را میبینیم و بعد از آن میبینیم که عدد 1 را در ثبات eax میریزد که این مشخصا به این معنی است که خروجی تابع را تنظیم میکند و آن پرش مسلما به آخر تابع هست.
خب حالا برمیگردیم به بلوک if و آن را ترجمه میکنیم. در ضمن دستوری که در آدرس 0x400992 میبینید صرفا برای پاکسازی اون ثبات برای تابعی که قرار است صدا کند می باشد.
خب در اینجا تابع save_command را با سه آرگومان صدا میزند و خروجی تابع را بطور مستقیم با -1 مقایسه میکند. آن پرش به آدرس 0x4009d1 مشخص میکند که بلوک ما از آنجا شروع میشود. این شرط بعد از بلوک else قرار دارد، بنظر کامپایلر احتمالا بخاطر optimization این شرط را اینجا گذاشته. متاسفانه بعد از اتمام عکسبرداری ها متوجه این موضوع شدم. برای همین در صورت امکان این کدها را بعد از بلوک else در نظر بگیرید.
خب حالا قسمت آن بلوک را ترجمه میکنیم.
حالا میرویم به قسمتی که شرط if در صورت برقرار نبودن به آنجا پرش میکند که مشخصا بیرون از بلوک است، از آنجایی که ادامه بلوک مستقیما به آن میرسد.
و ادامه را ترجمه و مقادیر دقیق رشته ها را میگذاریم.
خب در اینجا میبینیم یک پرش به پایین انجام میگیرد، یک شرط چک میشود و درصورت برقرار بودن یک پرش به بالا انجام میشود تا یکسری دستورات اجرا شوند و دوباره به آن شرط برسند تا موقعی که آن شرط برقرار نشود. خب… این مشخصا یک حلقهمی باشد، حلقه while برای آن مناسب تر است.
خب این شرط حلقه می باشد، و میبینیم در rbp-0x25 یک متغیر هست که از قبل مقدار دهی نشده است. این متغیر مشخصا از نوع char هست، بخاطر تبدیل به بایت و جابجایی sign دار در آدرس های 0x400a16 و 0x400a1a.
این از بدنه و بلوک حلقه که آن متغیر کاراکتری را که خوانده چاپ میکند.
اینجا هم بیرون از حلقه کاراکتر خط جدید یا همان ‘n’ را چاپ میکند.
خب در اینجا میبینیم که میخواهد مقدار صفر را برگرداند و مقدار stack canary را که اوایل تابع ذخیره کرده بود چک میکند. هر عددی که با خودش xor شود صفر خواهد شد، اینجا هم به همین ترتیب چک میکند تا ببیند که آن مقدار تغییر نکرده باشد، چرا که اگر عوض شده باشد، یعنی یک عامل خارجی آن را تغییر داده، بنابراین آن تابع را که میبینید صدا کرده تا خطا را نمایش دهد و به کاربر هشدار دهد، در غیر اینصورت به تابع صدا کننده برمیگردد.
این هم از تابع create_tempfile. حالا فقط تابع save_command باقی میماند که اونم بعنوان تمرین خودتون انجام میدین.