Molet

网络安全编程:远程线程编程

Molet 安全防护 2022-12-05 359浏览 0

网络安全编程:远程线程编程

Windows操作系统下,为了避免各个进程相互影响,每个进程地址空间都是被隔离的。所谓 “远程线程”,并不是跨计算机的,而是跨进程的。简单来说,就是进程A要在进程B中创建一个线程,这就叫远程线程。

远程线程被木马、外挂等程序广泛使用,反病毒软件中也离不开远程线程的技术。技术应用的两面性取决于自己的个人行为意识,良性的技术学习对自己的人生发展是非常有好处的,就算谈不上好处,至少不会给自己带来不必要的麻烦。

关于远程线程的知识,本文介绍3个例子,分别是DLL的注入、卸载远程DLL和不依赖DLL进行代码注入。

1. DLL远程注入

木马或病毒编写的好坏取决于其隐藏的程度,而不在于其功能的多少。无论是木马还是病毒,都是可执行程序。如果它们是EXE文件的话,那么在运行时必定会产生一个进程,就很容易被发现。为了不被发现,在编写木马或病毒时可以选择将其编写为DLL文件。DLL文件的运行不会单独创建一个进程,它的运行被加载到进程的地址空间中,因此其隐蔽性相对较好。DLL文件如果不被进程加载又如何在进程的地址空间中运行呢?方式是强制让某进程加载DLL文件到其地址空间中去,这个强制的手段就是现在要介绍的远程线程。

创建远程线程的函数CreateRemoteThread()的定义如下:

HANDLECreateRemoteThread(
HANDLEhProcess,
LPSECURITY_ATTRIBUTESlpThreadAttributes,
DWORDdwStackSize,
LPTHREAD_START_ROUTINElpStartAddress,
LPVOIDlpParameter,
DWORDdwCreationFlags,
LPDWORDlpThreadId
);

该函数的功能是创建一个远程的线程。我们把CreateThread()函数和CreateRemoteThread()函数进行比较。对于CreateThread()函数来说,CreateRem oteThread()函数比其多了一个hProcess参数,该参数是指定要创建线程的进程句柄。其实CreateThread()函数的内容实现就是依赖于CreateRemoteThread()函数来完成的。CreateThread()函数的代码实现如下:

/*
*@implemented
*/
HANDLE
WINAPI
CreateThread(LPSECURITY_ATTRIBUTESlpThreadAttributes,
DWORDdwStackSize,
LPTHREAD_START_ROUTINElpStartAddress,
LPVOIDlpParameter,
DWORDdwCreationFlags,
LPDWORDlpThreadId)
{
/*创建远程线程
returnCreateRemoteThread(NtCurrentProcess(),
lpThreadAttributes,
dwStackSize,
lpStartAddress,
lpParameter,
dwCreationFlags,
lpThreadId);
}

在上面的代码中,NtGetCurrentProcess()函数的功能是获得当前进程的句柄。

CreateRemoteThread()函数是给其他进程创建线程使用的,其第一个参数是指定某进程的句柄,获取进程的句柄使用API函数OpenProcess(),该函数需要提供PID作为参数。

除了hProcess参数以外,剩余的关键参数就只有lpStartAddress和lpParameter两个了。lpStartAddress指定线程函数的地址,lpParameter指定传递给线程函数的参数。前面提到,每个进程的地址空间是隔离的,那么新创建的线程函数的地址也应该在目标进程中,而不应该在调用CreateRemoteThread()函数的进程中。同样,传递给线程函数的参数也应该在目标进程中。

如何让线程函数的地址在目标进程中呢?如何让线程函数的参数也可以传递到目标进程中呢?在讨论这个问题以前,先来考虑线程函数要完成的功能。这里主要完成的功能是注入一个DLL文件到目标进程中,那么线程函数的功能就是加载DLL文件。加载DLL文件使用的是LoadLibrary()函数。LoadLibrary()函数的定义:

HMODULELoadLibrary(
LPCTSTRlpFileName
);
看一下线程函数的定义格式,具体如下:
DWORDWINAPIThreadProc(
LPVOIDlpParameter
);

比较两个函数可以发现,除了函数的返回值类型和参数类型以外,其函数格式是相同的。这里只考虑其相同的部分。因为其函数的格式相同,首先调用约定相同,都是WINAPI(也就是__stdcall方式);其次函数个数相同,都只有一个。那么,可以直接把LoadLibrary()函数作为线程函数创建到指定的进程中。LoadLibrary()的参数是欲加载的DLL文件的完整路径,只要在CreateRemoteThread()函数中赋值一个指向DLL文件完整路径的指针给LoadLibrary()函数即可。这样使用CreateRemoteThread()函数就可以创建一个远程线程了。不过,还有两个问题没有解决,首先是如何将LoadLibrary()函数的地址放到目标进程空间中让CreateRemoteThread()调用,其次是传递给LoadLibrary()函数的参数也需要在目标进程空间中,并且要通过CreateRemoteThread()函数指定给LoadLibrary()函数。

首先解决第1个问题,即如何将LoadLibrary()函数的地址放到目标进程空间中。LoadLibrary()函数是系统中的Kernel32.dll的导出函数,Kernel32.dll这个DLL文件在任何进程中的加载位置都是相同的,也就是说,LoadLibrary()函数的地址在任何进程中的地址都是相同的。因此,只要在进程中获得LoadLibrary()函数的地址,那么该地址在目标进程中也可以使用。CreateRemoteThread()函数的线程地址参数直接传递LoadLibrary()函数的地址即可。

其次解决第2个问题,即如何将欲加载的DLL文件完整路径写入目标进程中。这需要借助WriteProcessMemory()函数,其定义如下:

BOOLWriteProcessMemory(
HANDLEhProcess,//handletoprocess
LPVOIDlpBaseAddress,//baseofmemoryarea
LPVOIDlpBuffer,//databuffer
DWORDnSize,//numberofbytestowrite
LPDWORDlpNumberOfBytesWritten//numberofbyteswritten
);

该函数的功能是把lpBuffer中的内容写到进程句柄是hProcess进程的lpBaseAddress地址处,写入长度为nSize。

参数说明如下。

hProcess:该参数是指定进程的进程句柄。

lpBaseAddress:该参数是指定写入目标进程内存的起始地址。

lpBuffer:该参数是要写入目标进程内存的缓冲区起始地址。

nSize:该参数是指定写入目标内存中的缓冲区的长度。

lpNumberOfBytesWritten:该参数用于接收实际写入内容的长度。

该函数的功能非常强大,比如在破解方面,用该函数可以实现一个“内存补丁”;在开发方面,该函数可以用于修改目标进程中指定的值(比如游戏修改器可以修改游戏中的钱、红、蓝等)。

使用该函数可以把DLL文件的完整路径写入到目标进程的内存地址中,这样就可以在目标进程中用LoadLibrary()函数加载指定的DLL文件了。解决了上面的两个问题,还有第3个问题需要解决。WriteProcessMemory()函数的第2个参数是指定写入目标进程内存的缓冲区起始地址。这个地址在目标进程中,那么这个地址在目标进程的哪个位置呢?目标进程中的内存块允许把DLL文件的路径写进去吗?

第3个要解决的问题是如何确定应该将DLL文件的完整路径写入目标进程的哪个地址。对于目标进程来说,事先是不会准备一块地址让用户进行写入的,用户能做的是自己在目标进程中申请一块内存,然后把DLL文件的路径进行写入,写入在目标进程新申请到的内存空间中。在目标进程中申请内存的函数是VirtualAllocEx(),其定义如下:

LPVOIDVirtualAllocEx(
HANDLEhProcess,
LPVOIDlpAddress,
SIZE_TdwSize,
DWORDflAllocationType,
DWORDflProtect
);

VirtualAllocEx()函数的参数说明如下。

hProcess:该参数是指定进程的进程句柄。

lpAddress:该参数是指在目标进程中申请内存的起始地址。

dwSize:该参数是指在目标进程中申请内存的长度。

flAllocationType:该参数指定申请内存的状态类型。

flProtect:该参数指定申请内存的属性。

该函数的返回值是在目标进程申请到的内存块的起始地址。

到此,关于编写一个DLL注入的所有知识都已经具备了。现在开始编写一个DLL注入的工具,其界面如图1所示。

网络安全编程:远程线程编程

图1 DLL注入/卸载器

该工具有2个作用,分别是注入DLL和卸载被注入的DLL。关于卸载被注入的DLL的功能,将在后面进行介绍。在界面上要求输入两部分内容,第1部分是欲注入的DLL文件的完整路径(一定要是完整路径),第2部分是进程的名称。

首先看一下关于界面的操作,代码如下:

voidCInjectDllDlg::OnBtnInject()
{
//添加处理程序代码
charszDllName[MAX_PATH]={0};
charszProcessName[MAXBYTE]={0};
DWORDdwPid=0;
GetDlgItemText(IDC_EDIT_DLLFILE,szDllName,MAX_PATH);
GetDlgItemText(IDC_EDIT_PROCESSNAME,szProcessName,MAXBYTE);
//由进程名获得PID
dwPid=GetProcId(szProcessName);
//注入szDllName到dwPid
InjectDll(dwPid,szDllName);
}

代码中调用了另外两个函数,第1个是由进程名获得PID的函数,第2个是用于DLL注入的函数。GetProcId()函数的代码如下:

DWORDCInjectDllDlg::GetProcId(char*szProcessName)
{
BOOLbRet;
PROCESSENTRY32pe32;
HANDLEhSnap;
hSnap=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);
pe32.dwSize=sizeof(pe32);
bRet=Process32First(hSnap,&pe32);
while(bRet)
{
//strupr()函数是将字符串转化为大写
if(lstrcmp(strupr(pe32.szExeFile),strupr(szProcessName))==0)
{
returnpe32.th32ProcessID;
}
bRet=Process32Next(hSnap,&pe32);
}
return0;
}+

InjectDll()函数的代码如下:

VOIDCInjectDllDlg::InjectDll(DWORDdwPid,char*szDllName)
{
if(dwPid==0||lstrlen(szDllName)==0)
{
return;
}
char*pFunName="LoadLibraryA";
//打开目标进程
HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPid);
if(hProcess==NULL)
{
return;
}
//计算欲注入DLL文件完整路径的长度
intnDllLen=lstrlen(szDllName)+sizeof(char);
//在目标进程申请一块长度为nDllLen大小的内存空间
PVOIDpDllAddr=VirtualAllocEx(hProcess,NULL,nDllLen,MEM_COMMIT,PAGE_READWRITE);
if(pDllAddr==NULL)
{
CloseHandle(hProcess);
return;
}
DWORDdwWriteNum=0;
//将欲注入DLL文件的完整路径写入在目标进程中申请的空间内
WriteProcessMemory(hProcess,pDllAddr,szDllName,nDllLen,&dwWriteNum);
//获得LoadLibraryA()函数的地址
FARPROCpFunAddr=GetProcAddress(GetModuleHandle("kernel32.dll"),pFunName);
//创建远程线程
HANDLEhThread=CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)pFunAddr,pDllAddr,0,NULL);
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
}

InjectDll()函数有 2 个参数,分别是目标进程的 ID 值和要被注入的 DLL 文件的完整路径。在代码中获得的不是 LoadLibrary()函数的地址,而是 LoadLibraryA()函数的地址。在系统中其实没有 LoadLibrary()函数,有的只是 LoadLibraryA()和 LoadLibraryW()两个函数。这两个函数分别针对 ANSI 字符串和 UNICODE 字符串。而 LoadLibrary()函数只是一个宏。在编写程序的时候,直接使用该宏是可以的。如果要获取 LoadLibrary()函数的地址,就要明确指定是获取 LoadLibraryA()还是 LoadLibraryW()。

LoadLibrary()宏定义如下:

#ifdefUNICODE
#defineLoadLibraryLoadLibraryW
#else
#defineLoadLibraryLoadLibraryA
#endif//!UNICODE

只要涉及字符串的函数,都会有相应的ANSI版本和UNICODE版本;其余不涉及字符串的函数,没有ANSI版本和UNICODE版本的区别。

为了测试DLL加载是否成功,在代码的DllMain()函数中加入如下代码:

caseDLL_PROCESS_ATTACH:
{
MsgBox("!DLL_PROCESS_ATTACH!");
break;
}

现在测试一下注入的效果,如图2和图3所示。

网络安全编程:远程线程编程

图2 DLL文件被注入成功的提示

网络安全编程:远程线程编程

图3 查看进程中的DLL列表确认被装载成功

在图2中,弹出的对话框是DLL程序在DLL_PROCESS_ATTACH时出现的。其所在的进程为notepad.exe。从图2中可以看出,弹出提示框的标题处是notepad.exe进程的路径。图3是用工具查看进程中所加载的DLL文件列表,可以看出,通过注入工具注入的DLL文件已经被加载到notepad.exe的进程空间中。

如果要对系统进程进行注入的话,由于进程权限的关系是无法注入成功的。在打开目标进程时用到了OpenProcess()函数,由于权限不够,会导致无法打开进程并获得进程句柄。通过调整当前进程的权限,可以打开系统进程并获得进程句柄。如果在Win8或更高版本上运行注入程序的话,需要选中注入工具单击右键,选择“以管理员身份运行”才可以完成注入。

2. 卸载被注入的DLL文件

DLL注入如果应用在木马方面,危害很大,这里完成一个卸载被注入DLL的程序。卸载被注入DLL程序的思路和注入的思路是一样的,而且代码的改动也非常小。区别在于现在的功能是卸载,而不是注入。

DLL卸载使用的API函数是FreeLiabrary(),其定义如下:

BOOLFreeLibrary(
HMODULEhModule//handletoDLLmodule
);

该函数的参数是要卸载的模块的句柄。

FreeLibrary()函数使用的模块句柄可以通过Module32First()和Module32Next()两个函数获取。在使用Module32First()和Module32Next()两个函数的时候,需要用到MODULEENTRY32结构体,该结构体中保存了模块的句柄。MODULEENTRY32结构体的定义如下:

typedefstructtagMODULEENTRY32{
DWORDdwSize;
DWORDth32ModuleID;
DWORDth32ProcessID;
DWORDGlblcntUsage;
DWORDProccntUsage;
BYTE*modBaseAddr;
DWORDmodBaseSize;
HMODULEhModule;
TCHARszModule[MAX_MODULE_NAME32+1];
TCHARszExePath[MAX_PATH];
}MODULEENTRY32;
typedefMODULEENTRY32*PMODULEENTRY32;

该结构体中的hModule为模块的句柄,szModule为模块的名称,szExePath是完整的模块的名称(所谓完整,包括路径和模块名称)。

卸载远程进程中DLL模块的代码如下:

VOIDCInjectDllDlg::UnInjectDll(DWORDdwPid,char*szDllName)
{
if(dwPid==0||lstrlen(szDllName)==0)
{
return;
}
HANDLEhSnap=CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,dwPid);
MODULEENTRY32me32;
me32.dwSize=sizeof(me32);
//查找匹配的进程名称
BOOLbRet=Module32First(hSnap,&me32);
while(bRet)
{
if(lstrcmp(strupr(me32.szExePath),
strupr(szDllName))==0)
{
break;
}
bRet=Module32Next(hSnap,&me32);
}
CloseHandle(hSnap);
char*pFunName="FreeLibrary";
HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPid);
if(hProcess==NULL)
{
return;
}
FARPROCpFunAddr=GetProcAddress(GetModuleHandle("kernel32.dll"),pFunName);
HANDLEhThread=CreateRemoteThread(hProcess,NULL,0,
(LPTHREAD_START_ROUTINE)pFunAddr,me32.hModule,0,NULL);
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
}

卸载远程进程中DLL的实现代码比DLL注入的代码要简单,这里就不做过多的介绍了。

3. 无DLL的代码注入

DLL文件的注入与卸载都完成了,整个注入与卸载的过程其实就是让远程线程执行一次LoadLibrary()函数或FreeLibrary()函数。远程线程装载一个DLL文件,通过DllMain()调用DLL中的具体功能代码,这样注入DLL后就可以让DLL做很多事情了。是否可以不依赖DLL文件直接向目标进程写入要执行的代码,以完成特定的功能呢?答案是可以。

要在目标进程中完成一定的功能,就需要使用相关的API函数,不同的API函数实现在不同的DLL中。Kernel32.dll文件在每个进程中的地址是相同的,但是并不代表其他DLL文件在每个进程中的地址都是一样的。这样,在目标进程中调用API函数时,必须使用LoadLibrary()函数和GetProcAddress()函数动态调用用到的每个API函数。把想要使用的API函数及API函数所在的DLL文件都封装到一个结构体中,直接写入目标进程的空间中。同时也直接把要在远程执行的代码也写入目标进程的内存空间中,最后调用CreateRemoteThread()函数即可将其运行。

通过实现一个简单的例子让远程线程弹出一个提示对话框,但是不借助于DLL。本程序所使用的API函数在前面都已经介绍过了。根据前面的步骤先来定义一个结构体,其定义如下:

#defineSTRLEN20
typedefstruct_DATA
{
DWORDdwLoadLibrary;
DWORDdwGetProcAddress;
DWORDdwGetModuleHandle;
DWORDdwGetModuleFileName;
charUser32Dll[STRLEN];
charMessageBox[STRLEN];
charStr[STRLEN];
}DATA,*PDATA;

该结构体中保存了LoadLibraryA()、GetProcAddress()、GetModuleHandle()和GetModu leFileName()四个API函数的地址。这四个API函数都属于Kernel32.dll的导出函数,因此可以在注入前进行获取。User32Dll中保存“User32.dll”字符串,因为MessageBoxA()函数是由User32.dll的导出函数。Str中保存的是通过MessageBoxA()函数弹出的字符串。

注入代码类似于前面介绍的注入代码,不过需要在注入代码中定义一个结构体变量,并进行相应的初始化,代码如下:

VOIDCNoDllInjectDlg::InjectCode(DWORDdwPid)
{
//打开进程并获取进程句柄
HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPid);
if(hProcess==NULL)
{
return;
}
DATAData={0};
//获取kernel32.dll中相关的导出函数
Data.dwLoadLibrary=(DWORD)GetProcAddress(
GetModuleHandle("kernel32.dll"),"LoadLibraryA");
Data.dwGetProcAddress=(DWORD)GetProcAddress(
GetModuleHandle("kernel32.dll"),"GetProcAddress");
Data.dwGetModuleHandle=(DWORD)GetProcAddress(
GetModuleHandle("kernel32.dll"),"GetModuleHandleA");
Data.dwGetModuleFileName=(DWORD)GetProcAddress(
GetModuleHandle("kernel32.dll"),"GetModuleFileNameA");
//需要的其他DLL和导出函数
lstrcpy(Data.User32Dll,"user32.dll");
lstrcpy(Data.MessageBox,"MessageBoxA");
//MessageBoxA()弹出的字符串
lstrcpy(Data.Str,"InjectCode!!!");
//在目标进程申请空间
LPVOIDlpData=VirtualAllocEx(hProcess,NULL,sizeof(Data),
MEM_COMMIT|MEM_RELEASE,PAGE_READWRITE);
DWORDdwWriteNum=0;
WriteProcessMemory(hProcess,lpData,&Data,
sizeof(Data),&dwWriteNum);
//在目标进程空间申请的用于保存代码的长度
DWORDdwFunSize=0x4000;
LPVOIDlpCode=VirtualAllocEx(hProcess,NULL,dwFunSize,
MEM_COMMIT,PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess,lpCode,&RemoteThreadProc,
dwFunSize,&dwWriteNum);
HANDLEhThread=CreateRemoteThread(hProcess,NULL,0
(LPTHREAD_START_ROUTINE)lpCode,lpData,0,NULL);
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
}

上面的注入代码除了对结构体变量初始化外,还将线程函数代码写入目标进程空间的内存中。线程函数的代码如下:

DWORDWINAPIRemoteThreadProc(LPVOIDlpParam)
{
PDATApData=(PDATA)lpParam;
//定义API函数原型
HMODULE(__stdcall*MyLoadLibrary)(LPCTSTR);
FARPROC(__stdcall*MyGetProcAddress)(HMODULE,LPCSTR);
HMODULE(__stdcall*MyGetModuleHandle)(LPCTSTR);
int(__stdcall*MyMessageBox)(HWND,LPCTSTR,LPCTSTR,UINT);
DWORD(__stdcall*MyGetModuleFileName)(HMODULE,LPTSTR,DWORD);
//对各函数地址进行赋值
MyLoadLibrary=(HMODULE(__stdcall*)(LPCTSTR))
pData->dwLoadLibrary;
MyGetProcAddress=(FARPROC(__stdcall*)(HMODULE,LPCSTR))
pData->dwGetProcAddress;
MyGetModuleHandle=(HMODULE(__stdcall*)(LPCSTR))
pData->dwGetModuleHandle;
MyGetModuleFileName=(DWORD(__stdcall*)(HMODULE,LPTSTR,DWORD))
pData->dwGetModuleFileName;
//加载User32.dll
HMODULEhModule=MyLoadLibrary(pData->User32Dll);
//获得MessageBoxA函数的地址
MyMessageBox=(int(__stdcall*)(HWND,LPCTSTR,LPCTSTR,UINT))
MyGetProcAddress(hModule,pData->MessageBox);
charszModuleFileName[MAX_PATH]={0};
MyGetModuleFileName(NULL,szModuleFileName,MAX_PATH);
MyMessageBox(NULL,pData->Str,szModuleFileName,MB_OK);
return0;
}

上面就是无DLL注入的全部代码,编译连接并运行它。启动一个记事本程序来进行测试,可惜报错了。问题出在哪里呢?VC6的默认编译是Debug版本,这样会加入很多调试信息。而某些调试信息并不存在于代码中,而是在其他DLL模块中。这样,当执行到调试相关的代码时会访问不存在的DLL模块中的代码,就导致了报错。

将以上代码使用Release方式进行编译连接,然后可以无误地执行,如图4所示。

网络安全编程:远程线程编程

图4 Release方式下编译注入成功

编译的Debug版也可以进行无DLL的注入,只是实现起来略有不同。

继续浏览有关 安全 的文章
发表评论