端口訪問監控原理
NetRoc
本來不打算寫文章的,呵呵。既然AhnLab敢用,我當然也敢寫咯,哈哈
安博士的反外掛系統最近添加了一個功能,可以檢查出來按鍵精靈、簡單遊這些用增強版WinIo直接進行端口讀寫的程序。後來拿來看了一下,其原理就是自己前段時間實現過的那種。通過Hook int 1,設置IO斷點進行監控的方法。
原理如下:
Intel兼容CPU都內置了調試功能。可以設置的斷點類型包括執行斷點、內存訪問斷點和IO斷點。通過操作DrX寄存器和CR4 寄存器,可以在發生特定端口的讀寫操作時觸發斷點。AhnLab的這種檢測技術就是基於CPU的這種功能。以下的介紹都基於32位處理器。
CPU調試寄存器簡介:
DR0—DR3寄存器:它們是32位調試地址寄存器。根據DR7中設置的不同,它們可以包含內存地址,也可以包含IO端口號。很多調試器的硬件斷點也是通過這幾個寄存器實現的,所以一般硬件斷點只能設置4個。
DR4和DR5:這兩個寄存器是被系統保留的,當CR4中的DE被設置時,訪問這兩個寄存器會產生非法指令錯誤#UD;當CR4中的DE被清空時,這兩個寄存器和DR6、DR7關聯,即訪問它們和訪問DR6、DR7一樣。
DR6:調試狀態寄存器。這個寄存器用於在調試事件發生時報告狀態信息。要判斷是哪個斷點被觸發,觸發的原因之類的就是靠它裏面的值。DR6的定義如下:
typedef struct _DR6INFO
{
unsigned B0 : 1; //B0
unsigned B1 : 1; //B1
unsigned B2 : 1; //B2
unsigned B3 : 1; //B3
unsigned Reserved1 : 9; //reserved
unsigned BD : 1; //BD
unsigned BS : 1; //BS
unsigned BT : 1; //BT
unsigned Reserved2 : 16; //Reserved
}DR6INFO, *PDR6INFO;
l B0到B3用於指示哪個斷點被觸發。它們分別對應於DR0到DR3中的地址或端口。
l BD表示觸發斷點的下一條指令是對調試寄存器的訪問。當DR7的GD標誌被設置時,對調試寄存器進行訪問的指令會觸發調試事件,並且DR6的BD被設置。
l BS表示是由於單步執行觸發的調試事件。當EFLAGS的TF標誌被設置時,這種斷點會被觸發。
l BT指示是由於任務切換觸發的調試事件。當TSS中的T標誌被設置時會產生這種事件。
DR7:調試控制寄存器。對斷點是否啓用、斷點類型等的控制。設置斷點需要配合DR0—DR3和DR7寄存器。定義如下:
typedef struct _DR7INFO
{
unsigned L0 : 1; //L0
unsigned G0 : 1; //G0
unsigned L1 : 1; //L1
unsigned G1 : 1; //G1
unsigned L2 : 1; //L2
unsigned G2 : 1; //G2
unsigned L3 : 1; //L3
unsigned G3 : 1; //G3
unsigned LE : 1; //LE
unsigned GE : 1; //GE
unsigned Reserved1 : 3; //reserved
unsigned GD : 1; //GD
unsigned Reserved2 : 2; //reserved
unsigned RW0 : 2; //R/W0
unsigned LEN0 : 2; //LEN0
unsigned RW1 : 2; //R/W1
unsigned LEN1 : 2; //LEN1
unsigned RW2 : 2; //R/W2
unsigned LEN2 : 2; //LEN2
unsigned RW3 : 2; //R/W3
unsigned LEN3 : 2; //LEN3
}DR7INFO, *PDR7INFO;
l L0到L3:設置時爲當前任務啓用相應的斷點條件。每次任務切換時CPU都會自動清除Lx位,所以這幾位只控制當前任務的斷點。
l G0到G3:爲所有任務啓用相應的斷點條件。這是針對整個機器的。
l LE和GE:P6 family和之後的IA32處理器都不支持這兩位。當設置時,使得處理器會檢測觸發數據斷點的精確的指令。爲了兼容性,Intel建議使用精確斷點時把LE和GE都設置爲1。
l GD:設置GD位時啓用對調試寄存器的保護,這時對這些寄存器的訪問都會觸發調試中斷。進入中斷處理函數前,CPU會清掉GD位,使得中斷處理函數能夠訪問DRx寄存器。
l R/W0到R/W3:指定各個斷點的觸發條件。它們對應於DR0到DR3中的地址以及DR6中的4個斷點條件標誌。這幾位的意義會受到CR4中的DE位的影響。
當DE位爲1時,它們的意義如下:
00 — 僅在指令執行時中斷
01 — 僅數據寫入時中斷
10 — IO輸入輸出時中斷
11 — 數據讀取或寫入時中斷,但是不受指令預取的影響
當DE位爲0時,它們的意義如下:
00 — 僅在指令執行時中斷
01 — 僅數據寫入時中斷
10 —未定義
11 — 數據讀取或寫入時中斷,但是不受指令預取的影響
l LEN0到LEN3:指定在調試地址寄存器DR0到DR3中指定的地址位置的大小。如果R/Wx位爲0,則LENx位也必須爲0,否則會產生不確定的行爲。這幾位的意義如下:
00 — 1字節長度
01 — 2字節長度
10 — 未定義
11 — 四字節長度
IO監控的實現
介紹了上面這些內容,那麼IO監控的實現方法就很簡單了,鍵盤IO的端口是60和64,比如要監控60端口,就可以這樣進行:
l Hook掉Trap01,自己接管調試中斷
l 設置CR4的DE,以及DR7中的LE和GE。
l 在DR0到DR3中選一個來設置端口號,比如選擇DR0設置爲0x60。
l 設置DR7中的R/Wx和LENx位,這裏應該設置RW0爲10、LEN0爲00
l 在Hook的中端函數中,檢查DR6中的B0到B3,如果是B0的話,表明發生了對0x60端口的讀寫操作。
由於IO斷點是Trap,即在事件發生後才能觸發中斷,所以這種方法不能阻止對端口的讀寫,而僅能夠進行監控。判斷讀寫的數據以及要精確的判斷是讀還是寫需要更進一步的操作,也是有一些辦法可以實現的,這裏就不說完了,呵呵。
實現的關鍵代碼
Hook掉IDT
NTSTATUS HookIdt(ULONG ulId, PVOID pIntProc, PULONG pOldIntProc, PIDTENTRY pstOldEntry)
{
CCHAR CpuCount = 0;
PIDTENTRY IdtEntry = NULL;
IDTR stIdtr = {0};
CpuCount = *KeNumberProcessors;
while( CpuCount > 0)
{//處理多CPU
KeSetAffinityThread( KeGetCurrentThread(), CpuCount);//綁定CPU
//得到IDTR 中得段界限與基地址
_asm sidt stIdtr;
IdtEntry = (PIDTENTRY)stIdtr.Base;
//保存原有得IDT
if ( pstOldEntry)
{
RtlCopyMemory( pstOldEntry, &IdtEntry[ulId], sizeof( IDTENTRY));
}
_asm cli;//禁止中斷
if ( pOldIntProc)
{
*pOldIntProc = (ULONG)IdtEntry[ulId].OffsetLow | ((ULONG)IdtEntry[ulId].OffsetHigh<<16);
}
IdtEntry[ulId].OffsetLow = (unsigned short)pIntProc;
IdtEntry[ulId].OffsetHigh = (unsigned short)((unsigned int)pIntProc>>16);
_asm sti;//開中斷
CpuCount--;
}
return STATUS_SUCCESS;
}
//卸載鉤子
NTSTATUS UnhookIdt(ULONG ulId, ULONG pIntProc)
{
CCHAR CpuCount = 0;
PIDTENTRY IdtEntry = NULL;
IDTR stIdtr = {0};
CpuCount = *KeNumberProcessors;
while( CpuCount > 0)
{
KeSetAffinityThread( KeGetCurrentThread(), CpuCount);//綁定CPU
//得到IDTR 中得段界限與基地址
_asm sidt stIdtr;
IdtEntry = (PIDTENTRY)stIdtr.Base;
_asm cli;//禁止中斷
IdtEntry[ulId].OffsetLow = (unsigned short)pIntProc;
IdtEntry[ulId].OffsetHigh = (unsigned short)((unsigned int)pIntProc>>16);
_asm sti;//開中斷
CpuCount--;
}
return STATUS_SUCCESS;
}
//自己的
#pragma optimize( "", off )
void __declspec (naked) NewTrap01(void)
{
INT_CONTEXT stContext;
ULONG ulResult;
_asm
{
//保存環境;
push ebp;
mov ebp, esp;
sub esp, 100h;
mov stContext.cs, cs;
mov stContext.ds, ds;
mov stContext.eax, eax;
mov stContext.ebp, ebp;
mov stContext.ebx, ebx;
mov stContext.ecx, ecx;
mov stContext.edi, edi;
mov stContext.edx, edx;
mov stContext.es, es;
mov stContext.esi, esi;
mov stContext.esp, esp;
mov stContext.fs, fs;
mov stContext.ss, ss;
mov ax, 0x30;
mov fs, ax;
mov stContext.gs, gs;
}
ulResult = OnTrap01( &stContext);//實際處理
if ( ulResult == 1)
{
_asm
{
mov ax, stContext.ds;
mov ds, ax;
mov ebp, stContext.ebp;
mov ebx, stContext.ebx;
mov ecx, stContext.ecx;
mov edi, stContext.edi;
mov edx, stContext.edx;
mov esi, stContext.esi;
mov ax, stContext.es
mov es, ax;
mov ax, stContext.fs
mov fs, ax;
mov ax, stContext.ss
mov ss, ax;
mov ax, stContext.gs;
mov gs, ax;
mov eax, stContext.eax;
mov esp,ebp;
pop ebp;
//退出
iretd;
}
}
else
{
_asm
{
mov ax, stContext.ds;
mov ds, ax;
mov ebp, stContext.ebp;
mov ebx, stContext.ebx;
mov ecx, stContext.ecx;
mov edi, stContext.edi;
mov edx, stContext.edx;
mov esi, stContext.esi;
mov ax, stContext.es
mov es, ax;
mov ax, stContext.fs
mov fs, ax;
mov ax, stContext.ss
mov ss, ax;
mov ax, stContext.gs;
mov gs, ax;
mov eax, stContext.eax;
mov esp,ebp;
pop ebp;
//不是自己需要的事件,調用原來的Trap01;
jmp g_pOldTrap01;
}
}
}
#pragma optimize( "", on )
ULONG __stdcall OnTrap01(LPINT_CONTEXT pstContext)
{
DR6INFO stDr6;
ULONG ulEip = 0;
USHORT usCs = 0;
PUCHAR pucCode = 0;
ulEip = *((PULONG)(pstContext->ebp + 4));
usCs = *((PUSHORT)(pstContext->ebp + 8));
/*DbgPrint( "[ebp]=0x%X, [ebp+4]=0x%X, [ebp+8]=0x%X, [ebp+C]=0x%X/r/n", *((PULONG)(pstContext->ebp)),
*((PULONG)(pstContext->ebp + 0x4)),
*((PULONG)(pstContext->ebp + 0x8)),
*((PULONG)(pstContext->ebp + 0xC)));*/
pucCode = (PUCHAR)ulEip;
stDr6 = GetDR6();
/*DbgPrint( "%d:In trap 01.dr6.B0=%d, dr6.B1=%d, dr6.B2=%d,dr6.B3=%d, dr6.BD=%d, dr6.BS=%d, dr6.BT=%d/r/n", __LINE__,
stDr6.B0, stDr6.B1, stDr6.B2, stDr6.B3, stDr6.BD, stDr6.BS, stDr6.BT);
DbgPrint( "%d:Traped EIP=0x%X, CS=0x%X/r/n", __LINE__, (ULONG)ulEip, (ULONG)usCs);*/
if ( stDr6.B0 && g_bpInfo[0].blIsSet)
{
//
OnBreak( 0, pstContext, pucCode);
//DbgPrint("On bp 0/r/n");
return 1;
}else if ( stDr6.B1 && g_bpInfo[1].blIsSet)
{
//
OnBreak( 1, pstContext, pucCode);
//DbgPrint("On bp 1/r/n");
return 1;
}else if ( stDr6.B2 && g_bpInfo[2].blIsSet)
{
//
OnBreak( 2, pstContext, pucCode);
//DbgPrint("On bp 2/r/n");
return 1;
}else if ( stDr6.B3 && g_bpInfo[3].blIsSet)
{
//
OnBreak( 3, pstContext, pucCode);
//DbgPrint("On bp 3/r/n");
return 1;
}
else
{
return 0;
}
}
破解方法
既然知道了原理,那麼破解方法也就很明瞭了。讀寫端口之前想辦法清掉調試寄存器即可。但是如果處理了DR7中的GD標誌的話,清調試器的辦法要麻煩一些。這裏也不贅述了,呵呵。