连傻瓜都能看懂的基于代码注入的线程守护技术
2010年08月31日
连傻瓜都能看懂的基于代码注入的线程守护技术
Author: 叶紫孤(CPP肥兔)
(感谢冷风大哥提供技术支持) E-mail: yezigu921@126.com QQ:11916872 目标:在远程进程中注入一个线程,用来守护本地进程不被关闭
硬件平台:X86
操作系统:Windows XP Professional SP2
编程环境:VC6.0 Enterprise Edition
读者要求:知道计算机是什么,知道汇编语言是什么,由于本文中代码用C描述,所以还要知道C语言,好像只要具备这三点就够了
拜读冷风大哥的几篇技术文章后,对其给予新手的无私奉献本人由衷的膜拜,因此写此文传递冷风大哥衣钵
--写在前面的话
好了,我们正式开始:
程序执行的基本单位是什么?语句,表达式,函数,过程?说这些的立马拖出去打,正确答案是cpu的一条指令。
那cpu的指令又是什么,你随便看一本汇编语言的书就可以知道答案了。每一个可执行的程序都是由若干个指令组合到一块形成的。
那程序如何执行呢?当然是把我们编好的指令交给CPU了,让CPU在加电的情况下去取指令,去分析,去执行吧。
现在问题来了,当我们的程序在执行时,操作系统在干什么(不要忘记了操作系统本身也是由最基本的CPU指令组成)?答:当我们的指令被CPU执行时,他只好靠边站了,但是现代操作系统尤其像微软这样的流氓公司绝不会放弃对CPU的占有(毕竟一台电脑只有CPU能全面控制各个部件),他会每隔一会儿强制占有CPU运行自己的代码,这就是传说中的中断技术。
操作系统利用抢到的时间来管理系统的各个软硬件资源,这里我们只关心软件方面的本文线程守护技术涉及的方面。
它究竟利用这些时间干什么?答案是他想玩的花哨一点,比如说内存中有20个程序,他可以强制其中哪个程序下一个时间进入CPU中运行,这就是传说中的多任务并行系统基本原理。在操作系统看来你们这20个进程由我统一调配管理,老子就是老大,谁要是不听话,下一步不让你运行了,哈哈。要玩的再花哨一点,假设光一个程序中就有20个可独立运行的模块(传说中的线程),那么操作系统就可以强制确定这一个程序中的哪个模块(线程)下一个时间进入CPU运行。同样在操作系统看来你们这20个模块由我统一调配管理。
它究竟如何管理?听过系统进程链表吗?没听过就好好听,听过的可以掠过本段,操作系统(以下简称OS)把系统中所有进程以链接的方式连成一个串,它从串头可以访问到所有的进程,每当OS抢占到CPU时它就从串头到串尾访问每一个节点,顺便记录一些信息,比如001号进程给我孝敬了20块钱,002是30,003是40……..0020号是100快。嗯,好0020号是好孩子,下一次把CPU给0020号。就这样谁的优先级高谁下一次运行。等再下一次时OS发现0020号孝敬的100块钱依然是优先级最高的,于是就由0020号继续占据CPU。让系统管理技术再往前发展吧,单个进程可以有多个可执行模块时,OS这样处理:让各个进程在其内部把其的各个可执行模块(线程)也弄成一个串,OS在访问到一个进程时,就以进程内的模块为单位把所有模块也访问一遍,如果002号进程的010号可执行模块给我孝敬的钱多,下次就让002.010号线程占据CPU运行。这就是基本调度原理。为了OS统一方便管理要求进程号必须是唯一,线程号也必须是唯一的,进程和线程在标号问题上没有上下级区别(想到句柄是用来标识系统中的某资源这一概念和进程ID,线程ID时,高级读者可能会会心一笑)
现在再问开头的问题:程序执行的基本单位是什么?本作者期望的答案是线程,就是上面讲的进程中的一个可独立运行的模块。对于操作系统来说执行的基本单位是线程,理解了这一点对后面说到远程线程时是很有帮助的。
在上面讲的基础上立一个研究性课题(注意是研究性,而不是研究"性"):系统中的002号进程能不能通过"贿赂"OS在003号进程中安插一个卧底线程?注意OS并非没有人性到谁要是一分钱都不贿赂老子,老子就永远不给你运行的机会。也就是说只要是出现在进程链表中的线程是有机会得到运行的。研究性课题的答案是可行的,问题继续,如果可以那门路是什么?哈哈门路,操作系统开的门路还少吗?API函数(API简称门路,就是操作系统开辟的,用来帮助实现某种功能的门路,雅称接口)不都是一个个不同功能不同用途的门路嘛,所以最终目标被锁定在CreateRemoteThread()这个API函数上。那为什么锁定这个函数呢?因为本作者混社会的日子比较长了,固而知道要实现在其他进程中安插卧底线程的门路就是这个API!!!这里说个题外话:这是一个哲理哦,出现的问题和问题的解决方法是同时存在的,只不过经验老道的人知道门路在哪里,如果你没有能力独立解决问题,还是乖乖走该走的门路把。
让我们按照常规的路线继续走,对CreateRemoteThread() API门路进行分析:(信息参考MDSN,不知道MSDN的去Google百度一下)
PS:如果哪位前辈知道还有其他方法实现本文的相似功能,请告诉本作者,本人将怀着无限崇尚的精神对您顶礼膜拜
HANDLE CreateRemoteThread( HANDLE hProcess, // handle to process to create thread in LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes DWORD dwStackSize, // initial thread stack size, in bytes LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function LPVOID lpParameter, // argument for new thread DWORD dwCreationFlags, // creation flags LPDWORD lpThreadId // pointer to returned thread identifier ); Return Values If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value is NULL. To get extended error information, call GetLastError. PS:不要被洋文吓到,尽管我们祖先被洋人欺负了近一个世纪,但中国还是中国。
这个API门路是这样的:它接受7个参数,依次是: 1号、hProcess要安插卧底线程的进程句柄 2号、lpThreadAttributes 指向一个安全属性结构体的指针。我们直接传默认就行了 3号、dwStackSize 线程堆栈的大小,有默认值
线程堆栈是干什么的?本作者的理解是:线程函数声明的局部变量和线程函数传递的参数要放到这个堆栈中。如果理解不对请来信告知本人 4号、lpStartAddress线程函数的开始地址具体的下文会讲到 5号、lpParameter 给线程函数传递的参数的指针 (线程函数接受一个指针参数) 6号、dwCreationFlags 一个标记用来表明我们安插的卧底线程是一经创建立即执行呢还是先暂停 7号、lpThreadId 线程ID 这个参数要求我们提供一个接受ID的内存地址,如果函数执行成功他就会填充此地址 如果这个API函数执行成功,将返回我们创建的卧底线程的句柄
PS:本人也没有搞清楚进程、线程ID 和进程、线程句柄到底有什么区别,隐约感觉句柄会限制你通过句柄进行的某些动作,而ID是操作系统自身使用的标识方法。如果知道区别的前辈请告知本人
看来要成功进入这个门路,我们至少要搞定1号,4号,5号参数(找到门路了不代表你一定能把事办成,还有花点代价才行)
先看如何搞定1号:要求宿主进程的句柄,好吧我们去遍历系统的进程表找到目标进程,可是OS会大方到让你随便就遍历那张关键的表吗?绝不会,还是乖乖的从固有的API门路中想办法把,先等等,先等等,我们要把哪个进程确定为目标呢?随便找一个吧,行,可是拿什么找呢?用进程名字是最合适的,好,又离成功进了一步,现在解决这个问题:哪个API门路能帮助我们利用进程名字得到进程的其他信息呢?可惜本人没找到相关门路,后来翻啊翻啊,动用所有能用的社会关系网找到了EnumProcesses(),其用处是返回系统中所有进程的ID号,注意是系统中所有的。(这个故事告诉亲爱的读者,永远不要进行一个人的战斗,有朋友你路才能走的更快更好)。可我们要进程ID号没用啊,我们要的是进程句柄,再找门路把,看哪个API能把进程ID转化为进程句柄,找到了我们的目标就达到了,这里推荐OpenProcess()API。 BOOL EnumProcesses( DWORD * lpidProcess, // array to receive the process identifiers DWORD cb, // size of the array DWORD * cbNeeded // receives the number of bytes returned ); 这个函数接受3个参数,1号、一个数组,2号、数组大小,3号、(返回类型)里面的值表明传入的数组总共接受了进程ID的个数。
我们依样画葫芦
DWORD array_idProcess[500];//估计没有那个人的系统中能同时运行500个进程把
DWORD cb_ array_idProcess=sizeof(array_idProcess)
DWORD cbNeeded;
EnumProcesses(array_idProcess, cb_ array_idProcess, &cbNeeded);
这样调用后array_idProcess[500]就保存了系统中所有的进程的Id。 HANDLE OpenProcess( DWORD dwDesiredAccess, // access flag BOOL bInheritHandle, // handle inheritance flag DWORD dwProcessId // process identifier ); 这个函数若成功运行将返回指定的Id号的进程句柄,其同样接受三个参数,1号、访问标记:下文会重点讲,2号、句柄继承性标记:直接传FALSE,3号进程id号,可以用我们那array_idProcess[500]数组中随便一个有效的元素。
考虑这个问题:参数1号、访问标记,打开一个进程为什么要指明进程访问标记呢?这是因为OS想知道你打开一个进程想干什么,并且把你的目的记下来,一旦你试图进行超越权限的动作,他会立刻警告你的。下面是几个常用标记
PROCESS_ALL_ACCESS 所有权限都具有,偷懒人专用标记
PROCESS_QUERY_INFORMATION 允许调用相关的查询函数
PROCESS_SET_INFORMATION 允许设置相关的查询函数
PROCESS_TERMINATE 允许使用相关函数结束进程
PROCESS_VM_OPERATION 允许调用虚拟内存的相关函数
PROCESS_VM_READ 允许虚拟内存写
PROCESS_VM_WRITE 允许虚拟内存读
本人一般喜欢甭管干什么事直接使用PROCESS_ALL_ACCESS就搞定了,但还是推荐细心的读者设定具体的操作权限
同样依样画葫芦:
HANDLE hProcess;//进程句柄;
hProcess=OpenProcess(PROCESS_ALL_ACCESS,false, array_idProcess[随便一个值];
这样就搞定了CreateRemoteThread API的1号参数:目标进程句柄,如果我现在接着往下讲4号,5号参数的话,读者一定会认为本人太不负责任了,随便搞个进程句柄糊弄人,呵呵,好吧,让我们实现用指定进程名找到指定进程ID的方法:
有了进程句柄就能够调用相关的进程API函数,EnumProcessModules()和GetModuleBaseName()这两个家伙;
其中EnumProcessModules()返回有进程句柄标识的进程中所有的模块,注意这里的模块和前面讲的模块不一样,在Windows XP背景下,一个进程的模块有 可执行文件+载入的dll文件,我相信读者只对可执行文件的名字感兴趣,应为他正好就是进程名,有了可执行模块的句柄,就可以在调用GetModuleBaseName()得到进程名字。哈哈,问题解决了,只要我们判断一下得到的名字是不是我们指定的进程名,如果不是就换下一个进程句柄接着去名字判断,还不是就再换下一个,除非我们指定的文件名拼写错了,否则一定可以找到目标进程。
这里的模块问题是有是有点眩晕,其实很简单的,我们讨论问题的范围不同,模块就可能有不同的意思,比如在全球范围来考虑:中国就是一个模块,放中国范围来考虑:北京就是一个模块。不知道什么是dll的,没关系,因为本文不需要dll相关知识,确实想知道的请出门左拐找政府
言归正传开始分析这两个函数 BOOL EnumProcessModules( HANDLE hProcess, // handle to the process HMODULE * lphModule, // array to receive the module handles DWORD cb, // size of the array LPDWORD lpcbNeeded // receives the number of bytes returned ); 篇幅所限,参数不再明述(是个人都能看懂吧)。这里有个小技巧,因为接受的第一个hModule一定是可执行文件的,所以我们就没有必要定义一个HMODULE类型数组去接受那些既占地方有在本文中没有用处的标识dll模块的句柄
调用方法:
HMODULE hModule;
DWORD cb=sizeof(HMODULE);
DWORD cbNeeded;
EnumProcessModules(hProcess,&hModule,cb,&cbNeeded) ;
容我再罗嗦一下,句柄是用来标识资源的,这里我们的hModule标识了由hProcess标识的进程的可执行文件的模块。
现在调用GetModuleBaseName()得到进程可执行模块的名字(亦是进程名) DWORD GetModuleBaseName( HANDLE hProcess, // handle to the process HMODULE hModule, // handle to the module LPTSTR lpBaseName, // buffer that receives the base name DWORD nSize // size of the buffer); 参数自己分析,这里要讲的是函数名,这个GetModuleBaseName是不是名字起的有点怪,无缘无故放个base到中间,差点害大家找不到API门路。问一个问题文件名由什么组成?答:名字+类型的拖出去打,确切答案是存储路径+名字+类型,这样才可以唯一标识一个文件!请问读者你的姓名放中国能唯一标识你吗?再加个地名吧,建议你以后叫中国西部新疆准格尔盆地张家沟五组X某某。base的意思是只取文件的名字+类型,忽略路径。其实GetModuleBaseName还有一个孪生哥哥叫GetModuleFileNameEx,区别?本作者笑而不语。 OK,OK,距离阶段性胜利还有一小步了,我们把EnumProcesses(), OpenProcess(), EnumProcessModules(),GetModuleBaseName()这四个函数封装到一个我们自己定义的函数中,用来实现通过给定的进程名找到进程ID,进程同名的只找排名靠前的。
下面是本文中技术实现的第一个模块源代码
DWORD processtopid(char *processname)
{
DWORD lpidprocesses[1024],cbneeded,cprocesses;
HANDLE hprocess;
HMODULE hmodule;
UINT i;
TCHAR normalname[MAX_PATH]=("UnknownProcess");
if(!EnumProcesses(lpidprocesses,sizeof(lpidprocess es),&cbneeded))
{
return -1;
}
cprocesses=cbneeded/sizeof(DWORD);
for(i=0;i不能像我们自定义函数一样胡写,先看看标准形式吧
DWORD WINAPI ThreadProc( LPVOID lpParameter // thread data ); 除了ThreadProc这个函数名可以改变外,其他部分不可改动。他接受一个指向空类型的指针作为参数。这摆明了是让读者进行强制指针类型转换嘛。
现在参数4号,参数5号都搞定了,是不是可以调用CreateRemoteThread()函数了?让本作者自问自答吧,No,绝对不行,并不是前面我们做错了什么,而是后面我们还有很多没有做。 还记得OpenProcess()有个参数叫访问权限吗?忘记了的看其参数1,其中本作者列出的权限指定的后三个都和虚拟内存有关,那什么是虚拟内存呢?这可是一重头戏啊,你准备好了吗? 知道什么是内存吗?就是那个传说中存储了程序的指令和数据的地方,cpu从这里取指令和数据进行运算的。但是紧紧知道这些对本文来说就太小儿科了,我们要重点理解虚拟内存。这里我把实际存在的物理内存比作一个存货的仓库。假设这个仓库的容量是512个单位,当来这里存货的进程比较少比较小时,它还可以很好的运行,但是这终究有隐患就是超出容量了怎么办?技术的进步使得OS发明了虚拟内存概念,当OS全面接收内存管理后,他在实际物理内存上增加了一个层次叫虚拟内存,他规定要在内存中存货、取货的进程把货物或者存货请求直接发给他,由他来分配你货物的存取,并且他给了你4096的仓库容量(总线只有32位),不光给你是4096,给所有来存储的进程都是4096,但是不要忘记了实际的物理储量只有512,看似很荒谬吧,其实这是有可行性的,这就是:程序的局部访问性原理,程序的基本单位是指令,指令的执行是一条一条顺序进行的,就算出现了循环,跳转,它都基本上是在一个很小的范围内进行的,换句话说就算你的程序有4096这么大,你都不可能在这么大的空间内无序运行,对不对?再通俗一点,就算你有100万,你也不会把它天天带在身上吧,所以操作系统这么处理,比如按10个单位为一组,即把程序的代码切成若干片,每片10个单位,要运行哪片,就把哪片装入物理内存,要运行下一片了,就把原先的踢出,装入下一片。如果每个进程严格执行10个单位的片,那么我们的仓库中就可以供51.2个进程同时运行,而不会觉得卡!!!这就是虚拟内存的基本原理,操作系统虚拟了内存的存取,货物具体存到哪里,进程不用关心,他根本就不知道,无论何时你需要那部分,操作系统都可以给你弄到。 因此两个进程在实际物理内存上没有任何关系,他们都各自在各自的虚拟空间内运行。所以我们讨论的内存问题都是在这虚拟的层面上进行的。我们首先要做的,就是在别人的运行空间中挖上一点地方,来存放我们定义的货物(代码)。 代码注入的关键:首先就是在其他进程的虚拟内存中申请一块空间,然后把我们的代码写入这个空间,最后想办法让代码被操作系统仍给cpu执行! 申请虚拟内存函数是:VirtualAllocEx() LPVOID VirtualAllocEx( HANDLEhProcess, // process to allocate memory LPVOIDlpAddress, // desired starting address SIZE_TdwSize, // size of region to allocate DWORDflAllocationType, // type of allocation DWORDflProtect // type of access protection ); 有5个参数,参数1要分配内存的进程句柄,用OpenProcess()获得。参数2:期望的其实地址,可以传NULL,让系统自动分配,在本例中我们确实对代码的其实地址没有兴趣。参数3 分配大小,根据自己代码的大小估计一个值吧,参数4 分配类型,本例使用MEM_COMMIT,也是常用的,具体参见MSDN,参数5 访问类型保护,指定申请一块内存是干什么用,读?写?执行? 本例选择PAGE_EXECUTE_READWRITE
函数如果执行成功会返回分配的内存的起始地址,我们要想办法把这个地址存起来。这样代码注入的第一步就成功了,接着看第二步,把我们编写好的代码写入第一步分配的内存空间,这次使用的函数是:WriteProcessMemory() BOOL WriteProcessMemory( HANDLEhProcess, // handle to process LPVOIDlpBaseAddress, // base of memory area LPCVOIDlpBuffer, // data buffer SIZE_TnSize, // count of bytes to write SIZE_T *lpNumberOfBytesWritten // count of bytes written ); 第一个参数和上面的相同,第二个参数 写入的内存基址,使用上个函数的返回值 ,第三个参数 要写入的数据的基址,这里传递我们设计好的线程函数。,第四个参数传递第三个参数指定的数据块的大小,第五个参数用系统帮我们填充总共传递了多少个,可以传NULL,因为我们没有必要知道。通过依次调用这两个函数就可以把代码注入目标进程了,这正好解决了CreateRemoteThread()函数的第四个参数:线程函数的开始地址,这里请读者多想一步,既然代码是我们自己注入的,那调用CreateRemoteThread()能起到什么效果呢?其实CreateRemoteThread()此函数仅仅是通知操作系统把这里指定的进程的指定代码作为一个进程的一个线程加入到系统调用链表中而已。至此,基本上注入原理就是这些了,亲爱的读者你明白了吗?如果以上文字您已看明白,那么恭喜您,您可以继续向下看,看看如何编程实现线程守护让我们开始编程实现吧:
首先考虑我们的远程线程函数要实现的功能:保护要保护的本地进程不被关闭,说白了就是持续监视系统,看看那个要被保护的进程是否关闭了,如果关闭了就重新启动它。这要用到WaitForSingleObject() API函数。 DWORD WaitForSingleObject( HANDLEhHandle, // handle to object DWORDdwMilliseconds // time-out interval ); 这个函数是多线程编程中常用的方法,用来实现同步,我们常用的方法是给dwMilliseconds参数赋值为INFINITE,具体意义是:这个函数运行时就等待handle指向的对象的信号,因为等待时间设定为了无限,所以就无限等待, 我们编程实现时思想是:远程守护线程一经创建立即运行,然后调用WaitForSingleObject函数等候被守护进程发信号(被守护进程句柄用OpenPrcess()获得),事实上我并没有设计被等候进程给守护线程发信号,所以这个信号如果发了,就说明被守护进程被意外关闭了,于是守护线程从WaitForSingleObject中返回,想办法重启被守护进程。启动一个进程的函数我选用WinExec() (这个函数只能重启与16位windows程序兼容的exe,读者如果想改进可以使用CreateProcess来重启win32程序) UINT WinExec( LPCSTRlpCmdLine, // command line UINTuCmdShow // window style ) 参数1用来确定要重启的程序这要包括路径+程序名+启动参数,如果没有路径windows会按照默认的搜索方式搜索程序,建议读者还是填入路径的好,参数2:确定窗口显示方式使用SW_SHOW 表明显示窗口
注意:下面的讲解很重要,请读者好好听!
现在基本上算成功了,我们创建一个远程线程,在线程里面首先调用OpenProcess()得到被守护进程的句柄,接着把句柄当做参数调用WaitForSingleObject()函数并无限等待直到被守护进程关闭了,最后调用WinExec()函数在给他重新启动,线程执行完毕自动退出系统调度(因为重新执行的进程又要创建远程线程的,所以为了避免冲突,就让线程退出)。
可是离成功越近,越有当初从来没有考虑过的问题在等着你,越过去你就成功了,越不过去你就失败了。OpenProcess(),WaitForSingleObject(),WinExec()这三个函数是系统函数库中的函数,当我们的程序通过编译,在连接阶段,连接器把我们程序要用到的系统库函数代码写入我们的程序,并把这三个函数名和库中对应函数的代码关联起来,再通俗一点就是O,W,W这三个函数的代码系统已经写好了,在连接阶段系统把代码写入我们的程序,并把我们程序中的OpenProcess()的跳转地址更改为系统已经写好的OpenProcess()函数代码在我们程序虚拟空间中的起始地址。读者一定要把这个概念想明白(静态链接的思想就是如此,如果是dll动态链接思想是大同小异的),因为现在我们的问题已经来了:我们把一段代码写入其他进程的虚拟内存,而里面调用的函数跳转的地址确实我们程序自己虚拟内存空间的一个地址!!!如果忽略错误强制运行,远程进程崩溃的概率一般来说是100%
山重水复疑无路,但是别害怕,柳暗花明还又有一村呢,OpenProcess(),WaitForSingleObject(),WinExec()这三个函数都是Kernel32中定义的函数,不知道是不是一个漏洞呢?系统在每个进程中都会将kernel32.dll映射到同一个地址。所以我们只要想办法得到Kernel32.dll在本地进程的地址,然后在推算出我们用到的函数的地址,利用GetModuleHandle(),GetProcAddress()两个函数可以完成相关问题。
使用示例:
hKernel32=GetModuleHandle("Kernel32.dll");
pfn_RemoteThread_OpenProcess=GetProcAddress(hKerel 32,"OpenPrcess");
….
大不了我们定义一个结构体,里面的专门设定成员来存放 hKernel32, pfn_RemoteThread_OpenProcess,pfn_RemoteThread_Wait ForSingleObject, pfn_RemoteThread_ WinExec,然后把这个结构体作为参数传递给远程线程函数。(要这么做就必须再次调用VirtualAllocEx()在远程进程中分配空间,WriteProcessMemory()把其写入
下面是本文线程函数参数和线程函数的源代码
typedef struct _remoteparameter
{
DWORD rpWaitForSingleObject; //函数地址
DWORD rpOpenProcess; //函数地址
DWORD rpWinExec; //函数地址
//读者如果喜欢可是多弄几个函数,实现更多的功能。本例程只实现线程守护最基本
//的功能
DWORD rpProcessPID; // 被保护进程ID
HANDLE rpProcessHandle; // 被保护进程句柄
char path[MAX_PATH]; // 被保护进程在硬盘的路径,供重启用
}REMOTEPARAM;
//
DWORD WINAPI remote(LPVOID pvparam)
{
REMOTEPARAM *rp=(REMOTEPARAM*)pvparam;
typedef UINT(WINAPI *EWinExec)(LPCSTR, UINT);
typedef HANDLE(WINAPI *EOpenProcess) (DWORD, BOOL, DWORD);
typedef DWORD(WINAPI *EWaitForSingleObject)(HANDLE, DWORD);
EWinExec tWinExec;
EOpenProcess tOpenProcess;
EWaitForSingleObject tWaitForSingleObject;
tOpenProcess =(EOpenProcess)rp->rpOpenProcess;
tWaitForSingleObject =(EWaitForSingleObject)rp->rpWaitForSingleObject;
tWinExec =(EWinExec)rp->rpWinExec;
rp->rpProcessHandle=tOpenProcess(PROCESS_ALL_ACCESS ,FALSE,rp->rpProcessPID);
tWaitForSingleObject(rp->rpProcessHandle,INFINITE);
tWinExec(rp->path, SW_SHOW);
return 0;
}
本文涉及的技术已经向大家讲解完毕了,但是在实际运行时还存在进程提权问题,要对一个任意进程(包括系统安全进程和服务进程)进行指定了写相关的访问权的OpenProcess操作,只要当前进程具有SeDeDebug权限就可以了。
下面是进程提权代码:
BOOL EnablePriv()
{
HANDLE hToken;
if ( OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_ PRIVILEGES,&hToken) )
{
TOKEN_PRIVILEGES tkp;
LookupPrivilegeValue( NULL,SE_DEBUG_NAME,&tkp.Privileges[0].Luid ); //修改进程权限
tkp.PrivilegeCount=1;
tkp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges( hToken,FALSE,&tkp,sizeof tkp,NULL,NULL ); //通知系统修改进程权限
}
return 0;
}
下面是本技术的完整源代码:(参见冷风大哥)
#include
#include #include #include #include HANDLE CreateRemoteThreadProc(char* ProcessName); DWORD WINAPI remote(LPVOID pvparam); DWORD processtopid(char *processname); BOOL EnablePriv(); typedef struct _remoteparameter { DWORD rpWaitForSingleObject; DWORD rpOpenProcess; DWORD rpWinExec; DWORD rpProcessPID; HANDLE rpProcessHandle; char path[MAX_PATH]; }REMOTEPARAM; int main(int argc, char* argv[]) { HANDLE RemoteThreadHandle; EnablePriv(); cout调试方便,打开一个记事本程序,然后注入它
WaitForSingleObject(RemoteThreadHandle,INFINITE);
return 0;
}
HANDLE CreateRemoteThreadProc(char* ProcessName)
{
HANDLE ThreadHandle;
charFilePath[MAX_PATH];
cout内存失败\n");
CloseHandle(rphandle);
}
else
printf("在宿主进程中分配线程虚拟内存成功\n");
if(WriteProcessMemory(rphandle,remotethr,(LPVOID)r emote,cb,NULL)==FALSE) { printf("线程代码写入宿主进程失败\n");
CloseHandle(rphandle);
}
else
printf("线程代码写入宿主进程成功\n");
/************************************************* ****************/
/*将远程线程函数参数拷入目标进程*/
/*这里需要重定位远程线程需要的API*/
/************************************************* ****************/
REMOTEPARAM rp;
memset((char*)&rp,0,sizeof(rp));
hkernel32=GetModuleHandle("kernel32.dll");
if(hkernel32==NULL)
{
printf("hKernel32 is Error\n");
}
rp.rpProcessPID =GetCurrentProcessId();
rp.rpOpenProcess =(DWORD)GetProcAddress(hkernel32,"OpenProcess");
rp.rpWinExec =(DWORD)GetProcAddress(hkernel32,"WinExec");
rp.rpWaitForSingleObject=(DWORD)GetProcAddress(hke rnel32,"WaitForSingleObject");
_tcscpy(rp.path,FilePath);
cb=sizeof(char)*sizeof(rp);
remotepar=(PTSTR)VirtualAllocEx(rphandle,NULL,cb,M EM_COMMIT,PAGE_READWRITE);
if(remotepar==NULL)
{
printf("为远程线程分配数据栈失败\n");
CloseHandle(rphandle);
}
if(WriteProcessMemory(rphandle,remotepar,(LPVOID)& rp,cb,NULL)==FALSE)
{
printf("向远程线程数据栈写入数据失败\n");
CloseHandle(rphandle);
}
/************************************************* ****************/
/*将远程线程注入目标进程*/
/************************************************* ****************/
ThreadHandle=CreateRemoteThread(rphandle,NULL,0,(L PTHREAD_START_ROUTINE)remotethr,(LPVOID)remotepar,0 ,NULL);
if(ThreadHandle==NULL)
{
printf("远程线程失败\n");
CloseHandle(rphandle);
}
else
printf("远程线程成功运行\n");
return ThreadHandle;
} DWORD processtopid(char *processname) { DWORD lpidprocesses[1024],cbneeded,cprocesses; HANDLE hprocess; HMODULE hmodule; UINT i; TCHAR normalname[MAX_PATH]=("UnknownProcess"); if(!EnumProcesses(lpidprocesses,sizeof(lpidprocess es),&cbneeded)) { return -1; } cprocesses=cbneeded/sizeof(DWORD); for(i=0;irpOpenProcess;
tWaitForSingleObject =(EWaitForSingleObject)rp->rpWaitForSingleObject;
tWinExec =(EWinExec)rp->rpWinExec;
rp->rpProcessHandle=tOpenProcess(PROCESS_ALL_ACCESS ,FALSE,rp->rpProcessPID);
tWaitForSingleObject(rp->rpProcessHandle,INFINITE);
tWinExec(rp->path, SW_SHOW);
return 0;
}
2010年08月31日
连傻瓜都能看懂的基于代码注入的线程守护技术
Author: 叶紫孤(CPP肥兔)
(感谢冷风大哥提供技术支持) E-mail: yezigu921@126.com QQ:11916872 目标:在远程进程中注入一个线程,用来守护本地进程不被关闭
硬件平台:X86
操作系统:Windows XP Professional SP2
编程环境:VC6.0 Enterprise Edition
读者要求:知道计算机是什么,知道汇编语言是什么,由于本文中代码用C描述,所以还要知道C语言,好像只要具备这三点就够了
拜读冷风大哥的几篇技术文章后,对其给予新手的无私奉献本人由衷的膜拜,因此写此文传递冷风大哥衣钵
--写在前面的话
好了,我们正式开始:
程序执行的基本单位是什么?语句,表达式,函数,过程?说这些的立马拖出去打,正确答案是cpu的一条指令。
那cpu的指令又是什么,你随便看一本汇编语言的书就可以知道答案了。每一个可执行的程序都是由若干个指令组合到一块形成的。
那程序如何执行呢?当然是把我们编好的指令交给CPU了,让CPU在加电的情况下去取指令,去分析,去执行吧。
现在问题来了,当我们的程序在执行时,操作系统在干什么(不要忘记了操作系统本身也是由最基本的CPU指令组成)?答:当我们的指令被CPU执行时,他只好靠边站了,但是现代操作系统尤其像微软这样的流氓公司绝不会放弃对CPU的占有(毕竟一台电脑只有CPU能全面控制各个部件),他会每隔一会儿强制占有CPU运行自己的代码,这就是传说中的中断技术。
操作系统利用抢到的时间来管理系统的各个软硬件资源,这里我们只关心软件方面的本文线程守护技术涉及的方面。
它究竟利用这些时间干什么?答案是他想玩的花哨一点,比如说内存中有20个程序,他可以强制其中哪个程序下一个时间进入CPU中运行,这就是传说中的多任务并行系统基本原理。在操作系统看来你们这20个进程由我统一调配管理,老子就是老大,谁要是不听话,下一步不让你运行了,哈哈。要玩的再花哨一点,假设光一个程序中就有20个可独立运行的模块(传说中的线程),那么操作系统就可以强制确定这一个程序中的哪个模块(线程)下一个时间进入CPU运行。同样在操作系统看来你们这20个模块由我统一调配管理。
它究竟如何管理?听过系统进程链表吗?没听过就好好听,听过的可以掠过本段,操作系统(以下简称OS)把系统中所有进程以链接的方式连成一个串,它从串头可以访问到所有的进程,每当OS抢占到CPU时它就从串头到串尾访问每一个节点,顺便记录一些信息,比如001号进程给我孝敬了20块钱,002是30,003是40……..0020号是100快。嗯,好0020号是好孩子,下一次把CPU给0020号。就这样谁的优先级高谁下一次运行。等再下一次时OS发现0020号孝敬的100块钱依然是优先级最高的,于是就由0020号继续占据CPU。让系统管理技术再往前发展吧,单个进程可以有多个可执行模块时,OS这样处理:让各个进程在其内部把其的各个可执行模块(线程)也弄成一个串,OS在访问到一个进程时,就以进程内的模块为单位把所有模块也访问一遍,如果002号进程的010号可执行模块给我孝敬的钱多,下次就让002.010号线程占据CPU运行。这就是基本调度原理。为了OS统一方便管理要求进程号必须是唯一,线程号也必须是唯一的,进程和线程在标号问题上没有上下级区别(想到句柄是用来标识系统中的某资源这一概念和进程ID,线程ID时,高级读者可能会会心一笑)
现在再问开头的问题:程序执行的基本单位是什么?本作者期望的答案是线程,就是上面讲的进程中的一个可独立运行的模块。对于操作系统来说执行的基本单位是线程,理解了这一点对后面说到远程线程时是很有帮助的。
在上面讲的基础上立一个研究性课题(注意是研究性,而不是研究"性"):系统中的002号进程能不能通过"贿赂"OS在003号进程中安插一个卧底线程?注意OS并非没有人性到谁要是一分钱都不贿赂老子,老子就永远不给你运行的机会。也就是说只要是出现在进程链表中的线程是有机会得到运行的。研究性课题的答案是可行的,问题继续,如果可以那门路是什么?哈哈门路,操作系统开的门路还少吗?API函数(API简称门路,就是操作系统开辟的,用来帮助实现某种功能的门路,雅称接口)不都是一个个不同功能不同用途的门路嘛,所以最终目标被锁定在CreateRemoteThread()这个API函数上。那为什么锁定这个函数呢?因为本作者混社会的日子比较长了,固而知道要实现在其他进程中安插卧底线程的门路就是这个API!!!这里说个题外话:这是一个哲理哦,出现的问题和问题的解决方法是同时存在的,只不过经验老道的人知道门路在哪里,如果你没有能力独立解决问题,还是乖乖走该走的门路把。
让我们按照常规的路线继续走,对CreateRemoteThread() API门路进行分析:(信息参考MDSN,不知道MSDN的去Google百度一下)
PS:如果哪位前辈知道还有其他方法实现本文的相似功能,请告诉本作者,本人将怀着无限崇尚的精神对您顶礼膜拜
HANDLE CreateRemoteThread( HANDLE hProcess, // handle to process to create thread in LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes DWORD dwStackSize, // initial thread stack size, in bytes LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function LPVOID lpParameter, // argument for new thread DWORD dwCreationFlags, // creation flags LPDWORD lpThreadId // pointer to returned thread identifier ); Return Values If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value is NULL. To get extended error information, call GetLastError. PS:不要被洋文吓到,尽管我们祖先被洋人欺负了近一个世纪,但中国还是中国。
这个API门路是这样的:它接受7个参数,依次是: 1号、hProcess要安插卧底线程的进程句柄 2号、lpThreadAttributes 指向一个安全属性结构体的指针。我们直接传默认就行了 3号、dwStackSize 线程堆栈的大小,有默认值
线程堆栈是干什么的?本作者的理解是:线程函数声明的局部变量和线程函数传递的参数要放到这个堆栈中。如果理解不对请来信告知本人 4号、lpStartAddress线程函数的开始地址具体的下文会讲到 5号、lpParameter 给线程函数传递的参数的指针 (线程函数接受一个指针参数) 6号、dwCreationFlags 一个标记用来表明我们安插的卧底线程是一经创建立即执行呢还是先暂停 7号、lpThreadId 线程ID 这个参数要求我们提供一个接受ID的内存地址,如果函数执行成功他就会填充此地址 如果这个API函数执行成功,将返回我们创建的卧底线程的句柄
PS:本人也没有搞清楚进程、线程ID 和进程、线程句柄到底有什么区别,隐约感觉句柄会限制你通过句柄进行的某些动作,而ID是操作系统自身使用的标识方法。如果知道区别的前辈请告知本人
看来要成功进入这个门路,我们至少要搞定1号,4号,5号参数(找到门路了不代表你一定能把事办成,还有花点代价才行)
先看如何搞定1号:要求宿主进程的句柄,好吧我们去遍历系统的进程表找到目标进程,可是OS会大方到让你随便就遍历那张关键的表吗?绝不会,还是乖乖的从固有的API门路中想办法把,先等等,先等等,我们要把哪个进程确定为目标呢?随便找一个吧,行,可是拿什么找呢?用进程名字是最合适的,好,又离成功进了一步,现在解决这个问题:哪个API门路能帮助我们利用进程名字得到进程的其他信息呢?可惜本人没找到相关门路,后来翻啊翻啊,动用所有能用的社会关系网找到了EnumProcesses(),其用处是返回系统中所有进程的ID号,注意是系统中所有的。(这个故事告诉亲爱的读者,永远不要进行一个人的战斗,有朋友你路才能走的更快更好)。可我们要进程ID号没用啊,我们要的是进程句柄,再找门路把,看哪个API能把进程ID转化为进程句柄,找到了我们的目标就达到了,这里推荐OpenProcess()API。 BOOL EnumProcesses( DWORD * lpidProcess, // array to receive the process identifiers DWORD cb, // size of the array DWORD * cbNeeded // receives the number of bytes returned ); 这个函数接受3个参数,1号、一个数组,2号、数组大小,3号、(返回类型)里面的值表明传入的数组总共接受了进程ID的个数。
我们依样画葫芦
DWORD array_idProcess[500];//估计没有那个人的系统中能同时运行500个进程把
DWORD cb_ array_idProcess=sizeof(array_idProcess)
DWORD cbNeeded;
EnumProcesses(array_idProcess, cb_ array_idProcess, &cbNeeded);
这样调用后array_idProcess[500]就保存了系统中所有的进程的Id。 HANDLE OpenProcess( DWORD dwDesiredAccess, // access flag BOOL bInheritHandle, // handle inheritance flag DWORD dwProcessId // process identifier ); 这个函数若成功运行将返回指定的Id号的进程句柄,其同样接受三个参数,1号、访问标记:下文会重点讲,2号、句柄继承性标记:直接传FALSE,3号进程id号,可以用我们那array_idProcess[500]数组中随便一个有效的元素。
考虑这个问题:参数1号、访问标记,打开一个进程为什么要指明进程访问标记呢?这是因为OS想知道你打开一个进程想干什么,并且把你的目的记下来,一旦你试图进行超越权限的动作,他会立刻警告你的。下面是几个常用标记
PROCESS_ALL_ACCESS 所有权限都具有,偷懒人专用标记
PROCESS_QUERY_INFORMATION 允许调用相关的查询函数
PROCESS_SET_INFORMATION 允许设置相关的查询函数
PROCESS_TERMINATE 允许使用相关函数结束进程
PROCESS_VM_OPERATION 允许调用虚拟内存的相关函数
PROCESS_VM_READ 允许虚拟内存写
PROCESS_VM_WRITE 允许虚拟内存读
本人一般喜欢甭管干什么事直接使用PROCESS_ALL_ACCESS就搞定了,但还是推荐细心的读者设定具体的操作权限
同样依样画葫芦:
HANDLE hProcess;//进程句柄;
hProcess=OpenProcess(PROCESS_ALL_ACCESS,false, array_idProcess[随便一个值];
这样就搞定了CreateRemoteThread API的1号参数:目标进程句柄,如果我现在接着往下讲4号,5号参数的话,读者一定会认为本人太不负责任了,随便搞个进程句柄糊弄人,呵呵,好吧,让我们实现用指定进程名找到指定进程ID的方法:
有了进程句柄就能够调用相关的进程API函数,EnumProcessModules()和GetModuleBaseName()这两个家伙;
其中EnumProcessModules()返回有进程句柄标识的进程中所有的模块,注意这里的模块和前面讲的模块不一样,在Windows XP背景下,一个进程的模块有 可执行文件+载入的dll文件,我相信读者只对可执行文件的名字感兴趣,应为他正好就是进程名,有了可执行模块的句柄,就可以在调用GetModuleBaseName()得到进程名字。哈哈,问题解决了,只要我们判断一下得到的名字是不是我们指定的进程名,如果不是就换下一个进程句柄接着去名字判断,还不是就再换下一个,除非我们指定的文件名拼写错了,否则一定可以找到目标进程。
这里的模块问题是有是有点眩晕,其实很简单的,我们讨论问题的范围不同,模块就可能有不同的意思,比如在全球范围来考虑:中国就是一个模块,放中国范围来考虑:北京就是一个模块。不知道什么是dll的,没关系,因为本文不需要dll相关知识,确实想知道的请出门左拐找政府
言归正传开始分析这两个函数 BOOL EnumProcessModules( HANDLE hProcess, // handle to the process HMODULE * lphModule, // array to receive the module handles DWORD cb, // size of the array LPDWORD lpcbNeeded // receives the number of bytes returned ); 篇幅所限,参数不再明述(是个人都能看懂吧)。这里有个小技巧,因为接受的第一个hModule一定是可执行文件的,所以我们就没有必要定义一个HMODULE类型数组去接受那些既占地方有在本文中没有用处的标识dll模块的句柄
调用方法:
HMODULE hModule;
DWORD cb=sizeof(HMODULE);
DWORD cbNeeded;
EnumProcessModules(hProcess,&hModule,cb,&cbNeeded) ;
容我再罗嗦一下,句柄是用来标识资源的,这里我们的hModule标识了由hProcess标识的进程的可执行文件的模块。
现在调用GetModuleBaseName()得到进程可执行模块的名字(亦是进程名) DWORD GetModuleBaseName( HANDLE hProcess, // handle to the process HMODULE hModule, // handle to the module LPTSTR lpBaseName, // buffer that receives the base name DWORD nSize // size of the buffer); 参数自己分析,这里要讲的是函数名,这个GetModuleBaseName是不是名字起的有点怪,无缘无故放个base到中间,差点害大家找不到API门路。问一个问题文件名由什么组成?答:名字+类型的拖出去打,确切答案是存储路径+名字+类型,这样才可以唯一标识一个文件!请问读者你的姓名放中国能唯一标识你吗?再加个地名吧,建议你以后叫中国西部新疆准格尔盆地张家沟五组X某某。base的意思是只取文件的名字+类型,忽略路径。其实GetModuleBaseName还有一个孪生哥哥叫GetModuleFileNameEx,区别?本作者笑而不语。 OK,OK,距离阶段性胜利还有一小步了,我们把EnumProcesses(), OpenProcess(), EnumProcessModules(),GetModuleBaseName()这四个函数封装到一个我们自己定义的函数中,用来实现通过给定的进程名找到进程ID,进程同名的只找排名靠前的。
下面是本文中技术实现的第一个模块源代码
DWORD processtopid(char *processname)
{
DWORD lpidprocesses[1024],cbneeded,cprocesses;
HANDLE hprocess;
HMODULE hmodule;
UINT i;
TCHAR normalname[MAX_PATH]=("UnknownProcess");
if(!EnumProcesses(lpidprocesses,sizeof(lpidprocess es),&cbneeded))
{
return -1;
}
cprocesses=cbneeded/sizeof(DWORD);
for(i=0;i不能像我们自定义函数一样胡写,先看看标准形式吧
DWORD WINAPI ThreadProc( LPVOID lpParameter // thread data ); 除了ThreadProc这个函数名可以改变外,其他部分不可改动。他接受一个指向空类型的指针作为参数。这摆明了是让读者进行强制指针类型转换嘛。
现在参数4号,参数5号都搞定了,是不是可以调用CreateRemoteThread()函数了?让本作者自问自答吧,No,绝对不行,并不是前面我们做错了什么,而是后面我们还有很多没有做。 还记得OpenProcess()有个参数叫访问权限吗?忘记了的看其参数1,其中本作者列出的权限指定的后三个都和虚拟内存有关,那什么是虚拟内存呢?这可是一重头戏啊,你准备好了吗? 知道什么是内存吗?就是那个传说中存储了程序的指令和数据的地方,cpu从这里取指令和数据进行运算的。但是紧紧知道这些对本文来说就太小儿科了,我们要重点理解虚拟内存。这里我把实际存在的物理内存比作一个存货的仓库。假设这个仓库的容量是512个单位,当来这里存货的进程比较少比较小时,它还可以很好的运行,但是这终究有隐患就是超出容量了怎么办?技术的进步使得OS发明了虚拟内存概念,当OS全面接收内存管理后,他在实际物理内存上增加了一个层次叫虚拟内存,他规定要在内存中存货、取货的进程把货物或者存货请求直接发给他,由他来分配你货物的存取,并且他给了你4096的仓库容量(总线只有32位),不光给你是4096,给所有来存储的进程都是4096,但是不要忘记了实际的物理储量只有512,看似很荒谬吧,其实这是有可行性的,这就是:程序的局部访问性原理,程序的基本单位是指令,指令的执行是一条一条顺序进行的,就算出现了循环,跳转,它都基本上是在一个很小的范围内进行的,换句话说就算你的程序有4096这么大,你都不可能在这么大的空间内无序运行,对不对?再通俗一点,就算你有100万,你也不会把它天天带在身上吧,所以操作系统这么处理,比如按10个单位为一组,即把程序的代码切成若干片,每片10个单位,要运行哪片,就把哪片装入物理内存,要运行下一片了,就把原先的踢出,装入下一片。如果每个进程严格执行10个单位的片,那么我们的仓库中就可以供51.2个进程同时运行,而不会觉得卡!!!这就是虚拟内存的基本原理,操作系统虚拟了内存的存取,货物具体存到哪里,进程不用关心,他根本就不知道,无论何时你需要那部分,操作系统都可以给你弄到。 因此两个进程在实际物理内存上没有任何关系,他们都各自在各自的虚拟空间内运行。所以我们讨论的内存问题都是在这虚拟的层面上进行的。我们首先要做的,就是在别人的运行空间中挖上一点地方,来存放我们定义的货物(代码)。 代码注入的关键:首先就是在其他进程的虚拟内存中申请一块空间,然后把我们的代码写入这个空间,最后想办法让代码被操作系统仍给cpu执行! 申请虚拟内存函数是:VirtualAllocEx() LPVOID VirtualAllocEx( HANDLEhProcess, // process to allocate memory LPVOIDlpAddress, // desired starting address SIZE_TdwSize, // size of region to allocate DWORDflAllocationType, // type of allocation DWORDflProtect // type of access protection ); 有5个参数,参数1要分配内存的进程句柄,用OpenProcess()获得。参数2:期望的其实地址,可以传NULL,让系统自动分配,在本例中我们确实对代码的其实地址没有兴趣。参数3 分配大小,根据自己代码的大小估计一个值吧,参数4 分配类型,本例使用MEM_COMMIT,也是常用的,具体参见MSDN,参数5 访问类型保护,指定申请一块内存是干什么用,读?写?执行? 本例选择PAGE_EXECUTE_READWRITE
函数如果执行成功会返回分配的内存的起始地址,我们要想办法把这个地址存起来。这样代码注入的第一步就成功了,接着看第二步,把我们编写好的代码写入第一步分配的内存空间,这次使用的函数是:WriteProcessMemory() BOOL WriteProcessMemory( HANDLEhProcess, // handle to process LPVOIDlpBaseAddress, // base of memory area LPCVOIDlpBuffer, // data buffer SIZE_TnSize, // count of bytes to write SIZE_T *lpNumberOfBytesWritten // count of bytes written ); 第一个参数和上面的相同,第二个参数 写入的内存基址,使用上个函数的返回值 ,第三个参数 要写入的数据的基址,这里传递我们设计好的线程函数。,第四个参数传递第三个参数指定的数据块的大小,第五个参数用系统帮我们填充总共传递了多少个,可以传NULL,因为我们没有必要知道。通过依次调用这两个函数就可以把代码注入目标进程了,这正好解决了CreateRemoteThread()函数的第四个参数:线程函数的开始地址,这里请读者多想一步,既然代码是我们自己注入的,那调用CreateRemoteThread()能起到什么效果呢?其实CreateRemoteThread()此函数仅仅是通知操作系统把这里指定的进程的指定代码作为一个进程的一个线程加入到系统调用链表中而已。至此,基本上注入原理就是这些了,亲爱的读者你明白了吗?如果以上文字您已看明白,那么恭喜您,您可以继续向下看,看看如何编程实现线程守护让我们开始编程实现吧:
首先考虑我们的远程线程函数要实现的功能:保护要保护的本地进程不被关闭,说白了就是持续监视系统,看看那个要被保护的进程是否关闭了,如果关闭了就重新启动它。这要用到WaitForSingleObject() API函数。 DWORD WaitForSingleObject( HANDLEhHandle, // handle to object DWORDdwMilliseconds // time-out interval ); 这个函数是多线程编程中常用的方法,用来实现同步,我们常用的方法是给dwMilliseconds参数赋值为INFINITE,具体意义是:这个函数运行时就等待handle指向的对象的信号,因为等待时间设定为了无限,所以就无限等待, 我们编程实现时思想是:远程守护线程一经创建立即运行,然后调用WaitForSingleObject函数等候被守护进程发信号(被守护进程句柄用OpenPrcess()获得),事实上我并没有设计被等候进程给守护线程发信号,所以这个信号如果发了,就说明被守护进程被意外关闭了,于是守护线程从WaitForSingleObject中返回,想办法重启被守护进程。启动一个进程的函数我选用WinExec() (这个函数只能重启与16位windows程序兼容的exe,读者如果想改进可以使用CreateProcess来重启win32程序) UINT WinExec( LPCSTRlpCmdLine, // command line UINTuCmdShow // window style ) 参数1用来确定要重启的程序这要包括路径+程序名+启动参数,如果没有路径windows会按照默认的搜索方式搜索程序,建议读者还是填入路径的好,参数2:确定窗口显示方式使用SW_SHOW 表明显示窗口
注意:下面的讲解很重要,请读者好好听!
现在基本上算成功了,我们创建一个远程线程,在线程里面首先调用OpenProcess()得到被守护进程的句柄,接着把句柄当做参数调用WaitForSingleObject()函数并无限等待直到被守护进程关闭了,最后调用WinExec()函数在给他重新启动,线程执行完毕自动退出系统调度(因为重新执行的进程又要创建远程线程的,所以为了避免冲突,就让线程退出)。
可是离成功越近,越有当初从来没有考虑过的问题在等着你,越过去你就成功了,越不过去你就失败了。OpenProcess(),WaitForSingleObject(),WinExec()这三个函数是系统函数库中的函数,当我们的程序通过编译,在连接阶段,连接器把我们程序要用到的系统库函数代码写入我们的程序,并把这三个函数名和库中对应函数的代码关联起来,再通俗一点就是O,W,W这三个函数的代码系统已经写好了,在连接阶段系统把代码写入我们的程序,并把我们程序中的OpenProcess()的跳转地址更改为系统已经写好的OpenProcess()函数代码在我们程序虚拟空间中的起始地址。读者一定要把这个概念想明白(静态链接的思想就是如此,如果是dll动态链接思想是大同小异的),因为现在我们的问题已经来了:我们把一段代码写入其他进程的虚拟内存,而里面调用的函数跳转的地址确实我们程序自己虚拟内存空间的一个地址!!!如果忽略错误强制运行,远程进程崩溃的概率一般来说是100%
山重水复疑无路,但是别害怕,柳暗花明还又有一村呢,OpenProcess(),WaitForSingleObject(),WinExec()这三个函数都是Kernel32中定义的函数,不知道是不是一个漏洞呢?系统在每个进程中都会将kernel32.dll映射到同一个地址。所以我们只要想办法得到Kernel32.dll在本地进程的地址,然后在推算出我们用到的函数的地址,利用GetModuleHandle(),GetProcAddress()两个函数可以完成相关问题。
使用示例:
hKernel32=GetModuleHandle("Kernel32.dll");
pfn_RemoteThread_OpenProcess=GetProcAddress(hKerel 32,"OpenPrcess");
….
大不了我们定义一个结构体,里面的专门设定成员来存放 hKernel32, pfn_RemoteThread_OpenProcess,pfn_RemoteThread_Wait ForSingleObject, pfn_RemoteThread_ WinExec,然后把这个结构体作为参数传递给远程线程函数。(要这么做就必须再次调用VirtualAllocEx()在远程进程中分配空间,WriteProcessMemory()把其写入
下面是本文线程函数参数和线程函数的源代码
typedef struct _remoteparameter
{
DWORD rpWaitForSingleObject; //函数地址
DWORD rpOpenProcess; //函数地址
DWORD rpWinExec; //函数地址
//读者如果喜欢可是多弄几个函数,实现更多的功能。本例程只实现线程守护最基本
//的功能
DWORD rpProcessPID; // 被保护进程ID
HANDLE rpProcessHandle; // 被保护进程句柄
char path[MAX_PATH]; // 被保护进程在硬盘的路径,供重启用
}REMOTEPARAM;
//
DWORD WINAPI remote(LPVOID pvparam)
{
REMOTEPARAM *rp=(REMOTEPARAM*)pvparam;
typedef UINT(WINAPI *EWinExec)(LPCSTR, UINT);
typedef HANDLE(WINAPI *EOpenProcess) (DWORD, BOOL, DWORD);
typedef DWORD(WINAPI *EWaitForSingleObject)(HANDLE, DWORD);
EWinExec tWinExec;
EOpenProcess tOpenProcess;
EWaitForSingleObject tWaitForSingleObject;
tOpenProcess =(EOpenProcess)rp->rpOpenProcess;
tWaitForSingleObject =(EWaitForSingleObject)rp->rpWaitForSingleObject;
tWinExec =(EWinExec)rp->rpWinExec;
rp->rpProcessHandle=tOpenProcess(PROCESS_ALL_ACCESS ,FALSE,rp->rpProcessPID);
tWaitForSingleObject(rp->rpProcessHandle,INFINITE);
tWinExec(rp->path, SW_SHOW);
return 0;
}
本文涉及的技术已经向大家讲解完毕了,但是在实际运行时还存在进程提权问题,要对一个任意进程(包括系统安全进程和服务进程)进行指定了写相关的访问权的OpenProcess操作,只要当前进程具有SeDeDebug权限就可以了。
下面是进程提权代码:
BOOL EnablePriv()
{
HANDLE hToken;
if ( OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_ PRIVILEGES,&hToken) )
{
TOKEN_PRIVILEGES tkp;
LookupPrivilegeValue( NULL,SE_DEBUG_NAME,&tkp.Privileges[0].Luid ); //修改进程权限
tkp.PrivilegeCount=1;
tkp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges( hToken,FALSE,&tkp,sizeof tkp,NULL,NULL ); //通知系统修改进程权限
}
return 0;
}
下面是本技术的完整源代码:(参见冷风大哥)
#include
#include #include #include #include HANDLE CreateRemoteThreadProc(char* ProcessName); DWORD WINAPI remote(LPVOID pvparam); DWORD processtopid(char *processname); BOOL EnablePriv(); typedef struct _remoteparameter { DWORD rpWaitForSingleObject; DWORD rpOpenProcess; DWORD rpWinExec; DWORD rpProcessPID; HANDLE rpProcessHandle; char path[MAX_PATH]; }REMOTEPARAM; int main(int argc, char* argv[]) { HANDLE RemoteThreadHandle; EnablePriv(); cout调试方便,打开一个记事本程序,然后注入它
WaitForSingleObject(RemoteThreadHandle,INFINITE);
return 0;
}
HANDLE CreateRemoteThreadProc(char* ProcessName)
{
HANDLE ThreadHandle;
charFilePath[MAX_PATH];
cout内存失败\n");
CloseHandle(rphandle);
}
else
printf("在宿主进程中分配线程虚拟内存成功\n");
if(WriteProcessMemory(rphandle,remotethr,(LPVOID)r emote,cb,NULL)==FALSE) { printf("线程代码写入宿主进程失败\n");
CloseHandle(rphandle);
}
else
printf("线程代码写入宿主进程成功\n");
/************************************************* ****************/
/*将远程线程函数参数拷入目标进程*/
/*这里需要重定位远程线程需要的API*/
/************************************************* ****************/
REMOTEPARAM rp;
memset((char*)&rp,0,sizeof(rp));
hkernel32=GetModuleHandle("kernel32.dll");
if(hkernel32==NULL)
{
printf("hKernel32 is Error\n");
}
rp.rpProcessPID =GetCurrentProcessId();
rp.rpOpenProcess =(DWORD)GetProcAddress(hkernel32,"OpenProcess");
rp.rpWinExec =(DWORD)GetProcAddress(hkernel32,"WinExec");
rp.rpWaitForSingleObject=(DWORD)GetProcAddress(hke rnel32,"WaitForSingleObject");
_tcscpy(rp.path,FilePath);
cb=sizeof(char)*sizeof(rp);
remotepar=(PTSTR)VirtualAllocEx(rphandle,NULL,cb,M EM_COMMIT,PAGE_READWRITE);
if(remotepar==NULL)
{
printf("为远程线程分配数据栈失败\n");
CloseHandle(rphandle);
}
if(WriteProcessMemory(rphandle,remotepar,(LPVOID)& rp,cb,NULL)==FALSE)
{
printf("向远程线程数据栈写入数据失败\n");
CloseHandle(rphandle);
}
/************************************************* ****************/
/*将远程线程注入目标进程*/
/************************************************* ****************/
ThreadHandle=CreateRemoteThread(rphandle,NULL,0,(L PTHREAD_START_ROUTINE)remotethr,(LPVOID)remotepar,0 ,NULL);
if(ThreadHandle==NULL)
{
printf("远程线程失败\n");
CloseHandle(rphandle);
}
else
printf("远程线程成功运行\n");
return ThreadHandle;
} DWORD processtopid(char *processname) { DWORD lpidprocesses[1024],cbneeded,cprocesses; HANDLE hprocess; HMODULE hmodule; UINT i; TCHAR normalname[MAX_PATH]=("UnknownProcess"); if(!EnumProcesses(lpidprocesses,sizeof(lpidprocess es),&cbneeded)) { return -1; } cprocesses=cbneeded/sizeof(DWORD); for(i=0;irpOpenProcess;
tWaitForSingleObject =(EWaitForSingleObject)rp->rpWaitForSingleObject;
tWinExec =(EWinExec)rp->rpWinExec;
rp->rpProcessHandle=tOpenProcess(PROCESS_ALL_ACCESS ,FALSE,rp->rpProcessPID);
tWaitForSingleObject(rp->rpProcessHandle,INFINITE);
tWinExec(rp->path, SW_SHOW);
return 0;
}