By HKE.. 大家好 今天讲讲 线程插入技术 最近忙着考试,准备得有点仓促,见谅 没下载预习包的朋友下载下吧 里面有写资源 http://www.wg333.com/pick.aspx?down=ok&filename=lesson_pack_hke.rar&filepath=hke/upfiles/lesson_pack_hke.rar 以前玩过远程控制的朋友可能经常看过那些所谓的木马(当然比较强一点的)一般都声称自己采用了线程插入技术或者DLL注入技术、远程线程插入等等 这到底是什么?有什么用呢?怎么实现呢? 今天我简单的讲讲(高手就不用听了) 如果想边听边跟着我的代码调试需要NT系统,如果谁是9X/Me说一声你的某些代码需要稍微改下 只是听就不要紧了 首先要明白什么是线程 说到线程就不得不提到进程 SDK文档里是这样描述的 进程是一个正在运行的程序,它拥有自己的地址空间,拥有自己的代码、数据和其他系统资源。一个进程包含了一个或者多个运行在此进程内的线程。 说白了,其实进程就是一个磁盘上的程序载入内存执行后的东西 打开任务管理器看看,里面有好多进程... 从定义上看出进程一定要有线程,线程是进程内存中的独立实体。 讲了这么多废话 其实就是想说程序运行后就变成了进程,你的代码由线程来执行。 回到正题上,线程插入是什么呢?就是把一个线程弄到别的进程执行 你也许觉得奇怪,自己不是有个进程好好的吗? 其实这个作用大了,先说说开头提到的木马 一来,有些比较由经验的用户有时会打开任务管理器,发现陌生进程?!想想你的木马还能活吗? 那么插入Explorer等关键进程,你还想用电脑的话最好不要杀那个进程(不信试试) 二来,大家现在都装防火墙了,网络连接经常会被虑掉,木马岂不是没用了? 好,插入IE,防火墙想虑?除非你不想看网页 当然,还有很多用处,比如插入系统级别的进程,哈哈,我们有Ring0权限了,(不要告诉我你不知道Ring0...就是连物理内存都可以写的超级权限) HookAPI也需要把代码注入别的进程 知道了他的作用想知道怎么实现吧? 别着急,慢慢来 首先,这个应该属于系统编程,系统编程就不能不知道API API是什么?简单点讲就是Windows为我们提供的一些函数,利用他们我们可以做很多事情 Delphi花了很大力气用VCL还有一些乱七八糟的类把让我们可以尽量不要API 所以会编程序但不了解API还是可能的 API被封装在kernel32.dll、user32.dll等动态链接库里,程序使用时候把DLL映射到自己的内存了 编写的时候我们通常需要从DLL导出这些函数 function Beep; external 'kernel32.dll' name 'Beep'; 这样导出一个beep函数 当然,实际上不要这么麻烦 Delphi已经把这些导出声明写在一些单元里面了(比如windows单元) uses windows后就可以直接用了 (而且建立窗口时候默认就会引用很多单元) 所以实际上我们可以直接用的 这个理解一下就好了 开始讲线程插入了 一般来说线程插入有2种方法 1.DLL注入 2.直接的远程线程插入 DLL注入编写的时候比较简单,方法也多,但有个缺点进程会多出个模块来,可能被发现 远程线程是直接修改对方内存的方法,虽然隐蔽性好,但是不小心可能会出点问题,比如你让不能有界面的进程弹出个窗口,不能上网的进程开个端口,那就等着系统崩溃吧 今天重点讲DLL注入 首先要知道什么是DLL,dll就是动态链接库,大家应该知道吧? 怎么编写DLL呢? 和写普通程序差不多是一样的 新建一个工程,选DLL Wizard 发现了吧?除了program改成library 剩下几乎是一样的,只是需要程序加载他的入口点 我们先编写个简单的DLL library TestDll; uses Windows;{$R *.res} procedure func_a; begin MessageBox(0,'I love delphi','Function form Tset DLL',0); end; procedure func_b(MSG:pchar); begin MessageBox(0,MSG,'Function form Tset DLL',0); end; begin func_a; func_b('I like it too!'); end. 就是那个testdll.dpr 看得懂吧,弹出2个信息框 好了保存下,F9运行..出错,哈哈,DLL是不能直接运行的 那怎么办?编译下(按过F9就不用了,也会编译好) 看见那个DLL了吧? 我们弄个程序加载它的入口点 新建一个普通程序 加一个按钮 按钮事件只要写一句loadlibrary('testdll.dll'); MainShow.dpr 运行,单击按钮,怎么养?弹出东西了吧 当然DLL还可以做函数库,资源库等今天暂不讨论 现在DLL懂得写了吧?就是program改成library而已 你可以写自己的程序了 DLL会写了,现在的问题就是怎么注入了 我们目的只是让对方的程序运行一句loadlibrary('testdll.dll');而已 一切就OK了 通常有这么几种注入方法 1.利用全局消息钩子 Win32下程序一般都要用收发消息 用钩子函数下全局钩子,程序收到任何消息都加载我们的DLL的入口点 当然,用这种方法DLL进入后要判断自己是不是被插进目标进程 是的话,执行代码 不是退出 这个方法很麻烦但通用性很好,只要WINDOWS都可以用(但是有的系统进程消息是勾不住的,所以注入不了) 早期的DLL注入大部分是用这个原理实现的 2.写注册表 写HKEY_LOCAL_MAHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs 但是只能是NT下,而且必须是调用过user32.dll(Windows的一个内核,只要有界面的程序都调用它)的程序,在开机后所有程序会自动加载DLL,但是这个dll不能被卸载,而且不能调用某些函数,不然系统会挂掉(有危险性哦) 不推荐使用 3.利用远程线程注入来实现DLL注入 只要能打开句柄,就能成功(强吧?)而且实现起来比较简单 缺点就是9X内核的有点难度(也不是很困难) 我们今天就讲利用远程线程注入来实现DLL注入吧! 你可能要问,既然刚才说了远程线程可以直接注入一个线程,为什么还要多此一举反过来再调用DLL呢? 这是因为,远程线程技术一般是直接对目标程序的内存进行操作 我们知道不同程序的虚拟内存是不一样的 所以很多函数的地址不一定一样 而程序运行的时候实际上是Call函数地址进行函数调用的 所以我们要注意计算很多偏移之类的 这是非常烦琐的事情 而且像上面说的让不能有界面的进程弹出个窗口,那就不好玩了 而DLL呢? DLL调用时其实是被映射到进程内存里面 DLL拥有自己的导入表、资源、函数等东西,实际上就是一个完整的程序 映入内存后和执行一个程序效果是一样的 这样我们就不用考虑那些乱七八糟的东西,只要安心的写功能即可 好了, 要明白远程线程首先当然要把程序本地线程搞清楚了 不知道大家编多线程程序的时候是不是都用tthread类? 反正我是不喜欢那个 我们看看Windows给我们的原始API吧(tthread类也是用它写的) function CreateThread( lpThreadAttributes: Pointer; //安全指针一般nil就可以了 dwStackSize: DWORD; //线程初始化尺寸,一般用0,获得与主线程一样尺寸(不够自己会增加,别担心) lpStartAddress: TFNThreadStartRoutine; //一个指向要执行线程函数的指针,这个函数必须遵守stdcall约定,并且可带一个参数,参数必须是指针类型 lpParameter: Pointer; //函数的参数 dwCreationFlags: DWORD; //控制创建标志,用0表示线程立刻执行 var lpThreadId: DWORD) //返回标识变量我觉得没什么用,反正句柄都有了 : THandle; //返回线程的句柄 stdcall;//标准调用 Windows下API一般都是标准调用 大家先看下 看起来似乎比较复杂,等下举个例子 我们把DLL源码里面的func_b拷到刚才那个EXE上 稍微修改下 procedure func_b(MSG:pchar); stdcall; begin MessageBox(0,MSG,'Function form Tset DLL',0); sleep(10000);//线程暂停N久(不超过10s) end; 加上2个按钮 第一个 procedure TForm1.Button2Click(Sender: TObject); begin func_b('123'); end; 第二个 procedure TForm1.Button3Click(Sender: TObject); var tid:longword;//放返回值,不放她不让执行,郁闷 str:pchar;//便于获得pointer begin str:='123'; createthread(nil, 0, @func_b, //函数名前面加@是得到函数指针 pointer(str),//虽然str也是指针,但是delphi就是要pointer型的,那就转一下类型 0 , tid);//tid纯属放着占格式的,一般我们用不到 end; //上面CreateThread看得懂吧,几乎都是默认设置,以后套下去用就是了 实际上都是调用func_b,只是第二个过程用了信新线程 但是效果是不一样的 第一个按钮按下弹出窗口后,程序卡死了(暂停10000) 第二个却不会 为什么呢? 我们可以这样理解 窗口看做一个主线程,执行func_b,弹出窗口,然后主线程挂起,于是卡死了 而第二个过程创建一个新线程,新线程执行func_b,弹出窗口,挂起10000,但是由于主线程没有挂起,所以看起来关掉窗口后没什么事情发生(实际上那个线程还在偷偷执行,直到线程代码运行完,只是它卡死不会影响你) 这个如果明白了那么下面就容易理解了 看看这个函数 function CreateRemoteThread( hProcess: THandle; lpThreadAttributes: Pointer; dwStackSize: DWORD; lpStartAddress: TFNThreadStartRoutine; lpParameter: Pointer; dwCreationFlags: DWORD; var lpThreadId: DWORD) : THandle; stdcall; 除了函数名不一样,下面的参数多了个hProcess: THandle;,剩下的完全一样 呵呵,这个东西就是本节课的关键了 先看函数名就知道是干什么用的了 ‘创建远程线程’ 用法和刚才基本一致 就是hProcess: THandle是什么呢? 这里要填的是被注入线进程的句柄 什么是句柄? 打个比方,对象是一个门,句柄就是那个把手,通过句柄我们可以对门进行操作 也就是说我们利用句柄来操作某些东西(包括进程、线程等等) 你有没有注意到,CreateThread和CreateRemoteThread都返回一个THandle,也就是线程的句柄 还有loadlibrary也会返回DLL的句柄,我们可以利用他们对相关对象进行操作 那么怎么获得进程句柄呢? 一般采用先得到进程PID再用下面的函数取得句柄 function OpenProcess( dwDesiredAccess: DWORD; //访问标志一般填写 PROCESS_ALL_ACCESS,这样这个句柄可以获得最大操作权限 bInheritHandle: BOOL; //可否继承,这个跟子程序有关,无所谓了,填false和true都可以,反正我们自己能操作久可以 dwProcessId: DWORD): //要获得句柄的进程ID THandle; stdcall;//返回句柄 有时候会返回0,说明打开句柄失败了 一般是你的权限不够(比如你想对Winlogon这些系统级程序操作) 这时候我们需要提升权限 一般Debug权限就可以了(其实操作权限里面最高了) 提升的过程我写好了 直接调用就可以了(修改进程令牌到Debug级别,为什么这样写这里不详细讲了,自己去网上搜索下) procedure GetDebugPrivs; var hToken: THandle; tkp: TTokenPrivileges; retval: dword; begin If (OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, hToken)) then begin LookupPrivilegeValue(nil, 'SeDebugPrivilege' , tkp.Privileges[0].Luid); tkp.PrivilegeCount := 1; tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, False, tkp, 0, nil, retval); end; end; 不会晕吧? 应该记得我刚才提到了要PID,那怎么得到呢? 一般用FindWindow和GetWindowThreadProcessId配合的到 这样写 先var Pid:longword;//储存那个PID GetWindowThreadProcessId(FindWindow('Notepad', nil), @PID); 这样就找到笔记本的PID,再如'Shell_TrayWnd'可以找到Explorer的 窗口类名据说可以用SPY++查询,不过这东西我没见过,呵呵 当然还可以枚举进程判断进程名等等 这个先告一段落。 好了,拿Windows的笔记本下手吧 procedure TmyForm.Button4Click(Sender: TObject); var h:longword; //PID和THandle 的类型其实都是longword,改个名字而已,所以可以通用 begin winexec('notepad',1);//运行笔记本 GetWindowThreadProcessId(FindWindow('notepad', nil), @h);//得到Pid存在h h:=OpenProcess(PROCESS_ALL_ACCESS, False, h); //得到handle存在h,后面那个是变量pid,算完放到前面的h是句柄(两个不同的东西,只是类型一样而已) sleep(2000);//等2秒 TerminateProcess(h,0);//关闭笔记本,h是那个句柄,0表示正常退出 end; 运行起来就是打开一个笔记本,大约2s狗关掉它 不知道大家看懂了没有,没有不要紧,只是为了证明我们可以拿到一个课操作的进程句柄 好像万事具备了吧?那试试远程线程了吧 再建一个按钮 前面的还是这样写,再把那个建立线程的拷过来 改成CreateRemoteThread加上h参数 procedure TmyForm.Button5Click(Sender: TObject); var h:longword; tid:longword; str:pchar; begin str:='123'; winexec('notepad',1); GetWindowThreadProcessId(FindWindow('notepad', nil), @h); h:=OpenProcess(PROCESS_ALL_ACCESS, False, h); CreateRemoteThread(h,nil, 0, @func_b, pointer(str), 0 , tid); end; 运行起来 笔记本出来了,对话框也出来了... 可是对话框却不是我们弄的那个,是个报错的 看看写了什么 内存'0x00000000'不能为'writen' 为什么呢?记得我刚才说的么 远程线程是在别的程序里运行一个线程 相当于让里一个函数执行CreateThread 所以,函数的地址不一定是一样的,更何况笔记本里面怎么可能会有func_b这个我们自己写的函数呢 这么一来当然要出错了 这下傻了,那怎么注入我们要的函数呢? 记得我们要讲什么吗?-利用远程线程进行DLL注入 我们可以把函数写在DLL里面,用远程线程让目标进程加载它 这样函数就执行了 我们只要想办法让对方程序loadlibrary('testdll.dll');那就OK了 看看LoadLibrary的原型 function LoadLibrary(lpLibFileName: PAnsiChar): HMODULE; stdcall; 你应该发现了它和线程要求的函数格式几乎一样 参数是指针型PAnsiChar就是pchar,一个指向字符串的指针 返回HMODULE,HMODULE实质是longword(改个名字而已) ^_^,那就远程运行它吧 这时候你可能会想,LoadLibrary的地址要怎么得到呢? 要知道,LoadLibrary是一个API(在Kernel32.dll里面),实际上,每个Win32程序都需要里面的函数 所以,大部分程序运行代码前会装入这个DLL,把里面的函数映射到自己的内存了 这么一来,只要是这个DLL里面同一个函数在所有的进程里地址都是一样的 哈哈,这样就容易了 地址我们一般用GetProcAddress function GetProcAddress( hModule: HMODULE; //模块句柄,DLL被加载后就成立模块,等下告诉大家怎么得到这个 lpProcName: LPCSTR //函数在DLL中的导出名LoadLibrary实际上是LoadLibraryA //这个大家看看DelphiWindows单元的源码就知道了 ): FARPROC; stdcall;//返回指针 那些类型看得乱乱的吧,不要管他们,在Delphi上不鼠标停在函数上,类型的原型就出来了 好了 现在是怎么得到那个模块的句柄的问题 用GetModuleHandle function GetModuleHandle( lpModuleName: PChar)//模块名,DLL被加载后就成立模块,所以就是DLL的文件名了 : HMODULE; stdcall;//返回模块句柄 好了。知道了这些得到函数地址就容易了 GetProcAddress(GetModuleHandle('KERNEL32.DLL'), 'LoadLibraryA'); 一句搞定 问题似乎都解决了吧? 先别高兴,不要忘记了,它还带了个参数,就是那个DLL的名字 参数类型是一个指向字符串地址的指针 这个是个大问题,一来你不能保证别人的程序内存里有这个字符串 二来有你也不知道他的位置,这可怎么办呢? 自己写! 我们把那个字符串写到对方内存里 呵呵,很霸道的方法,但的确是个好方法 不废话了,开始 我们首先要在目标进程申请一块内存,以便把那个参数写进去 申请内存用VirtualAllocEx,看看它的原型 function VirtualAllocEx( hProcess: THandle;//目标进程句柄,这个不用说了吧 lpAddress: Pointer;//分配内存位置,一般用nil,这样会在系统认为最合适的位置分配 dwSize: DWORD;//分配的地址范围,也就是大小了 flAllocationType: DWORD;//如何分配地址,一般用MEM_COMMIT为指定空间提交物理内存 flProtect: DWORD//该段内存的保护类型,PAGE_READWRITE表示可读可写 ): Pointer; stdcall;//返回内存地址,哈哈,这就是我们要的那个参数的指针了 好了,分配完内存当然是要把我们的数据写过去了 这时候需要用到WriteProcessMemory来写进程的内存 function WriteProcessMemory( hProcess: THandle; //目标进程句柄 const lpBaseAddress: Pointer; //要写的内存地址,就填我们那个参数的指针 lpBuffer: Pointer; //数据的地址,我们把字符串存这里,让他拷 nSize: DWORD; //要拷贝的数据长度 //字符串在Windows定义是以null(就是16进制的0)结尾的 //所以长度就是字符串的长度+1 var lpNumberOfBytesWritten: DWORD)//返回的什么东西,没什么用 : BOOL; stdcall; //返回成功或失败 我们来写个完整的代码吧 procedure TmyForm.Button6Click(Sender: TObject); var h:longword; //放句柄,中间顺便暂放下PID tmp:longword;//这个专门来占格式收集垃圾 DllName:pchar; Mysize:longword;//放字符串长度 Parameter:pointer;//放那个参数的指针(位置在目标进程内) begin DLLName:='Testdll.dll'; Mysize:=strlen(Dllname)+1; winexec('notepad',1); GetWindowThreadProcessId(FindWindow('notepad', nil), @h); h:=OpenProcess(PROCESS_ALL_ACCESS, False, h); Parameter:= VirtualAllocEx(h, nil, Mysize, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(h, Parameter, Pointer(DllName), MySize, tmp); CreateRemoteThread(h,nil, 0, GetProcAddress(GetModuleHandle('KERNEL32.DLL'), 'LoadLibraryA'), Parameter, 0 , tmp); end; 又看到那两个熟悉的对话框了 哈哈,这么说我们成功了 如把那个DLL换成其他的功能,那就... 今天的东西差不多就讲到这里 你可能想,单纯的远程线程是怎么实现的呢? 刚才我们能往对方内存里拷进去字符串 当然可以把整个函数拷进去 时间关系不详细讲了 我给大家一个函数可以直接用 这个是国外一个叫Aphex的写的 很方便可以直接注入函数 但是要改写入口点 procedure Inject(ProcessHandle: longword; EntryPoint: pointer); var Module, NewModule: Pointer; Size, BytesWritten, TID: longword; begin Module := Pointer(GetModuleHandle(nil));//得到模块句柄,nil表示得到自身模块的 Size := PImageOptionalHeader(Pointer(integer(Module) + PImageDosHeader(Module)._lfanew + SizeOf(dword) + SizeOf(TImageFileHeader))).SizeOfImage; VirtualFreeEx(ProcessHandle, Module, 0, MEM_RELEASE); NewModule := VirtualAllocEx(ProcessHandle, Module, Size, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE); WriteProcessMemory(ProcessHandle, NewModule, Module, Size, BytesWritten); CreateRemoteThread(ProcessHandle, nil, 0, EntryPoint, Module, 0, TID); end; 用的时候写个无参数的函数(遵守标准调用)比如func吧 inject(目标句柄,@func); 就OK了 注意那个func里面只能有API函数,自己写的函数都不能调用,想用就直接写过程进去吧 不然会怎样你试试就知道了 注意了 VirtualAllocEx VirtualFreeEx CreateRemoteThread 在NT下才能用 9X/me系统可以利用一些辅助单元实现 预习包里面的9X_Files里面有个 此外MadRemote等也可以实现 OK 今天先讲到这里