远程线程作为一项"合法"的代码注入技术,在windows上被大量使用, 它的本质就是把一块可执行代码写入到对方进程,然后让其起运行起来。

      一般它的实现过程是这样的, 通过VirtualAllocEx在目标进程分配内存空间,然后通过WriteProcessMemory将我们的可执行代码写入到目标进程,最后通过CreateRemoteThread让我们的可执行代码在目标进程里运行起来。

     一般实现远程线程有2种方法, 一种是《windows核心编程》里介绍的,通过线程函数和LoadLibrary API函数申明的相似性, 直接在目标进程里调用LoadLibrary加载我们DLL,这样我们只要在DLL_PROCESS_ATTACH里执行我们的代码就可以了。代码如下, 通过InjectLib在目标进程加载我们的DLL, 通过EjectLib卸载我们的DLL:
///////////////////////////////////////////////////////////////////////////////
BOOL WINAPI InjectLibW(DWORD dwProcessId, PCWSTR pszLibFile) 
{

   BOOL fOk = FALSE; // Assume that the function fails
   HANDLE hProcess = NULL, hThread = NULL;
   PWSTR pszLibFileRemote = NULL;

   __try {
      // Get a handle for the target process.
      hProcess = OpenProcess(
         PROCESS_QUERY_INFORMATION |   // Required by Alpha
         PROCESS_CREATE_THREAD     |   // For CreateRemoteThread
         PROCESS_VM_OPERATION      |   // For VirtualAllocEx/VirtualFreeEx
         PROCESS_VM_WRITE,             // For WriteProcessMemory
         FALSE, dwProcessId);
      if (hProcess == NULL)
      {
          __leave;
      }

      // Calculate the number of bytes needed for the DLL's pathname
      int cch = 1 + lstrlenW(pszLibFile);
      int cb  = cch * sizeof(WCHAR);

      // Allocate space in the remote process for the pathname
      pszLibFileRemote = (PWSTR) 
         VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE);
      if (pszLibFileRemote == NULL)
      {      
         __leave;
      }

      // Copy the DLL's pathname to the remote process's address space
      if (!WriteProcessMemory(hProcess, pszLibFileRemote, 
         (PVOID) pszLibFile, cb, NULL))
      {      
         __leave;
      }

      // Get the real address of LoadLibraryW in Kernel32.dll
      PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
         GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
      if (pfnThreadRtn == NULL)
      {
          __leave;
      }


      // Create a remote thread that calls LoadLibraryW(DLLPathname)
      hThread = CreateRemoteThread(hProcess, NULL, 0, 
         pfnThreadRtn, pszLibFileRemote, 0, NULL);
      if (hThread == NULL)
      {
         __leave;
      }

      // Wait for the remote thread to terminate
      WaitForSingleObject(hThread, INFINITE);

      fOk = TRUE; // Everything executed successfully
   }
   __finally { // Now, we can clean everthing up

      
// Free the remote memory that contained the DLL's pathname
      if (pszLibFileRemote != NULL) 
         VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);

      if (hThread  != NULL) 
         CloseHandle(hThread);

      if (hProcess != NULL) 
         CloseHandle(hProcess);
   }

   return(fOk);
}


///////////////////////////////////////////////////////////////////////////////


BOOL WINAPI InjectLibA(DWORD dwProcessId, PCSTR pszLibFile) 
{

   // Allocate a (stack) buffer for the Unicode version of the pathname
   PWSTR pszLibFileW = (PWSTR) 
      _alloca((lstrlenA(pszLibFile) + 1) * sizeof(WCHAR));

   // Convert the ANSI pathname to its Unicode equivalent
   wsprintfW(pszLibFileW, L"%S", pszLibFile);

   // Call the Unicode version of the function to actually do the work.
   return(InjectLibW(dwProcessId, pszLibFileW));
}


///////////////////////////////////////////////////////////////////////////////


BOOL WINAPI EjectLibW(DWORD dwProcessId, PCWSTR pszLibFile) 
{

   BOOL fOk = FALSE; // Assume that the function fails
   HANDLE hthSnapshot = NULL;
   HANDLE hProcess = NULL, hThread = NULL;

   __try {
      // Grab a new snapshot of the process
      hthSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
      if (hthSnapshot == INVALID_HANDLE_VALUE)
      {
         __leave;
      }

      // Get the HMODULE of the desired library
      MODULEENTRY32W me = { sizeof(me) };
      BOOL fFound = FALSE;
      BOOL fMoreMods = Module32FirstW(hthSnapshot, &me);
      for (; fMoreMods; fMoreMods = Module32NextW(hthSnapshot, &me)) {
         fFound = (lstrcmpiW(me.szModule,  pszLibFile) == 0) || 
                  (lstrcmpiW(me.szExePath, pszLibFile) == 0);
         if (fFound) break;
      }
      if (!fFound)
      {
         __leave;
      }

      // Get a handle for the target process.
      hProcess = OpenProcess(
         PROCESS_QUERY_INFORMATION |   // Required by Alpha
         PROCESS_CREATE_THREAD     | 
         PROCESS_VM_OPERATION,  // For CreateRemoteThread
         FALSE, dwProcessId);
      if (hProcess == NULL)
      {    
         __leave;
      }

      // Get the real address of LoadLibraryW in Kernel32.dll
      PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
         GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "FreeLibrary");
      if (pfnThreadRtn == NULL)
      {
         __leave;
      }

      // Create a remote thread that calls LoadLibraryW(DLLPathname)
      hThread = CreateRemoteThread(hProcess, NULL, 0, 
         pfnThreadRtn, me.modBaseAddr, 0, NULL);
      if (hThread == NULL)
      {
         __leave;
      }

      // Wait for the remote thread to terminate
      WaitForSingleObject(hThread, INFINITE);

      fOk = TRUE; // Everything executed successfully
   }
   __finally { // Now we can clean everything up

      if (hthSnapshot != NULL) 
         CloseHandle(hthSnapshot);

      if (hThread     != NULL) 
         CloseHandle(hThread);

      if (hProcess    != NULL) 
         CloseHandle(hProcess);
   }

   return(fOk);
}


///////////////////////////////////////////////////////////////////////////////


BOOL WINAPI EjectLibA(DWORD dwProcessId, PCSTR pszLibFile)
{

   // Allocate a (stack) buffer for the Unicode version of the pathname
   PWSTR pszLibFileW = (PWSTR) 
      _alloca((lstrlenA(pszLibFile) + 1) * sizeof(WCHAR));

   // Convert the ANSI pathname to its Unicode equivalent
   wsprintfW(pszLibFileW, L"%S", pszLibFile);

   // Call the Unicode version of the function to actually do the work.
   return(EjectLibW(dwProcessId, pszLibFileW));
}


///////////////////////////////////////////////////////////////////////////////
     上面这种方法注入的代码它的优点是开发比较简单,我们只要用C++写一个DLL,然后调用InjectLibW(processID, dllName)就可以了,但是因为代码是运行在一个DLL里,别人可以通过一些枚举模块的工具看到我们的DLL,所以隐蔽性不是很好。

      还有一种远程线程的实现方法是罗云彬Windows环境下32位汇编语言程序设计》里介绍的, 我们不通过DLL,而是直接把可执行代码拷贝到目标进程后运行,所以它是真正的远程线程,通过这种方法,我们的代码和目标进程已经完全融为一体,其他人根本无法察觉。

      用这种方法实现, 它的要点是:

      (1) Kernel32.DLL加载的基址在任何进程里都是一样的(其实上一种LoadLibrary方法也用到了这点), 所以
GetProcAddress,GetModuleHandleA(W), LoadLibraryA(W)这些API的地址在任何进程里都是一样的, 所以我们在其他进程中用和本进程相同的地址调用这些API。

    (2) 因为涉及到全局变量的重定位问题, 所以注入的代码需要用汇编编写, 并用以下汇编解决重定位问题
call @F
@@:
pop ebx
sub ebx, offset @B

     下面写了一个无DLL实现远程线程的测试代码,他会在桌面Shell进程(explorer.exe)里弹一个MessageBox,按照这个例子,我们已经可以通过调用LoadLibraryA(W)和GetProcAddress来调用任何API了, 所以要实现一些复杂的功能也不是难事。

      如果要避免用汇编来重定位,可以把全局变量都打包放在线程参数里,这样单用C++就可以完全实现无DLL的远程线程了,当然我们只能用Release版本(Debug版加了一些用到全局变量的调试信息, 堆栈检测等)。

     还有一个问题是远程线程很难调试, 很多时候你在目标进程里注入代码后可能就只会看到一个Crash的窗口,也不知道哪里代码有问题。如何才能单步调试呢? 我的方法是在CreateRemoteThread之前把线程的入口地址用MessageBox打印出来,然后用Windbg  Attach到目标进程,在该地址上设置断点,这样继续运行就可以用Windbg单步调试了(当然只能以汇编的形式)。

远程线程测试代码下载(Asm): RemoteThreadTest
远程线程测试代码下载(C++): RemoteThreadTest_New
posted on 2012-06-20 15:38 Richard Wei 阅读(3901) 评论(5)  编辑 收藏 引用 所属分类: windows desktop

FeedBack:
# re: 远程线程入门
2012-06-20 21:06 | 饭中淹
注入代码其实无所谓用什么写,线程有个LPVOID的参数,把需要的数据全部事先写入远程进程,然后把数据地址作为参数传给远程线程就可以了。这样无需访问全局变量。  回复  更多评论
  
# re: 远程线程入门
2012-06-20 21:30 | Richard Wei
@饭中淹
是的,我们可以先在目标进程中把需要的数据分配好,然后通过线程参数把地址传进去,这样可以避免全局变量。
关于代码注入,这篇文章不错:http://www.vckbase.com/index.php/wv/1580  回复  更多评论
  
# re: 远程线程入门
2012-06-20 23:53 | Richard Wei
@饭中淹
不依赖汇编重定位的代码注入也做了下尝试,发现挺好用的,测试代代码也放上去了,单纯用C++就可以了,就是注入代码大小不太好算。  回复  更多评论
  
# re: 远程线程入门
2013-10-20 12:53 | flighta
Dear,

我碰到一种情况,需要将系统正在调用的dll文件,替换成新的dll文件. 它问题的起因是我们在做系统开发时,我是负责其中的一个dll文件,开发需要不断调试.但是每次打开整个软件需要比较长的时间.我是想能否直接在软件中将这个dll热替换,避免重起开启整个软件. (新旧两个dll的接口完全相同)

Best regards,
Flighta  回复  更多评论
  
# re: 远程线程入门
2013-10-24 19:45 | Richard Wei
@flighta
如果别人已经保存了你这个DLL的HINSTANCE,你想热替换它,这个恐怕不好弄, 只能让别人Free后再Load。  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理