大拿们新年快乐!
在看雪潜了一年的水了,一直在看大佬们的帖子,学了很多东西,可以说看雪就是我的半个师傅。
感谢看雪给新手提供了学习的平台。
入内核不久,这段时间一直在迷分段和分页机制,光看理论学起来真的累。看了大佬们的帖子,知道了怎么写调用门和中断门,正好写个东西练练手,加深下映像。
看《X86_X64体系探索及编程》了解到的分页分段机制,里面很详细。
这次做的是IDTHook,用的是win7X86虚拟机,预计X64会很难,还是算了…(⊙_⊙;)…
思路很简单,尝试了两种方式。
一、直接Patch IDT,改3号中断描述符的offset。
二、改IDT 3号中断描述符的选择子字段但是不改offset,同时Patch GDT的base,总之让段描述符的Base + 中断描述符的offset等于跳转的函数地址就成。
图一、中断例程寻址是这样的:
Patch GDT有两个选择,找个空位重写一个描述符或者直接改Index == 1的描述符(IDT里的选择子清一色08。index==1 TI==0 RPL==00)
图二 、windbg 读 IDT:
第二个显然是不行的...我还试了下,刚开始写虚拟机就报警提示CPU不愿意,然后重启了(忘截图了)
首先尝试直接Patch IDT:
首先通过windbg查看三号中断处理例程,照葫芦画瓢写个类似的
具体什么作用先不看了。
图三、
_declspec(naked)
VOID NewTrap()
{
_asm
{
push 0; ErrorCode
push ebp
push ebx
push esi
push edi
push fs
;Ring0下 fs:[0]指向的是_KPCR的地址
mov ebx, 30h
mov fs, bx
mov ebx, dword ptr fs : [0]
push ebx
sub esp, 4
push eax
push ecx
push edx
push ds
push es
push gs
sub esp, 30h //对齐TrapFrame首地址
call TrapCall
add esp, 0x30
pop gs
pop es
pop ds
pop edx
pop ecx
pop eax
add esp, 4
pop ebx
pop fs
pop edi
pop esi
pop ebx
pop ebp
add esp, 0x4
//跳回原例程入口
//push 08h
//push g_RealTrap
//retf
jmp g_RealTrap
}
}
VOID TrapCall()
{
DbgPrint("TrapCall\r\n");
}
我们的目的是将3号中断钩到以上代码中。
现在就要找IDT然后写东西进去。找IDT可以用sidt将IDT地址保存到栈里。有了地址就可以直接写了。
图四、中断描述符:
根据这个我们可以定义出中段描述符结构体。
//中断门描述符 仅做参考
typedef struct _INTER_DESC
{
short OffsetLow;
short selector;
char IST : 3;
char dumb : 5;
char Attr0 : 1;
char Attr1 : 1;
char Attr2 : 1;
char Attr3 : 1;
char Attrzero : 1;
char AttrDPL : 2;
char Attrp : 1;
short OffsetHigh;
}INTER_DESC, *PINTER_DESC;
只要改offset其他的依旧照葫芦画瓢,先不研究。
VOID WriteTrap(ULONG32 FuncAddr)
{
//参数传NewTrap
_asm
{
;写调用门用到的索引
mov ebx,g_eax
}
_asm
{
sub esp, 20h
push esi
; mov esi, ptr IDT
sidt[esp - 2]
pop esi
; 找到第三个中断描述符的位置
add esi, 18h
mov ebx, [esi]
mov g_DescLow, ebx
mov ebx, [esi + 4]
;用的时候取高16位
mov g_DescHigh, ebx
mov ebx, FuncAddr
mov[esi + INTERGATE_OFFSETLOW], bx
shr ebx, 16
mov[esi + INTERGATE_OFFSETHIGH], bx
mov[esi + INTERGATE_ATTR], 0xee; ? ?
mov[esi + INTERGATE_DUMB], 00
; mov ebx, g_eax
; shr ebx, 3
; 1 0 00 Index == 1,RPL == 0
mov [esi + INTERGATE_SELECTOR], 08h
//保存真实例程地址
lea eax, g_RealTrap
mov ebx, g_DescLow
mov[eax], bx
mov ebx, g_DescHigh
shr ebx, 16
mov[eax + 2], bx
/*
Hook INT 3的时候不好调试,在这先调试下恢复
mov ebx, g_DescLow
mov[esi + INTERGATE_OFFSETLOW], bx
mov ebx, g_DescHigh
shr ebx, 16
mov[esi + INTERGATE_OFFSETHIGH], bx
*/
add esp,20h
}
}
WriteTrap后再在windbg单击单步中断就能看到DbgPrint("TrapCall\r\n")了
图五、
成功钩出。
然后我们来看第二种方法,Path GDT:
用windbg输入! idt -a可以查看各INT中断处理例程入口。
db idtr可以看到很多描述符的selector字段是0x0008。
都是找GDT表索引为1号的描述符项,那么一号描述符不让写也很容易理解。
但是空的表项很多,找到第一个空的写进去便是(见图二)。
图六、GDT
图七、段描述符
由此我们可以定义出段描述符结构体:
//段描述符要用
typedef struct _SEG_DESC
{
short limit;
ULONG32 BaseLow : 24;
union OneByte
{
struct onebyte
{
char type0 : 1;
char type1 : 1;
char type2 : 1;
char type3 : 1;
char s : 1;
char DPL : 2;
char P : 1;
};
char dumb0; //赋值时用
};
union anotherone
{
struct ByteAgain
{
char lim : 4;
char AVL : 1;
char L : 1;
char DB : 1;
char G : 1;
};
char dumb1;
};
char BaseHigh;
}SEG_DESC, *PSEG_DESC;
VOID CoverGDT(ULONG32 FuncAddr)
{
//修改SegmentBase里的描述符第16~39位 26~63位的base字段
//本来想直接修改索引号为1的段描述符,这样就不用改IDT了
//然而一改虚拟机就提示CPU不干....
ULONG32 Pointer = 0;
PSEG_DESC pSeg = NULL;
ULONG32 uTemp = 0;
_asm
{
pushfd
pushad
; 找到三号中断入口地址s
push esi
; mov esi, ptr IDT
sidt[esp - 2]
pop esi
; 找到第三个中断描述符的位置
add esi, 18h
mov ebx, [esi]
mov g_DescLow, ebx
mov ebx, [esi + 4]
mov g_DescHigh, ebx
;计算原先的中断处理例程地址
lea eax, g_RealTrap
mov ebx, g_DescLow
mov [eax], bx
mov ebx, g_DescHigh
shr ebx, 16
mov[eax + 2], bx
;算出函数地址偏移 准备赋给Segment Descriptor的base字段
mov eax, g_RealTrap
mov ebx, FuncAddr
sub ebx, eax
mov uTemp,ebx
lea ebx,uTemp
;开始遍历GDT
push esi
sgdt[esp - 2]
; esi: ptr Segment Descriptor
pop esi
;eax作计数器
mov eax, 8
LoopStart :
lea edx, [esi + eax]
xor ecx, ecx
mov cl, [edx + 5]
test cl, 80h
; 字面上来看就是 P位为1,其它为0的就过,但是实际上找的是cl == 0的,就是8Bytes全0
jnz notZero
; 计数and写段描述符
mov g_eax, eax
; 找到空的位置就写我们的描述符
; 这里只管我们算出来的Base字段,其它的都抄Index == 1的Segment Descriptor了
mov[edx + SEGDESC_LIMIT], 0xff
mov[edx + SEGDESC_LIMIT + 1], 0xff
mov eax,[ebx]
mov[edx + SEGDESC_BASELOW], ax
mov al,[ebx + 2]
mov[edx + SEGDESC_BASELOW + 2],al
mov al,[ebx + 3]
mov[edx + SEGDESC_BASEHIGH], al
mov[edx + SEGDESC_TYPE], 9bh
mov[edx + SEGDESC_FLAGS], 0xcf; 为啥有的后面加h就行,有的必须写0x ?
;开始PatchIDT
push esi
; mov esi, ptr IDT
sidt[esp - 2]
pop esi
add esi, 18h
mov ebx, g_eax
; 因为描述符大小是8字节 除8得索引
shr ebx, 3
mov[esi + INTERGATE_SELECTOR], bx
popad
popfd
mov esp, ebp
pop ebp
ret
notZero :
add eax, 8
test eax, GDT_LIMIT
jnz LoopStart
;找不到空的,那就算了
mov eax, 0
popad
popfd
ret
}
}
图八、写入GDT:
调试结果可见,原3号中断描述符里的Offset字段存在g_RealTrap全局里面,
值为0x8485bc90
跳转的目标地址是0x9eb031f0,二者相减得0x1a2a7560。
查看gdt表,80b95048处已经写入了我们自己构造的段描述符,base字段在第3~5和第8字节为写入的offset。成功(`・ω・´)
ResumeHook只要将原先的中断描述符放回去就行了,GDT就不用管它了。而原先的数据都保存在全局变量里了。
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
DbgPrint("DriverUnload()\r\n");
// WPOFF();
_asm
{
;Resume IDT
sub esp, 20h
push esi
; mov esi, ptr IDT
sidt[esp - 2]
pop esi
; 找到第三个中断描述符的位置
add esi, 18h
mov ebx, g_DescLow
mov[esi + INTERGATE_OFFSETLOW], bx
mov ebx, g_DescHigh
shr ebx, 16
mov[esi + INTERGATE_OFFSETHIGH], bx
add esp, 20h
}
// WPON();
}
最后这个纯属无聊...
之前学习完写CallGate之后又看到了图一,于是脑抽,正常情况下IDT的中断门描述符自己提供选择子,然后找GDT里段描述符的Base
俩人一加,得到地址。而CallGate也是提供选择子的,并且调用门是写在GDT里的,如果IDT的选择子选到了GDT里的调用门,那么CS寄存器最终会加载哪个选择子?
这个很好试嘛,然后开搞。
思路很简单,GDT找个空位置,写个调用门进去。然后改IDT 3号中断描述符的选择子,Index为调用门在GDT中的偏移除8
//调用门描述符 参考
typedef struct _CALLGATE_DESC
{
SHORT OFFSETL;
SHORT SELECTOR;
CHAR DCOUNT;
CHAR GTYPE;
SHORT OFFSETH;
}CALLGATE_DESC;
写调用门跟前面写段描述符一样。
ULONG32 AddCallGate(ULONG32 FuncAddr)
{
INT32 i = 0;
_asm
{
; push ebp
; mov ebp, esp
sub esp, 20h
pushfd
pushad
push esi
; esp - 2里写入的是低16位的limit,esp里写入的是32位的Base
sgdt[esp - 2]
; 得到ptr GDT
pop esi
; 用eax遍历GDT
mov eax, 8
mov i, eax
LoopStart :
lea edx, [esi + eax]
; edx: ptr CALLGATE
xor ecx, ecx
mov cl, [edx + 5]
;0x80 Attr == 0x80 P == 1,其它标志位为0
test cl, 80h
jnz notZero
mov g_eax, eax
mov ebx, FuncAddr
mov[edx], bx
;selector == 08 1 0 00
mov[edx + 2], 08h
mov[edx + 4], 0
mov[edx + 5], GATE_TYPE
;将高16位放在结构体的高16位里
shr ebx, 16
mov[edx + 6], bx
popad
popfd
mov esp, ebp
pop ebp
ret
notZero :
add eax, 8
test eax, GDT_LIMIT
jnz LoopStart
mov eax, 0
popad
popfd
ret
}
}
然后把WriteTrap这里的两个分号放开,08h改成bl就成。
结果崩了0.0,老哥们可以测测看,下周考试,先不搞了。
近期再碰3号中断我就一掌震死自己,为什么要搞3号中断呢?为什么要跟自己过不去,一整天调的眼睛都瞎了(ー̀дー́)
新人刚上车,大佬们轻拍,赶上元旦,祝老哥们新的一年功力大进(逃
_declspec(naked)
VOID NewTrap()
{
_asm
{
push 0; ErrorCode
push ebp
push ebx
push esi
push edi
push fs
;Ring0下 fs:[0]指向的是_KPCR的地址
mov ebx, 30h
mov fs, bx
mov ebx, dword ptr fs : [0]
push ebx
sub esp, 4
push eax
push ecx
push edx
push ds
push es
push gs
sub esp, 30h //对齐TrapFrame首地址
call TrapCall
add esp, 0x30
pop gs
pop es
pop ds
pop edx
pop ecx
pop eax
add esp, 4
pop ebx
pop fs
pop edi
pop esi
pop ebx
pop ebp
add esp, 0x4
//跳回原例程入口
//push 08h
//push g_RealTrap
//retf
jmp g_RealTrap
}
}
VOID TrapCall()
{
DbgPrint("TrapCall\r\n");
}
我们的目的是将3号中断钩到以上代码中。
现在就要找IDT然后写东西进去。找IDT可以用sidt将IDT地址保存到栈里。有了地址就可以直接写了。
图四、中断描述符:
根据这个我们可以定义出中段描述符结构体。
//中断门描述符 仅做参考
typedef struct _INTER_DESC
{
short OffsetLow;
short selector;
char IST : 3;
char dumb : 5;
char Attr0 : 1;
char Attr1 : 1;
char Attr2 : 1;
char Attr3 : 1;
char Attrzero : 1;
char AttrDPL : 2;
char Attrp : 1;
short OffsetHigh;
}INTER_DESC, *PINTER_DESC;
//中断门描述符 仅做参考
typedef struct _INTER_DESC
{
short OffsetLow;
short selector;
char IST : 3;
char dumb : 5;
char Attr0 : 1;
char Attr1 : 1;
char Attr2 : 1;
char Attr3 : 1;
char Attrzero : 1;
char AttrDPL : 2;
char Attrp : 1;
short OffsetHigh;
}INTER_DESC, *PINTER_DESC;
只要改offset其他的依旧照葫芦画瓢,先不研究。
VOID WriteTrap(ULONG32 FuncAddr)
{
//参数传NewTrap
_asm
{
;写调用门用到的索引
mov ebx,g_eax
}
_asm
{
sub esp, 20h
push esi
; mov esi, ptr IDT
sidt[esp - 2]
pop esi
; 找到第三个中断描述符的位置
add esi, 18h
mov ebx, [esi]
mov g_DescLow, ebx
mov ebx, [esi + 4]
;用的时候取高16位
mov g_DescHigh, ebx
mov ebx, FuncAddr
mov[esi + INTERGATE_OFFSETLOW], bx
shr ebx, 16
mov[esi + INTERGATE_OFFSETHIGH], bx
mov[esi + INTERGATE_ATTR], 0xee; ? ?
mov[esi + INTERGATE_DUMB], 00
; mov ebx, g_eax
; shr ebx, 3
; 1 0 00 Index == 1,RPL == 0
mov [esi + INTERGATE_SELECTOR], 08h
//保存真实例程地址
lea eax, g_RealTrap
mov ebx, g_DescLow
mov[eax], bx
mov ebx, g_DescHigh
shr ebx, 16
mov[eax + 2], bx
/*
Hook INT 3的时候不好调试,在这先调试下恢复
mov ebx, g_DescLow
mov[esi + INTERGATE_OFFSETLOW], bx
mov ebx, g_DescHigh
shr ebx, 16
mov[esi + INTERGATE_OFFSETHIGH], bx
*/
add esp,20h
}
}
WriteTrap后再在windbg单击单步中断就能看到DbgPrint("TrapCall\r\n")了
图五、
成功钩出。
VOID WriteTrap(ULONG32 FuncAddr)
{
//参数传NewTrap
_asm
{
;写调用门用到的索引
mov ebx,g_eax
}
_asm
{
sub esp, 20h
push esi
; mov esi, ptr IDT
sidt[esp - 2]
pop esi
; 找到第三个中断描述符的位置
add esi, 18h
mov ebx, [esi]
mov g_DescLow, ebx
mov ebx, [esi + 4]
;用的时候取高16位
mov g_DescHigh, ebx
mov ebx, FuncAddr
mov[esi + INTERGATE_OFFSETLOW], bx
shr ebx, 16
mov[esi + INTERGATE_OFFSETHIGH], bx
mov[esi + INTERGATE_ATTR], 0xee; ? ?
mov[esi + INTERGATE_DUMB], 00
; mov ebx, g_eax
; shr ebx, 3
; 1 0 00 Index == 1,RPL == 0
mov [esi + INTERGATE_SELECTOR], 08h
//保存真实例程地址
lea eax, g_RealTrap
mov ebx, g_DescLow
mov[eax], bx
mov ebx, g_DescHigh
shr ebx, 16
mov[eax + 2], bx
/*
Hook INT 3的时候不好调试,在这先调试下恢复
mov ebx, g_DescLow
mov[esi + INTERGATE_OFFSETLOW], bx
mov ebx, g_DescHigh
shr ebx, 16
mov[esi + INTERGATE_OFFSETHIGH], bx
*/
add esp,20h
}
}
WriteTrap后再在windbg单击单步中断就能看到DbgPrint("TrapCall\r\n")了
图五、
成功钩出。
然后我们来看第二种方法,Path GDT:
用windbg输入! idt -a可以查看各INT中断处理例程入口。
db idtr可以看到很多描述符的selector字段是0x0008。
都是找GDT表索引为1号的描述符项,那么一号描述符不让写也很容易理解。
但是空的表项很多,找到第一个空的写进去便是(见图二)。
图六、GDT
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)