این کار "هک" نیست و با کاری که من در محل کارم انجام میدهم خیلی متفاوت است. ولی، دستکاری save این بازی فرصت خوبی است تا با یکسری از مفاهیم امنیت بازیهای کامپیوتری آشنا شویم.
اصل این نوشته را میتوانید در بلاگ انگلیسی من بخوانید و مانند همیشه یک کپی از این نوشته در بلاگ فارسی من در https://parsiya.github.io/parsiya.fa/post/game-hacking-1 در دسترس است.
دستکاری سِیو، یکی از قدیمیترین و سادهترین روشها برای تقلب در بازیهای کامپیوتری است. تقلب در بازی یکی از عوامل اصلی آشنایی افراد با مقوله امنیت است. خیلی از افراد از جمله من از اینجا شروع کردیم و اولین قدمهای خود را با مهندسی معکوس ساختار سِیوهای بازی برداشتیم.
هیچ وقت فکر نمیکردم که حقوق بگیرم که بازی کامپیوتری هک کنم.
در این بازی ما یک مکانیک هستیم که باید قطعات مختلف اتومبیل را عوض کند. دو مشخصه داریم:
هدف ما دستکاری سِیو بازی و تغییر این مقادیر است. سِیوهای بازی(نگارش Steam) در ویندوز در آدرس زیر ذخیره میشوند:
%AppData%\LocalLow\Red Dot Games\Car Mechanic Simulator 2015\
در اینجا چند دایرکتوری داریم که هر کدام مخصوص یک پروفایل در بازی هستند و نام آنها profile0 و غیره است.
اولین قدم همیشه تبدیل مقداری که میخواهیم عوض کنیم به مبنای 16 یا hex و جستجوی آن در فایل است. این کاری است که در اکثر بازیهای قدیمی جواب میدهد. مثلاً اولین دستکاری سِیو من در بازی Heroes of Might and Magic بود. فرض کنید در این بازی میخواستم تعداد Unicorn هایم را اضافه کنم. دو گروه 13 عددی تشکیل میدادم و بعد در فایل سِیو به دنبال 0D میگشتم. اگر دو مقدار نزدیک به هم پیدا میکردم آنها را به FF (یا 255) تغییر میدادم. اگر درست بود، که چه خوب وگرنه دوباره جستجو میکردم.
در بازی SimCity 2000 هم مقدار پول در مبنای 16 در فایل سِیو ذخیره میشد. نکتهای که در سِیو این بازی وجود داشت این بود که تعداد بایتها با افزایش عدد افزایش پیدا میکرد. مثلاً مقدار 10000 به صورت 0x2710 در دو بایت ذخیره میشد. برای افزایش پول اگر این دو بایت را به FF FF تغییر میدادم به 65535 میرسیدم که پول زیادی نبود. اما اگر خودم یک بایت به فایل اضافه میکردم دیگر فایل قابل بارگذاری نبود. این به احتمال زیاد برای این بود که offset قسمتهای مختلف فایل به هم میریخت.
برای حل این مشکل اول مقدار پول خود را به 65535 تغییر میدادم. سپس با انتشارjunk bond (چیزی شبیه اوراق مشارکت) مقدار پول خودم را اضافه میکردم تا نیاز به بایتهای بیشتری برای ذخیره آن باشد و سه بایت شود. سپس آن را به FF FF FF و یا 16 میلیون و خردهای تغییر میدادم. با این کار دیگر احتیاجی به مهندسی معکوس بقیه فایل نبود و سریع به هدفم رسیدم.
در اینجا هم به دنبال 0x07D0 (معادل 2000 در مبنای 16) و کلمه money در کل فایلهای دایرکتوری یک پروفایل گشتم:
$ grep -arb $'\xd0\x07' global:631:▒▒▒▒{~gameVer▒▒▒▒▒1.1.6.0{~date▒▒▒▒▒2017-11-28 22:56:20{ $ grep -arb money global:610:▒▒▒{~money
این مقادیر در فایلی به نام global پیدا شدند:
یکی از مهمترین تواناییها در مهندسی معکوس حدس زدن آگاهانه است (فکر کنم ترجمه فارسی educated guess). یعنی با داشتن اطلاعاتی محدود و معمولاً بر اساس تجربه حدس میزنیم. سپس این حدس را امتحان میکنیم و اگر درست بود زمان معمولاً زیادی صرفهجویی شده است. اگر غلط بود حدسمان را با نتیجه آزمایش تغییر میدهیم و دوباره آزمایش میکنیم.
مثلاً بعد از دیدن فایل با این فرمت من مطمئن هستم که این یک فایلِ serialize شده است. بعد از دیکامپایل کردن بازی و جستجو در کد، فهمیدم که از کتابخانه Easy Save 2 استفاده شده است.
این دو عدد به صورت little-endian در چهار بایت ذخیره شده اند. حدس من این است که این مقادیر در یک متغیر از نوع int 32 ذخیره شدهاند. برای آزمایش حدس این مقادیر را به FF FF FF FF تغییر دادم.
و بازی را شروع کردم اما نتیجه آن چیزی که میخواستم نبود.
این یعنی چه؟ این یعنی مقدارها در یک متغیر از نوع signed int 32 ذخیره میشوند. در یک متغیر signed اولین بیتِ بزرگترین بایت (یعنی بایت چهارم) مخصوص علامت عدد است (اگر صفر باشد مثبت است و اگر یک باشد منفی). اگر عدد منفی باشد بقیه عدد به صورت two's complement (فارسیش واقعاً نمیدانم چه میشود) ذخیره میشود. برای بدست آوردن این مقدار همه بیتها را تغییر میدهیم (اگر صفر بود یک میشود و برعکس) و سپس با یک جمع میکنیم. برای همین است که FF FF FF FF در واقع منفی یک است.
برای داشتن بزرگترین عدد ممکن در یک signed int 32 باید عدد 7F FF FF FF را وارد کنیم.
اینجا فکر کردم کار من تمام شده و حواسم به integer overflow نبود. بعد از شروع بازی و کمی تجربه کسب کردن، تجربهام دوباره منفی شد.
چرا؟ چون مقدار در یک signed int 32 ذخیره میشود و ما بزرگترین عدد ممکن (max int) را در تجربه داشتیم. اضافه کردن یک به چنین عددی باعث میشود که integer overflow داشته باشیم و تبدیل به کوچکترین عدد ممکن شود. اضافه کردن 1 به 7F FF FF FF مقدار 00 00 00 80 را به ما میدهد که معادل min int است.
بازی هم کنترلی برای چِک کردن این حالت ندارد چون در داخل بازی هیچوقت به چنین مقدار پول یا تجربهای نمیرسیم.
برای حل این مشکل باید عددی کوچکتر وارد کنیم. مثلاً من مقدار 7F 00 00 00 را وارد کردم.
معمولاً دستکاری سِیو یک مشکل امنیتی (security issue) نیست. اکثر سِیوها برای بازیهای تک نفره (single player) هستند و مشکل امنیتی این بازیها بسیار محدود است. اگر بشود با چنین سِیو در یک بازی چند نفره (multiplayer) بازی کرد آن وقت مشکل داشتیم. مثلاً اگر این بازی یک حالت چند نفره داشت و همه با هم در یک تعمیرگاه کار میکردند آن وقت مشکل داشتیم.
اکثر مشکلات بازیهای تک نفره امنیتی نیستند. مثلاً یکی از بازیهای تک نفره ما Jedi: Fallen Order است. اگر شما بتوانید در این بازی تقلب کنید تاثیری در بازی بقیه کاربران ندارد. هنگام تست امنیتی چنین بازی این مسائل برایم مهم نیست.
تنها حالتی که ممکن است دستکاری سِیو یک بازی فقط تک نفره مشکل امنیتی باشد وقتی است که باز کردن این سِیو در یک کامپیوتر دیگر باعث remote code execution یا دستکاری فایلهای کامپیوتر مقصد شود.
این مشکل در بازی Untitled Goose Game وجود داشت. این بازی هم با موتور Unity تولید شده است و سِیوهای این بازی هم serialize شده هستند و هنگام بارگذاری آن و deserialize کردن کنترلی ندارد. اطلاعات بیشتر را در لینک زیر میتوانید بخوانید. چیز عجیبی نیست یک insecure deserialization کلاسیک داتنت است.
دستکاری سِیو این بازی بسیار ساده است اما ما را با مفاهیمی مانند serialization، معندسی معکوس فایل و int 32 آشنا کرد و دستگرمی خوبی است.