古詩詞大全網 - 藝術簽名 - 驅動程序如何成了病毒

驅動程序如何成了病毒

對於壹個經常寫程序的人來說,寫驅動不是壹件困難的事情。因為網絡上有很多現成的

代碼,要實現某個功能,直接 Ctrl+C和Ctrl+V 就能解決問題。但是寫出來了驅動能不能加

載進入內核就是另外壹回事了,準確的說是能不能存在於別人的硬盤上就是另外壹回事了。

因為很多殺毒軟件(特別像360這種沒技術含量的)見到後綴名為sys的文件就直接刪除,

甚至連調用NtLoadDriver的機會都沒有。對於壹般的軟件來說,給出壹個聲明說明壹下解

決方法就算了。但是對於惡意程序,是不能給出聲明的。於是很多惡意軟件的作者另辟蹊徑,

利用大公司寫好的而且有數字簽名的驅動來做壞事。

有人說,大公司做好的驅動怎麽可能被用來做壞事呢?其實,這是很容易理解的事情。

很多安全類或者系統優化類的軟件,甚至系統毫不相關的軟件(比如:迅雷)都附帶有驅動。

這些驅動都帶有壹定的通用性。q_lai_a_qu網友在其博客裏說:“ComputerZ.sys……沒事

逆了逆是魯大師的驅動,發現這個驅動功能齊全,而且沒有調用者驗證!既可以讀、寫Msr

寄存器,也可以用in、out指令讀寫端口,而且char/short/long數據長度齊全!”。這個是

個人之言,可信度請自行揣度。下面說個可信度比較高的例子:曾經有病毒利用了360的

AntiRK.dll來刪除殺毒軟件的文件(請自行用谷歌搜索“360 antirk.dll”,會有驚喜發現。

AntiRK.dll雖然不是驅動,但也是被非法利用了)。破壞殺毒軟件的病毒已經算是小兒科了,

其實利用某些驅動還能破壞硬件!我最近在筆記本上折騰硬件,“本友會”上的網友給我推

薦了幾款軟件:SetFSB、ThrottleStop、NvFlash、WinFlash。它們分別是修改CPU外頻、設

置CPU倍頻(可以調節CPU電壓)、讀寫顯卡BIOS和讀寫主板BIOS的軟件。壹言概括他們的特性,

就是它們都支持NT x86/x64,它們的驅動都有正規數字簽名(特別是最後兩個,分別帶的是 NVIDIA和ASUS的數字簽名)。

最為重要的是,他們的驅動沒有加花加殼,沒有校驗調用者,

如果利用這幾個驅動,加上壹丁點的逆向知識,就能做出破壞性的病毒(以下摘自我在紫水

晶編程論壇的帖子):

1.SetFSB能調節處理器的外頻,如果直接把外頻調到600MHz,電腦會瞬間黑屏,可能

會損壞 CPU或主板;

2.ThrottleStop能調節 CPU的倍頻(如果CPU沒有鎖倍頻),如果直接把倍頻調到 31,

電腦會瞬間黑屏,可能會損壞CPU 或主板;ThrottleStop還能調節CPU的核心電壓,如果

把CPU的核心電壓調到3V,能直接燒毀CPU 甚至主板;

3.NvFlash、WinFlash等軟件能直接讀寫BIOS(顯卡BIOS 和主板BIOS),我們可以把

BIOS全部寫零;

4.如果做病毒的話,先寫壞顯卡BIOS 和主板BIOS,然後通過調節電壓燒掉顯卡和CPU

(有可能會連同主板壹起損壞);

解決方案

由此可見,沒有驗證調用者的驅動實在是有著巨大的危害。我最近受學院委托,做壹個

需要驅動的軟件(那個驅動會被加上數字簽名)。為了防止上述悲劇發生,我決定在正式寫

驅動之前,先解決如何防止自己的驅動被惡意利用。以前我曾經在紫水晶編程論壇上問過這

個問題,網友的回答五花八門,不過大概是可以分成三類:第壹類是信息驗證,比如應用程

序發個信息給驅動來驗證壹下是“自己人”;第二類是加殼保護,比如給驅動和應用程序加

上極強難脫的殼,利用VMP加密通信部分(類似XueTr 的做法);還有人提出混合應用,綜

合第壹類和第二類的做法。

這三種想法看似都不錯,但是我認為不妥。第壹種:別人只要把驅動全部逆向完畢就行

了;第二種:雖然VMP保護和加保護殼使得破解不容易,但是不是使破解變得不可能。而且

VMP 和保護殼能使程序執行的效率降低,我不太喜歡。最可惡的是,殺毒軟件對加了殼(甚

至包括 UPX)和 VMP的程序壹律報毒,得不償失。於是我想出了第三種思路:校驗調用者的

特征。如果符合,就執行功能語句,否則不予執行。如何校驗調用者的特征碼呢?不少人想

到的是使用CRC32 或者 MD5。使用它們不是不可以,不過我還有自己的想法。我的想法是自

己設計壹套驗證算法,它的規則如下:

1.獲得調用者的EPROCESS

2.通過調用者的EPROCESS獲得調用者的文件路徑

3.獲取調用者的文件全部內容,放到字節數組buff裏

4.把 buff裏所有的元素依次相加減(fb1 + fb2 - fb3...),得到y1

5.把 buff裏所有的元素依次異或(0 XOR fb1 XOR fb2 XOR fb3...),得到y2

把 y1和 y2與已經計算出來的數值對比,如果都相同則執行功能代碼,如果不相同則不

執行功能代碼

獲得調用者的EPROCESS直接用 PsGetCurrentProcess()就行了,獲得調用者的文件路

徑比較麻煩,大家可以使用我以前向高手購買的代碼(已經封裝為函數,方便調用):

//依據 EPROCESS得到進程全路徑

VOID GetFullPathByEprocess( ULONG eprocess, PCHAR ProcessImageName )

{

ULONG object;

PFILE_OBJECT FileObject;

UNICODE_STRING FilePath;

UNICODE_STRING DosName;

STRING AnsiString;

FileObject = NULL;

FilePath.Buffer = NULL;

FilePath.Length = 0;

*ProcessImageName = 0;

//Eprocess->sectionobject(offset_SectionObject)

if(MmIsAddressValid((PULONG)(eprocess+offset_SectionObject)))

{

object=(*(PULONG)(eprocess+offset_SectionObject));

//KdPrint(("[GetProcessFileName] sectionobject :0x%x\n",object));

if(MmIsAddressValid((PULONG)((ULONG)object+0x014)))

{

object=*(PULONG)((ULONG)object+0x014);

//KdPrint(("[GetProcessFileName] Segment :0x%x\n",object));

if(MmIsAddressValid((PULONG)((ULONG)object+0x0)))

{

object=*(PULONG)((ULONG_PTR)object+0x0);

//KdPrint(("[GetProcessFileName]

ControlAera :0x%x\n",object));

if(MmIsAddressValid((PULONG)((ULONG)object+0x024)))

{

object=*(PULONG)((ULONG)object+0x024);

if (NtBuildNumber >= 6000) object=((ULONG)object &

0xfffffff8);

//KdPrint(("[GetProcessFileName]

FilePointer :0x%x\n",object));

}

else

return ;

}

else

return ;

}

else

return ;

}

else

return ;

FileObject=(PFILE_OBJECT)object;

FilePath.Buffer = ExAllocatePool(PagedPool,0x200);

FilePath.MaximumLength = 0x200;

//KdPrint(("[GetProcessFileName]

FilePointer :%wZ\n",&FilePointer->FileName));

ObReferenceObjectByPointer((PVOID)FileObject,0,NULL,KernelMode);

RtlVolumeDeviceToDosName(FileObject-> DeviceObject, &DosName);

RtlCopyUnicodeString(&FilePath, &DosName);

RtlAppendUnicodeStringToString(&FilePath, &FileObject->FileName);

ObDereferenceObject(FileObject);

RtlUnicodeStringToAnsiString(&AnsiString, &FilePath, TRUE);

if ( AnsiString.Length >= 216 )

{

memcpy(ProcessImageName, AnsiString.Buffer, 0x100u);

*(ProcessImageName + 215) = 0;

}

else

{

memcpy(ProcessImageName, AnsiString.Buffer, AnsiString.Length);

ProcessImageName[AnsiString.Length] = 0;

}

RtlFreeAnsiString(&AnsiString);

ExFreePool(DosName.Buffer);

ExFreePool(FilePath.Buffer);

}

以上代碼需要三個硬編碼,分別是NtBuildNumber(系統版本號)、EPROCESS中的

SectionObject項和UniqueProcessId項的偏移。我測試的操作系統是Windows 2003。所以

我在代碼裏如下定義:

#define offset_SectionObject 0x124

#define offset_UniqueProcessId 0x94

ULONG NtBuildNumber=3790;

獲得進程路徑後就校驗特征碼。由於流程已經說清楚了,所以直接給出代碼:

VOID CalcChar(PUNICODE_STRING logFileUnicodeString, LONG *XorChar, LONG

*AnSChar)

{

OBJECT_ATTRIBUTES objectAttributes;

IO_STATUS_BLOCK iostatus;

HANDLE hfile;

NTSTATUS ntStatus;

FILE_STANDARD_INFORMATION fsi;

PUCHAR pBuffer;

ULONG i=0,y1=0,y2=0;

//初始化 objectAttributes

InitializeObjectAttributes(&objectAttributes,

logFileUnicodeString,

OBJ_CASE_INSENSITIVE,//對大小寫敏感

NULL,

NULL);

//創建文件

ntStatus = ZwCreateFile(&hfile,

GENERIC_READ,

&objectAttributes,

&iostatus,

NULL,

FILE_ATTRIBUTE_NORMAL,

FILE_SHARE_READ,

FILE_OPEN,//即使存在該文件,也創建

FILE_SYNCHRONOUS_IO_NONALERT,

NULL,

0 );

if (!NT_SUCCESS(ntStatus))

{

dprintf("The file is not exist!\n");

return;

}

//讀取文件長度

ntStatus = ZwQueryInformationFile(hfile,

&iostatus,

&fsi,

sizeof(FILE_STANDARD_INFORMATION),

FileStandardInformation);

dprintf("The program want to read %d bytes\n",fsi.EndOfFile.QuadPart);

//為讀取的文件分配緩沖區

pBuffer = (PUCHAR)ExAllocatePool(PagedPool,

(LONG)fsi.EndOfFile.QuadPart);

//讀取文件

ZwReadFile(hfile,NULL,

NULL,NULL,

&iostatus,

pBuffer,

(LONG)fsi.EndOfFile.QuadPart,

NULL,NULL);

dprintf("The program really read %d bytes\n",iostatus.Information);

//異或計算

for(i=0;i<iostatus.Information;i++)

y1=y1^(LONG)(*(pBuffer+i));

*XorChar=y1;

//加減計算

for(i=0;i<iostatus.Information;i++)

{

if(i%2==0)

y2=y2+(LONG)(*(pBuffer+i));

else

y2=y2-(LONG)(*(pBuffer+i));

}

*AnSChar=y2;

//關閉文件句柄

ZwClose(hfile);

//釋放緩沖區

ExFreePool(pBuffer);

}

接下來就要調用了。我們需要編寫壹個函數VerifyCaller,在此函數裏有兩個值需要

固化在驅動裏,就是合法調用者的兩個特征值。為了方便計算這兩個特征值,我特地寫了個

應用程序,核心代碼如下:

Option Explicit

Private Function ReadFile(ByVal strFileName As String, Optional ByVal

lngStartPos As Long = 1, Optional ByVallngFileSize As Long = -1) As Byte()

Dim FilNum As Long

FilNum = FreeFile

Open strFileName For Binary As #FilNum

If lngFileSize = -1 Then

ReDim ReadFile(LOF(FilNum) - lngStartPos)

Else

ReDim ReadFile(lngFileSize - 1)

End If

Get #FilNum, lngStartPos, ReadFile

Close #FilNum

End Function

Private Function WriteFile(ByVal strFileName As String, bytData() As Byte,

Optional ByVal lngStartPos As Long = -1,Optional ByVal OverWrite As Boolean =

True)

On Error GoTo erx

Dim FilNum As Long

FilNum = FreeFile

If OverWrite = True And Dir(strFileName) <> "" Then

Kill strFileName

End If

Open strFileName For Binary As #FilNum

If lngStartPos = -1 Then

Put #FilNum, LOF(FilNum) + 1, bytData

Else

Put #FilNum, lngStartPos, bytData

End If

Close #FilNum

erx:

End Function

Private Sub Command1_Click()

Dim buff() As Byte, i As Long, y As Long, ub As Long

'text1.text is the file name

buff = ReadFile(Text1.Text, 1, -1)

ub = UBound(buff)

'calc xor char

y = 0

For i = 0 To ub

y = y Xor buff(i)

Next

Text2.Text = CLng(y)

DoEvents

'calc add/sub char

y = 0

For i = 0 To ub

If i Mod 2 = 0 Then

y = y + CLng(buff(i))

Else

y = y - CLng(buff(i))

End If

Next

Text3.Text = CLng(y)

End Sub

Private Sub Form_Load()

Me.Icon = LoadPicture("")

End Sub

驅動裏的 VerifyCaller代碼如下:

LONG VerifyCaller(void)

{

PEPROCESS cur_ep;

char cur_pp[260];

char *nt_cur_pp;

ANSI_STRING asCur_pp;

UNICODE_STRING usCur_pp;

LONG xorc, ansc;

cur_ep=PsGetCurrentProcess();

GetFullPathByEprocess((ULONG)cur_ep, cur_pp);

//在文件名前面加上\?\

nt_cur_pp=cs("\\?\\",cur_pp);

DbgPrint("%s",nt_cur_pp);

RtlInitAnsiString(&asCur_pp, nt_cur_pp);

RtlAnsiStringToUnicodeString(&usCur_pp, &asCur_pp, TRUE);

DbgPrint("%wZ",&usCur_pp);

CalcChar(&usCur_pp, &xorc, &ansc);

DbgPrint("XorChar: %ld; AnSChar: %ld",xorc,ansc);

//這個就是事先算好的合法程序的特征碼,必須固化在驅動裏!

if(xorc==186 && ansc==136176)

return 1;

else

return 0;

}

在 DispatchIoctl函數的每個功能執行之前,都調用VerifyCaller()校驗壹下調用者:

switch(uIoControlCode)

{

case IOCTL_VERIFY:

{

DbgPrint("[MyDriver] DispatchIoctl - IOCTL_VERIFY");

if(VerifyCaller()==1)

DbgPrint("[MyDriver] {IOCTL_VERIFY} Function code run now!");

else

DbgPrint("[MyDriver] {IOCTL_VERIFY} You're illegal caller!");

status = STATUS_SUCCESS;

break;

}

//下面省略

}

運行測試

3.首先把合法的調用者,非法的調用者(用eXeScope隨便把合法的調用者Patch壹下,

比如刪掉程序的版本信息)和驅動復制到虛擬機

4.用合法的調用者來加載驅動並執行

5.用非法的調用者來加載驅動並執行

6.對比以上兩者在DbgView的輸出

調用者合法時:

調用者非法時:

寫在最後

寫完這篇文章,我必須再次重申:只有當驅動程序攜帶正式數字簽名時,驗證調用者的

代碼才有使用價值。為什麽這麽說呢?因為別人無法patch 帶有正式數字簽名的驅動(壹旦

驅動被 patch,簽名就失效了,就像被破處的女人,不值錢了。這個比喻雖然粗俗,但是很

恰當)。而沒有加上簽名的驅動,本來就沒有使用價值。即使別人要使用,直接把驅動扔到

IDA 裏,什麽代碼都出來了。