ویرگول
ورودثبت نام
امیر فراهانی
امیر فراهانییه برنامه نویس ساده
امیر فراهانی
امیر فراهانی
خواندن ۷ دقیقه·۶ ماه پیش

کدهایی که اجرا میشوند اما دیده نمی‌شوند! روایت compile و marshal در پایتون و نحوه استخراج کد منبع

شاید شماهم کدای پایتونی دیده باشید که توی نگاه اول، منطقش رو درست درک نمیکنید ولی وقتی اجراش میکنید در کمال ناباوری اجرا میشه!

البته شایدم اجرا نشه، چون احتمالا نسخه پایتون اونی که کد رو کامپایل و مارشال کرده با شما یکی نبوده...

اینی که گفتی یعنی چی؟ چرا یه مثال نمیزنی برامون؟

چشم، مثالم میزنم.

ببینید

مثال اول، کدی که کامپایل و مارشال شده:

import marshal
exec(marshal.loads(b'c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\xf3\x1c\x00\x00\x00\x97\x00\x02\x00e\x00d\x00\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00d\x01S\x00)\x02z\x0eHello marshal!N)\x01\xda\x05print\xa9\x00\xf3\x00\x00\x00\x00\xda\x07example\xfa\x08<module>r\x05\x00\x00\x00\x01\x00\x00\x00s\x1c\x00\x00\x00\xf0\x03\x01\x01\x01\xd8\x00\x05\x80\x05\xd0\x06\x16\xd1\x00\x17\xd4\x00\x17\xd0\x00\x17\xd0\x00\x17\xd0\x00\x17r\x03\x00\x00\x00'))

(کد بالا با استفاده از نسخه پایتون 3.11 کامپایل شده، احتمالا روی نسخه های دیگه پایتون اجرا نشه)

خروجی:

Hello marshal!

مثال دوم، کدی که با base64 انکد و معکوس شده:

_ = lambda __ : __import__('base64').b64decode(__[::-1]);exec((_)(b'pIGIsEGK05WayBnCiQjNlNXYiJCI9AiYKISaIJCI9ASY'))

خروجی:

Hi base64

مثال سوم، کدی که معکوس، با base64 انکد و با zlib فشرده شده:

_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));exec((_)(b'=QQDbFSA0IJF1JBNKNvMKqAuUOpMypKFSNvEUwUzTtoETGrAKJ+VJnczIJPUVBLVLxJe'))

خروجی:

Hello base64 and zlib

همونطور که گفتم در نگاه اول ممکنه درک نکنید این کدا اصن چی هستن و چطوری کار میکنن(البته اگه مبتدی باشید!)

کدوم آدم مریضی نشسته این کارو کرده؟ هدفش چی بوده؟

خب هدفش که مشخصه، مبهم کردن کد ها برای اینکه کسی که میخواد کد رو اجرا کنه به کد منبع دسترسی نداشته باشه و صرفا اجرا کننده باشه! و احتمالا یا میخواد الگوریتمش لو نره یا اطلاعات مهمی داخلش هست که نمیخواد کسی ببینه یا حالا به هر دلیلی.

البته (داخل پرانتز بگم که یه سریاهم چون کد مخرب میخوان بدن تا شما اجرا کنید و از بی اطلاعیتون سوء استفاده کنن ممکنه کد رو به این شکل بهتون بدن تا امکان بررسی کد رو نداشته باشید!)

خب خب حالا بریم بررسی کنیم هر کدوم از این کد ها رو چطوری ببینیم و بفهمیم توش چیه؟ :)

عرضم به خدمتتون همونطور که میدونید مثال دوم و سوم که کلا بچه بازیه! چون شما دارید کد رو اجرا میکنید در حالی که کد منبع رو اصلا در اختیار دارید!

میگی نه؟ نگا کن کار خیلی سادست فقط کافیه به جای تابع exec از تابع print استفاده کنی

مثال دوم:

_ = lambda __ : __import__('base64').b64decode(__[::-1])
print((_)(b'pIGIsEGK05WayBnCiQjNlNXYiJCI9AiYKISaIJCI9ASY').decode())

چیکار کردم؟ هیچی فقط جای exec اومدم print گذاشتم و برای اینکه کدمون خوشگل و استرینگی چاپ بشه نه به صورت باینری، آخرش یه .decode() هم اضافه کردم.

در حقیقت خود کد طوریه که قبل از اجرا شدن توسط exec میاد و دیکد میشه و بعد اجرا میشد و ما بهش میگیم نه! به جای اگزکیوت شدن به ما نشون بده چی داری؟ و اونم میگه چشم!

خروجی:

a = "Hi"

b = "base64"

print(a, b)

و اما مثال سوم:

_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]))
print((_)(b'=QQDbFSA0IJF1JBNKNvMKqAuUOpMypKFSNvEUwUzTtoETGrAKJ+VJnczIJPUVBLVLxJe'))

توی این کد عمدا decode() رو صدا نزدم تا تفاوتشو حس کنید

خروجی:

b'a = "Hello"\nb = "base64 and zlib"\nprint(a, b)'

که در حقیقت میشه همون

a = "Hello"

b = "base64 and zlib"

print(a, b)

چه جذاب، چه آسون!

سوال پیش میاد

اصلا این مثالایی که زدی رو از کجا آوردی چطوری اینا به این شکل انکد شدن؟

پاسخ: هر کسی که یه مقدار پایتون بلد باشه میدونه با استفاده از کتابخونه base64 و zlib و یا حالا هر کتابخونه یا هر روش دیگه ای که مربوط به انکد و دیکد کردن رشته باشه میتونه چنین حرکتایی بزنه و در حقیقت کافیه کدمون رو به شکل رشته در بیاریم و انکدش کنیم! و برای اجرا کردن کافیه قبل از exec بیایم و اون رو دیکد کنیم و تمام. دقیقا مشابه مثال هایی که دیدین.

اکثر کسایی که این کارو انجام میدن از سورس کد معروف زیر برای این کار استفاده میکنن

https://github.com/Mr-Spect3r/PyObfuscate/blob/main/PyObfuscate.py

شما هم میتونید تستش کنید و اگه اون رو اجرا کنید میبینید که روش های مختلفی و گاها ترکیبی از چند تا روش برای انکد کردن کدتون وجود داره!

به روش هایی که مارشال دارن دقت کنید
به روش هایی که مارشال دارن دقت کنید

یعنی همشون همینقدر کشکی باز میشن؟ فقط با عوض کردن exec با print؟

جواب سادست! نه

حقیقت اینه که تمام روش های بالا به جز 16 که اصلا کاری باهاش نداریم و به جز روش هایی که در اون از مارشال استفاده نشده(2 تا 8) به همون سادگی که بالاتر توضیح دادم دیکد میشن!

پس قاعدتا دوستان عزیزی که کدهاشون رو با استفاده از سورس کد Mr-Spect3r انکد میکنن زیاد از اون روش ها استفاده نمیکنن و بیشتر تمایل به روش marshal دارن چون به سادگی باز نمیشه!

و اینجا تازه اصل داستان شروع میشه...

روش مارشال اصلا چیه؟

ببینید خیلی ساده بخوام بگم این روش صرفا محدود به مارشال نیست و حداقل ترکیبی از تابع compile و مارشاله

اینکه کاربرد کتابخونه مارشال و جزئیات تابع compile چیه رو اینجا نمیگیم چون حرفمون درباره چیز دیگست.

فقط دوتا مثال میزنم برای اینکه ببینیم چطوری کدمون رو کامپایل و مارشال کنیم:

در اینجا کدمون رو صرفا مارشال کردیم و میبینید که خیلی اتفاق خاصی نمیفته و کدی که فقط مارشال شده باشه با روش های قبلی تفاوت آنچنانی نداره و میشه بعد از آنمارشال کردنش، خیلی راحت اونو به جای exec، پرینت کرد. (هرچند که بدون آنمارشال کردن هم تا حد زیادی مشخصه چی به چیه)

import marshal

code = """
a = 4
b = 10
print("sum:", a + b)
"""

marshaled_data = marshal.dumps(code)
print(marshaled_data)
print(marshal.loads(marshaled_data))
exec(marshal.loads(marshaled_data))

خروجی:

marshaled code: b'\xfa#\na = 4\nb = 10\nprint("sum:", a + b)\n'

code:

a = 4

b = 10

print("sum:", a + b)

sum: 14

حالا که فهمیدیم مارشال کردن به تنهایی فایده ای نداره پس بیاید ببینیم اصل کاری یعنی compile چیکار میکنه:

code = """
a = 4
b = 10
print("sum:", a + b)
"""

compiled_data = compile(code, "example", "exec")
print("compiled code:", compiled_data)
exec(compiled_data)

خب در بالا میبینید که کدمون رو با استفاده از تابع compile به یک code object تبدیل کردیم و تونستیم با استفاده از exec اونو اجرا کنیم.

خروجی:

compiled code: <code object <module> at 0x000002129E781110, file "example", line 1>

sum: 14

اما نکته اینجاست که ما توی روش های قبلی برای هر dumps یه loads و برای هر انکدی یه دیکدی داشتیم ولی اینجا ما فقط تابع compile داریم و معکوسش یعنی decompile نداریم :)

اینجاست که دلیل استفاده از این تابع مشخص میشه! "بازگشت ناپذیری آسان"

از طرفی سوالی که پیش میاد اینه، آیا من میتونم یک code object رو به کسی بدم تا بخواد اجراش کنه؟ یا برای خودم بخوام نگه دارم تا بعدا اجرا کنم؟ چون توی مثالی که زدیم کد منبع مستقیما وجود داشت ولی ما اینو نمیخوایم!

قاعدتا من نمیتونم عبارت <code object <module> at 0x000002129E781110, file "example", line 1> رو بعدا به عنوان کد اجرا کنم و اینجا اهمیت مارشال برامون مشخص میشه!

برای نگه داری code object باید اونو به رشته تبدیل کنیم یا حالا به هر طریقی ذخیرش کنیم طوری دفعات بعدی بدون کامپایل کردن فقط با exec اجراش کنیم!

پس خواهیم داشت:

import marshal
code = """
a = 4
b = 10
print("sum:", a + b)
"""

mc_data = marshal.dumps(compile(code, "example", "exec"))
print("compiled and marshaled code:", mc_data)

خروجی:

compiled and marshaled code: b'c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\xf3,\x00\x00\x00\x97\x00d\x00Z\x00d\x01Z\x01\x02\x00e\x02d\x02e\x00e\x01z\x00\x00\x00\xa6\x02\x00\x00\xab\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00d\x03S\x00)\x04\xe9\x04\x00\x00\x00\xe9\n\x00\x00\x00z\x04sum:N)\x03\xda\x01a\xda\x01b\xda\x05print\xa9\x00\xf3\x00\x00\x00\x00\xda\x07example\xfa\x08<module>r\t\x00\x00\x00\x01\x00\x00\x00s+\x00\x00\x00\xf0\x03\x01\x01\x01\xe0\x04\x05\x80\x01\xd8\x04\x06\x80\x01\xd8\x00\x05\x80\x05\x80f\x88a\x90!\x89e\xd1\x00\x14\xd4\x00\x14\xd0\x00\x14\xd0\x00\x14\xd0\x00\x14r\x07\x00\x00\x00'

به به! خیلی عالی و زیبا تونستیم code object رو مارشال کنیم و به باینری داشته باشیمش، الان دیگه به راحتی قابل نگداری و قابل انتقاله تا در آینده بتونیم اجراش کنیم.

چطوری در آینده اجرا میشه؟

خیلی ساده مثل اولین مثال مقاله:

import marshal
exec(marshal.loads(b'...'))

تا اینجا فهمیدیم که روش های مارشال چطوری کار میکنن. پایه و اساس همشون همینه ولی یه سریاشون میان و باینری که به دست میاد رو دوباره با base64 یا چیزای دیگه انکد میکنن و گاها ممکنه این کار رو توی چند لایه تو در تو انجام بدن تا دیکد کردنش سخت تر بشه.


نصف داستان رو فهمیدیم.

حالا بریم سراغ اصل کاری یعنی استخراج کد منبع از دل این عبارت باینری نامفهوم که من انسان هیچی ازش نمیفهمم!

یادتونه گفتم تابع کامپایل داریم ولی دیکامپایل نداریم؟

ولی الان باید اعتراف کنم به جاش یه چیز دیگه داریم که خیلی بهمون کمک میکنه و اون چیزی نیست جز دیساسمبلر پایتون

نحوه استفاده:

مثلا فرض کنید کد مثال اول که مارشال شده بود رو داریم و میخوایم اون رو دیساسمبل کنیم

کافیه به جای exec این بار dis بذاریم

from dis import dis
import marshal
dis(marshal.loads(b'c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\xf3\x1c\x00\x00\x00\x97\x00\x02\x00e\x00d\x00\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00d\x01S\x00)\x02z\x0eHello marshal!N)\x01\xda\x05print\xa9\x00\xf3\x00\x00\x00\x00\xda\x07example\xfa\x08<module>r\x05\x00\x00\x00\x01\x00\x00\x00s\x1c\x00\x00\x00\xf0\x03\x01\x01\x01\xd8\x00\x05\x80\x05\xd0\x06\x16\xd1\x00\x17\xd4\x00\x17\xd0\x00\x17\xd0\x00\x17\xd0\x00\x17r\x03\x00\x00\x00'))

خروجی:

0 0 RESUME 0

1 2 PUSH_NULL

4 LOAD_NAME 0 (print)

6 LOAD_CONST 0 ('Hello marshal!')

8 PRECALL 1

12 CALL 1

22 POP_TOP

24 LOAD_CONST 1 (None)

26 RETURN_VALUE

یا مثلا همین کدی که یه کم پیش کامپایلش کردیم بعد از دیساسمبلی شدن خروجی زیر رو میده:

0 0 RESUME 0

2 2 LOAD_CONST 0 (4)

4 STORE_NAME 0 (a)

3 6 LOAD_CONST 1 (10)

8 STORE_NAME 1 (b)

4 10 PUSH_NULL

12 LOAD_NAME 2 (print)

14 LOAD_CONST 2 ('sum:')

16 LOAD_NAME 0 (a)

18 LOAD_NAME 1 (b)

20 BINARY_OP 0 (+)

24 PRECALL 2

28 CALL 2

38 POP_TOP

40 LOAD_CONST 3 (None)

42 RETURN_VALUE

بی نظیره! این برای من انسان خیلی قابل درک تره ولی هنوزم چیزی که من میخواستم نیست!

کد منبع رو چطوری به دست بیاریم؟

روش اول: آستینا رو بالا میزنیم و خودمون با توجه به چیزی که میبینیم کدشو مینویسیم!

توی کدایی که تعداد خط کداش بیشتره، منطقی به نظر نمیرسه پس میریم سراغ روش دوم.

روش دوم: خروجی آدمیزدی که به دست آوردیم رو میدیم به یکی از هوش مصنوعی های موجود(ترجیحا دیپ سیک) و ازش خواهش میکنیم تا بهمون کد پایتونی رو بده D:

اونم میگه چشم!

دیدین که تونستیم کدی که کامپایل-مارشال شده بود رو به کد منبع برسونیم و بفهمیم محتوای داخلش چیه:)

ببخشید اگه یه مقدار طولانی شد، امیدوارم براتون مفید باشه.

پایتونبرنامه نویسیکدنویسیکامپایل
۵
۰
امیر فراهانی
امیر فراهانی
یه برنامه نویس ساده
شاید از این پست‌ها خوشتان بیاید