利用远程线程技术实现木马程序的隐藏早已不是什么新技术了。线程插入的实现方法,网络上也有很多介绍,不是本文的重点。本文主要针对Jeffrey Richter大师的讲述的这种方法,做一些思考和总结,目的是深入理解windows进程控制等相关的知识。作为习作,本文也会附上C语言实现的代码。文章的后一半,会着重介绍一下我实现的dll后门小程序,它的主要功能是把实现远程进程访问重定向到CMD shell,实现远程的控制。
首先谈一谈木马。有过一些“杀马”经验的人都知道,木马达到侵害的目的,一定要在受主主机上不为人知的“秘密”运行,而运行一定会留下痕迹,不管是以什么方式(若要人不知,除非己莫为)。以往的观察方式,往往是观察系统中的可疑进程,但是如果一个木马的运行不需要启动一个进程,又该怎么办。防火墙,往往是人们防范木马后门的常见方法,通过关闭一些端口,可以达到一定的防范目的。但是,端口就好像是一户人家的窗子,总要开窗通风的,否则还要窗子干什么?也不见得怕偷防贼,就门窗紧闭吧。所以防火墙的端口控制,只能是在一定程度上有效,比如说关闭一些已知木马的常用端口(但是人家就不会换端口么?)。目前的软件防火墙,大都采用了应用程序过滤功能,禁止某些应用程序使用网络,同时也允许某些应用程序毫无阻拦的穿梭防火墙。从应用角度来说,这为用户的应用,如Ftp程序,提供了方便,但是也为木马留下了生存的空间。实际上,很多dll木马就是通过“依附”在一些必要的系统进程中,从而实现了穿越防火墙。
插入Dll木马有很多种方法,静态插入的方法(rundll32)等有一点过时, 特洛伊DLL也有一点不太实用。比较流行的技术就是动态的插入一个正在运行的进程中去。
听起来,这种技术很了不起,“黑客们的技术让微软无可奈何?”其实不是,这些技术的创造者正是微软的程序员们。
先谈谈windows的进程管理。对于windows核心来说,一个进程和文件、管道等一样,都是一个内核对象。内核负责对这些对象的控制和管理,应用程序只能通过系统提供的API来实现对这些对象的控制。一个进程会占用一些资源,微软为了保证系统的稳定性和安全性,使得一个进程不得任意访问另外一个进程的资源(当然不是不能访问),也不能随意控制其它的进程,如果是这样,那么系统的安全性可以得到很大的提高,但是从应用角度来看,却存在很多的不便,比如调试程序就没有办法运行。实际上,是微软的程序员自己提供了一些灵活的方法,使得进程之间的通信和控制变得更加容易。而所谓的黑客,只不过是简单的应用这些程序员提供的方法来达到自己的目的而已,其实没有什么技术含量可言。
虽然如此,对于“技术”的学习是学习技术“本身”,研究这些方法的原理,对于理解windows系统和编程都很有好处。那么下面就讨论一下windows的远程线程插入技术,原理和实现。
归根结底,这种方法,就是利用了两个API函数CreateRemoteThread和LoadLibrary。这个函数实现的功能是在一个进程中,为另外一个进程创建一个线程。关于进程和线程的概念,操作系统都有讲,在windows系统中也是大同小异。这里面一个重要的概念就是进程的地址空间。对于一个32位平台的系统,每一个进程拥有4G的地址空间,但是这不意味着每个进程有4G的内存空间。也就是说,进程所拥有的4G的地址空间,是虚拟的,独立的。它有,意思是说他可以有不是真的有。那么进程的地址空间和真实的内存空间是什么关系呢,进程的地址空间之间又是什么关系呢?事实上,对于一个进程来说,虽然它拥有4G的地址空间,但是不是随便一个什么地址都可以拿来使用,windows内核对这4G的空间有预先的分配规则,同样,进程只有把它的地址空间预先“保留”下来,并且为之分配内存空间,才可以真正的使用这块内存。而这个过程的实现,是通过调用函数VirtualAlloc来实现的。这个函数既可以用于保留虚拟的地址空间,又可以为这块空间分配一块真实的物理内存。只有这样,进程才可以使用这个空间。至于进程地址空间之间的关系,我认为一个小例子可以很好的说明这个问题。一个进程在自己的地址空间中,分配了一块空间,用指针p表示这个空间的首地址,可是在另一进程中指针P的值却毫无意义,该进程无法识别这个地址,事实上,在这个进程中,有可能这个地址还没有被分配。Windows线程没有自己的资源,他们共用自己所属的进程的资源。实际上,每一个进程至少有一个主线程,每一个线程在进程空间中占有一块空间,作为线程的堆栈。
还是回过来讨论CreateRemoteThread这个函数。函数的原形是
HANDLE CreateRemoteThread (
HANDLE hProcess,
PSECURITY_ATTRIBUTES psa,
DWORD dwStackSize,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadId);
简单查一下msdn,就知道这些参数的意思了。还是简述一下,因为后面要讨论这些参数的使用和得到这些参数值得一些方法和思路。hProcess是要插入DLL的目标进程,psa是线程安全性的描述符结构体,dwStackSize是初始线程堆栈的大小,pfnStartAddr比较重要,是远程线程hProcess的一个函数的地址,这个函数作为线程的起始入口点存在。后面几个参数分别是给函数传递的参数,创建线程的一些flag,和用于函数输出的线程的ID。我们就是想通过调用这个函数,在远程的进程中添加一个线程,并且让这个线程去做我们想要它实现的功能。
可以想一下,如果要一个已有的进程中的一个线程去实现我们自己定义的功能,必须要它加载我们的程序代码。也就是我们要把我们的代码,添加到线程的起始入口点函数pfnStartAddr中去。而这个函数必须是在远程进程中已经存在的。这一点很难实现,所以我们使用为远程进程添加dll的方法来实现的。这就用到了LoadLibrary函数。
怎么样得到远程的HANDLE,hProcess呢?关于HANDLE,还要在多废话几句。句柄,是进程相关的,只不过是进程中一个句柄表的索引(或是唯一表识的标志)。既然句柄不是进程的唯一标示,那么怎么找到一个目标进程呢?答案是PID,Pid是系统全局唯一的。可以简单的以PID为参数调用OpenProcess,得到远程进程的HANDLE。
接下来的问题就是怎样得到启动函数的地址。我们要使用的函数是LoadLibrary,它的函数地址是在kernel32.dll中。每一个调用kernel32.dll都回把kernel32.dll的image映射到自己的地址空间。我们要得到的是目标进程中的kernel32中的一个函数的地址,而这个地址和本进程的地址空间毫无关系。很幸运的是,我们可以通过察看自己进程中kernel32中的LoadLibrary的地址,来“猜测”目标进程的地址。事实上,这样的猜测往往是准确的。因为,windows总是把同一个dll加载到不同进程的地址空间中的相同地址。而在同一个dll的image中的函数相对地址是固定的。但是不可以直接以函数名LoadLibrary作为地址。原因是模块的输入节会调用形实替换程序,并用替换程序的地址代替LoadLibrary的地址,和预期的结构不符。一个合理的做法是,调用GetModuleHandle函数得到kernel32的句柄,再用这个句柄和函数名LoadLibrary(实际上调用的是LoadLibraryA)调用GetProcAddress函数,得到LoadLibrary函数的地址,作为参数传递给CreateRemoteThread。
LoadLibrary需要一个参数,就是要加载的dll的路径名。问题是怎样把它传递给远程的进程。我们知道在本进程中的字符串的地址都是在本进程的地址空间中的,当远程进程启动LoadLibrary函数试图访问这个字符串的地址时,必然造成访问失败。解决问题的方法就是调用VirtualAllocEx在远程的进程中分配一个空间,再用WriteProcessMemory函数写入dll的路径名,把这个地址作为参数传递给CreateRemoteThread。其实,从原理上讲,如果仅仅为了达到传递字符串地址这个目的的话,完全可以用进程间通信的其他方法,但是考虑到目标进程在这样的应用环境下完全是“无知”的,不可能和我们的进程共享任何信息。所以这种方法是不可行的。
还剩下一个问题,回到前面,怎么得到进程的pid。一个很简单的方法是通过进程管理器。比较傻的方法,因为你不可能预先确定一个进程的pid(除非是系统进程),它们每次启动电脑都回更新。得到pid的方法也比较简单,可以通过调用CreateToolhelp32Snapshot得到一个Snapshot的HANDLE。然后通过Process32First,Process32Next函数来遍历所有的进程,比较进程名称(PROCESSENTRY32结构的szExeFile)和你要插入的进程的名称。如果一样,返回该进程的PID(th32ProcessID)。在我的代码中,我把目标进程定位一些需要使用网络的程序,最好是系统程序,比如svchost.exe。
这样,在DLL注入的过程中可能遇到的问题就都解决了。我用这个原理实现了dll木马的起动器程序。代码列在文章后面。虽然它很简单。
下面我会简单介绍一下我写的dll木马程序。
作为一个Dll木马,它一定不能简单的提供一些API供其他的应用程序调用。那么一个DLL木马是怎样运行的呢。回到刚才的CreateRemoteThread,它实际上是启动一个远程线程,然后让这个线程去执行kernel32.dll镜像中的LoadLibrary来加载一个dll。加载以后的dll会由runtime库来执行一个入口点函数DllMain,这个函数可以自己在DLL中实现,也可以让runtime使用它自己定义的默认函数。现在我们要在dll中实现自己的代码,当然要自己实现DllMain了。
我本来的想法是简单的在DllMain中实现一个监听socket端口的程序,但是很遗憾不可以这样做。原因很简单,因为DllMain只是用来实现dll的初始化工作的,如果在这里面调用其它的dll,比如socket要用到的ws2_32.dll,可能会发生循环等待。在实际的编程过程中,无论我用这种方法插入什么进程中,都会造成那个进程的崩溃。
为了解决这个问题,我在DllMain中调用了CreateThread来启动一个新的线程,而这个线程的执行函数,是我在dll中定义的一个堆栈函数,在这个函数中,我实现了端口的监听等工作。这样,可以不被察觉的把我的dll插入到目标进程中了。
作为一个木马,至少作为一个后门,它总得有点作用,不然光监听端口有什么用?于是我在这个堆栈函数中实现了一些必要的功能。首先是一个基本的基于TCP的端口监听(是不是阻塞的这里是不重要的,反正只是一个后台进程)。Socket编程就不多说了,大家都会,有一点注意的要调用setsockopt函数,设置参数SO_REUSEADDR来带到可重新绑定的目的。好像这个东东用处不大,但是至少在调试程序的时候我受益良多。接着就是服务器端accept了,受到一个连接请求后,CreateThread创建一个新的线程处理这个连接,“主线程”(只是我的木马的主线程)继续监听端口。
下面就是这个木马的功能实现了,我比较懒,也没有时间写很多复杂的功能。不如就搞出一个cmd shell来的爽。反正我自己搞肉鸡的时候有一个cmd shell我就觉得很舒服了。而且这个shell可以完成很多后续的工作,个人感觉比木马实现的有限功能实用的多。
现在的状态是一个新开启的线程来处理TCP连接请求。为了让远程进程使用本地的shell,我必须要完成的是:1、我的线程和远程的进程实现数据通信(很简单的网络编程);2、我的线程和一个CMD进程实现数据通信。第二个目标其实就是进程间通信,而且还是比较简单的那一种——可以在我的线程中CreateProcess,来生成一个子进程CMD——父子进程通信可以用无名管道。原理上很简单,在linux上很容易搞,windows上也不太难。因为管道是单向的,首先生成两个无名管道。CreatePipe的参数里面,SECURITY_ATTRIBUTES结构体要设置一下,允许管道句柄被子进程继承。然后调用CreateProcess,参数中可以实现新进程的标准输入输出和标准错误的重定向,这里就重定向到每个管道的一端的句柄中。注意,这里创建的子进程要被允许继承父进程中可继承的句柄。否则重定向的输入输出的句柄在新的进程中没有意义。
进程创建好了,线程下面的工作就是在客户端和CMD进程中提供数据的转接。没什么技术含量了,纯粹编程技巧。
写到这里,废话写了一堆,应该算写完了。作为一个木马,应该可以自启动。我没有做。主要是这一块的技术手段太多,而我只会写注册表,太土了点。以后有时间看看书再把这一块补上。
Trackback: http://tb.donews.net/TrackBack.aspx?PostId=692547