本帖最后由 Kali-J 于 2016-11-16 20:39 编辑
一个老漏洞了,主要是最近老师让查找这个漏洞的相关资料,我一搜还没有介绍漏洞具体成因的。索性自己分析了一下,比较入门,大牛就不要看啦。
从主函数入手分析
从TEB结构的偏移0x30处获得PEB的位置。
获取当前进程PID
得到PsLookuoProcessByProcessID函数的入口地址,并赋给函数指针g_PsLookupProcessByProcessIdPtr。下面进入GetPsLookupProcessByProcessId()函数内部。
得到内核中已加载的第一个模块在磁盘中的全路径。经过动态调试可知这个模块是ntoskrnl.exe
得到进程中加载的ntoskrnl.exe模块的优先加载地址,又因为此时的这个ntoskrnl.exe是在系统启动中在内核模式下记载到内存中的,所以此时的加载地址应该在内核内存空间中。经过调试观察KernelBase变量的值可以验证。
函数PsLookuoProcessByProcessID就在这个ntoskrnl.exe中。但此时ntoskrnl.exe在内核地址空间中,程序此时在用户态无法访问这一空间,就无法得到PsLookuoProcessByProcessID在ntoskrnl.exe中的RAV和在内存中的地址,就无法进行调用。我们可以在用户态加载ntoskrnl.exe,这样就可以访问ntoskrnl.exe内部函数了。
用户态加载ntoskrnl.exe,返回模块句柄到MappedKernel中,也就是ntoskrnl.exe模块加载到用户态内存地址空间的地址。
GetProcAddress函数在MappedKernel指向的模块句柄中通过关键词PsLookupProcessByProcessId搜索此函数,并返回该函数在用户态内存中的地址。通过调试可看出
FuncAddress=0x00000001403440e4;MappedKernel=0x0000000140000000。则MappedKernel-FuncAddress=0x3440e4就是PsLookupProcessByProcessId函数在ntoskrnl.exe中的RAV。再加上KernelBase就是该函数在内核态地址空间的位置。 既内核态FuncAddress = KernelBase +(用户)FuncAddress - (ULONG_PTR)MappedKernel;如下图
GetPsLookupProcessByProcessId()函数最终返回这个FuncAddress值。并回到主函数中。
创建并注册一个窗口类,其中lpfnWnProc是这个窗口的过程处理函数,是MainWindowProc
PEB->KernelCallbackTable是PEB的回调函数表,在这里的索引是0x36,即指向了User32.dll中的ClientCopyImage函数。g_originalCCI= InterlockedExchangePointer(g_ppCCI, &hookCCI);这行代码是关键,Hook了ClientCopyImage函数,替换为hookCCI函数。g_ppCCI原来的值(就是原本的ClientCopyImage函数)返回给了g_originalCCI,g_ppCCI的新值是攻击的函数地址。
这行代码根据刚才注册的窗口类class_atom创建了Window对象,xxxCreateWindows在分析一系列参数后,会使用HMAllocateObject分配一个窗口类型的win32对象, 并为其填充一些参数,如窗口的Class(类)对象等,里面有这样的一个逻辑过程 [C] 纯文本查看 复制代码 1 2 3 4 5 | if ( pcls->spicn &&!pcls->spicnSm ) {
CreateClassSmIcon(pcls);
}
pwnd->hModule = hMoudle;
pwnd->lpfnWndProc =MapClientNeuterToClientPfn(pcls, 0, bansi);
|
这里CreateClassSmIcon的目的是为该窗口类的图标创建小图标缓存,接下来,系统会通过MapClientNeuterToClientPfn根据窗口类为窗口设置WindowProc。这个CreateClassSmIcon函数是通过KeUserModeCallback来实现的,这个调用最终是配合用户模式回调来实现的。KeUserModeCallback实际最后会调用放置在PEB->KernelCallbackTable中的对应函数来实现功能的,而这些函数都是最终实现在用户模式的,例如这里就将最终调用user32.dll中的ClientCopyImage函数来实现。而这个函数在之前就被Hook了,实际执行函数是hookCCI所指向的函数,如下图所示:
hookCCI函数先把正常的ClientCopyImage地址重新存入g_ppCCI中,然后调用SetWindowLongPtr函数,然后再调用g_originalCCI也就是ClientCopyImage函数,完成功能,这样就在正常执行ClientCopyImage函数之前先执行了SetWindowLongPtr函数。 通过查找相关资料可知,SetWindowLongPtr是设置窗口相关数据、属性的函数,这里GWLP_WNDPROC这个功能索引(index)的作用是对窗口进行子类化(subclass)/去子类化(unsubclass),可以通过子类化,替换窗口的调用过程为自己的函数,来接管窗口的一些处理,也可以通过设置为DefWindowProc来去子类化,取消接管过程。 这里面GetFirestThreadHWND是一个获得当前正在被创建的窗口句柄的一个技巧,因为现在CreateWindowEx正在被中断在内核过程中,仅仅通过用户模式的代码和CreateClassSmIcon的信息,是无法得知当前正在被创建的窗口对象/句柄的。 但是在Win32k内核中,所有的内核窗口信息是全部被映射到用户模式的一块内存地址上的,通过user32!gSharedInfo可以得到它的地址(是内核模式窗口信息列表的一个只读映射),而刚才说过内核窗口对象在中断时已经经由HMAllocateObject被创建了,那么它实际就已经可以在gSharedInfo中检索到。 这里代码使用SetWindowLongPtr将当前线程正在创建的窗口的WindowProc替换为了DefWindowProc。SetWindowLongPtr,当index(GWLP_WNDPROC(-4) ) <0,会调用SetWindowData来完成最终的设置。SetWindowData在判断到index是GWLP_WNDPROC时,会执行如下逻辑:
[C++] 纯文本查看 复制代码 1 2 3 4 5 6 | ptr = MapClientToServerPfn(dwData);
if ( ptr ) {
ClrWF(pwn,WFANSIPROC);
SetWF(pwn, WFSERVERSIDEPROC);
pwn->lpfnWndProc=ptr;
}
|
这里的逻辑,是检查此处GWLP_WNDPROC是不是一个去子类化操作(unsubclass),如果是的话,就认为这里需要设置为内核来接管窗口过程,给窗口设置Server Side Proc的标志,这个标志的含义是窗口的窗口过程函数将在内核模式下调用。同时将窗口过程函数修改为DefWindowProc对应的内核处理函数。 从CreateClassSmIcon返回,继续调用MapClientNeuterToClientPfn转化当前窗口类函数的默认WindowProc(也就是用户模式可控的函数),再将窗口对象的WindowProc设置为用户自己的窗口对象 因为这个中断过程恰好在CreateWindowProc为窗口设置WindowProc前面,所以SetWindowData修改窗口的WindowProc为DefWindowProc是无效的,窗口的WindowProc还是被修改为用户模式应用程序设置的WindowProc,窗口过程处理函数也变成了MainWindowProc。而此时,这个窗口的标志已经被设置为是需要在内核模式执行WindowProc,那么接下来再遇到SendMessage等函数对这个窗口发送消息时,就会在内核模式下直接跳转、调用实际在用户模式的函数来进行处理,从而直接导致内核模式代码执行。MainWindowProc函数如下图所示
此时就可以在内核态执行StealProcessToken();获得内核态最高权限System的安全令牌就可以获得具有System权限的Cmd了。
|