2007年1月31日星期三

Using Process Infection to Bypass Windows Software Firewalls

出处  http://www.blogdriver.com/kaby/362144.html

-[0x00] :: Table Of Contents ---------------------------------------------


  [0x01] introduction

  [0x02] how software firewalls work

  [0x03] process Infection without external .dll

  [0x04] problems of implementation

  [0x05] how to implement it

  [0x06] limits of this implementation

  [0x07] workaround: another infection method

  [0x08] conclusion

  [0x09] last words

 

  [0x0A] references

 

  [0x0B] injector source code

  [0x0C] Tiny bypass source code

  [0x0D] binaries (base64)

 

[0x01]导言

本文致力于讨论Windows操作系统下软件防火墙的"外出监测"功能。

这功能和传统防火墙理念-"阻止网络来包"没有什么瓜葛。"外出

检测"机制主要用来防止运行于本地系统中的恶意程序与外网通信,

进而防止敏感信息泄漏。通俗一些的说,"外出检测"控制本机程序

和Internet间的通信。

 

在一个充斥着特洛伊木马、蠕虫和泛滥着病毒的世界。"外出监测"

确实是一个实用而又好用的功能。然而,然而啊~~每当我提及软件

防火墙,我总是怀疑它们是否能够提供所宣称的安全级别?毕竟他们

只是一些保护你免受其他软件烦恼的软件罢了。

 

让我们切入正题:软件防火墙所谓的"外出监测"功能是可以被绕过

的,至于如何实现就是接下来本文中所要讨论的。另外可以想象:既

然某一项功能是可以被绕过的,你还认为其他的功能也安全吗?。个

人防火墙是软件,时一种控制其他软件的软件。既然个人防火墙可以

控制其他软件,当然也有软件可以控制软件防火墙了。

 

在下面的论述中我不大算只讲述抽象的概念,我将详细描述如何通过

注入代码到被信任进程,而达到绕过软件防火墙的目的的。另:本文

中的代码注入方案没有外部DLL。有的仅是独立的、小小的一个可执

行文件,相信你会感兴趣的。

 

本文和编程有关,或者说和Win32编程有关。为了能够看懂附带的例

子代码,你要熟悉Windows系统结构,API调用和基本的x86汇编。

能对PE格式和相关知识有所了解当然更好。而我的责任是将尽可能的

把每个细节描述清楚。[编者注:括号里的数字,代表引用它文,请查

阅[0x0A]的附录部分]

 

 

 

[0x02]软件防火墙工作原理

对于这个话题,我只能谈谈我所接触过的软件防火墙。它们应该已经

包括绝大多数的主流产品。而且因为软件防火墙实现机制的类似性,

下面讲到相关概念都将是普遍适用的。

 

软件防火墙提供的普通功能,其思想无非都是模仿硬件防火墙的思路:

阻塞指定端口。我对这些功能的实现并没有深入的了解,而且关于它

们的论述超出了本文的范围。

 

软件防火墙的另一项重要功能:基于不同安全级别对本地应用程序外

出通信的监测。每个二进制可执行文件都被软件防火墙提取校验值[编

者注:Checksum,具体算法不定],当某个可执行文件载入内存后的进程

需要访问网络时,软件防火墙根据保存的源文件的校验值以及用户的

设定的可信与否,来确定阻塞或允许该通信。[译者:这种方案,为的

是做到与文件名无关&&防止被信任可执行文件被更改。]

 

为了实现网络拦截功能,软件防火墙往往含有内核驱动模块用来截取

内核中的底层网络调用。而由用户来指定是否允许一个进程调用

connect()连接到Internet其他主机,或是listen()来自Internet的

连接。记住以下要点:当用户赋予可执行文件信任时,这信任是可继

承的-所有产生于该文件的进程都将获得软件防火墙的信任。也许你

要说那我们可以修改文件。实际上行不通,别忘了防火墙中保存的校

验值。

 

另一要点:在其可执行文件不该变的情况下,防火墙一直信任该进

程[译者:应该说防火墙无法、或者很难实现运行态进程的检查]。另

外:你见过不用网页浏览器和email客户端的人么?它们在目标机器

上肯定是被信任的。[译者:笑的好阴险啊]

 

 

[0x03]无外部DLL的进程注入

先前我们已经提及过进程注入。因为更改可执行文件是行不通的,所

以我们将直接操作进程、将我们的代码注入到被信任进程的内存中运

行,进而绕过防火墙的限制。

 

没听懂?让我细细道来。当储存在磁盘中的二进制可执行文件被执行

时,Windows操作系统将负责把程序载入到内存中-也就是前面说的

进程了[译者:程序保存在文件里时是可执行文件这个形态,载入到内

存之后又是进程这个形态。Just Like液体气化。]。所谓进程,就是内

存中的一砣二进制数据。当然进程不只是二进制数据,而程序的载入

也不是仅仅把文件读入内存。实际过程要复杂的多。

 

对于熟悉运行态进程注入的读者,我想说:我非常讨厌DLL注入,因

为它实在算不上是一种优美而且隐蔽的方法。[你和我一样讨厌它吗?

不然不许看我的文章了。]

 

若采取DLL实现进程注入,意味着你的程序为了绕过防火墙,必须附

带有至少一个DLL文件。不仅增大了程序的体积,而且DLL的使用很

容易在系统中留下痕迹。而且嘛~~嘿嘿,既然你打算编写一个意图

绕过软件防火墙的程序,你肯定不喜欢它这样大大的一砣。为了避免

这些缺陷,本文中的进程注入方案以不使用任何外部DLL文件为大前

提,是纯手工x86汇编打造。

 

现在,咳,各位看官注意了。现在的重点是:我们必须想办法访问到

可信进程的内存空间,然后拷贝我们的代码过去,并在那远程执行它们。

 

听起来很邪门?其实并不困难。如果你对Win32 API有了解,就知道

Bill Gates其实已经为我们准备好了一切。其中最重要的API调用是

CreateRemoteThread().

 

一下摘录自MSND(1)

  The CreateRemoteThread function creates a thread that

  runs in the address space of another process.

 

  HANDLE CreateRemoteThread(

    HANDLE hProcess,

    LPSECURITY_ATTRIBUTES lpThreadAttributes,

    DWORD dwStackSize,

    LPTHREAD_START_ROUTINE lpStartAddress,

    LPVOID lpParameter,

    DWORD dwCreationFlags,

    LPDWORD lpThreadId

  );

 

嘿嘿,它可以让我们在被信任进程的内存中执行任意的代码,我们甚

至可以为函数传递一个DWORD的实参过去。除了它我们还需要的两

个API是:

  VirtualAllocEx()

  WriteProcessMemory()

 

通过他们,我们可以注入任意代码到其他进程的内存空间中。当代码

被注入后,建立一个线程并远程执行它。

 

小结:我们将创建一个二进制可执行文件,它含有注入程序和被注入

的代码。说得再通俗一点,我们的exe文件有两个函数:一个用来实



进程注入,而另一个函数将被注入到被信任进程中。

 

[0x04]可能存在的问题

 

这个进程注入方案虽然听起来很完美,但实际并不如此。比如,使用

C你几乎不可能编写出一个能够在非本地进程中运行的函数,我保证

那个非本地进程贴定崩溃的。而且几时你实现了在C中调用相应的API,

这个高级语言还是隐瞒了太多的重要的底层细节。为什么?因为编译

器在生成汇编代码时,出于优化的目的总是尽量在指令中包含确定的

立即数、根据C代码计算出来的确定偏移进行寻址。比如说:当你调

用一个C常量字符串,它在内存中的位置是已确定的,所有对它的引

用将是直接操作该地址。这意味着:当我们的被注入函数需要传递该

字符串的地址到某个函数是,它也必须明确的知道这字符串在被注入

进程中的地址。[译者:我好像说的不太清楚。。]

 

注意,注意,还没听懂的注意了:

  void main() {

      printf("Hello World");

      return 0;

  }

 

假定字符串"Hello World"被保存可执行文件中偏移为0x28048的地方。

当程序被载入到起始地址为0x00400000的内存空间时,在这情况下,

程序指令将通过地址0x00428048操作该字符串。

 

以下是通过VC++ 6编译后的反编译代码:

   00401597 ...

   00401598 push 0x00428048  ; the hello world string

   0040159D call 0x004051e0  ; address of printf

   0040159E ...

 

那么,这问题之于我们又是什么呐?如果你的进程老实的呆在自己的内

存空间里。Ca Va。但是……当代码被移动[注入]到另一个进程的内存后,

这些立即数地址所指向的东西可就不是原来那么一回事了。"Hello World"

字符串在例子中间隔程序代码部分有0x20000[折合131072 dec]byte远。

当你把这段代码注入到另一个进程中,你必须保证在对方内存中的

0x00428048储存着的,是一个合法的C字符串或者看起来像字符串的什

么东西。但是我保证它一定不再会是什么"Hello World"了。

 

当然前面的例子很简单,出错了也不会有什么大影响。但是,真正让人

头疼的是,C中所有函数的调用,都是立即数寻址,就像前面例子中调

用printf()那样。而且在不同的进程中,函数的地址往往是不同的,有时

可能目标进程里根本没有这个函数。这将导致最!@#$%^&[译者:按偶

老大的说法,前面的乱码念cuo2]的错误。所以,为了保证操作地址的

正确性和每一条西皮优指令的正确性,我们必须接受痛苦的现实:用汇

编来编写那些被注入的代码。

 

外话:目前广泛使用的软件防火墙"外出监测"绕过的实现中,都使用

基于外部动态链接库的进程注入。这些程序都有一个可执行文件和至少

一个DLL。可执行文件强制被信任进程载入它的DLL。当DLL被载入

到可信进程的内存中,它将可以用来完成对网络的任意操作。这种实现

很完美的绕开了防火墙,而且编写起来异常简单。但是我讨厌对DLL

的依赖,于是决定编写一个只有可执行文件,并用它完成全部工作的程

序。如果你对DLL注入有兴趣,请看(2)

 

 Also, LSADUMP2 (3) uses exactly the same measure to grab

 the LSA secrets from LSASS.EXE and it is written in C.

[译者:我不知道怎么翻译。。。god help me~~~~]

 [0x05]具体实现

此前,我们的讨论还仅仅停留在理论阶段。在编写代码的过程中你将碰

到各种各样乱七八糟的问题。此外,还将面对一些和我们的目标不太

相关实现细节的问题。所以,让我们暂时放下细节问题来写一些可以工

作的代码。[编者:我强烈建议你先把A中的代码浏览下,再看这一部分]

 

我们编码的出发点:尽可能的避免使用需要立即数寻址的对象。编程所

需要做的第一件事情是得到用户默认浏览器的文件路径。我们将通过查

询注册表键值"HKCR\htmlfile\shelopen\command"而不是直接引用

"C:\Porgram Files\Internet Explorer\iexplore.exe"得到它。

 

注册表查询有必要讲吗?呵呵。下一步是调用CreateProcess()创建被信任

进程[译者:就四IE进程了]。注意:如果你打算让比人看到你在干什么,

请把把实参STARTUP_INFO中的wShowWindows置SW_HIDE。

 

[编者:如果你不想让用户看到你在干什么,必须在如何隐蔽上面花更多

功夫。比如可以弄个钩子来隐藏窗口。我的例子中只是简单使用

SW_HIDE,当和其他一大堆窗口混在一起时,它还算有效的。][译者:

窗口编程我不大了解,SW_HIDE会在任务栏上留下什么?]

 

为确保IE进程初始化完全并载入了所有必要的库。我们调用WaitForInputIdle()

给它一点时间。

 

下一步:调用VirtualAllocEx()在被信任进程中弄块内存干革命,然后用

WriteProcessMemory()把我们的被注入函数代码拷贝过去。剩下来要做

的就是CreteRemoteThread()产生我们的线程、运行了。就像你们看到的,

进程注入本身没有什么难度。

 

被注入函数,可以有一个double word类型的形参。在后面的例子代码中,

被注入函数将连接到www.phrack.com 80 并发送一个HTTP GET请求。返

回HTML头将被显示在一个消息框中。当然这只是一个简单的的例子,

所实现的只是必要的功能。

 

被注入函数的参数也必须利用起来。我们将通过它传递被注入函数在被

信任进程中的基地址。这是十分有用的。因为我们无法直接通过读取EIP

寄存器等方法拿到被注入函数在被信任进程中的位置,并且有时候我们

需要引用可信任进重中的一些东西。

 

当被注入函数被配置到远程进程中后,它实际还什么都干不了。

FirstThingFirst, 首先确定kernel32.dll在目标进程中的基地址,进而得到

GetProcAddress()函数的地址,调用它载入我们需要的一些东西。我不打

算详细讲述kernel32.dll的定位过程,它本身已经够的着是另一篇文章的

主题了。如果你对其细节感兴趣,我推荐LSD的论文(4)。我本人也引用

了他们很多东西。

 

简单的说,我们通过PEB获得kernel32的基地址,PEB可以通过TEB取

得。TEB的偏移被保存在FS寄存器中[译者:又是这个鸟FS。我一直以

为FS里面没东西的。],所以要得到PEB的偏移很简单。当知道kernel32.dll

被载入在哪儿之后,我们只需要检查他的输出部分,找到GetProcAddress()

的地址就行了。不熟悉PE格式?没什么关系。

 

一个动态链接库文件中,有一部分叫输出部分。其被输出以供调用他人

调用的函数都和他们在语义名对应起来[保存在一张表中]。实际上这个

表是个二维数组。当然,输出部分还保存着其他很多东西,远不只这么

一张表,任务无关照例不谈。顺便说下,在下面的文章中,表和数组两

个词的意义将是等价的:在这个级别上的编程,它们的差别已经没有意

义了。二维数组中的一列是标准C字符串,保存了输出函数的语义名。

第二列是函数的偏移地址。

 

我们要做的:在第一列中寻找"GetProcAddress",在相应的第二列中取出

函数的偏移地址。就是这么简单。

 

理论和实践总是有差距:[麦寇索芙特公司]近来的一种新的DLL输出方

案把事情复杂化了。这个方案代号"代理人",它允许一个DLL输出

另一个DLL中的函数[译者:我又想起了万恶的MS,iphelper->icmp发

现漏洞了就那么硬生生的剪掉。]。这种代理实例中,第一列依然是保

存着函数的语义名,而第二列的地址则是指向一个字符串。举个例子:

kernel32中的RtlAllocateHeap()被重定向到ntdll.dll中,表中第二列的偏

移不会在指向函数在kernel32.dll中的函数了,而是指向字符串"NTDLL.RtlAllocateHeap"。

 

这字符串实际上是紧靠着第一列中的函数名保存的。因此你可以在

kernel32.dll的某一部分会发现下面这块数据:

 

   48 65 61 70 41 6C 6C 6F    HeapAllo

   63 00 4E 54 44 4C 4C 2E    c.NTDLL.

   52 74 6C 41 6C 6C 6F 63    RtlAlloc

   61 74 65 48 65 61 70 00    ateHeap.

 

   = "HeapAlloc\0NTDLL.RtlAllocateHeap\0"

 

读者可能发现第一列中的字符串数量比第二维中的偏移还要更多,这确

实挺别扭。每个代理路径看起来都像一个单独的函数名。其实也没啥,

这些东西在程序里可以很容易被分辨。

 

要识别"GetProcAddress",我使用LSD小组的关于短字符串的哈西函数(4),

代码如下:

 

 unsigned long hash(const char* strData) {

   unsigned long hash = 0;

   char* tChar = (char*) strData;

   while (*tChar) hash = ((hash<<5)|(hash>>27))+*tChar++;

   return hash;

 }

 

计算出来"GetProcAddr"的哈希值是.x099C95590 。我们将用它在kerenel32.dll

的输出部分寻找相应的表项。当获得GetProcAddress()在kerenel32中的

地址之后,我们可以很方便的调用我们所需要的其他API和库了。现在

剩下要干的就是载入ws2_32.dll,调用其中的socket系统调用干我们想

干的事情。[编者:在一起建议没有看代码的,赶紧看例子代码先。]

 

 

[0x06]可能存在的缺陷

本文后附带的例子是一个运行于Ring3的可执行文件。可以肯定大多数

软件防火墙都有内核驱动部分,它们能做的当然比这个简单的进程注入

程序多的多。因此,程序应用的局限性是很大的。我以下的软件防火墙

测试过该程序,结果如下:

 

   Zone Alarm 4        vulnerable

   Zone Alarm Pro 4    vulnerable

   Sygate Pro 5.5      vulnerable

   BlackIce 3.6        vulnerable

   Tiny 5.0            immune

 

当我们的程序注入到被信任进程时,Tiny会向用户报警。但是Tiny

实质和其他软件防火墙是一样的,只不过的实现更加仔细罢了。它会

监测类似于CreateProcess()和CreateRemoteThread()的系统调用,进而

可以防止类似本文的对被信任进程注入。。

 

因此,这个方案并不是100%有效的。它仅是一个例子,一个防火墙可

能被绕过的证据。

 

[0x07]研究进展:另一种进程注入机制

关于实现进程注入的另一套方案,有兴趣的读者可以详细听我阐述相关

的工作进展。我所想证明的是:只要下功夫就可以愚弄那些所谓的软件

防火墙。

 

需要的基本API是GetThreadContext()和SetThreadContext()。它们都

有完善的文档在MSDN中可以查阅。通过这两个函数,我们可以修

改一个进程的CONTEXT。什么是CONTEXT呐?它是一个结构体,

其中保存有进程运行时的所有CPU寄存器内容。因此,通过上面的

函数,你可以获得这些寄存器中的数据、更重要的是可以修改它们。

而关于我们的重点,是寄存器EIP-其中保存了线程的指令指针。

 

首先,还是和原方案一样,我们找一个正在运行而且被信任的进程,

并注入我们的代码。但是这回我们不再从被注入代码开始处再创建

线程执行了。我们将劫持被信任进程的主线程-把它CONTEXT中

的IP指向我们的代码。

 

以上就是新方案的基本思路。实际操作中,我们必须更加小心不留

下任何蛛丝马迹。开始编码时,我们要做的第一件事情不再是写那

个进程注入函数了。我们需要先编写一个ASM代码用来在任务完

成后还原被劫持线程的CONTEXT。具体代码参看[0x0C]。我们将

向目标进程注入类似如下一砣壳代码:

 

  <base + 0x00> PUSHAD             ; safe all registers

  <base + 0x01> PUSHFD             ; safe all flags

  <base + 0x02> PUSH <base + 0x13> ; first argument: own address

  <base + 0x07> CALL <base + 0x13> ; call the injected code

  <base + 0x0C> POPFD              ; restore flags

  <base + 0x0D> POPAD              ; restore registers

  <base + 0x0E> JMP <orignal EIP>  ; "restore" original context

  <base + 0x13> ...                ; inject function starts here

 

记住,这些代码注入的位置离原程序越远越好。这也就是我们为什

么做JMP的原因。

 

总之,这是个防止注入代码运行完成后,可信进程崩溃的简单方案。

而且我打算使用一个事件对象来发送消息,当HTTP事例完成之后

由此通知主程序被注入函数的使命已经完成,主程序然后释放所申

请的可信进程中内存并且清理现场,不留下任何证据。

 

[0x0C]中的程序没有[0x0B]中的那么稳定。但是,基于第二种注入

方案的可以逃避我测试过的所有的软件防火墙[没测试过的估计也

没问题]。然而,这一切一切的假设是Internet Explorer是被信任进

程,我没有设置其他的目标。

 

我仅在Internet Explorer上实验过新的程序,其他的程序也许不允

许我们劫持其主线程。一个进程的主线程往往是可靠的:我们可以

假设当我们注入时它不会阻塞,挂起。当然了,从理论上说远程进

程的用户界面可能卡死那么一瞬间,当它在执行我们的代码而不是

其程序既定代码时。但是在我的测试中,Internet Explorer没有这个

问题。而且我们的程序和OutLook也能愉快的合作。我认为新方案

可以算是可行而且可靠的方案了。

 

 

[0x08]结论

模拟测试的结果是令我满意的。虽然和内核模式软件防火墙比较,它

只是一个小小的通用进程注入程序。但是它可以轻松的愚弄市面上80%

的主流软件防火墙。

 

我的第二套方案则可以愚弄它们所有的,可以说对任何防火墙:有效

的绕过都是有可能性的。本文中的两个例子都只是简单的发送一个HTTP

请求,但是在此基础上要实现其他的网络应用很方便的。比如发送含

有敏感信息的邮件……因为它们实现起来没有太大区别。

 

记住我只用了5K大小的程序就实现了这一切。可以想象,在RING0

中直接操作底层调用,要实现相同的目的会更加简单。也许类似的成

果已经被怀有相同目的的人所使用。但可以确信的是:软件防火墙是

不安全的[译者:硬件安全吗?]而我的观点是:这一切的原因是软件

防火墙这一概念的错误,实现并不是主要的问题。[译者:主席说的

"方向性"错误?]。

 

一个软件并不能保证你不受其他软件的危害,而它自身也可能被愚弄。

 

危险在哪里?软件防火墙在Windows操作系统中的广泛使用确实是一

个巨大的隐患。在一个网络中,同时使用软件和硬件防火墙是常有的

事。而在这种网络中,软件防火墙的作用仅在于通过"外出监测"来

防止木马程序。而这种保护也实在事太脆弱了。

 

除了蠕虫和木马,溢出一个使用软件防火墙的Windows服务器时本文

的内容同样适用。两套方案的ASM代码可以通过缓冲区溢出注入系

统服务。例子代码中是连接到www.phrack.org的80端口,但是你的

实现中可以注入代码到远程系统服务,然后把SYSTEM级的shell送

到自己的面前。

 

 

 

[0x09]结语

在此我强调,对本文章的方案和代码的滥用,编者概不承担任何责任。

本文的写作仅出于教育、传播知识的目的。不得用于非法用途。

 

在此感谢Paris2K在程序的开发及模拟测试时提供的帮助。

 

另外,感谢drew,cube,the_mystic和许多许多人,感谢jason的建议。

 

如果你有什么需要或想法,联系我:

      Email, MSN - rattle@awarenetwork.org

      ICQ - 74684282

      Website - http://www.awarenetwork.org/

 

 

[0x09+0x08/0x10]

下面的东西我就不翻了吧?呵呵……

本文文章和代码的滥用,译者概不承担任何责任。本文的翻译仅出

于教育、传播知识的目的。不得用于非法用途。

没有评论: