服务热线:010-65014696

安全研究

椒图关注 FOCUS ON JOWTO

ASP下一句话木马的动态拦截技术

[2014-11-17]

一句话木马如<%eval>,攻击者通过组织好的参数访问木马,就可以让参数中的恶意脚本在服务器端执行。为了防止执行恶意脚本,可以在IIS脚本引擎中拦截EVAL函数来过滤非法请求。

以下分析都在windows server 2003 x86下调试:
w3wp.exe是在IIS(Internet Information Server)与应用程序池相关联的一个进程,如果你有多个应用程序池,就会有对应的多个w3wp.exe的进程实例运行。当需要解析ASP脚本时会加载vbscript.dll,通过IDA搜索vbscript.dll中和“eval”有关的函数,找到

view plaincopy to clipboardprint?

01. VbsEval(VAR *,int,VAR *) .text 7337da95 

在windbg中调试发现脚本执行Eval函数时都会调用到VbsEval,见下图:
URL:

localhost/test.asp?MH=1*2

test.asp:


windbg:

image001

在内存中可以看到eval函数执行的脚本。现在只需要将此脚本与URL中请求参数内容对比,如果内存中执行的EVAL脚本在URL中存在则可以认为是URL请求试图调用一句话木马并执行。如上图截取的底层执行代码为1*2,判断1*2是否在localhost/test.asp?MH=1*2中存在,如果存在则直接返回恶意代码提示,最终效果如下图:

image003

HOOK住vbscript!VbsEval很容易截取到要执行的脚本,现在的问题是任何脚本执行eval都要调用vbseval函数,那么怎么才能找到当前执行脚本是由哪个URL触发的呢?如果无法取到URL则不能与EVAL中执行脚本对比,过滤是无从谈起的。

现在从vbseval函数往上看整个脚本解释执行的流程!

image005

下asp!CViperAsyncRequest::OnCall断点,堆栈回溯如下:

image007

可以看出脚本执行eval函数是通过启动线程调用回调函数来触发的。但是下新建线程的API都断不下来,需要换个思路。通过上图可以看到线程启动后最先执行函数是asp.dll里的asp!CViperAsyncRequest::OnCall,查看asp.dll的导出函数如下图:

image009

通过这3个导出函数可以判断asp.dll是ISAPI中的ISA。ISA简介如下:

ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应
,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。

ISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何
ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。
ISA大概工作流程如下:

1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服
务器版本,并与自己的版本相比较,以保证版本兼容。

2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中
根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(
Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供
的几个回调函数的入口地址。函数原型如下:

view plaincopy to clipboardprint?


  1. DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB )  

ECB的结构定义如下(IN表示入口参数,OUT表示出口参数):

view plaincopy to clipboardprint?

  1. typedef struct _EXTENSION_CONTROL_BLOCK   

  2. {  

  3.    DWORD     cbSize;        //IN,本结构的大小,只读  

  4.    DWORD     dwVersion      //IN,版本号,高16位为主版本号,低16位为次版本号  

  5.    HCONN     ConnID;        //IN,连接句柄,由服务器分配,ISA只能读取该值  

  6.    DWORD     dwHttpStatusCode;                 //OUT,当前完成的事务状态  

  7.    CHAR      lpszLogData[HSE_LOG_BUFFER_LEN];  //OUT,需要写入到日志文件中的内容  

  8.    LPSTR     lpszMethod;    //IN,等价于CGI的环境变量REQUEST_METHOD  

  9.    LPSTR     lpszQueryString;                  //IN,等价于环境变量QUERY_STRING  

  10.    LPSTR     lpszPathInfo;                     //IN,等价于环境变量PATH_INFO  

  11.    LPSTR     lpszPathTranslated;               //IN,等价于环境变量PATH_TRANSLATED  

  12.    DWORD     cbTotalBytes;                     //IN,等价于环境变量CONTENT_LENGTH  

  13.    DWORD     cbAvailable;                      //IN,缓冲区中的可用字节数  

  14.    LPBYTE    lpbData;                          //IN,缓冲区指针,指向客户端发来的数据  

  15.    LPSTR     lpszContentType;                  //IN,等价于环境变量CONTENT_TYPE  

  16.   

  17. //回调函数,用于返回服务器的连接信息或特定的服务器详细情况  

  18.    BOOL ( WINAPI * GetServerVariable )   

  19.       ( HCONN       hConn,  

  20.        LPSTR       lpszVariableName,  

  21.        LPVOID      lpvBuffer,  

  22.        LPDWORD     lpdwSize );  

  23.   

  24.    BOOL ( WINAPI * WriteClient )      //回调函数,从客户端的HTTP请求中读取数据  

  25.       ( HCONN      ConnID,  

  26.       LPVOID     Buffer,  

  27.       LPDWORD    lpdwBytes,  

  28.       DWORD      dwReserved );  

  29.   

  30.    BOOL ( WINAPI * ReadClient )       //回调函数,向客户端发送数据  

  31.       ( HCONN      ConnID,  

  32.       LPVOID     lpvBuffer,  

  33.       LPDWORD    lpdwSize );  

  34.   

  35.    BOOL ( WINAPI * ServerSupportFunction )  //回调函数,访问服务器的一般和特定功能  

  36.       ( HCONN      hConn,  

  37.       DWORD      dwHSERRequest,  

  38.       LPVOID     lpvBuffer,  

  39.       LPDWORD    lpdwSize,  

  40.       LPDWORD    lpdwDataType );  

  41.   

  42. } EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;  

在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给
ISA调用,从而使ISA可以获得更详尽的信息。

上述理论看完后直接上调试器看更清楚,每一次URL请求都会经过HttpExtentionProc函数,因此下HttpExtentionProc断点,查看某一次请求中ECB的结构,如下:

image011

从上图可以看出ECB中包含请求参数及脚本的物理路径,识别一句话木马时需要这两项内容。怎么才能在vbscript!VbsEval函数中取到这个ECB的结构。
跟着Httpexecutionproc函数往下看

image013

此时V2的地址是2721e90,内存数据如下:

image015

在windbg里下vbseval断点,F5继续走断在vbseval,

70dc75c7 8b4624          mov     eax,dword ptr [esi+24h] ds:0023:026a1eb4=02721e90

70dc75d4 8945fc          mov     dword ptr [ebp-4],eax ss:0023:0130fd88=00000000

image017

堆栈回溯中可以看到在Httpexecutionproc中封装的类基址2721e90在函数asp!CViperAsyncRequest::OnCall中被用到,下asp!CViperAsyncRequest::OnCall断点继续往后跟。

  1. ChildEBP RetAddr    

  2. 0309f560 73333f1c vbscript!VbsEval  

  3. 0309f574 733365d7 vbscript!StaticEntryPoint::Call+0x11  

  4. 0309f848 733333e0 vbscript!CScriptRuntime::Run+0x1f08  

  5. 0309f940 733337d1 vbscript!CScriptEntryPoint::Call+0x5c  

  6. 0309f998 73333b9c vbscript!CSession::Execute+0xb4  

  7. 0309f9e8 73331849 vbscript!COleScript::ExecutePendingScripts+0x13e  

  8. 0309fa04 70dc2ada vbscript!COleScript::SetScriptState+0x150  

  9. 0309fa30 70dc2a9c asp!CActiveScriptEngine::TryCall+0x19  

  10. 0309fa6c 70dc26d0 asp!CActiveScriptEngine::Call+0x31  

  11. 0309fa88 70dc25d4 asp!CallScriptFunctionOfEngine+0x5b  

  12. 0309fadc 70dc24ff asp!ExecuteRequest+0x17e  

  13. 0309fb44 70dc23f7 asp!Execute+0x249  

  14. 0309fb98 70dc2753 asp!CHitObj::ViperAsyncCallback+0x3f3  

  15. 0309fbb4 4a1db5ea asp!CViperAsyncRequest::OnCall+0x92  ;脚本引擎函数执行起始位置  

  16. 0309fbd0 77560d30 comsvcs!CSTAActivityWork::STAActivityWorkHelper+0x32  

  17. 0309fc1c 775617dc ole32!EnterForCallback+0xc4  

  18. 0309fd7c 775303b4 ole32!SwitchForCallback+0x1a3  

  19. 0309fda8 774dc194 ole32!PerformCallback+0x54  

  20. 0309fe40 7756433a ole32!CObjectContext::InternalContextCallback+0x159  

  21. 0309fe60 4a1db78c ole32!CObjectContext::DoCallback+0x1c  

将以上函数设置断点跟踪2721e90位置,最后得到vbseval函数时ECB结构的地址为:

dda poi(poi(poi(poi(poi(poi(ebp+8)+5c)+5c)+18)+24)+4)

image019

只需要将VbsEval函数HOOK住就可以截取到请求参数内容。

通过调试找到了想要的数据后就可以动手编码实现拦截框架了。首先要找到vbseval函数的地址,通过特征码定位是个不错的选择,但是在vbseval函数体内并没有合适的特征码,如下图:

image021

下面是sub_7337D95F函数:

image023

Compile函数原型为:

view plaincopy to clipboardprint?

  1. unsigned int __thiscall COleScript::Compile(void *this, int a2, const OLECHAR *psz, int a4, int a5, UINT len, int a7, int a8, int a9, int a10)  

通过调试找到Compile函数内部获取ECB及Eval内容的地址,如下:
ECB数据地址:

  1. dd poi(poi(poi(poi(ecx+5c)+18)+24)+4)  

image026

数据地址找完后剩下的就是编码实现拦截,大致思路是创建自己的ISAPI,在加载时将vbscript.dll提前加载到进程(这样可以加载完vbscript.dll后HOOK住vbseval函数,默认情况下vbscript.dll只有在访问asp页面时才会加载)。然后通过在整个内存中查找“eval code”来定位COlescript!Compile函数地址,将找到的函数地址HOOK住,在新函数中使用上面分析的地址获得相应的值。因为调用COlescript!Compile的函数不止vbseval一个,但是可以通过”eval code”来判断,只有vbseval函数会调用COlescript!Compile函数传递”eval code”字符串变量。因此先判断调用COlescript!Compile传递的参数是不是eval code,如果是则继续判断ECB结构中的请求参数(本例中“MH=1*2”)字符串是否包含VbsEval函数中要执行的脚本(本例中“1*2”),如果包含了则是非法操作,调用ECB中回调函数writeclient向浏览器输出“URL中包含EVAL脚本”等信息,最后要中断COlescript!Compile函数以防止脚本引擎继续执行恶意脚本。中断执行脚本可以将执行的脚本如”1*2″修改为”1″,这样函数可以继续执行而不用担心恶意脚本被执行。
需要注意的是,在将vbseval欲执行的恶意脚本内容修改成“1”后,当其它请求同一asp页面并且参数合法的情况下,vbseval函数执行的脚本仍是“1”,可能IIS在加载页面时就将脚本缓存在内存中,因此我们将缓存的页面脚本强制修改成“1”,后面再请求同一页面时会继续执行缓存中的脚本,解决办法是在HOOK函数结尾将asp文件中追加空格等无效字符串,这样IIS去动态更新脚本缓存。

另:x64平台下iis启动的都是32位的w3wp.exe,因此此过滤URL方法在x86、x64平台均可使用。

最后效果图:
非法情况:

image031

正常情况:

image033