作者:星轨(oRbIt)
E_Mail :[email protected]
输入您的搜索字词 提交搜索表单 |
上一篇文章介绍了如何发现正在运行的“Source Insight”窗口,本篇将介绍“TabSiPlus”是如何进行代码注入的。Windows 9x以后的Windows操作系统都对进程空间进行了严格的保护,进程之间不能想16位的Windows那样通过一个确定的内存地址互相访问数据,甚至是代码,这就意味着要实现一个软件的扩展功能就必须将自己的代码“注入”到该软件的进程空间中,当然,一个进程也可以利用Windows的消息传递机制或mailslot、pipe之类的进程通信方式影响另一个进程,但是,并不是所有的程序都这么“好心”,会留这些接口给别人操作,所以将自定义的代码注入到另一个进程中运行就成了不是方法的方法。
将代码注入到其它进程的方法很多。。。。。(此处省略XXXXX字)。
【#$%$%^&,拜托,把砖头放下,“此处省略XXXXX字”并不是为了哗众取宠,只是因为关于代码注入的方法太多了,随便搜索一下就是一大堆,不说多如牛毛,也是多如羊毛狗毛的,无论我再怎么精心构造,遣词排句,也拜托不了抄袭的“厄运”,所以干脆不写了,如果想了解这些技术的可以参考Codeproject上的一篇文章:Three Ways to Inject Your Code into Another Process】
Windows为可执行文件加载动态链接库的时候并不为动态链接库创建单独的进程空间,而是将其复制一份,映射到加载它的可执行文件的进程空间中,也就是说,一个动态链接库如果被两个可执行文件同时使用,那么这个动态链接库就有两份拷贝,它们之间的数据互相不会有影响。这种加载方式就是实现代码注入的基础:将自定义的代码放在动态链接库中让被挂程序加载,这样自定义的代码就进入了被挂程序的进程空间,就可以访问属于被挂程序的进程空间中的资源,包括内存、句柄和内核对象,当然还有更重要的,就是函数导入表,这个函数导入表是注入的程序能否正常运行的关键。
现在问题的关键就是如何让被挂程序加载我们的定制动态链接库,这是实现外挂的关键之处,各种代码注入的方法不同之处就是这一步的实现方法,我们从这些多如羊毛狗毛的方法中挑选了一种方法,那就是使用CreateRemoteThread() API将定制的动态链接库加载到“Source Insight”的进程空间中。需要说明一点,Windows 95/98/Me 是不支持这个API的,所以这种方法只能用于基于NT技术构建的Windows(话又说回来了,现在使用这些老的操作系统的人已经不多了,不支持需要理由吗?不需要吗?需要吗?.....Q$#^#%#^$%&%^*)。CreateRemoteThread()的用法和CreateThread()相似,只是CreateThread()只能启动进程内的线程,而CreateRemoteThread()可以在另一个进程中启动一个线程。使用CreateRemoteThread()需要两个条件,一是线程函数的起始地址,当然这个函数地址当然是在被挂进程的进程空间中,另一个条件是线程函数参数,CreateRemoteThread()需要的线程函数类型和CreateThread()一样,有一个无类型指针参数,通常线程的创建者利用这个指针向线程传递一个struct的指针,线程所需的参数都在这个struct中,当然,这个struct也必须位于被挂进程的进程空间中。很显然,被挂进程中不会存在这样一个”乖巧”的函数等着我们的CreateRemoteThread()使用,更不会“巧到”还有这样一个符合要求的struct,所以,这些条件都要自己创造。
幸运的是,Windows提供了让我们创造条件的一切手段,来看看:VirtualAllocEx()和VirtualFreeEx()这两个API用于在指定的进程中分配和释放一块内存,这个指定的进程可以是另一个进程;WriteProcessMemory()和ReadProcessMemory()这两个API可以写和读一个指定进程中的内存,当然这个指定的进程也可以是另一个进程。现在有了能够在其它进程中操作内存的手段,可以读还可以写,可是问题的关键是读什么、写什么?其实要写入另一个进程的东西有两部分,一是线程函数的代码,另一个是线程函数的参数。线程参数就是一块内存数据,没有什么诡秘的地方,关键是对线程函数代码,这块代码是另一个编译器产生的,它的映射地址和内存访问地址都是相对于本进程中的虚地址计算的偏移,所以要将这个函数的代码复制到另一个进程中并运行,还需要处理很多细节。首先是这个函数必须远离任何运行库函数和API,很简单,一旦使用了这些库函数和API,你就需要在代码复制到另一个进程后根据另一个进程的函数导入表,手工计算修改这些库函数和API的调用地址,这和病毒的手法是一致的,我可不想这么麻烦,所以最好不使用它们。可是,不能使用库函数和API,还有什么办法能够让这个线程函数起到加载我们定制的动态链接库?答案是:没有。很显然,如果不调用运行库函数或API,标准的C/C++语言是没有加载动态链接库的语法的,所以还必须使用(至少)windows的API。
在Windows平台上加载一个动态链接库并运行其中一个导出函数需要三个步骤:1.加载动态链接库;2.定位到导出函数的地址,运行函数;3.释放动态链接库。这些操作涉及三个API:LoadLibraryA(W),GetProcAddressA(W)和FreeLibrary,这三个API都位于kernel32.dll中,几乎所有的windows应用程序都要使用kernel32.dll,当然也有一些程序确实是不使用这个动态链接库的,不过这样的程序通常也没有挂的必要。Windows平台上这三个API的地址在每个进程中的位置是固定的,这是本文介绍的加载方法的关键依赖,就是在本地进程中将这三个API的地址得到,然后作为参数传递给在被挂进程中的启动的线程函数,这样这个线程函数就可以使用这三个API(直接通过函数地址调用)加载我们定制的动态链接库。
现在总结一下我们的方法,首先使用OpenProcess()得到被挂进程的进程句柄,然后调用VirtualAllocEx()在被挂进程中分配两块内存,一块用于填写被CreateRemoteThread()调用的启动线程的代码,一块用于存放需要传递给远程线程函数的参数,用于传递参数的这一块内存大小比较容易确定,由参数大小决定,主要包括三个API的地址,还有定制动态链接库的文件名(全路径),本文介绍的启动方法是在定制动态库中中设置一个导出函数,通过调用这个导出函数完成外挂程序的初始化,所以还要包括这个导出函数的名字。这个导出函数的名字当然是事先就定下的,那为什么还要动态传递呢?直接使用就行了,比如:
ParaBlock->pGetProcessAddress(hMudule,"InitFunc");
如果你真是这样想的,那就请在看下文之前完成以下动作:面向一堵墙站好,然后低下头,用5米/秒的速度前进,直到停下为止........................
.......................
.......................
做完了?现在看看为什么要这样做:因为"InitFunc"会被编译器放到本地进程的静态数据段中,这句代码实际操作的是一个静态数据段中的地址,而被挂进程中是没有这个静态数据的,当你将代码复制到被挂进程中后,照着这个地址调用,轻则得到一个无用的数据导致ParaBlock->pGetProcessAddress调用失败,重则访问非法地址导致被挂程序意外中止(比如被挂程序没有捕获这个异常),现在明白了吗?这能使你脑袋开窍,就这样。这个远程线程函数不能引用任何外部变量,所有外部变量都要通过VirtualAllocEx()在被挂进程中分配一块内存,然后复制过去。
关于参数的内存比较好处理,关键之处是复制线程函数代码,总结前面的描述,这个线程函数不能内部调用运行库函数和API,不能使用外部变量,除此之外,对这个函数还有其它的要求,比如不能使用异常机制(包括Windows的结构化异常处理),不能使用跳转函数jump和longjump,甚至是不能使用CASE语句(没有理论依据,但是很多黑客都承认这一点,大概和不同的编译器生成代码方式不同有关),这很容易理解,因为要保证编译器生成的函数代码是连续存放的,便于复制。这个线程函数还不能太大,毕竟,想要成为一个受人尊敬的外挂就要处处为被挂程序着想,尽量节省内存。所要分配的内存大小由实际的函数代码大小决定,可以使用一个估计的足够大的数值,比如TabSiPlus使用1024字节,因为TabSiPlus的线程函数很小,1K字节足够了,如果为了追求完美,可以使用相邻函数地址相减(通过函数名完成)的方法计算出函数代码块的精确大小,不过,没有编译器能够保证生成机器码的时候按照C/C++代码的顺序,它们只是尽力而为。
现在做最后的总结,这种方法需要在被挂进程中分配两块内存,首先是分配远程线程参数,以下是参数结构的定义:
typedef HMODULE (WINAPI *PLoadLibraryW)(LPCWSTR);
typedef BOOL (WINAPI *PFreeLibrary)(HMODULE);
typedef FARPROC (WINAPI *PGetProcAddress)(HMODULE, char*);
struct RemoteThreadPara
{
DWORD LastError;
PLoadLibraryW fnLoadLibraryW;
PFreeLibrary fnFreeLibrary;
PGetProcAddress fnGetProcAddress;
WCHAR lpModulePath[MAX_PATH]; // the DLL path
CHAR lpFunctionName[256]; // the called function
//...还可以有其它参数
};
下面是参数处理代码:
RemoteThreadPara *c = 0;
DWORD rc = (DWORD)-1;
HMODULE hKernel32 = 0;
RemoteThreadPara localCopy;
// allocate memory for parameter block
c = (RemoteThreadPara*) VirtualAllocEx( hProcess, 0, sizeof(RemoteThreadPara), MEM_COMMIT, PAGE_READWRITE );
// 先填充一个本地结构
#ifdef _UNICODE
lstrcpyW( localCopy.lpModulePath, lpDllPath );
#else
wsprintfW( localCopy.lpModulePath, L"%hs", lpDllPath );//lpDllPath是定制动态库的全路径名
#endif
if ( lpFunctionName == NULL )
localCopy.lpFunctionName[0] = '/0';
else
lstrcpynA( localCopy.lpFunctionName, lpFunctionName, SIZEOF_ARRAY(localCopy.lpFunctionName) );//lpFunctionName是启动函数名称
// kernel32.dll
hKernel32 = GetModuleHandle( _T("kernel32.dll") );
// get the addresses for the functions, what we will use in the remote thread
localCopy.fnLoadLibraryW = (PLoadLibraryW)GetProcAddress( hKernel32, "LoadLibraryW" );
localCopy.fnFreeLibrary = (PFreeLibrary)GetProcAddress( hKernel32, "FreeLibrary" );
localCopy.fnGetProcAddress = (PGetProcAddress)GetProcAddress( hKernel32, "GetProcAddress" );
WriteProcessMemory( hProcess, c, &localCopy, sizeof localCopy, 0 );//现在明白为什么需要一个本地结构了吧?因为只有这个函数可以操作c的内存
下面是函数处理代码:
// allocate memory for injected code
p = VirtualAllocEx( hProcess, 0, 1024, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
// copy function there, we will execute this piece of code
WriteProcessMemory( hProcess, p, RemoteDllThread, 1024, 0 );
下面就是关键的RemoteDllThread线程函数代码:
DWORD __stdcall RemoteDllThread( LPVOID pPara)
{
RemoteThreadPara *prBlock = (RemoteThreadPara *)pPara;
HMODULE hModule = NULL;
// load the requested dll
if ( prBlock->bLoadLibrary )
{
hModule = (HMODULE)(*prBlock->fnLoadLibraryW)( prBlock->lpModulePath );
}
// call function
if ( prBlock->lpFunctionName[0] != 0 )
{
//execute a function if we have a function name
PFN pfn = (PFN)(*prBlock->fnGetProcAddress)( hModule, prBlock->lpFunctionName );
// execute the function, and get the result
if ( pfn != NULL )
{
DWORD ret = 0;
ret = (*pfn)();//调用外挂动态链接库的启动函数
}
}
// free library
if ( pfn->bFreeLibrary )
{
pfn->fnFreeLibrary( hModule );
}
//.........其它处理代码
return 0;
}
PFN是定制动态链接库启动函数的原型:
typedef DWORD (WINAPI *PFN)();
现在万事具备,只欠东风啦,就是调用CreateRemoteThread()启动远程线程,装载我们的定制动态链接库了:
ht = CreateRemoteThread( hProcess, 0, 0, (DWORD (__stdcall *)( void *)) p, c, 0, &ThreadId );
这一篇就到这里,下一篇文章将介绍如何构建那个提供定制功能的定制动态链接库。
Source Insignt文件标签外挂:TabSiPlus的下载地址:
点击下载