HanDs
NO.2

[Visual Studio文章] 远程线程插入 





学习中请遵循国家相关法律法规,黑客不作恶。没有网络安全就没有国家安全

本站需要登陆后才能查看

    说到隐藏进程,下面的文字其实是非常牵强的。本文主要讨论如何将自己的代码注入到别的
进程(文中的远程进程)中运行,来达到隐藏的目的。实际上是完全没有了进程这个概念。
    文中的例子在Win2k Professional sp2 + VC++6.0上测试通过。其中用到的api好多是ANSI
版的,如,LoadLibraryA,MessageBoxA等,也可以改为宽字节版的,即,LoadLibraryW,MessageBoxW
等。毕竟在Win2k下,最终调用的还是宽字节版的api。文中提到的api的参数具体含义请参考MSDN。
    关于隐藏进程的文章,网上也挺多。以下主要是写一下自己使用的感受,以及发现的问题,
就算是以笔记的形式记下自己的所学吧。

方法1.让系统加载包含欲执行代码的DLL,如backdoor.DLL

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows]
"AppInit_DLLs"="backdoor.DLL"
wantjob就是用这个法子。微软知识库Q134655和Q125680里介绍过,大家不妨弄来看看。
写一个backdoor.DLL,再加上这个键值,系统启动后就会加载这个模块。因为没有自己
的进程,所以也是看不见的。backdoor.DLL还可以学wantjob 的损招,不断检查这个键值,
被人删了就再重写回来。

方法2.向合法进程中注入自己的DLL,如RemoteDLL.DLL

注入函数:
BOOL WINAPI InjectDLLToRemoteProcess(char *pDLLPath    /*待注入的DLL的路径*/,
     DWORD dwProcessID /*远程进程的进程ID*/)
{
if((pDLLPath == NULL) && (dwProcessID < 0))
return FALSE;

//打开进程,获得进程句柄
HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
if(hRemoteProcess == NULL)
return FALSE;
//PROCEE_ALL_ACCESS:All possible access rights for a process object

//在远程进程中申请一块可读可写的内存,将DLL的路径写入
int nLen = strlen(pDLLPath) + 1;
LPVOID pszDLLRemote = VirtualAllocEx(hRemoteProcess,
NULL, nLen, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
if(pszDLLRemote == NULL)
return FALSE;

BOOL bRet = WriteProcessMemory(hRemoteProcess, pszDLLRemote,
pDLLPath, nLen, NULL);
if(!bRet)
return FALSE;

//将函数LoadLibraryA的入口地址作为我们的远程线程入口地址
//这样当线程开始工作时,就将调用LoadLibraryA装载咱们的DLL
PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle("kernel32.DLL", "LoadLibraryA";
if(pfnStartAddr == NULL)
return FALSE;

HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess,
NULL, 0, pfnStartAddr, pszDLLRemote, 0, NULL);
if(hRemoteThread == NULL)
return FALSE;

return TRUE;
}
在调用CreateRemoteThread函数创建远程线程时,我们没有将DLL的路径直接传入,如下:
1).HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess,
NULL, 0, pfnStartAddr, pDLLPath, 0, NULL); 或
2).HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess,
NULL, 0, pfnStartAddr, "xxxxx.DLL", 0, NULL);
对于1),pDLLPath指向本进程空间的路径字符串的地址,而不是我们要注入的远程进程的。
对于2),"xxxxx.DLL"作为常量,在编译过后将会放置在PE文件的.data段中,代码在.text段,
所以此处将仍然是一个指向本进程空间的路径字符串的地址。后面介绍的第3种直接注入代码的方法,
同样遇到此问题。解决办法是将要传入的数据(或代码)事先写入远程线程的地址空间,然后传入
它们在远程地址空间中的地址即可。

还要注意pDLLPath必须是绝对路径或相对于远程进程的相对路径。偶当时由于习惯,将RemoteDLL.DLL
 copy到了本进程的Debug目录下了,~~~~, 郁闷了半天~

远程线程ID可以通过EnumProcess, CreateToolhelp32SnapShot/Process32First/Process32Next,
NtQuerySystemInformation等函数获取,当然最简单的是察看任务管理器-->进程-->PID :)

RemoteDLL.DLL演示代码:
#include "stdafx.h"
#include "RemoteDLL.h"
#include <stdlib.h>

BOOL APIENTRY DLLMain( HANDLE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
 
{
    char szProcessID[32];
    switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH://首次加载DLL时从此处执行
_itoa(GetCurrentProcessId(), szProcessID, 10);
MessageBox(NULL, szProcessID, "Remote DLL", MB_OK);
/*当然,这里你可以开一个线程,do what you want to do
*/
break;
case DLL_THREAD_ATTACH:
// _itoa(GetCurrentProcessId(), szProcessID, 10);
// MessageBox(NULL, szProcessID, "Remote DLL", MB_OK);
break;
case DLL_THREAD_DETACH:
//MessageBox(NULL, "thread detach", "Remote DLL", MB_OK);
//做一些善后工作,如释放内存
break;
case DLL_PROCESS_DETACH:
//善后
break;
    }
    return TRUE;
}
对于入口函数DLLMain的参数ul_reason_for_call:
DLL_PROCESS_ATTACH:
    第一次映射一个DLL到进程的地址空间,系统以此作为ul_reason_for_call的值调用DLLMain,
也只有第一次会用此值。在此处理一切与进程有关的初始化。隐式连接DLL时由主线程先调用DLLMain,返回
之后才会执行EXE的C start code。显示连接DLL时由调用LoadLibrary的线程调用。
DLL_PROCESS_DETACH:
    解除一个DLL时,系统以此值调用DLLMain,进行一切进程相关的结束处理。隐式解除时由调用
ExitProcess函数的线程调用DLLMain,显示解除时由调用FreeLibrary的线程调用。
DLL_THREAD_ATTACH:
    当进程创建一个线程时,系统会检查当前映射到进程地址空间的所有DLL文件映象,以此值由此线程
调用这些DLL的DLLMain,完成一切线程相关的初始化。若映射一个新的DLL到一个进程空间时已有若干线程正
在运行,系统不为已存在的线程以此值来调用DLLMain。对于主线程,系统不以此值调用任何DLLMain。
DLL_THREAD_DETACH:
    当线程调用ExitThread终止时(或线程正常返回,TerminateThread则不会),系统将检查当前映射
到进程地址空间的所有DLL文件映象,并以此值调用这些DLLMain,进行线程相关的结束处理。若在解除DLL时
所有线程还在运行,不调用任何DLLMain.

    由上面可以看出,当远程线程成功注入后,将执行LoadLibraryA装载RmtDLL.DLL,因为是首次映射
RmtDLL.DLL到远程进程,远程线程将以DLL_PROCESS_ATTACH为参数调用DLLMain,然后执行我们的代码。
由于没有调用FreeLibrary解除RmtDLL.DLL,所以当我们再次注入时,LoadLibraryA会发现DLL已经在进程空间
中,就不会再次以DLL_PROCESS_ATTACH为参数调用DLLMain,也就不会执行我们的代码。此时不要以为线程注入
没有成功。
    当把我们的代码放在 case DLL_THREAD_ATTACH时可以保证每次注入都能成功执行,但当远程进程自己
创建线程时也会以DLL_THREAD_ATTACH调用DLLMain而调用我们的代码。
    在 case DLL_THREAD_DETACH 下可以做一些善后处理,如释放先前申请的内存等,但每个的线程终止
这些代码都会被执行。
    case DLL_PROCESS_DETACH 最适合做善后,唯一不好的地方是必须等此远程进程结束,一般我们注入的
远程进程都不会很快结束的。

另外注入时会涉及到权限不足的问题,以下函数可以提升进程权限。
提升注入进程的权限:(权限管理这一块一直没有找到好的资料,以后好好研究一下:-))
void WINAPI EnableDebugPrivilege(void)
{
HANDLE hToken;
LUID sedebugnameValue;
TOKEN_PRIVILEGES tkp;

if(!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &hToken))
return;

if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &sedebugnameValue))
{
CloseHandle(hToken);
return;
}

tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = sedebugnameValue;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL))
CloseHandle(hToken);
}
它可使本进程在internet、winLogin、lsass等进程中创建线程,Win2k的进程查看器无法将其杀除。


方法3.直接注入代码

原理和方法2差不多,只是这里注入的不是DLL,而是本进程中的代码。
原理:在远程进程中申请一段可读、可写和可执行的内存,然后写入我们的代码,创建远程线程时将它作为
线程入口地址即可。

要解决的问题:
1).就是在方法2中遇到的问题,将常量直接写入远程进程空间中来解决
2).对API调用地址确定的问题。
对于问题2):
知道问题所在的朋友,大可以跳过以下繁琐的说明(闲着没事,写了下面一大堆)
//////////////////////////////////////// Analysis Begin//////////////////////////////////////////////
    对PE文件格式比较熟悉的朋友都知道,对于外部DLL中函数的调用,PE文件会将所有的调用信息保存在
输入表中,对应于PE文件的.idata块。由于系统加载DLL时并不能保证每次映射的基址相同,所以PE文件不可能
预知被调用的外部函数的真实地址,下面看看PE文件到底作了些什么。

    使用W32dsm8.93,Stud_PE1.6和UltraEdit32来分析一个PE格式的exe文件(elsGame.exe,俄罗斯方块游戏程序)
先用W32dsm8.93将elsGame.exe进行反汇编,从下面可以看到import的函数信息。

+++++++++++++++++++ IMPORTED FUNCTIONS ++++++++++++++++++
Number of Imported Modules =    2 (decimal)

   Import Module 001: USER32.DLL
   Import Module 002: KERNEL32.DLL

+++++++++++++++++++ IMPORT MODULE DETAILS +++++++++++++++

   Import Module 001: USER32.DLL

 Addr:0002D634 hint(00B9) Name: EndDialog
 Addr:0002D620 hint(0001) Name: AdjustWindowRect
 Addr:0002D610 hint(025B) Name: SetWindowPos
 Addr:0002D604 hint(0195) Name: KillTimer
 Addr:0002D5F6 hint(01BE) Name: MessageBoxA
 Addr:0002D5E6 hint(01DE) Name: PostMessageA
……
Addr为载DLL中的相对偏移,hint为本函数在DLL中的输出函数序号,Name是函数名

在菜单Function的下拉菜单中选择Import,然后在弹出的import functions列表中搜索MessageBoxA(以此函数举例说明,
因为我们经常使用此函数),找到后双击,光标将停留在调用此函数处(本程序一共有两处):
* Reference To: USER32.MessageBoxA, Ord:01BEh
                                  |
:0040151F FF157CD34200            Call dword ptr [0042D37C]
:00401525 3BF4                    cmp esi, esp
:00401527 E854230000              call 00403880
:0040152C 8BF4                    mov esi, esp
:0040152E 6A00                    push 00000000
:00401530 6A00                    push 00000000
:00401532 6A02                    push 00000002
:00401534 8B4508                  mov eax, dword ptr [ebp+08]
:00401537 50                      push eax

* Reference To: USER32.MessageBoxA, Ord:01BEh
                                  |
:00403858 FF257CD34200            Jmp dword ptr [0042D37C]

其中Call dword ptr [0042d37c]和Jmp dword ptr [0042d37c]是对MessageBoxA的调用。
0042d37c是本进程空间中的地址,exe映射基址为00400000,虚拟偏移地址(RVA)为0002d37c,现在我们将它转换为文件偏移
地址,看看此处到底是什么。

启动Stu_PE.exe,打开elsGame.exe,在Sections属性页信息如下:
我们只关心.idata段:
Name VirtualSize VirtualOffset RawSize RawOffset Characteristics
.idata 00000BCB 0002D000 00001000 00028000 C0000040
其中Name为段名,VirtualSize是本段的实际大小,RawSize为对齐后的大小,
VirtualOffset是本段的虚拟偏移地址(RVA),加上基址00400000就是进程空间中的虚拟地址,
RawOffset为本段的文件偏移地址,Characteristics是段属性(可读、可写、可执行等等)。

MessageBoxA的虚拟偏移地址(RVA)为0002d37c,减去段始的RVA 0002d000就是绝对偏移为0000037c.
绝对偏移加上本段始在文件中的偏移地址,就是它的文件偏移地址为0002837c。

现在用UltraEdit32打开elsGame.exe,来到偏移为0002837c的地方,
看到的四个字节值依次为 F6 D5 02 00,倒过来就是0002d5f6,可见这也是.idata段中的虚拟偏移地址(RVA),
同上理转换成文件偏移地址为000285f6,察看此处发现字符串"MessageBoxA"。

呵呵~,原来PE文件中确实不知道确切的外部函数地址,而只保存了对函数名的调用。熟悉PE文件格式的朋友都知道,
系统加载PE文件和所需DLL后,通过此处的函数名来确定DLL中的函数地址,然后用函数地址改写指向函数名字符串的指针值,
在本例中也就是用MessageBoxA函数的地址代替0042d37c地址处的4字节值。这样Call dword ptr [0042d37c]就可以真正
调用函数MessageBoxA了。

以上的所有说明,实际上是对输入表结构的分析,具体结构大家可以参考PE文件格式分析的文章。

每个PE文件的段信息都不相同(如.idata),这种间接的调用外部函数的方式,导致不同的程序对相同DLL中相同函数调
用的间接地址不同,
如,在本程序中是 Call dword ptr [0042D37C]
在别的程序中却是 Call dword ptr [00427434]等等

问题就出在这里,如果将本程序中的代码(含有对外部函数的调用)注入到别的进程中,将没法运行。
象下面这样(只给出了关键性的示范代码):
DWORD WINAPI RemoteThread(void *pData)
{
MessageBox(……);
return 0;
}
……
……
WriteProcessMemory( hRemoteProcess/*远程进程句柄*/,
pRemoteBuffer/*远程进程中申请的内存指针*/,
&RemoteThread,
nCodeSize/*线程函数代码大小*/,
0
   ;
//////////////////////////////////////Analysis End/////////////////////////////////////////////
虽然调用代码不同,但最终调用的函数地址还是一样的,所以我们只要事先获得函数的地址,直接调用就可以了。
具体是(以调用user32.DLL中的MessageBoxA为例):
HANDLE hDLL = LoadLibrary("User32.DLL";
void * pMessageBoxA = (void *)GetProcAddress("MessageBoxA";
获得函数地址后,就可以在创建远程线程时将它作为传给线程函数的参数:
CreateRemoteThread( hRemoteProcess, 0, 0,
(DWORD (WINAPI *)(void *))pRemoteBuffer/*远程写入的线程代码入口*/,
pMessageBoxA/*通用的函数地址*/, 0, NULL
   ;

第3种方法的具体实现参考安焦上的文章“WinNT & Win2K下实现进程的完全隐藏”,
链接为:http://www.xfocus.net/articles/200201/328.html,  方法2中引用了其中的权限提升函数。

    注入DLL后,我们没有说明如何释放此DLL。至于如何释放注入的DLL,一方面可以用同样的方法创建远程线程,
此时线程入口函数的地址为FreeLibrary的地址,参数还是DLL的路径;另一方面可以在方法3中的远程线程入口函数
中处理。

    从上面三钟注入的方法看:
第1种最简单,写个DLL加到那个键下就可以了,但最容易被发现;
第2种感觉用起来最舒服,不用考虑方法3中的函数调用问题,但就是多了个DLL;
第3种最爽的地方就是,代码插入后一点痕迹也没有,当然它也相对比较麻烦。

    注入DLL后我们可以做很多事情(很多无进程木马就是这样的),比如前面提到的创建一个线程。

    有了这个DLL我们可以拦截API(hook api),最简单的方法就是将api函数代码的前几个字节改写为 Jmp MyApi,
当然写入的是机器码,长跳转的机器码格式为"e9xxxxxxxx",xxxxxxxx为相对跳转偏移,一共5个字节。还有就是改写
已经加载到内存的PE文件输入表的IAT,将其改写为我们的API地址,按照上面的Analysis部分分析的,就是将地址写到语句
call dword ptr [xxxxxxxx],jmp [xxxxxxxx]中的xxxxxxxx处。当然,这里只涉及到普通API的拦截,内核级API的
拦截不在此讨论范围内。

    我们还可以在DLL启动时设置键盘或鼠标钩子,这样我们还可以和DLL交互,这种无进程的远程控制的感觉简直
爽死了,哈哈~~~~~~

    呵呵,写玩了,好多是想到什么就写了什么,细节讨论的比较多,有点喧宾夺主的感觉。不过也是,其实真正
的注入技术并不复杂,也就是CreateRemoteThread,复杂的还是那些细节问题。


学习中请遵守法律法规,本网站内容均来自于互联网,本网站不负担法律责任
远程 线程 插入
#1楼
发帖时间:2016-7-9   |   查看数:0   |   回复数:0
游客组