[轉] Hackshield內幕



標 題: 【原創】Hackshield內幕
作 者: thisIs
時 間: 2012-05-17,13:09:14
鏈 接: http://bbs.pediy.com/showthread.php?t=150758




最近討論遊戲保護的帖子又見增多, 正好我以前逆過HS, 摳出點東西來跟大家分享下
今天也是我接觸驅動, 逆向這一塊整整一週年, 寫個帖子聊表紀念

論壇上這方面帖子大多都是寫的如何跳過驅動層的hook點來達到過保護的目的
再聊這些東西也沒什麼意思, 我就爆料一些HS內部的處理的細節吧

這裡面有很多很好的安全編程方法值得我們借鑑和學習, 大家有時間的話可以拖到IDA中看一下,
比起寫個驅動在內核跳來跳去, 學習到一些實用的編程技巧對自身的成長是大有益處的

寫這篇帖子的時候我的電腦中並沒有裝帶有HS保護的遊戲,
所以下面討論的內容全部根據以前逆向時留下的筆記和回憶來總結的,
所以具體的細節上可能與實際有所出入
這裡的討論的HS版本全部都是去年11月份左右的版本, 應該是5.5.xxx
而我最後一次玩某遊戲好像版本有了小幅度的更新


HS也是通過一系列的內核HOOK來達到保護遊戲目的的, 主要有以下這些<部分函數只是存在於驅動中, 實際並沒有被使用>:

SSDT hook:
1.NtReadVirtualMemory
2.NtWriteVirtualMemory
3.NtOpenProcess
4.NtSuspendProcess
5.NtTerminateThread

引用到的結構如下:
// SSDT hook 函數相關結構
typedef struct _SSDTHookInfo{
ULONG SSDTNAME; // 函數名稱, 對應一個加密後的字串地址
ULONG SSDTINDEX; // 函數索引
ULONG ProxyAddress; // 代理函數地址
ULONG PreValue; // hook前地址
ULONG PrevBeenHooked; // 該函數是否被別人hook
ULONG someflag; // 標誌
}SSDTHookInfo; //+0x18大小



SSDT Inline hook:
1 KeAttachProcess //4
2 KiAttachProcess //4
3 NtDeviceIoControlFile //4
4 NtClose //4
5 IopXxxControlFile //4
6 NtOpenProcess //4
7 NtReadProcessMemory //4
8 NtWriteProcessMemory //4
9 PsSuspendThread //4
10 NtTerminateThread //4
11 unknow
12 NtSetContextThread //4
13 NtGetContextThread //4
14 NtQueryPerformanceCounter //2
15 NtGetContextThread_End //4

引用到的結構如下:
// inline hook 函數相關的結構
typedef struct _SSDTInlineHookInfo{
ULONG FunctionName; //函數名字串指針
ULONG FunctionRVA; //+0X04 函數距離模塊基址RVA
ULONG HookOffsetFromFunction; //+0X08 HOOK處距離函數頭偏移
ULONG HookAddress; //+0X0C HOOK線性地址
ULONG JmpCodeAddress; //+0X10 JmpCodeBuffer [0x18大小], 要跳轉到的中間跳緩衝地址
BOOLEAN ProxySwitchOn; //+0X14 過濾是否啟用
BOOLEAN unknow;
LARGE_INTEGER OriginalData; //+0X18 原始值
ULONG Flag; //+0X20 一些標誌
ULONG unknow; //+0X24
}SSDTInlineHookInfo; //size = 0x28



Shadow SSDT Hook:
0 NtUserQueryWindow 0x0
1 NtUserBuildHwndList 0x0
2 NtUserFindWindowEx 0x0
3 NtUserGetForegroundWindow 0x0
4 NtUserWindowFromPoint 0x0
5 NtGdiBitBlt 0x20000
6 NtGdiStretchBlt 0x20000
7 NtGdiMaskBlt 0x20000
8 NtGdiPlgBlt 0x20000
9 NtGdiTransparentBlt 0x20000
10 NtUserSendInput 0x20
11 NtUserGetDC 0x20000
12 NtUserGetDCEx 0x20000
13 NtUserGetWindowDC 0x20000

引用到的結構與SSDT Hook一樣


Shadow SSDT Inline Hook:
NtGdiGetPixel 0x20000
unknown 0x20 <未使用>

引用到的結構如下:
typedef struct _ShadowSSDTInlineHookInfo{
ULONG FunctionName: // 函數名稱字串指針
ULONG PreAddress; // 函數源地址
PVOID JmpCodeAddress; // 跳轉指令的地址 這裡的內存是分配的大小0x18 從這裡跳到代理函數
BYTE IsHookInstalled; // Hook是否已經安裝
BYTE UnknowA; // +0xD
USHORT Fill; // 間隙填充
UCHAR OldValue[8]; // 原始值
ULONG Flag; // 一些標誌
ULONG UnknowB; // +0x1C
}ShadowSSDTInlineHookInfo;



涉及到Hook的結構就只有上面這些, hs在驅動內部小心地安裝或卸載Hook然後配合對各種名單的過濾來實現對遊戲的保護
對於名單這一塊由於不少操作相關的函數都被vm掉, 所以只逆出來了很少一部分, 主要有一下幾個名單:


// 進程白名單
typedef struct _ProcessWhiteList{
ListEntry ProcList; // 白名單鏈
ULONG Flag; // +0X08 標誌<大多數情況下有4就代表存在>
ULONG UnknowA; // +0X0C 這裡是個枚舉 0 1 2
PEPROCESS eprocess; // +0x10 進程環境體
PWCHAR pProcName[20]; // +0x14 進程名

..
UCHAR // +56 是否打開過遊戲進程標誌
}ProcessWhiteList;
這個是個白名單的列表,列出了很多進程名稱,如果有對應的存在 EPROCESS 域不是空的
這個結構數組是在初始化的時候就寫定了, 全部內容如下:
進程白名單:
1. Name: patcher.exe
Flag: 00080004
Flag: 2
+56 :

2. Name: WerFault.exe
Flag: 00080004
Flag: 0
+56 :

3. Name: IAANTmon.exe
Flag: 00080000
Flag: 2
+56 :

4. Name: avp.exe
Flag: 00080000
Flag: 2
+56 :

5. Name: WmiApSrv.exe
Flag: 00080000
Flag: 2
+56 :

6. Name: xsync.exe
Flag: 00080000
Flag: 2
+56 :

7. Name: fssm32.exe
Flag: 00080000
Flag: 2
+56 :

8. Name: LGDCORE.exe
Flag: 00000020
Flag: 2
+56 :

9. Name: ACS.EXE
Flag: 00080000
Flag: 2
+56 :

10.Name: ITPYE.EXE
Flag: 00000020
Flag: 2
+56 :

11.Name: Joy2Key.exe
Flag: 00000020
Flag: 2
+56 :

12.Name: JOYTOKEYHIDE.EXE
Flag: 00000020
Flag: 2
+56 :

13.Name: JOYTOKEYKR.EXE
Flag: 00000020
Flag: 2
+56 :

14.Name: JOYTOKEY.EXE
Flag: 00000020
Flag: 2
+56 :

15.Name: DWM.EXE
Flag: 00080000
Flag: 0
+56 :

16.Name: WMIPRVSE.EXE
Flag: 00080000
Flag: 2
+56 :

17.Name: DK2.EXE
Flag: 00080000
Flag: 2
+56 :

18.Name: CSTRIKE-ONLINE.EXE
Flag: 00080000
Flag: 2
+56 :

19.Name: RAGII.EXE
Flag: 00080000
Flag: 2
+56 :

20.Name: EKRN.EXE
Flag: 00080000
Flag: 2
+56 :

21.Name: GOM.EXE
Flag: 00080000
Flag: 2
+56 :

22.Name: GAMEMON.DES
Flag: 00080000
Flag: 2
+56 :

23.Name: VAIOCAMERACAPTUREUTILITY.EXE
Flag: 00080000
Flag: 2
+56 :

24.Name: IPOINT.EXE
Flag: 00000020
Flag: 2
+56 :

25.Name: NMCOSRV.EXE
Flag: 00080004
Flag: 2
+56 :

26.Name: DEKARON.EXE
Flag: 00080000
Flag: 2
+56 :

27.Name: AUDIODG.EXE
Flag: 00080000
Flag: 0
+56 :

28.Name: NGM.EXE
Flag: 00080004
Flag: 2
+56 :

29.Name: TASKMGR.EXE
Flag: 00080004
Flag: 0
+56 :

30.Name: HGSCRAPEDITORHELPER.EXE
Flag: 00080004
Flag: 2
+56 :

31.Name: SETPOINT.EXE
Flag: 00000020
Flag: 2
+56 :

32.Name: NMSERVICE.EXE
Flag: 00080004
Flag: 2
+56 :

33.Name: NSVCAPPFLT.EXE
Flag: 00080000
Flag: 2
+56 :

34.Name: UPSHIFTMSGR.EXE
Flag: 00080004
Flag: 2
+56 :

35.Name: NOD32KRN.EXE
Flag: 00080000
Flag: 2
+56 :

36.Name: IMJPCMNT.EXE
Flag: 00080000
Flag: 2
+56 :

37.Name: TCSERVER.EXE
Flag: 00080000
Flag: 2
+56 :

38.Name: SPOOLSV.EXE
Flag: 00080000
Flag: 0
+56 :

39.Name: IEXPLORE.EXE
Flag: 000A0024
Flag: 2
+56 :

40.Name: EXPLORER.EXE
Flag: 000A0024
Flag: 1
+56 :

41.Name: WINLOGON.EXE
Flag: 000A0024
Flag: 0
+56 :

42.Name: SERVICES.EXE
Flag: 00080024
Flag: 0
+56 :

43.Name: CSRSS.EXE
Flag: 000A0024
Flag: 0
+56 :

44.Name: LSASS.EXE
Flag: 00080024
Flag: 0
+56 :

45.Name: SVCHOST.EXE
Flag: 00080024
Flag: 0
+56 : 1



// 進程黑名單
typedef struct _ProcessBlackList{
ListEntry list; // 進程鏈
ULONG ProcessId; // +0x08
ULONG Flag; // +0x0c
}ProcessBlackList;
這裡的標記:4應該是代表有效 0代表無效
進程銷毀之後在這裡會斷鏈


// 句柄名單
句柄名單是有三個的, 應該都是使用了同一種結構
typedef struct _HandleTable{
ListEntry list;
ULONG ProcessId; // +0x0x8
Handle FileHandle; // +0x0C
}HandleTable;

句柄名單應該是兩個白名單一個黑名單, 不過內部沒有找到很多對於這三個名單的引用,
最直接的是在NtDeviceIoControlFile相關的Hook流程上用來動態添加名單,
順便說一下NtDeviceIoControlFile中是對ARK進行過濾檢測用的<或者說這是一部分功能>,

具體的做法是根據NtDeviceIoControlFile的文件句柄取得文件對象, 進而得到設備對象, 驅動對象,
然後根據驅動對象就得到目標驅動模塊的DispatchDeviceIoControl接口, 然後通過一些算法在一個名單中進行匹配,
依此來進行過濾


// 驅動名單
這個結構與一個全局的結構數組相關
該全局數組一共0x19大小,保存了部分驅動名稱,初始化的時候填寫這部分內容
typedef struct _DriverTable{
ULONG Flag: // 標誌 一般都是4
DRIVEROBJECT DriverObject; // 驅動對應的驅動對象地址 使用到的時候才會更新
ULONG Index; // 索引序號
PWCHAR DriverName[0x40]; // 驅動名稱
}DriverTable; // size = 0x4C
這個名單在未被VM的代碼中沒有見到被引用


// 下面這個是最重要的內部結構, 不過很多域都沒有看到被引用, 可能是被VM掉了, 遺憾
typedef struct _Important{
ListEntry list;
FileObject fileobj; //+0x08
EPROCESS eprocess; //+0x0C 遊戲主進程
ETHREAD ethread; //+0x10
ULONG //+0X14
ULONG //+0x18
ULONG //+0x1C
ULONG //+0x20 這裡是一個標記 本進程是否還存在有效
ULONG flag; //+0x24 這裡是個標記
ULONG //+0x28
ULONG //+0x2C
LARGE_INTEGER CpuCycleCount; //+0x30 cpu週期計數, 用作反調試
ULONG ProcId; //+0x38 主進程ID

HANDLE hThreads[3F]; //+0X3C 從這裡到 0x130都是線程ID 保存了一些遊戲主進程的線程ID
..
ULONG //+0x130
PWCHA //+0x13C

PIRP Irp; //+0x15C
.. //+0x160 貌似是個字串指針
.. //+x0164
PIRP Irp: //+0x168
.. //+0x16C
.. //+0x170
PIRP Irp; //+0x174
}Important;



還有幾個與遊戲線程相關的名單, 不過不怎麼重要, 就不列出了


//*****************************************************************

下面說一下HS的過濾, 以進程相關的操作為例 open read write, 過濾步驟如下:
if (當前進程在黑名單中)
{
if (操作目標是要保護進程)
{
if (是遊戲進程發起的操作)
{
從黑名單中移除自己;
放行;
}
else
{
拒絕訪問;
}
}
else
{
放行
}
}
else
{
if (操作目標是要保護進程)
{
if (是遊戲進程發起的操作)
{
放行;
}
else
{
if (當前進程在白名單中)
{
放行
}
else
{
加入黑名單;
拒絕訪問;
}
}
}
else
{
放行
}
}

乍一看過濾方式夠嚴密, 不過具體實施上卻暴露出了一個大的漏洞,
因為在這一系列的操作中它都是進程ID來判斷過濾的, 呵呵, 這樣PID+1 +2 +3 就能簡單地在三層繞過它了

當然這只針對NtOpenProcess可行, read write函數是傳遞的是進程句柄,
HS內部通過ZwQueryInformationProcess通過句柄得到進程ID來進行過濾

所以我們還是將自身進程添加到白名單中比較穩妥, 有了上面的白名單結構信息,
我們只需要隨便跟蹤hs的一個代理函數並找到名單的ListHead就OK了



另外一個重點是反調試這邊了, 涉及到這方面的內核處理有:
KiAttachProcess inline Hook
NtSetContextThread inline hook
NtGetContextThread inline hook
PsSuspendThread inline hook <win7>
dpc例程

RING3層:
DbgBreakPoint 指令修改
DbgUiRemoteBreakin 指令修改

我所瞭解到的只有這些, 也可能還有別的地方, 內核inline Hook的幾個函數大家都心知肚明, 不必多說
dpc例程主要用來獲取CMOS中的時鐘信息, CPU的時鐘週期數, 然後進行一些運算, 應該是用來反內核調試用
關於dpc, 我的另一片帖子中有介紹: http://bbs.pediy.com/showthread.php?t=148135

另外RING3層的兩個函數的修改也是很猥瑣的, 我們在用OD附加到遊戲上時會創建一個遠程線程,
這個線程就是為了執行DbgBreakPoint而存在, 原來的DbgBreakPoint指令為 int 3 , retn
也就是執行一個中斷指令以中斷到調試器, 現在已經被修改為 retn, retn 直接返回了,
這樣調試器就接收不到中斷信號

另外一個DbgUiRemoteBreakin, 是執行DbgBreakPoint前的一個調用, 具體的作用我也沒仔細研究,
不過看名稱也是中斷用的, hs會將這個函數頭寫入一個jmp, 直接跳轉到LdrShutdownProcess,
LdrShutdownProcess將直接給每個已經加載的dll模塊發消息, dll將被卸載
這樣就導致了OD一加載, dll等模塊就被卸載掉了

關於KiAttachProcess NtSetContextThread NtGetContextThread PsSuspendThread如何繞過,
這個是仁者見仁智者見智的了, 不過這幾個代理函數中都有這樣的指令序列:

最後的jz指令就跳轉到原函數了, 呵呵, 這裡就不言自明了吧


//*****************************************************************
hs內部的一套Hook框架很不錯, 當初讓我學到了很多東西, 受益匪淺, 在這裡也跟大家分享下,
<這框架是我在hs框架基礎上完善的>

// 用於索引
typedef enum _SSDTNameIndex{
ssdtZwCreateFile,
ssdtZwOpenFile,
MAX_SSDT_HOOK_ITEM_COUNT
}SSDTNameIndex;

#define SERVICE_NAME_LEN 64
typedef struct _SSDTHookInfo{
ULONG ServiceIndex; // ssdt索引
hintSSDTNameIndex Number; // 序號
PVOID RealAddress; // ssdt真實地址
PVOID PrevAddress; // hook ssdt之前地址
PVOID ProxyAddress; // 代理地址
ULONG ProxyBusyNow; // 正忙標誌
BOOLEAN IsHookInstalled; // 是否安裝了hook
BOOLEAN IsInUses; // 是否啟用這個hook
WCHAR ServiceName[SERVICE_NAME_LEN]; // ssdt名稱
}SSDTHookInfo, *PSSDTHookInfo, *PSSDTHOOKINFO;

NTSTATUS CollectSSDTInfo()
{
在這裡初始化結構數組, 獲取函數索引, 解析PE得到其真實地址等等
}


// 安裝Hook
NTSTATUS SetupAllSSDTHook()
{
..
for ( i = 0; i < MAX_SSDT_HOOK_ITEM_COUNT; i++ ) // 遍歷結構數組
{
pEntry = (PSSDTHookInfo)&g_SSDTHookItems[i];
if ( !pEntry ) continue;
if ( !pEntry->IsInUses ) continue; // 根據此標誌啟動這個函數Hook
if ( pEntry->IsHookInstalled ) continue; // is reinstall?
if ( !pEntry->RealAddress ) continue;
if ( pEntry->ServiceIndex > KeServiceDescriptorTable->ulNumberOfServices ) continue;

..進行Hook

pEntry->PrevAddress = OldAddress;
pEntru->IsHookInstalled = TRUE;
...
}
}



// 代理例程
NTSTATUS Proxy_ZwOpenFile()
{
..
pEntry = (PSSDTHookInfo)&g_SSDTHookItems[ssdtZwCreateFile]; // 取得對應項

InterlockedIncrement(&pEntry->ProxyBusyNow);

..過濾

InterlockedDecrement(&pEntry->ProxyBusyNow);
return status;
}


// 恢復Hook
NTSTATUS RestoreAllSSDTHook()
{
..

for ( i = 0; i < MAX_SSDT_HOOK_ITEM_COUNT; i++ )
{
pEntry = (PSSDTHookInfo)&g_SSDTHookItems[i];
if ( !pEntry ) continue;
if ( !pEntry->IsInUses ) continue;
if ( !pEntry->IsHookInstalled ) continue;
if ( !pEntry->RealAddress ) continue;

..取當前SSDT地址
if ( CurrentAddress == pEntry->ProxyAddress )
{
// 沒有被別人恢復時, 恢復Hook
if ( MmIsAddressValid(pEntry->PrevAddress) )
{
..使用Hook之前地址恢復
}
else
{
..使用真實地址恢復
}
}
}

// 在這裡等待所有代理函數都處理完畢 正常退出

for ( i = 0; i < MAX_SSDT_HOOK_ITEM_COUNT; i++ )
{
pEntry = (PSSDTHookInfo)&g_SSDTHookItems[i];
if ( !pEntry->IsInUses ) continue;

if ( pEntry->ProxyBusyNow > 0 )
{
DueTime.QuadPart = 0;
KeInitializeTimerEx(&timer, SynchronizationTimer);
KeSetTimerEx(&timer, DueTime, 100, NULL);

for ( j = 0; j < 20; j++ )
{
KeWaitForSingleObject(&timer, Executive, KernelMode, FALSE, NULL);
if ( (LONG)pEntry->ProxyBusyNow <= 0 ) break;
}
KeCancelTimer(&timer);
}

pEntry->PrevAddress = NULL;
pEntry->IsHookInstalled = FALSE;
pEntry->ProxyBusyNow = 0;
}
}


inline hook的框架總結起來比較麻煩了, 大家就去看一下IDA數據庫自己總結下吧~~

留言

本月最夯

偷用電腦,怎知?事件檢視器全記錄!(開機時間、啟動項時間...)