首页
论坛
课程
招聘
[原创]分析了一下360安全卫士的HOOK(二)——架构与实现
2009-10-14 21:13 9971

[原创]分析了一下360安全卫士的HOOK(二)——架构与实现

2009-10-14 21:13
9971
Author:achillis
blog  :http://hi.baidu.com/_achillis

上一篇的分析中漏掉了三个函数,现补上:
NtSetSystemInformation                0x24       
ProcessNotify                                0x45 //这个并非Hook,只是HookPort安装的一个Notify
KeUserModeCallback                        0x4B
这样一共是从0到0x4B,共0x4C个过滤函数,齐了~~

上次先列出了360所hook的系统服务,让大家对它做了什么有了一些了解。这次分析的重点是360的KiFastCallEntry钩子安装全过程,更为重点的是360这样一个安全软件,是如何理好地处理好这众全多的Hook,即我所谓的“架构”问题。

一、准备工作
(1)准备要Hook的系统服务的服务号ServiceIndex,对于导出的服务,采用获取Zw*函数地址后再取服务号的方法,这个想必大家都很熟悉。未导出的,则根据不同系统版本,采用硬编码的方法。
(2)准备缓冲区,存放原始服务例程地址、过滤开关、代理函数地址等,记为ServiceFilterInfoTable,这些都是HOOK中要使用到的数据。
HookPort.sys申请了一块很大的内存用于存放这些数据,该内存大小为0x5DDC=6007*4=(6006+1)*4,为什么这么写呢?因为它实际上是下面一个结构:
typedef struct _SERVICE_FILTER_INFO_TABLE{
ULONG SSDTCnt;
ULONG SavedSSDTServiceAddress[1001];		//起始偏移0001*4,保存被Hook的SSDT函数的地址
ULONG ProxySSDTServiceAddress[1001];		//起始偏移1002*4,保存被Hook的SSDT函数对应的代理函数的地址
ULONG SavedShadowSSDTServiceAddress[1001];	//起始偏移2003*4,保存被Hook的ShadowSSDT函数的地址
ULONG ProxyShadowSSDTServiceAddress[1001];	//起始偏移3004*4,保存被Hook的ShadowSSDT函数对应的代理函数的地址
ULONG SwitchTableForSSDT[1001];				//起始偏移4005*4,保存SSDT Hook开关,决定该函数是否会被Hook
ULONG SwitchTableForShadowSSDT[1001];		//起始偏移5006*4,保存ShadowSSDT Hook开关,决定该函数是否会被Hook
}SERVICE_FILTER_INFO_TABLE;


考虑到服务数最多的就是Win7的ShadowSSDT服务了,共827个,所以这里使用1001个项。显然,每个表之间都有空隙,每个表内部也有大量空隙,觉得浪费内存了是吧?确实是这样的,但是不要忘了有一条法则,叫做“以时间换空间,以空间换时间”,这里虽然浪费了内存,但是保证了效率最高,因为被Hook的地方是调用非常非常频繁的KiFastCallEntry.
(3)准备缓冲区,存放过滤函数的地址和规则表,记为FilterFunRuleTable。这个部分的实现虽然是在HookPort.sys中,但是它对外留出了这个接口,实际上却是由360SelfProtect.sys调用完成的。
这个缓冲区的结构如下:
#define FILTERFUNCNT 0x4C //过滤函数的个数
typedef struct _FILTERFUN_RULE_TABLE{
ULONG bSize; //本结构的大小,为0x144=0x51*4=(0x4C+5)*4=(FILTERFUNCNT+5)*4
ULONG Unknown1; //未明确
ULONG IsFilterFunFilledReady; 			//标志,表明过滤函数表是否准备好
ULONG FakeServiceRoutine[FILTERFUNCNT]	//偏移为0xC,过滤函数数组,共有过滤函数0x4C个
PULONG SSDTRuleTableBase;  				//偏移为0x13C,是SSDT函数的过滤规则表,表的大小为SSDTCnt*4
PULONG ShadowSSDTRuleTableBase; 		//偏移为0x140,是ShadowSSDT函数的过滤规则表,表的大小为ShadowSSDTCnt*4
}FILTERFUN_RULE_TABLE;

这些只是一些准备工作,具体有些是什么时候完成的,在后面会讲到。

二、KiFastCallEntry的Hook
(1)目标KiFastCallEntry,但具体Hook在哪里最好?
KiFastCallEntry是ring3经sysenter进入内核后的第一个必经之地(不考虑sysenter hook),但是KiFastCallEntry所作的工作也很多,比如设置ds,es,fs段寄存器的值,从ETHREAD中取ServiceTable,判断ServiceIndex是否合法,若合法则判断应使用哪张表(SSDT还是ShadowSSDT),然后从表中取出服务例程地址,从用户栈复制参数到内核栈,然后调用服务例程,调用完之后再做一点准备工作然后就由KiServiceExit再飞回ring3.大致的流程就是这样的.我贴一下关键的一段:
kd> 
8053d7e4 8b5f0c          mov     ebx,dword ptr [edi+0Ch] //edi指向SSDT或ShadowSSDT
8053d7e7 33c9            xor     ecx,ecx //ecx清零
8053d7e9 8a0c18          mov     cl,byte ptr [eax+ebx]  //cl得到参数的长度,即参数个数*4
8053d7ec 8b3f            mov     edi,dword ptr [edi] //edi指向KiServiceTable或W32pServiceTable
8053d7ee 8b1c87          mov     ebx,dword ptr [edi+eax*4] //eax是服务号,然后ebx得到服务函数地址
8053d7f1 2be1            sub     esp,ecx //ecx得到参数的总长度,这里是开辟栈空间
8053d7f3 c1e902          shr     ecx,2 		//除以4,得参数个数
8053d7f6 8bfc            mov     edi,esp 	//准备复制参数
8053d7f8 3b35b48b5580    cmp     esi,dword ptr [nt!MmUserProbeAddress (80558bb4)] //判断参数地址是否有效
8053d7fe 0f83a8010000    jae     nt!KiSystemCallExit2+0x9f (8053d9ac)
8053d804 f3a5            rep movs dword ptr es:[edi],dword ptr [esi] //复制参数
8053d806 ffd3            call    ebx //调用系统服务

我们不能hook在开头,这样太多的准备工作需要自己来完成,而且涉及到的操作太多的话,兼容性和稳定性就很成问题。显然我们又不能hook在call服务例程之后,这时该办的事都办完了,我们再接手就已经晚了,所以必须在call服务例程之前。KiFastCallEntry中的指令,真是有点寸士寸金的感觉,寄存器也不能随意改变,挑哪儿下手呢?来看看360是怎么做的:
Hook之后:
kd> 
nt!KiFastCallEntry+0xcc:
8053d7dc ff0538f6dfff    inc     dword ptr ds:[0FFDFF638h]
8053d7e2 8bf2            mov     esi,edx
8053d7e4 8b5f0c          mov     ebx,dword ptr [edi+0Ch]
8053d7e7 33c9            xor     ecx,ecx
8053d7e9 8a0c18          mov     cl,byte ptr [eax+ebx]
8053d7ec 8b3f            mov     edi,dword ptr [edi]
8053d7ee 8b1c87          mov     ebx,dword ptr [edi+eax*4]
8053d7f1 e94a49e901      jmp     823d2140  //这里被改成了一个跳转
8053d7f6 8bfc            mov     edi,esp
8053d7f8 3b35b48b5580    cmp     esi,dword ptr [nt!MmUserProbeAddress (80558bb4)]
8053d7fe 0f83a8010000    jae     nt!KiSystemCallExit2+0x9f (8053d9ac)
8053d804 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
8053d806 ffd3            call    ebx
8053d808 8be5            mov     esp,ebp
8053d80a 8b0d24f1dfff    mov     ecx,dword ptr ds:[0FFDFF124h]
8053d810 8b553c          mov     edx,dword ptr [ebp+3Ch]

这个Hook的位置确实选得非常好啊,自己省去了很多准备工作,都由系统准备好了,此时edi指向服务表的基址(KiServiceTable或W32pServiceTable),ebx是刚刚取出的原始服务例程的地址,eax是服务号,这时再做处理不是就很容易了嘛,重要的几个信息都有了。
来看看跳转的地址,又是个跳转,看来这里只是个中间跳
kd> u 823d2140
823d2140 e95d982a76      jmp     Hookport+0x79a2 (f867b9a2)

f867b9a2这里才是真正的目的地,在HookPort.sys中.
kd> u f867b9a2
Hookport+0x79a2:
f867b9a2 8bff            mov     edi,edi
f867b9a4 9c              pushfd
f867b9a5 60              pushad
f867b9a6 57              push    edi //edi指向KiServiceTable或W32pServiceTable
f867b9a7 53              push    ebx //ebx是原始的KiFastCallEntry从SSDT中取到的服务函数地址
f867b9a8 50              push    eax //eax是服务号
f867b9a9 e840ffffff      call    Hookport+0x78ee (f867b8ee) //KiFastCallEntryFilterFunc
f867b9ae 89442410        mov     dword ptr [esp+10h],eax
f867b9b2 61              popad
f867b9b3 9d              popfd
f867b9b4 2be1            sub     esp,ecx
f867b9b6 c1e902          shr     ecx,2
f867b9b9 ff3574d767f8    push    dword ptr [Hookport+0x9774 (f867d774)]  //这里是回跳的地址,push/ret方式跳回去
f867b9bf c3              ret

(2)360是如何具体地去安装这个Hook的?
360首先获取ZwSetEvent的服务号,然后安装了一个SSDT Hook,目标就是NtSetEvent.
然后,自己调用了一下ZwSetEvent,触发自己安装的Hook,代码如下:
HANDLE g_FakeEventHandle = (HANDLE)0x288C58F1;
ZwSetEvent(g_FakeEventHandle, NULL); 
  
0x288C58F1,好奇怪,你看过谁家的句柄长成这个样子嘛,显然这是个假句柄了,它的作用就是在hook函数Fake_NtSetEvent中辨别一下是不是自己人,充当了暗号。
Fake_NtSetEvent中确实也是这样写的:
if ( EventHandle != g_FakeEventHandle || ExGetPreviousMode()==UserMode )// 不是我们自己调用,或者调用来自UserMode,直接调用原函数
{
	result = OriginalNtSetEvent(EventHandle, PreviousState);
}

不是自己人就调用原函数去了,等对上暗号,是自己人了,才真正开始干事.
先申请一个5字节的buffer,写入一个跳转指令,这个就是上面看到的中间跳,这里还有一个细节,判断了一下系统版本,Vista前后处理上有稍许不同,根据当前系统版本选择合适的JmpStub.
再准备另外一个跳转指令,跳转目标就是刚才准备的中间跳的地址,这个跳转指令将要被写入KiFastCallEntry中。那么360是如何找到要写入的位置呢?继续看.
跳转指令准备好之后,先还原刚才SSDT中安装的NtSetEvent钩子,然后从栈中回溯返回地址,也就是取[ebp+4]的值,并保存起来.上面对KiFastCallEntry的分析知道,SSDT中的函数都是从KiFastCallEntry中call过来的,那么返回地址肯定也在KiFastCallEntry中,具体地讲,就是8053d806处call ebx的下一条指令处。取得这个地址后,往上匹配寻找8053d7f3 c1e902          shr     ecx,2
8053d7f6 8bfc            mov     edi,esp
找到之后,就确定了要Hook的位置。接下来怎么做写过hook的都知道,就是关写保护然后把刚才准备好的跳转指令写入再打开保护而已,不多说。

(3)KiFastCallEntry被hook后,360是如何处理的?
KiFastCallEntry被hook后,经二级跳跳到了JmpStub里。来看看:
kd> u f867b9a2
Hookport+0x79a2:
f867b9a2 8bff            mov     edi,edi
f867b9a4 9c              pushfd
f867b9a5 60              pushad
f867b9a6 57              push    edi //edi指向KiServiceTable或W32pServiceTable
f867b9a7 53              push    ebx //ebx是原始的KiFastCallEntry从SSDT中取到的服务函数地址
f867b9a8 50              push    eax //eax是服务号
f867b9a9 e840ffffff      call    Hookport+0x78ee (f867b8ee) //KiFastCallEntryFilterFunc
f867b9ae 89442410        mov     dword ptr [esp+10h],eax
f867b9b2 61              popad
f867b9b3 9d              popfd
f867b9b4 2be1            sub     esp,ecx
f867b9b6 c1e902          shr     ecx,2
f867b9b9 ff3574d767f8    push    dword ptr [Hookport+0x9774 (f867d774)]  //这里是回跳的地址,push/ret方式跳回去
f867b9bf c3              ret

可以看到,edi,ebx,eax三个重要数据入栈后,调用了另一个判断函数。该函数逆向如下:
ULONG __stdcall KiFastCallEntryFilterFunc(ULONG ServiceIndex, ULONG OriginalServiceRoutine, ULONG ServiceTable)
{

	//判断是否是SSDT中的调用
	if ( ServiceTable == g_KiServiceTable && ServiceIndex <= g_SSDTServiceLimit )// 如果是正常的win32调用,则
	{
	  if (ServiceFilterInfoTable->SwitchTableForSSDT[ServiceIndex] && HookOrNot(ServiceIndex, FALSE))
	  {
		  ServiceFilterInfoTable->SavedSSDTServiceAddress[ServiceIndex]=OriginalServiceRoutine;//保存原始例程,以便后面调用
		  return ServiceFilterInfoTable->ProxySSDTServiceAddress[ServiceIndex];//返回我们代理函数的地址
	  }
	}
	//判断是否是ShadowSSDT中的调用,过程同上
	if ( ServiceTable == g_GUIServiceTable && ServiceIndex <= g_GUIServiceTableLimit )
	{
		if ( ServiceFilterInfoTable->SwitchTableForShadowSSDT[ServiceIndex] && HookOrNot(ServiceIndex, TRUE) )
		{
			ServiceFilterInfoTable->SavedShadowSSDTServiceAddress[ServiceIndex]=OriginalServiceRoutine;
			return ServiceFilterInfoTable->ProxyShadowSSDTServiceAddress[ServiceIndex];  
		}                           
	}
  
	return OriginalServiceRoutine; // 不明调用,就直接返回原始例程
}


结合上面逆出来的代码和数据结构,相信不难看懂。对于一个调用,通过判断ServiceTable确定是SSDT调用还是Shadow调用,两者过程基本一样,以SSDT为例:
首先根据ServiceIndex判断SwitchTable中的Hook开关是否打开,是则调用HookOrNot函数根据FilterFunRuleTable表中的Rule((根据PreviousMode有进一步判断))来判断是否需要Hook。经过这两重检查和判断,最终若需要Hook,就保存原始服务例程地址并返回我们的代理函数,若不需要hook,就直接返回原始例程。由于使用了良好的数据结构,这里的效率是非常高的。
KiFastCallEntryFilterFunc的返回结果,要么是原始例程,要么是代理函数,返回至Jmpstub后,这个结果被保存在了[esp+0x10]处。不要忘了刚才有个pushad,所以[esp+0x10]处实际保存的是ebx的值,这样修改了栈中的ebx的值,再popad时,ebx的值就被修改为了KiFastCallEntryFilterFunc的返回值,再一个popfd恢复刚才保存的标志寄存器,然后执行被jmp指令覆盖掉的那两句指令,最后将刚才回溯到的返回地址压栈,一个ret就又飞回到了KiFastCallEntry中。若已Hook,此时ebx的值就已经被修改了,再下来call ebx时调用的就是刚才返回的代理函数了。很巧妙的处理啊。

三、代理函数如何处理?
Hook的细节搞清楚了,本来就已经差不多了,但是看了360代理函数中的处理,又发现一些有趣的东西。
每一个代理函数,都会首先调用一个CallFilterFunByIndex,调用时传入的第一个参数就是过滤函数在过滤函数表中的索引,也就是我上一篇文章中所列出来的那些函数名称后面的索引。第二个参数则是栈中的参数数组,相当于一下把所有参数都传过去了。CallFilterFunByIndex会先根据FilterFunRuleTable->IsFilterFunFilledReady判断该表是否已经准备好(这个过滤函数表实际上是由360SelfProtect.sys调用HookPort.sys提供的接口填充的,所有过滤函数的实现也都在360SelfProtect.sys中),根据传入的过滤函数的索引在FilterFunRuleTable表(也就是我前面提到的第二张表)中查找对应的过滤函数,若过滤函数存在,就调用过滤函数,传入的参数同样有过滤函数索引和参数数组,在这个过滤函数中才真正实现了对参数的判断。
判断完毕之后,若检查通过,就予以放行,此时再调用JmpStub中保存的原始服务例程的地址,调用完原始例程之后,若调用成功,还会有一个循环的检查,检查目标是调用原始服务例程后返回的结果。这些循环检查的函数哪儿来的呢?就是在CallFilterFunByIndex调用过滤函数时返回的,由于返回的这些函数是在调用原始服务例程之后对结果进行检查,所以称之为CheckResult系列函数,而过滤函数表中的那些函数则称为CheckArguments函数。
为什么要有CheckArguments函数,又要有CheckResult函数?
这里来个小小的科普,hook某函数后,检查参数的时机是怎么样的呢?参数是分IN、OUT的,一般来说,传入的参数要在调用原始函数前检查,传出的参数要在调用原始函数后检查,而有些则在调用前后检查都可以,但效果不同。大致可以分为四种情况:
第一种以(Nt)TerminateProcess为例,它的原型是这样的:
NTSTATUS
NtTerminateProcess(
    __in_opt HANDLE ProcessHandle,
    __in NTSTATUS ExitStatus
    );
除了状态码,它没有有效的返回值,它的作用更多的在于结束进程这个“过程”,等该函数返回的时候,进程已经被结束掉了,再来检查有个P用。所以这种强调过程而且重点不在返回值(即使它有)的函数,必须要在调用原函数前检查。
第二种,一个例子是recv,原型如下:
int recv (
  SOCKET s,      
  char FAR* buf,  
  int len,        
  int flags      
);
显然该函数的重点在于第二个参数中返回的数据,但是在调用原始函数之前,缓冲区里什么都没有,检查,怎么检查?像这种函数也是过程性的,但是它的第二个参数是OUT型的,就必须在调用原函数之后检查。
第三类,比如CreateFile,OpenProcess,OpenThread,OpenEvent等函数,它们有一个共同的特征,就是传入特性相关的数据(文件名,pid等),返回一个句柄,重点不在过程而在于返回值。这类函数即可以在调用原函数前检查传入的参数,也可以在调用原函数后检查返回的句柄。但是一个是对象的名称等外在信息,一个直接指向对象,显然检查后者更为可靠一些,因为它更贴近对象本身。
对于那些异步方式调用的函数更要注意了,比如异步方式调用的ReadFile或ReadFileEx,调用原函数之前显然缓冲区没有数据,调用了原函数缓冲区也不见得有数据,但是异步有个特征就是它的通知机制,可能是Apc,也可能是Event,这时就必须替换或其它方式处理它的通知机制才能在合适的时候拿到数据。
科普就先到这儿。现在来解释为什么要有CheckResult系列函数的存在,相信大家就该明白了。以NtOpenSection这个服务的hook为例,我们知道有一个重点照顾对象叫做\Device\PhysicalMemory,经常被大家用于各种XX中,一段典型的打开该对象的代码是:
RtlInitUnicodeString(&physmemString, L"\\Device\\PhysicalMemory");

  attributes.Length             = sizeof(OBJECT_ATTRIBUTES);
  attributes.RootDirectory         = NULL;
  attributes.ObjectName           = &physmemString;
  attributes.Attributes           = 0;
  attributes.SecurityDescriptor     = NULL;
  attributes.SecurityQualityOfService   = NULL;

  status = ZwOpenSection(&g_hMPM, SECTION_MAP_READ|SECTION_MAP_WRITE, &attributes); 

怎么检查?检查第三个参数attributes.ObjectName是否是\\Device\\PhysicalMemory?显然是不行的,看看MJ的《续PhysicalMemory攻击》你就知道检查这个参数有多困难。显然NtOpenSection属于我上面提到的第三类函数,看看360是怎么做的:
代理函数Proxy_NtOpenSection首先调用CallFilterFunByIndex,CallFilterFunByIndex根据所传入的过滤函数索引在FilterFunRuleTable中找到Fake_NtOpenSection并调用之,然而Fake_NtOpenSection除了简单判断下调用者之外不做任何检查,只是返回了一个函数的地址给CallFilterFunIndex,记为CheckResult_After_NtOpenSection,CallfilterFunIndex再把这个地址返回给代理函数Proxy_NtOpenSection,此时Proxy_NtOpenSection调用原函数NtOpenSection,若返回不成功,就直接返回这个状态值,不成功当然就不管啦。若成功,就会调用刚才返回的CheckResult_After_NtOpenSection来检查返回的句柄所指向的对象是不是\Device\PhysicalMemory,若是,就关掉该句柄,并返回一个禁止的状态码(STATUS_ACCESS_DENIED),若不是,绿灯大开,放行之.有时,FakeXXX函数返回的CheckResultXXX函数可能不止一个,此时ProxyXXX会根据返回的函数个数循环调用这些CheckResultXXX,有一个不通过即为不通过,只有所有CheckResultXXX都检查通过了,ProxyXXX函数才会返回原始结果给调用者,真是把关极严啊。
值得一提的是,所有的过滤函数对于来自KernelMode的调用都不做处理,所以使用驱动来破坏360是轻而易举的,但是完全没有意义。按照MJ一贯的理念,如果你都能加载驱动了,那么所有ring0的保护也就失去了意义,再来保护完全是无用功,事实上也根本起不到保护效果了。

四、总结回顾
现在让我们站得高一点,略去一些细节,来总结一下360整个hook架构:
1.HookPort.sys准备了ServiceFilterInfoTable(上面的表一),里面保存了SSDT函数的代理函数地址、原始例程地址、Hook开关,ShadowSSDT也是。
2.HookPort.sys对外留出了三个接口(保存在了DEVICE_EXTENSION中),第一个用于准备FilterFunRuleTable(上面的表二),第二个用于向FilterFunRuleTable中注册过滤函数,第三个用于设置FilterFunRuleTable中的过滤规则。
3.HookPort.sys安装KiFastCallEntry hook.
4.HookPort.sys!KiFastCallEntryFilterFunc根据Hook开关和RuleTable中的过滤规则来决定某个系统服务是否会被Hook,被hook后将会调用Proxy函数。
5.360SelfProtect.sys使用HookPort.sys提供的接口初始化RuleTable,并向RuleTable中注册过滤函数、设置过滤规则。
6.每个Proxy函数使用CallFilterFunByIndex调用RuleTable中相应的FilterFun进行参数检查,通过后调用原函数,若调用成功再调用FilterFun函数提供的CheckResult函数检查结果(非SSDT、ShadowSSDT函数的Hook也采用同样结构)。

很显然地:
如果要增加一个系统服务的Hook,只需要打开ServiceFilterInfoTable中该服务Index对应的开关,并提供一个过滤函数就可以了。
如果要去掉一个系统服务的hook,只需要关闭ServiceFilterInfoTable中该服务Index对应的开关,立刻生效,而无须其它改变。
如果要修改一个系统服务的过滤函数,只需要使用HookPort的接口设置新的过滤函数就可以了,无须其它改变。

所以说,这个架构设计非常好,易于修改,易于扩充,易于分工,无愧于优秀二字。

结束语:单独写一个或几个函数的Hook,很多人都会,但是要实现这样一个产品级的优秀架构,就不是谁都能完成的了。分析了360这样一个安全产品所使用的Hook架构,让我收获颇多,也让大家领略了360安全卫士的技术魅力。文中如有错误,还请MJ和大家指出。

第五届安全开发者峰会(SDC 2021)10月23日上海召开!限时2.5折门票(含自助午餐1份)

收藏
点赞0
打赏
分享
最新回复 (17)
雪    币: 251
活跃值: 活跃值 (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
leftup 活跃值 2009-10-14 21:25
2
0
如果一切照你所述,判断返回值失败直接放行是个漏洞,可用多线程同步方式攻击。
有时失败仅仅是因为返回值无法写入。
雪    币: 459
活跃值: 活跃值 (54)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
qihoocom 活跃值 9 2009-10-14 22:08
3
0
分析得不错。

另外,楼上不要不经测试就YY吧,多线程同步攻击实际成功率很低,呵呵,同时需要找到一个很巧妙的点,例如NtOpenSection这个就不行。绝大部分需要返回有价值数据的函数的API,返回失败时,都不会成功返回数据,引发攻击,例如OpenProcess/Thread等,若返回失败,RING3程序就不会去close这个句柄,岂非会造成泄漏?呵呵。

当然多线程攻击可以用于在一些比较巧妙的位置,对于那些仅对输入感兴趣或主要对输入感兴趣的函数做攻击,只是成功率非常低,可能你狂开线程调API,等到花儿都谢了也没有把你BREAK出去,这跟配置还有点关系~同时你可以看看源码,微软的Nt*函数基本都考虑到了这个问题。对于第三方的安全软件,防御则不可能做到滴水不漏,与其用笨重、成功率低的多线程参数欺骗,还不如找一些更效的点。。
雪    币: 225
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Sovereign 活跃值 2009-10-14 22:31
4
0
敢问楼主到底是何方神圣?真NB。。。
雪    币: 133
活跃值: 活跃值 (17)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
kalrey 活跃值 1 2009-10-14 22:36
5
0
现在还不怎么看的懂,支持教主
雪    币: 251
活跃值: 活跃值 (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
leftup 活跃值 2009-10-14 22:50
6
0
    HANDLE Handle;
    KPROCESSOR_MODE PreviousMode;
    NTSTATUS Status;

    PAGED_CODE();
    //
    // Get previous processor mode and probe output arguments if necessary.
    //

    PreviousMode = KeGetPreviousMode();
    if (PreviousMode != KernelMode) {
        try {
            ProbeForWriteHandle(SectionHandle);
        } except (EXCEPTION_EXECUTE_HANDLER) {
            return GetExceptionCode();
        }
    }

    //
    // Open handle to the section object with the specified desired
    // access.
    //

    Status = ObOpenObjectByName (ObjectAttributes,
                                 MmSectionObjectType,
                                 PreviousMode,
                                 NULL,
                                 DesiredAccess,
                                 NULL,
                                 &Handle);

    try {
        [COLOR="sienna"]*SectionHandle = Handle;[/COLOR]
    } except (EXCEPTION_EXECUTE_HANDLER) {
        return Status;
    }

    return Status;

*SectionHandle = Handle;
有这句在肯定可以攻击,只是掐时间不太容易而已
单CPU可以这样,设置两个线程优先级为17,16,
优先级为17的先运行,yield一个时间片,给优先级16的耗时间到差不多,然后执行NtOpenSection,执行到一半,时间片完,轮到优先级17的把SectionHandle 所在的内存废掉。
然后问题只是找到Handle,而一般handle很容易找到
雪    币: 7506
活跃值: 活跃值 (298)
能力值: ( LV9,RANK:610 )
在线值:
发帖
回帖
粉丝
achillis 活跃值 15 2009-10-14 23:03
7
0
楼上这个想法很有意思,打时间差,可行性到底如何?
雪    币: 459
活跃值: 活跃值 (54)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
qihoocom 活跃值 9 2009-10-14 23:04
8
0
找handle确实可以,这点是我疏忽了。
掐时间确实不容易, 理论上可行,实际上。。。楼上可以试试,成功率非常低,可能你的程序要100%CPU运行3个月才能成功攻击。类似的攻击此前我试过一次,在我的2GHZ测试机上跑一个星期,没出结果,放弃了~~
雪    币: 23
活跃值: 活跃值 (742)
能力值: ( LV17,RANK:1820 )
在线值:
发帖
回帖
粉丝
riusksk 活跃值 41 2009-10-14 23:12
9
0
楼主实在是太强大了,只能膜拜了
雪    币: 117
活跃值: 活跃值 (23)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
xzchina 活跃值 1 2009-10-14 23:35
10
0
Shin Seiki Evangerion.
雪    币: 251
活跃值: 活跃值 (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
leftup 活跃值 2009-10-15 00:22
11
0
你的方法太善良了,也许下面这种方法不适合测试的那个函数
要增加NtOpenSection的运行时间,
只要让程序不停缺页读硬盘就行了
NtOpenSection有个指针参数ObjectAttributes
衍生出另几个指针
OBJECT_ATTRIBUTES
OBJECT_ATTRIBUTES->ObjectName
OBJECT_ATTRIBUTES->ObjectName->Buffer
SecurityDescriptor
这样读上4,5次硬盘,恐怕一个时间片也就差不多了
雪    币: 251
活跃值: 活跃值 (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
leftup 活跃值 2009-10-15 00:29
12
0
更有可能第一次从硬盘调页就重新调度线程了
雪    币: 459
活跃值: 活跃值 (54)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
qihoocom 活跃值 9 2009-10-15 02:13
13
0
无BIN无真相,再怎么YY也只是YY而已,实践你就会遇到问题了

这种攻击技术很早以前我就想过了,当时也想当然地认为成功率不会低,实际操作一下才知道是什么结果

为了让楼上信服,我写了个程序来验证楼上的猜想

在我的2GHZ双核机器上(VISTA SP2),100% CPU运行了20分钟,进行了100万次测试,结果是无法达成攻击。

楼主如果对我这个代码的实现(例如uniname 没有page out , 抱歉我不知道怎么在RING3下主动PAGE OUT 内存。。。哈哈,楼上显然想得太简单,你又要多次访问这个地址,又要它PAGE OUT,怎么可能呢,还“硬盘调度几次”。。一次都调度不了。。, 例如干扰线程的处理),或者是对我运行的时间(也许楼上觉得需要运行一个星期或一个月)有什么异议,都可以做出修改,然后自己测试一下~

以下是代码(因为VISTA上打不开物理内存对象,所以用了一个kernel32.dll的knowndlls section来代替,没什么区别):

UNICODE_STRING uniname = RTL_CONSTANT_STRING(L"\\KnownDlls\\kernel32.dll");

void Feidiaoneicun(PVOID pNeicun)
{
	ULONG oldp ;
//备注:尝试了几次修改线程优先级,没什么效果,系统调度还是比较复杂,光靠在RING3瞎猜,还是很难把握。	

	while(TRUE)
	{
		//先把内存改为不可写,这样若在写入section handle时,就会失败
		VirtualProtect(pNeicun , 0x1000 , PAGE_READONLY , &oldp);
		Sleep(0);
		//把内存改为可写,这样可过probeforwrite
		VirtualProtect(pNeicun , 0x1000 , PAGE_READWRITE , &oldp);
		Sleep(0);
	}
	return ; 
}
int main(int argc, char* argv[])
{
	HMODULE hmod = LoadLibrary("ntdll.dll");
	PVOID pOpen = GetProcAddress(hmod , "NtOpenSection");
	PVOID pQuery = GetProcAddress(hmod , "NtQueryObject");
	PHANDLE pSecHandle = (PHANDLE)malloc(0x4000);
	PVOID pNameBuff = malloc(0x1000);
	OBJECT_ATTRIBUTES oba ; 
	ULONG HandleCount ; 

	//获取初始化的句柄个数
	GetProcessHandleCount(GetCurrentProcess() , &HandleCount);
	ULONG times = 0 ; 
	

	InitializeObjectAttributes(&oba , &uniname , 0 , 0 ,0  );

	ULONG tid ; 

	//创建干扰线程
	HANDLE hThread = CreateThread(0 , 0 , (LPTHREAD_START_ROUTINE)Feidiaoneicun ,pSecHandle, 0 , &tid);
	
	CloseHandle(hThread);

	printf("init handle value = %u\n" , HandleCount);
	getchar();

	while(TRUE)
	{
		printf("try %u\n" , times);
		LONG stat ; 
		ULONG newhandlecount ;

		//调用API
		__asm
		{
			lea eax ,oba
			push eax
			push 4
			push pSecHandle
			call pOpen
			mov stat , eax
		}

		//获取新的句柄个数
		GetProcessHandleCount(GetCurrentProcess() , &newhandlecount);

		HANDLE handlevalue = *pSecHandle ; 
		printf("stat = %08x SectionHandle = %x HandleCount = %x\n" , stat , handlevalue , newhandlecount);


		//如果成功了,那么关闭打开的句柄
		if (stat == 0 )
		{
			//getchar();
		

			CloseHandle(handlevalue);
		}
		//如果不成功,但是handlecount发生了变化,那么说明有新的句柄产生
		
		//这可能是多线程攻击成功了,也可能是系统触发了一个句柄的增加
		
		//我们检查所有的句柄,看新增的里有没有我们想要的

		else if (newhandlecount != HandleCount)
		{
			ULONG i ; 
			ULONG retlen ; 
			ULONG handlequeryed = 0 ; 
			for (i = 4 ; i < 0xffffffff ; i +=4)
			{

				__asm
				{
					lea eax ,retlen 
					push eax
					push 0x1000
					push pNameBuff
					push 1
					push i 
					call pQuery
					mov stat , eax
				}
				//zwquery object 取名字
				if (stat == 0)
				{
					PUNICODE_STRING pUniName = (PUNICODE_STRING)pNameBuff;
					//存在了

					if (pUniName->Buffer && wcsnicmp(pUniName->Buffer , uniname.Buffer , uniname.Length / sizeof(WCHAR)) == 0 )
					{
						printf("get it\n");
						getchar();
					}
					
					//增加已获得句柄值
					handlequeryed ++ ;
				}
				//如果句柄不是无效(失败的可能还有object 无法query name等)
				else if (stat != 0xc0000008)
				{
					//也增加已获得句柄值
					handlequeryed ++ ; 
					
				}
				//都检查过了,break
				if (handlequeryed == newhandlecount)
				{
					break ; 
				}

			}
			//保存新的句柄值了
			HandleCount = newhandlecount ; 
		}
		times++;

	}
	return 0;
}




基本上运行结果,要么是进入API前,到检查开始过程中,内存始终是无效的,或者是进入API前,到完成API后(包括写入SECTION HANDLE),都是有效的~
雪    币: 459
活跃值: 活跃值 (54)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
qihoocom 活跃值 9 2009-10-15 03:55
14
0
顺便说一下,晚上看了刑房系列的恐怖星球,非常不错,比同系列的金刚不坏要爽。。推荐一下
雪    币: 251
活跃值: 活跃值 (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
leftup 活跃值 2009-10-15 08:11
15
0
主动pageout 当然不太可能,但是可执行文件或者Memory Mapped File是从硬盘延迟载入的,(前提是内存中没有这些内容的缓存)。
弄个较大的可执行文件,比如单独把某个数据放在一个孤立的位置,保证附近至少64k没有任何其他数据,由于Prefetch之类的因素,可能还要更大,肯定可以做到第一次访问时发生缺页。
雪    币: 459
活跃值: 活跃值 (54)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
qihoocom 活跃值 9 2009-10-15 09:09
16
0
prefetch?我还readyboost呢!你第一次调度磁盘就能轮到你执行了?以后你还能构建pageou吗?都跟你说不要再YY了。你要是有诚意,就把代码和测试结果贴出来。好歹也尊重一下我帮你的YY的劳动成果,否则就闭嘴!
雪    币: 240
活跃值: 活跃值 (10)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
三根火柴 活跃值 4 2009-10-15 09:30
17
0
文章太精彩了,期待下一篇
雪    币: 232
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lgccaa 活跃值 2009-10-15 10:09
18
0
跟着各位牛人学习
游客
登录 | 注册 方可回帖
返回