تفکرات صفر و یکی

نوشته های من در مورد سیستم عامل، درایور نویسی، مهندسی معکوس، امنیت و هر چیز سطح پایین دیگر

تفکرات صفر و یکی

نوشته های من در مورد سیستم عامل، درایور نویسی، مهندسی معکوس، امنیت و هر چیز سطح پایین دیگر

آخرین نظرات

در آموزش های قبلی به صورت خیلی خلاصه در مورد روش هایی که یک درایور می تواند با برنامه سطح کاربر ارتباط برقرار کند صحبت کردیم و برای یکی از این روش ها که  IOCTL بود مثالی آوردیم. در این آموزش سعی می کنم این روش ها را با جزئیات بیشتری به همراه مثال شرح بدهم. در طول آموزش موضوعات دیگری هم مطرح می شود و برای این که ساختار آموزش بهم نریزد توضیحات مربوطه را بصورت جداگانه در انتهای این آموزش آورده ام. 

سرفصل مطالب این بخش

لینک مثال ها

راه های دسترسی به بافر داده

در روش هایی که جلوتر توضیح خواهم داد I/O Manager نقش مهمی را ایفا میکند به این دلیل که بر اساس روش دسترسی به حافظه داده، I/O Manager  یکسری شرایط را قبل از اینکه درخواست کاربر را به درایور مقصد بفرستد برقرار می کند. حالا سوال مهم این است که I/O Manager چطور تشخیص می دهد که کدام روش مورد استفاده قرار گرفته است؟ در واقع این کار به دو طریق زیر انجام می شود.

  1.  برای درخواست های IRP_MJ_READ و IRP_MJ_WRITE درایور نویس می تواند عضو داده Flags از ساختار DEVICE_OBJECT را به یکی از موارد زیر مقدار دهی کند.
    • DO_BUFFERED_IO
    • DO_DIRECT_IO
    • درخواست هایی که نه DO_BUFFERED_IO و نه DO_DIRECT_IO در عضو داده Flags آنها مقدار دهی شده باشد
    در واقع درایور باید عضو Flags از DeviceObject را مقدار دهی کند. ما قبلا دیده بودیم که با تابع IoCreateDevice می توانیم یک DeviceObjectبسازیم.
  2.  برای درخواست های IRP_MJ_DEVICE_CONTROL  و IRP_MJ_INTERNAL_DEVICE_CONTROL ما به همراه درخوست یک کد IOCTL می فرستادیم. در مورد ساختار این کد من در آموزش دوم ساختار IOCTL را با جزئیات توضیح داده بودم ولی برای تکرار مجدد من مقادیر مربوط به پارامتر TransferType کد IOCTL را اینجا هم می آورم.
    • METHOD_BUFFERED
    • METHID_IN_DIRECT و METHOD_OUT_DIRECT
    • METHOD_NEITHER

نکته: لازم به ذکر است که سورس مثال های این آموزش بر اساس تغییر Flags هست ولی با کمی تغییر می توان به روش IOCTL تبدیلش کرد. و برای اطلاعات بیشتر به آموزش دوم رجوع کنید که در مورد روش IOCTL مثال زده بودیم.

روش Buffered I/O

این روش بیشتر در درایورهای عمومی مثل کیبورد، ماوس، گرافیک، دستگاه های بر پایه رابط Serial و Parallel مورد استفاده قرار می گیرد. و همچنین به دلیل اینکه دستگاه های مرتبط به این درایورها از نظر عملکرد کند هستند و اندازه اطلاعاتی که بین سیستم عامل و این دستگاه ها منتقل می شود حجم  کمی دارند از این روش استفاده می شود.

I/O Manager در صورتی که درخواست به صورت Buffered I/O (با توجه به یکی از راه هایی که بالاتر گفتم. مقدار دهی Flags یا از طریق IOCTL) فرستاده شده باشد، قبل از فرستادن این درخواست به درایور ما یک "حافظه ی سیستمی" (یک حافظه  Nonpaged) به اندازه حافظه داده ای که از سمت کاربر آمده تخصیص می کند. در صورتی که درخواست "نوشتن"(Write) باشد I/O Manager داده سطح کاربر را در این حافظه ی سیستمی کپی میکند و این حافظه در دسترس درایور قرار می گیرد، و در صورتی که درخواست "خواندن"(Read) باشد بعد از آنکه درایور و دیگر درایور ها کارشان را انجام دادند، داده بازگشت داده شده از سمت درایور در حافظه سطح کاربر کپی می شود در نتیجه داده برگشتی در دسترس برنامه سطح کاربر قرار خواهد گرفت.

مثال - سطح کرنل

حالا برای اینکه مطلب بهتر جا بیافتد یک مثال میزنیم. لینک به سورس کامل این مثال را در ابتدای آموزش قرار داده ام.

اول) با IoCraeteDevice یک Device Object می سازیم بعد Flags را به DO_BUFFERED_IO مقدار می دهیم. (در یک بخش جدا از همین آموزش این موضوع را شرح داده ام). مقدار دادن Flags هم به این شکل (با حفظ مقادیر قبلی Flags)

objectDevice.Flags |= DO_BUFFERED_IO;

نکته: اگر بخواهید از روش IOCTL استفاده کنید ما نیاز به تغییر متغییری خاصی نداریم. در واقع چون برای فرستادن درخواست به روش IOCTL ما ار تابع DeviceIoControl از سمت کاربر استفاده میکنیم IO Manager به صورت اتوماتیک پیش نیاز های لازم را برقرار می کند.

دوم) درایور ما بسته به نیاز باید بتواند درخواست های IRP_MJ_READ و IRP_MJ_WRITE را پردازش کند. این دو را در کنار دیگر IRP هایی که پردازش می کنیم قرار می دهیم. در نتیجه در DriverEntry داریم

NTSTATUS
DriverEntry(
    __in PDRIVER_OBJECT   DriverObject,
    __in PUNICODE_STRING  RegistryPath
    )
{
  ...
  ...
/* تعریف دیگر درخواست ها */ DriverObject->MajorFunction[IRP_MJ_CREATE] = IrpCreateClose; DriverObject->MajorFunction[IRP_MJ_CLOSE] = IrpCreateClose; DriverObject->MajorFunction[IRP_MJ_READ] = IrpRead; DriverObject->MajorFunction[IRP_MJ_WRITE] = IrpWrite; ... ... }

نکته: در روش IOCTL درایور ما بجای درخواست های IRP_MJ_READ  و IRP_MJ_WRITE باید درخواست های IRP_MJ_DEVICE_CONTROL  و IRP_MJ_INTERNAL_DEVICE_CONTROL را نیز پردازش کند.

سوم) و در نهایت پیاده سازی توابعی که درخواست های IRP_MJ_READ و IRP_MJ_WRITE را دریافت  می کند

NTSTATUS
IrpRead(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
    )
{
...
...
irpSp = IoGetCurrentIrpStackLocation( Irp ); outLength = irpSp->Parameters.Read.Length;

...
...

/* بافر هم برای خواندن و نوشتن */ outBuf = Irp->AssociatedIrp.SystemBuffer;
/* اینجا داده لازم را داخل بافر حافظه می ریزیم*/ RtlCopyBytes(outBuf, msg, sizeof(msg)); Irp->IoStatus.Information = sizeof(msg);

...
... } NTSTATUS IrpWrite( PDEVICE_OBJECT DeviceObject, PIRP Irp ) { ... }

چهارم) اینجا چون پردازش این دو درخواست شبیه به هم است من یکی را توضیح می دهم (فقط درخواست خواندن). در این مرحله ما اطلاعات مربوط به درخواست را می توانیم از ساختار  IO_STACK_LOCATION که داخل ساختار IRP است بگیریم. فعلا به جزئیات این ساختار کار نداریم ولی همینقدر لازم است بدانیم که داخل این ساختار یک ساختار داده union است که داخل این union به ازای هر درخواست یک ساختار مجزا وجود دارد.

مثلا تابعی که سطح کاربر صدا زده می شود و ساختار معادل که داخل IO_STACK_LOCATION مقدار دهی می شود در جدول زیر قابل مشاهده است.

 UserMode Function | IO_STACK_LOCATION strcut
______________________________________________
  CreateFile       |        Create
  ReadFile         |        Read
  WriteFile        |        Write
  DeviceIOControl  |        DeviceIoControl 
  ...              |        ...          

در نتیجه در مورد مثال ما اگر کاربر از تابع ReadFile استفاده کند که درخواستی به درایور ما بفرستد. IO Manager ساختار مربوط به Read (که پایین محتویات آن را می بینید) را با مقادیر آمده از سطح کاربر مقدار دهی می کند. و در نهایت ما Irp که از طرف IO Manager آماده شده را داخل تابع مربوط به  IRP_MJ_READ دریافت می کنیم.

محتویات این ساختارها را من از اینجا گرفته ام.

  //
  // Parameters for IRP_MJ_READ 
  //
  struct {
    ULONG  Length;
    ULONG POINTER_ALIGNMENT  Key;
     LARGE_INTEGER  ByteOffset;
  } Read;

  //
  // Parameters for IRP_MJ_WRITE 
  //
  struct {
    ULONG  Length;
    ULONG POINTER_ALIGNMENT  Key;
    LARGE_INTEGER  ByteOffset;
  } Write;

برای دسترسی به مقادیر IO_STACK_LOCATION داخل IRP از IoGetCurrentIrpStackLocation استفاده می کنیم بعد مقدار Length را میخوانیم.

  irpSp = IoGetCurrentIrpStackLocation( Irp );
  outLength = irpSp->Parameters.Read.Length;

نکته: در روش IOCTL ما در آموزش دوم دیده بودیم که این قبیل پارامترها مربوط به IO_STACK_LOCATION را از ساختار DeviceIoControl (توجه به irpSp->Parameters.DeviceIoControl) می گرفتیم. برای اینکه این مطلب مروری شود کد مربوطه را اینجا هم کپی میکنم.

  irpSp = IoGetCurrentIrpStackLocation( Irp );
  inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
  outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength;

برای درخواست "خواندن" ما باید اطلاعاتی را برای کاربر بفرستیم. چطور اینکار را انجام می دهیم؟ قبلا گفته بودیم که IO Manager یک بافر سیستمی تخصیص می کند و اگر درخواست "خواندن" بود داخل این حافظه داده لازم را می ریزیم و اگر درخواست "نوشتن" از آن حافظه سیستمی باید داده آمده از سمت کاربر را بخوانیم. به حافظه سیستمی هم به صورت زیر دسترسی داریم.

  outBuf = Irp->AssociatedIrp.SystemBuffer;

من کد سمت کاربر را اینجا نیاوردم ولی لینک سورس کامل این مثال را بالاتر قرار داده ام که می توانید بگیرید و یادتان نرورد که خروجی سمت درایور را با ابزار DbgView می توانید مشاهده کنید.

روش Direct I/O

این روش معمولا برای درایورهای مربوط به دستگاه های نگه داری اطلاعات است (مثل هارد دیسک، و...) یا اگر بخواهیم از DMA استفاده کنیم. در این موارد اندازه داده هایی که بین سیستم عامل و دستگاه منتقل می شود حجم زیادی دارند. 

I/O Manager در صورتی که درخواست به صورت Direct I/O فرستاده شده باشد، قبل از فرستادن درخواست به درایور ما آدرس سطح کاربر را قفل می کند و از آن یک Mdl می سازد. Mdl همراه درخواست به درایور ما فر ستاده می شود و درایور با استفاده از یکسری تابع که در مثال خواهیم دید به حافظه سطح کاربر می تواند دسترسی پیدا کند. توجه شود که در این روش ما داده سطح کاربر را مانند روش قبلی کپی نمی کنیم در نتیجه سرعت کارایی و عملیات بیشتر می شود و همچنین ما بطور مستقیم هم به آدرس سمت کاربر دسترسی نداریم در واقع Mdl اینجا نقش یک واسط را بازی می کند که ارتباط درایور با آدرس سطح کاربر را ممکن می سازد.

مثال - سطح کرنل

توضیحات مربوط به این مثال تا حد زیادی مشابه با روش Buffered I/O است در نتیحه من فقط قسمت هایی که متفاوت هستند را اینجا ذکر می کنم.

اول) مقدار دادن Flags به این صورت می شود

objectDevice.Flags |= DO_DIRECT_IO;

دوم) رجوع به مثال Buffered I/O

سوم) پیاده سازی توابع مربوط به درخواست ها 

NTSTATUS
IrpRead(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
    )
{
  ...
  ...

  irpSp = IoGetCurrentIrpStackLocation( Irp );
  outLength = irpSp->Parameters.Read.Length;

  ...
  ...

  outBuf = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, 
	                                NormalPagePriority);

  /* اینجا داده لازم را داخل بافر حافظه می ریزیم*/
  RtlCopyBytes(outBuf, msg, sizeof(msg));
	
  Irp->IoStatus.Information = sizeof(msg);

  ...
  ...
}

NTSTATUS
IrpWrite(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
    )
{
 ...
}

چهارم) همانطور که می بینید خیلی مشابه روش Buffered I/O است با این تفاوت که اینجا IO Manager از حافظه سطح کاربر یک Mdl ساخته و آن را داخل ساختار IRP قرار داده است. ما برای استفاده از Mdl از تابع MmGetSystemAddressForMdlSafe استفاده کرده ایم. این تابع چکار می کند، در واقع یک Mdl می گیرد به شما خروجی آدرس حافظه ای را می دهد که با آن کار کنیم. به همین سادگی

روش نه Buffered I/O و نه Direct I/O

در این روش ما مستقیم به آدرس سطح کاربر دسترسی داریم. استفاده از این روش می تواند مشکلاتی ایجاد کند به این خاطر که اگر درایور یکسری نکات امنیتی را رعایت نکند می تواند باعث بهم ریختن امنیت سیستم شود (درایور های زیادی متاسفانه دارای این نقطه ضعف هستند). 
در روش های پیشین توضیح دادیم که I/O Manager یکسری کارها قبل از آمدن در خواست به ما انجام می دهد. ولی در این روش همه چی را خودمان باید انجام دهیم. در واقع ما باید مانند IO Manager عمل کینم.

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

مقدار دادن DEVICE_OBJECT.Flags

ما قبلا در مورد روش IOCTL صحبت کرده ایم (رجوع به آموزش دوم). حالا در مورد مقدار دادن Flags باید صحبت کنیم. عضو داده Flags مقادیر مختلفی می گیرد (سایت ماکروسایت مقادیر کامل تر را ببینید) و می توانیم این مقادیر را با هم OR (یکی از عملگرهای بولی) کنیم. کد DriverEntry داشتیم به این صورت

NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, 
IN PUNICODE_STRING regPath) {
PDEVICE_OBJECT  deviceObject = NULL; ... ...

ntStatus = IoCreateDevice(...,
...,
&deviceObject)
deviceObject.Flags = مقدار های معتبر; /* DO_BUFFERED_IO
DO_DIRECT_IO */

...
... return STATUS_SUCCESS; }

ما داخل این قسمت با IoCreateDevice یک Device Object  می سازیم و حالا می توانیم مقدار Flags مربوط به ساختار این Object را تغییر دهیم.

اگر بخواهیم هم DO_BUFFERED_IO داشته باشیم و هم DO_DIRECT_IO  به این صورت می شود (که با هم OR شده اند).

deviceObject.Flags = DO_BUFFERED_IO | DO_DIRECT_IO;

یا اگر بخواهیم مقدار قبلی Flags را هم نگه داریم

deviceObject.Flags |= (DO_BUFFERED_IO | DO_DIRECT_IO)

اگرحالت سوم که نه Buffered I/O و نه Direct I/O بود را بخواهیم. دیگر با هیچکدام از این دو مقدار Flags را مقدار دهی نمی کنیم.

موضوعات جداگانه

درخواست ها

ما در طول آموزش در مورد درخواست هایی مثل IRP_MJ_READ, IRP_MJ_WRITE و ... صحبت کردیم. وقتی صحبت از "درخواست" میکنیم منظور ما ساختاری است به نام IRP. داخل این ساختار یک ساختار دیگر وجود دارد به نام IO_STACK_LOCATION که یک جایی داخل این ساختار عضو داده ای وجود دارد به نام MajorFunction. مقادیری که این عضو داده می تواند داشته باشد به شرح زیر است.

IRP_MJ_CREATE
IRP_MJ_PNP
IRP_MJ_POWER
IRP_MJ_READ
IRP_MJ_WRITE
IRP_MJ_FLUSH_BUFFERS
IRP_MJ_QUERY_INFORMATION
IRP_MJ_SET_INFORMATION
IRP_MJ_DEVICE_CONTROL
IRP_MJ_INTERNAL_DEVICE_CONTROL
IRP_MJ_SYSTEM_CONTROL
IRP_MJ_CLEANUP
IRP_MJ_CLOSE
IRP_MJ_SHUTDOWN

در نتیجه وقتی می گوییم درخواست IRP_MJ_READ منظور ما این است که یک ساختار IRP.IO_STACK_LOCATION داریم که عضو داده MajorFunction آن IRP_MJ_READ است.


یک درخواست یا همان IRP وقتی به یک درایور می رسد. تابعی با توجه به آرایه ی MajorFunction داخل DriverObject مربوط به آن درایور صدا زده می شود. (در مورد آرایه ای از MajorFunction رجوع به آموزش دوم کنید).

حافظه از نوع Nonpaged

در سیستم عامل یک مفهومی داریم به نام Swapping که قابلیتی است که سیستم عامل می تواند از یک فایل یا پارتیشن به عنوان حافظه اضافی روی دیسک در کنار حافظه اصلی (RAM) استفاده کند (در سیستم عامل ویندوز این فایل pagefile.sys نام دارد). در نتیجه فضایی که در کل خواهید داشت بیشتر از فضای حافظه اصلی سیستم می شود. و همچنین یک مفهومی داریم به نام Paging (این امکان از طرف CPU باید پشتیبانی شود) که در این حالت حافظه اصلی تقسیم شده به واحد های کوچکتر و یک اندازه به نام Page. اندازه Page ها در حالت پیش فرض 4 KiB می باشد. سیستم عامل با یکسری از روش ها تصمیم می گیرد که یکسری از Page ها که مورد نیاز نیستن به دیسک منتقل شوند(به این عمل اصطلاحا میگن swap-out).

اگر حافظه را به صورت Nonpaged تخصیص کنیم در واقع به سیستم عامل می گوییم که حافظه ای که می گیریم همیشه در حافظه اصلی RAM باقی بماند و به دیسک منتقل نشود. جدا از موضوع آموزش این پست این کار یکسری مزایا و معایب دارد. مزیت این کار این است که چون آن ناحیه همیشه در حافظه است سرعت دسترسی هم بیشتره، در واقع زمانی برای منتقل کردن Page های مربوطه به دیسک صرف نمی شود. عیب این کار این است که وقتی یک فضایی از حافظه اصلی می گیریم در نتیجه حافظه فیزیکی کمتری خواهیم داشت، در نتیجه سیستم عامل در صورت مواجه شدن با کمبود حافظه مجبور است از حافظه اضافی که روی دیسک است استفاده کند که این کار تاثیر روی کارایی سیستم می گذارد.

در ویندوز ما یک جاهایی مجبوریم حافظه را از نوع Nonpaged بگیریم و راستش من نمی خواهم وارد جزئیات این مساله بشوم چون مارو خیلی از موضوع آموزش این پست دور خواهم شد. ولی بعد سعی می کنم پستی در این رابطه بنویسم.

نکته: زمانی که یک حافظه از نوع Nonpaged گرفتین و دیگر کارتان با این حافظه تمام شد حتما آنرا آزاد کنید که تاثیر روی کاراریی سیستم نگذارد.

نظرات (۴)

سلام
ایول خیلی خوشحال شدم آپ کردی دمت گرم بازم بکن
پاسخ:
ممنون :)
سلام. 
آیا امکان مانیتور رویدادهای write روی درایو cd از طریق فیلتر درایور وجود دارد؟ به طور کلی راه حلی برای جمع آوری آن در سطح کرنل وجود دارد؟
پاسخ:
سلام
بله میشه با فیلتر درایور این کار را انجام داد،
با سلام مجدد :)
من این قسمت Direct I/O رو چند بار مطالعه کردم حتی رفتم جلو و برگشتم بازم خوندمش ولی دقیقا متوجه نمیشم که چطور ممکنه از بافر داده کپی نکنه و دسترسی مستقیمم نداشته باشه بهش ولی بتونه بخونتش ؟ ینی اون mdl باعث میشه که بتونه بخونه ؟ اگه پاسخ مثبته میشه کمی درمورد mdl توضیح بدید ؟ لینکی هست به بافر داده ؟ یا اشاره گری به مکان بافر دادس یا .... ؟؟!! و بازم اگه پاسخ مثبته اینجوری که یجورایی به دیتا دسترسی مستقیم داره خب !!
راستی دیگه مطلب نمیذارید ؟؟؟! :)
بذارید ;)
پاسخ:
سلام مجدد :)

خوب راست می گین الان خودم خوندم این قسمت یک ابهام داره که توضیح در موردش ندادم. اینجا منظورم از آدرس، آدرس virtual اون ناحیه است، که مستقیم باهاش سروکار نداریم و خوب از این جهت دسترسی مستقیم هم بهش نداریم. ما در واقع دسترسی مستقیم به آدرس فیزیکی اون ناحیه داریم با mdl و دسترسی هایی که موقع تعریف Mdl مشخص کردیم. این Mdl به ما یک آدرس virtual دیگه میده که اشاره میکنه به آدرس فیزیکی اون ناحیه. (این مفاهیم خیلی شبیه مفهوم shortcut ایجاد کردن فایل در filesystem ها است)

کل این موضوع هم مربوط میشه به مدیریت حافظه و نحوه نگهداری جداول ناحیه های حافظه پروسه ها. پیشنهاد میکنم فصل دوم کتاب Rootkit Arsenal نسخه اول رو بخونید که در این زمینه خوب توضیح داده.

در مورد مطلب ننوشتن راستش زمان هایی که دارم کوتاه کوتاهن و نوشتن مطلب های اینطوری حداقل برای من زمان زیاد میبره و زمان یک تیگه و قلمبه می خواد برای مطالعه و تمرکز داشتن روی موضوع به خاطر جزئیاتشون. برای همین حوصلم نمیاد بنویسم :) 
مثلا در مورد مدیریت حافظه یک مطلب داشتم آماده می کردم که نصفه مونده که تا حدودی مربوط به سوال شما هم میشد. حالا باید ببینین چی میشه بعد حالش بیاد تکمیلش میکنم.

و ممنون که انرژی به آدم میدین برای ادامه این کار. :)

با سلام مجدد !!
توی آموزشایی که زحمت کشیدین و دادین این کد رو
objectDevice.Flags |= DO_BUFFERED_IO;
فرمودین ینی فلگ دیوایس آبجکت ما حالت قبلی خودشو حفظ میکنه و حالت دسترسی به بافر دادرو از حالت buffered_io رو هم به خودش میگیره (بخاطر =|). من در یه درایور دیگه این کد رو
  pDeviceObject->Flags&=~DO_DEVICE_INITIALIZING;
دیدم . میخواستم ببینم کار این بخش از کد چیه !؟
پاسخ:
سلام

تو آموزش "نوشتن یک File system Filter Driver" یک عنوان به نام "پاک کردن فلگ DO_DEVICE_INITIALIZING" یک توضیحی در مورد این موضوع دادم. داخل اون آموزش از یک ماکرو به نام ClearFlag استفاده کردم که اون ماکرو دقیقا معادل همین کدی است که شما گذاشتین.

حالا این کد چکار می کنه
اول) DO_DEVICE_INITIALIZING~ : این کد مقدار DO_DEVICE_INITIALIZING که مقدار 0x00000080 است را عمل not روش انجام میده (با ~)
دوم) با مقدار فعلی pDeviceObject->Flags عمل and روش انجام میشه و مقدار هم داخل pDeviceObject->Flags قرار می گیره
نتیجه) مقدار DO_DEVICE_INITIALIZING از pDeviceObject->Flags پاک میشه.
ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
شما میتوانید از این تگهای html استفاده کنید:
<b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
تجدید کد امنیتی