Windows核心编程 远程线程注入

目录

线程安全

C线程

[C++ STL线程](#C++ STL线程)

远程线程注入概述

相关API

CreateRemoteThread

LoadLibrary

VirtualAllocEx

FreeLibrary

GetProcAddress

远程线程注入

DLL卸载

调试DLL


线程安全

变量在单线程和在多线程都不会出问题 - 线程安全

变量在多线程出问题,在单线程不会出问题 - 线程不安全

C线程

在使用多线程的时候,printf函数可能会被切,所以输出的值有可能对应不上。

下面的例子演示了在多线程环境中,printf 输出信息被切走一部分的情况:

#include <stdio.h>
#include <thread>

void printThreadId(int id)
{
    printf("Thread %d starts.\n", id);
    printf("Thread %d prints some information.\n", id);
    printf("Thread %d continues to print some information.\n", id);
    printf("Thread %d finishes.\n", id);
}

int main()
{
    std::thread t1(printThreadId, 1);
    std::thread t2(printThreadId, 2);
    t1.join();
    t2.join();
    return 0;
}

这个例子中,两个线程分别输出它们的信息,但由于没有使用线程同步机制,多个线程之间会发生竞争条件,输出的信息可能会错乱或被切走一部分。在我的测试环境中,有时会出现以下输出:

可以看到,线程 1 和线程 2 的输出信息被交叉输出,部分信息被切走了。这种情况是因为多个线程同时访问 printf 函数,导致输出信息被竞争条件影响。

这个问题的出现是随机的,并不是每次都会出现。在多线程环境中,输出信息被切走一部分的情况取决于多个线程之间的竞争条件和调度策略。如果竞争条件和调度策略不利于某个线程,那么这个线程的输出信息就有可能被切走一部分。如果你的环境中没有出现这个问题,那么可能是由于你的测试数据较少,或者你的测试数据并不足以导致竞争条件和调度策略的影响。故引出下文。

_beginthread_beginthreadex 都是用于创建新的线程的函数,但是它们在一些方面有所不同。
_beginthread 是一个Windows API函数,它使用 C运行时库 来创建新的线程。它的函数原型如下:

uintptr_t _beginthread(void(__cdecl *start_address)(void*), unsigned stack_size, void* arglist);

其中参数的解释如下:

  • void (__cdecl *start_address)(void*):函数指针,指向线程函数的地址。线程函数应具有void返回类型,接受一个void*类型的参数,用于传递线程函数的参数。

  • unsigned stack_size:表示新线程的堆栈大小(以字节为单位)。如果指定为0,将使用默认堆栈大小。

  • void* arglist:指向传递给新线程函数的参数的指针。可以将该参数设置为NULL,如果不需要传递参数给线程函数。

返回值:如果成功创建线程,返回线程的标识符(线程ID);如果失败,返回-1。

请注意,_beginthread函数是C运行时库提供的函数,用于创建新的线程。在使用该函数时,请确保链接相应的运行时库。

_beginthread 只支持使用 __stdcall 调用约定的函数作为线程入口点,并且没有提供任何与线程安全性相关的参数。它还可以指定线程堆栈的大小,但是这个功能在较新的Windows版本中已经被废弃了,因为系统可以自动为每个线程分配默认的堆栈大小。
_beginthreadex 是一个更为通用和灵活的函数,它可以在 Windows 平台上创建一个线程,并且可以指定线程入口点、线程安全性、线程堆栈大小、线程标志和创建后立即启动线程等。它的函数原型如下:

uintptr_t _beginthreadex(
  void*         security,
  unsigned      stack_size,
  unsigned (__stdcall *start_address)(void*),
  void*         arglist,
  unsigned      initflag,
  unsigned*     thrdaddr
);

其中参数的解释如下:

  • security:指向SECURITY_ATTRIBUTES结构的指针,用于指定新线程的安全性。可以将该参数设置为NULL,以使用默认安全性。

  • stack_size:表示新线程的堆栈大小(以字节为单位)。如果指定为0,将使用默认堆栈大小。

  • start_address:函数指针,指向线程函数的地址。线程函数应具有unsigned返回类型,并接受一个void*类型的参数,用于传递线程函数的参数。

  • arglist:指向传递给新线程函数的参数的指针。可以将该参数设置为NULL,如果不需要传递参数给线程函数。

  • initflag:表示新线程的初始标志。可以将该参数设置为0,表示创建线程后立即启动线程。

  • thrdaddr:指向一个变量,用于接收新线程的ID。可以将该参数设置为NULL,如果不需要获取线程ID。

返回值:如果成功创建线程,返回线程的句柄;如果失败,返回NULL。注意,返回的线程句柄可以用于后续的线程操作,比如等待线程的结束或关闭线程句柄。

_beginthreadex函数创建一个新的线程,并开始执行指定的线程函数。线程函数的地址由start_address参数指定。可以将其他参数传递给线程函数,通过arglist参数传递。

_beginthread 不同,_beginthreadex 支持使用 __stdcall 或 __cdecl 调用约定的函数作为线程入口点,并且可以通过参数指定线程安全性。它还可以指定线程堆栈的大小,但是如果未指定,则使用默认大小。_beginthreadex 还支持立即启动线程或者在稍后显式地启动线程,这可以让调用者有更多的控制权。

总的来说,_beginthread 是一个简单的API函数,适用于创建较为简单的线程,而 _beginthreadex 则更为灵活和通用,适用于创建更加复杂的线程。

#include <iostream>
#include <windows.h>
#include <process.h>


unsigned int g_nVal = 0;

DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
  for (int i = 0; i < 0x10000; i++)
  {
    InterlockedIncrement(&g_nVal);
    printf("g_nVal:%08X tid:%08X \r\n", g_nVal, GetCurrentThreadId());
  }

  return 0;
}

VOID main(VOID)
{
  HANDLE aryThreads[4] = {};

  for (size_t i = 0; i < 4; i++)
  {
    aryThreads[i] = (HANDLE)_beginthreadex(
      NULL,                        // no security attributes 
      0,                           // use default stack size  
      (_beginthreadex_proc_type)ThreadFunc,                  // thread function 
      NULL,                // argument to thread function 
      0,                           // use default creation flags 
      NULL);                // returns the thread identifier 
  }

  WaitForMultipleObjects(4, aryThreads, TRUE, INFINITE);

  for (size_t i = 0; i < 4; i++)
  {
    CloseHandle(aryThreads[i]);
  }

  printf("hello world");
}

C++ STL线程

STL(标准模板库)中提供了一些与线程相关的函数,包括以下几个:

  1. std::thread:创建并启动一个新线程。
  2. std::this_thread::get_id:获取当前线程的ID。
  3. std::this_thread::sleep_for:使当前线程休眠一段时间。
  4. std::mutex:互斥锁,用于实现线程同步。
  5. std::unique_lock:基于互斥锁的独占锁,用于实现更灵活的线程同步。
  6. std::condition_variable:条件变量,用于实现线程间的等待和通信。
  7. std::atomic:原子类型,用于实现线程安全的操作。

测试代码

#include <iostream>
#include <thread>
using namespace std;
unsigned int g_nVal = 0;
void ThreadFunc()
{
    for (size_t i = 0; i < 0x10000; i++)
    {
        ++g_nVal;

    }
    printf("%08X\r\n", g_nVal);
}
int main()
{

    thread t1(ThreadFunc);//创建线程   内部机制:function<void(void)> 
   
    t1.join(); //等待线程结束

运行原理和机制:

**当我们创建一个线程对象t1并传入一个函数ThreadFunc时,线程对象t1内部实际上是保存了一个函数对象,该函数对象的类型是std::function<void(void)>,它代表一个无参数、无返回值的函数对象。**具体来说,这个过程包括以下几个步骤:

  1. 将传入的函数ThreadFunc封装成一个std::function<void(void)>类型的函数对象即创建一个可以调用ThreadFunc的函数对象。
  2. 创建一个线程对象t1,并将上一步中创建的函数对象作为构造函数参数传入。
  3. 线程对象t1会将std::function<void(void)>类型的函数对象保存到内部,以便在新线程启动时调用。
  4. 调用线程对象t1的成员函数start()启动新线程。
  5. 在新线程中,调用保存的std::function<void(void)>类型的函数对象作为线程入口函数,并不需要传入任何参数。
  6. 线程入口函数开始执行,并可以访问ThreadFunc中的外部变量等资源。

需要注意的是,**std::function是一个通用的函数封装器,它可以保存任意可调用对象,包括函数指针、函数对象、lambda表达式等等。**在使用线程对象时,将ThreadFunc封装成std::function类型的函数对象可以提高代码的灵活性和可重用性,因为我们可以传入不同的可调用对象,而不需要修改线程对象的定义。

此外,线程对象t1和std::function类型的函数对象的生命周期是相互独立的,也就是说,当std::function类型的函数对象执行完毕并退出后,线程对象t1仍然存在,可以继续使用和管理。因此,在使用线程对象时需要注意线程对象和std::function类型的函数对象的生命周期问题,避免使用已经退出的函数对象或者已经销毁的线程对象。

有参数的

void ThreadFunc(int n, string str, float f)
{
    for (size_t i = 0; i < 0x10000; i++)
    {
        ++g_nVal;

    }
    printf("%08X n:%d str:%s float:%f\r\n", g_nVal, n, str.c_str(), f);
}
int main()
{

    thread t(ThreadFunc, 1, "weewr", 2.0);//创建线程   内部机制:function<void(void)> 

    t.join(); //等待线程结束
}

当参数不是普通变量

如果你的参数是普通的变量,而不是对象或成员函数,那么你可以直接将它们作为参数传递给std::thread构造函数,而不需要使用std::bind或Lambda表达式。

#include <thread>
#include <iostream>

void printValue(int n) {
  std::cout << "The value is: " << n << std::endl;
}

int main() {
  std::thread t(printValue, 42);
  t.join();
  return 0;
}

**如果不使用std::bind,我们需要使用Lambda表达式或函数对象来传递函数和参数,而不是将参数传递给std::thread构造函数。**例如,假设我们有一个类A和一个成员函数foo(int),我们可以使用Lambda表达式来创建一个可调用对象,并将其传递给std::thread构造函数,以便在新线程中执行该函数:

#include <thread>

class A {
public:
  void foo(int n) {
    // do something
  }
};

int main() {
  A obj;
  std::thread t([&obj]() { obj.foo(42); });
  t.join();
  return 0;
}

在这个例子中,我们使用Lambda表达式来创建一个可调用对象,该对象调用对象的成员函数foo,并将42作为参数传递给该函数。注意,我们需要使用引用捕获对象,以便在Lambda表达式中访问该对象。

当然,使用Lambda表达式和函数对象也可以实现std::bind的功能,但是通常需要更多的代码和更复杂的语法。因此,使用std::bind可以更简洁和直观地传递函数对象和参数。

在类中使用 bind

class CFoo
{
public:
    CFoo()
    {
    }
    ~CFoo()
    {
    }
public:
    void Foo1(int n)
    {
        for (size_t i = 0; i < 0x100; i++)
        {
            cout << __FUNCTION__ << " n:" << m_n << endl;
        }
    }
    void Foo2(int n, string str)
    {
        for (size_t i = 0; i < 0x100; i++)
        {
            cout << __FUNCTION__ << " n:" << m_n << endl;
        }
    }
private:
    int m_n = 99;
};

int main()
{
    CFoo foo;
    thread t1(bind(&CFoo::Foo1, &foo, 12));



    thread t2(bind(&CFoo::Foo2, &foo, 12, "Hello world"));
    t1.join(); //等待线程结束
    t2.join(); //等待线程结束
}

当函数重载时

void ThreadFunc(int n)
{
    for (size_t i = 0; i < 0x10000; i++)
    {
        ++g_nVal;

    }
    printf("%08X n:%d \r\n", g_nVal, n);
}
void ThreadFunc(int n, string str)
{
    for (size_t i = 0; i < 0x10000; i++)
    {
        ++g_nVal;

    }
    printf("%08X n:%d str:%s \r\n", g_nVal, n, str.c_str());
}
int main()
{

    // 使用lambda表达式传递参数
    std::thread t1([](int arg) { ThreadFunc(arg); }, 1); // 调用ThreadFunc(int arg)
    std::thread t2([](int arg1,std::string arg2) { ThreadFunc(arg1,arg2); }, 1,"hello"); // 调用ThreadFunc(const std::string& arg)
    t1.join();
    t2.join();

    // 使用std::bind函数传递参数
    std::thread t3(std::bind(static_cast<void(*)(int)>(ThreadFunc), 2)); // 调用ThreadFunc(int arg)
    std::thread t4(std::bind(static_cast<void(*)(int, std::string)>(ThreadFunc),2, "world")); // 调用ThreadFunc(const std::string& arg)
    t3.join();
    t4.join();
}

static_cast

static_cast 是C++中的一种类型转换运算符,用于将一种类型的值强制转换为另一种类型。static_cast 可以在编译时进行类型检查,因此它比较安全,但在某些情况下也可能会导致编译错误或运行时错误。
static_cast 可以用于许多类型转换,例如将整数转换为浮点数、将指针类型转换为另一种指针类型、将基类指针或引用转换为派生类指针或引用等。在本例中,我们使用static_castThreadFunc 的函数指针转换为具有指定函数签名的函数指针类型,以便在std::bind 函数中明确指定要调用的函数版本。
static_cast的一般语法如下:

static_cast<目标类型>(源类型);

其中,目标类型源类型 分别表示要转换的目标类型和源类型。在转换时,static_cast 将源类型的值强制转换为目标类型,并返回转换后的值。

需要注意的是,static_cast 并不适用于所有类型转换,特别是对于指针类型的转换,有时需要使用更具体的类型转换运算符,例如reinterpret_castconst_cast。因此,使用类型转换运算符时需要谨慎,并遵守语言标准中的规定。

join()

join()函数是用来等待一个线程结束的函数,它可以阻塞当前线程,直到被等待的线程执行完成。

在不同的编程语言和操作系统中,join()函数的具体实现方式可能会有所不同,下面以常见的C++标准库的std::thread为例进行说明。

在C++中,使用std::thread类创建线程,可以通过join()函数来等待线程的结束。join()函数的调用会阻塞当前线程,直到被等待的线程执行完成。

下面是一个简单的示例代码:

#include <iostream>
#include <thread>

void myThreadFunc() {
    std::cout << "Hello from myThreadFunc!" << std::endl;
}

int main() {
    std::thread myThread(myThreadFunc); // 创建线程

    // 等待线程执行完成
    myThread.join();

    std::cout << "Thread finished." << std::endl;

    return 0;
}

在上面的代码中,myThreadFunc()函数是我们要在新线程中执行的函数。通过std::thread类创建了一个名为myThread的线程,并传入了myThreadFunc作为线程函数。

main()函数中,调用myThread.join()等待线程执行完成。这会将当前线程阻塞,直到myThread线程执行完成,然后才继续执行后面的代码。

请注意,join()函数只能在已经开始执行的线程上调用,而不能在尚未启动的线程上调用。如果线程已经被detach(),即与std::thread实例解绑,那么就无法使用join()函数等待该线程的结束。

为什么要阻塞?

如果不加阻塞,对象出函数作用域的时候要析构,但线程未必结束,析构会把线程的相关属性给直接释放,线程就会出现问题,断言;或则认为线程没结束,不能析构对象,抛出异常;

方法:

用join函数阻塞;这样可以实时的监控到线程是否结束了;

立马结束线程 detach t1.detach();

this_thread类

提供了一些静态成员函数:

printf("nId:%08X g_nVal:%08X tid:%08X \r\n", nID, g_nVal,  this_thread::get_id());
this_thread::sleep_for(std::chrono::seconds(2 * 1000));//睡两秒钟
//this_thread::sleep_for(std::chrono::milliseconds(2 * 1000));//精准睡眠

远程线程注入概述

原理:远程线程注入 DLL 的基本原理是利用目标进程中的某个 API 函数(例如CreateRemoteThread()函数)来创建一个远程线程,并在其中执行指向恶意 DLL 的代码。

与DLL劫持的区别:

  1. 劫持的原理:dll替换函数转发。是进程跑起来之前,加载我们的dll
  2. 远程注入:能够为已经运行的程序注入dll,和卸载dll。

远程线程注入思路

  1. 本进程要想给目标进程创建线程, 在线程里加载自己的dll;
  2. 巧合的是线程的回调函数是一个参数, 而LoadLibrary (加载dll)也是一个参数, 所以可以把LoadLibrary 作为线程的回调函数, 现在就需要找到目标进程中的LoadLibrary
  3. 而LoadLibrary恒定在 kernel32.dll里在同一台电脑上,NTdll.dll和kernel32.dll在不同进程中地址是一样的
  4. 所以自己进程的LoadLibrary 地址就是目标进程中的LoadLibrary地址
  5. 现在需要向目标进程的LoadLibrary中传入参数

给目标进程注入本进程dll的步骤

  1. 通过Findwindow函数传入进程窗口名,获取窗口句柄
  2. 通过窗口句柄用GetWindowThreadProcessId函数获取目标进程id
  3. 通过目标进程句柄用VirtualAllocEx函数打开目标进程
  4. 打开目标进程后用WriteProcessMemory 函数向目标进程内存中写入自己要注入的dll全路径
  5. 通过 GetModuleHandle函数获取同一台电脑每一个进程中地址都相同的kernel32的dll模块句柄:;
  6. 通过kernel32.dll的模块句柄用GetProcAddress 函数获取kernel32.dllLoadLibraryA函数的地址;
  7. 通过CreateRemoteThread 函数在目标进程创建线程,并把之前写到目标进程内存中的要注入的dll的路径 作为CreateRemoteThread 函数的与线程回调函数自定义参数相同的参数的值,把目标进程中的kernel32.dllLoadLibraryA 函数的地址作为该跨进程创建的线程的回调函数,此时目标进程中的LoadLibraryA函数就可以加载需要注入的dll了

相关API

CreateRemoteThread

CreateRemoteThread是一个Windows API函数,用于在目标进程中创建一个远程线程。

HANDLE CreateRemoteThread(
  HANDLE                 hProcess,
  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  SIZE_T                 dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID                 lpParameter,
  DWORD                  dwCreationFlags,
  LPDWORD                lpThreadId
);

参数解释如下:

  • hProcess:目标进程的句柄,表示在哪个进程中创建远程线程。获取目标进程句柄的方式通常是使用OpenProcess函数。

  • lpThreadAttributes:线程的安全属性,指向SECURITY_ATTRIBUTES结构体的指针,可以设置为NULL

  • dwStackSize:线程的堆栈大小,可以设置为0以使用默认大小。

  • lpStartAddress:线程函数的入口点,指向线程函数的函数指针。

  • lpParameter:传递给线程函数的参数,可以为NULL

  • dwCreationFlags:线程的创建标志,可以设置为0或其他标志,如CREATE_SUSPENDED等。

  • lpThreadId:用于接收新线程的ID的指针,可以为NULL

返回值为新创建线程的句柄。如果创建失败,返回NULL

使用CreateRemoteThread函数,可以在目标进程中创建一个远程线程,并指定线程函数的入口点和参数。这样就可以在目标进程中执行指定的代码。

需要注意的是,使用CreateRemoteThread函数需要获得目标进程的句柄,并具有足够的权限来进行线程创建。同时,在进行远程线程注入时,需要确保注入的代码在目标进程中被正确加载和执行。

LoadLibrary

LoadLibrary是一个Windows API函数,用于加载指定的动态链接库(DLL)到当前进程的地址空间中。函数原型如下:

HMODULE LoadLibrary(
  LPCTSTR lpFileName
);

参数解释如下:

  • lpFileName:指定要加载的DLL文件的路径。可以是绝对路径,也可以是相对路径。

返回值为加载成功时,返回DLL模块的句柄(HMODULE);加载失败时,返回NULL

使用LoadLibrary函数可以将一个DLL加载到当前进程的地址空间中,使得该DLL中的函数和数据可在当前进程中使用。

加载DLL后,可以使用GetProcAddress函数获取DLL中导出函数的地址,进而调用DLL中的函数。

测试代码

#include <iostream>
#include <Windows.h>

int main() {
    // 加载DLL
    HMODULE hModule = LoadLibrary("mydll.dll");
    if (hModule != NULL) {
        std::cout << "DLL loaded successfully." << std::endl;

        // 获取导出函数的地址
        FARPROC funcAddress = GetProcAddress(hModule, "MyFunction");
        if (funcAddress != NULL) {
            // 调用导出函数
            typedef void (*MyFunctionType)();
            MyFunctionType myFunction = (MyFunctionType)funcAddress;
            myFunction();
        }

        // 卸载DLL
        FreeLibrary(hModule);
    } else {
        std::cout << "Failed to load DLL." << std::endl;
    }

    return 0;
}

VirtualAllocEx

VirtualAllocEx是一个Windows API函数,用于在指定的进程中分配内存空间。

函数原型如下:

LPVOID VirtualAllocEx(
  HANDLE hProcess,
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect
);

参数解释如下:

  • hProcess:目标进程的句柄,表示在哪个进程中分配内存。获取目标进程句柄的方式通常是使用OpenProcess函数。

  • lpAddress:分配内存的起始地址,可以设置为NULL,让系统自动选择一个合适的地址。

  • dwSize:要分配的内存大小,以字节为单位。

  • flAllocationType:内存分配的类型,可以设置为以下标志的组合:

    • MEM_COMMIT:将分配的内存提交,使之可用。
    • MEM_RESERVE:为分配的内存保留地址空间,但不进行物理或交换文件上的分配。
    • MEM_RESET:将分配的内存内容重置为0。
    • MEM_RESET_UNDO:将分配的内存内容重置为0,但可通过调用VirtualFreeEx来还原。
  • flProtect:内存的保护属性,指定内存区域的访问权限,可以设置为以下标志之一:

    • PAGE_EXECUTE:可执行代码。
    • PAGE_EXECUTE_READ:可执行和可读的代码。
    • PAGE_EXECUTE_READWRITE:可执行、可读和可写的代码。
    • PAGE_NOACCESS:无法访问。

返回值为分配到的内存的起始地址,如果分配失败,返回NULL

使用VirtualAllocEx函数可以在指定的进程中分配一块内存空间,以供该进程使用。通常与WriteProcessMemory函数一起使用,将数据写入分配的内存空间。

测试代码

#include <iostream>
#include <Windows.h>

int main() {
    // 打开目标进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetProcessId);
    if (hProcess == NULL) {
        std::cout << "Failed to open target process." << std::endl;
        return 1;
    }

    // 在目标进程中分配内存
    LPVOID pRemoteBuffer = VirtualAllocEx(hProcess, NULL, bufferSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (pRemoteBuffer == NULL) {
        std::cout << "Failed to allocate memory in target process." << std::endl;
        CloseHandle(hProcess);
        return 1;
    }

    // 将数据写入目标进程的内存空间
    if (!WriteProcessMemory(hProcess, pRemoteBuffer, bufferData, bufferSize, NULL)) {
        std::cout << "Failed to write to target process memory." << std::endl;
        VirtualFreeEx(hProcess, pRemoteBuffer, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return 1;
    }

    // ...

    // 清理资源
    VirtualFreeEx(hProcess, pRemoteBuffer, 0, MEM_RELEASE);
    CloseHandle(hProcess);

    return 0;
}

在上述示例中,我们使用VirtualAllocEx函数在目标进程中分配了一块内存空间。然后,我们通过WriteProcessMemory函数将数据写入目标进程的内存空间。

FreeLibrary

FreeLibrary是一个Windows API函数,用于释放通过LoadLibrary函数加载的动态链接库(DLL)模块。函数原型如下:

BOOL FreeLibrary(
  HMODULE hModule
);

参数解释如下:

  • hModule:要释放的DLL模块的句柄(HMODULE),这个句柄是在调用LoadLibrary函数时返回的。

返回值为释放成功时,返回非零值;释放失败时,返回0。

使用FreeLibrary函数可以卸载一个已加载的DLL模块,释放该模块占用的系统资源。在卸载前,应该确保不再使用该DLL中的函数和数据,以避免引起未定义的行为或内存泄漏。

GetProcAddress

FARPROC GetProcAddress(
  HMODULE hModule,      // DLL文件句柄
  LPCSTR lpProcName     // 要获取的函数名
);

该函数用于获取DLL文件中指定函数的地址。函数的参数包括:

  • hModule:DLL文件的句柄。
  • lpProcName:要获取的函数名。

该函数返回指定函数的地址。如果获取失败,则返回NULL。

远程线程注入

获取LoadLibrary 在模块中的地址

  • 方法一:扫模块,查看LoadLibary导出函数在哪个DLL里面。发现在Kernerl32.dll里面。【目标进程中Kermer32.DLL映射的首地址 + 导出函数地址偏移=LoadLibary函数地址 】
  • 方法二:同一台电脑上,Kernerl32,ntdll,kernerbase的地址相同。因为他们先于进程被加载,加载时机特别早。所以他们的地址是相同的

CreateRemoteThread函数和DLL注入技术是一种常用的远程注入方式,可以将一个DLL模块注入到另一个进程中,从而实现在另一个进程中执行恶意代码的目的。

具体的使用步骤如下:

  1. 打开目标进程并获取其句柄,可以使用OpenProcess函数来打开目标进程,该函数的参数为目标进程的PID和所需的访问权限。
  2. 在目标进程的虚拟地址空间中分配一段内存空间,可以使用VirtualAllocEx函数来分配内存,该函数的参数为目标进程的句柄、分配的内存大小、内存保护属性和分配内存的起始地址。
  3. 将DLL模块的路径名写入到目标进程的内存空间中,可以使用WriteProcessMemory函数来将DLL路径名写入到目标进程的内存空间中,该函数的参数为目标进程的句柄、目标进程的内存地址、要写入的数据、数据大小和实际写入的字节数。
  4. 在目标进程中加载注入的DLL模块,可以使用LoadLibrary函数来加载DLL模块,该函数的参数为DLL模块的路径名。
  5. 在目标进程中创建一个新的线程,执行DLL模块中的代码,可以使用CreateRemoteThread函数来创建远程线程,该函数的参数为目标进程的句柄、线程的安全描述符、线程堆栈大小、线程入口点地址和传递给线程的参数。
  6. 清理内存和句柄,可以使用VirtualFreeEx函数释放在目标进程中分配的内存空间,使用CloseHandle函数关闭目标进程的句柄。

被注入的DLL

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "resource.h"


//窗口回调函数
BOOL CALLBACK DeleteItemProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        
    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDC_BUTTON1:
            MessageBox(NULL,"测试","标题",MB_OK);
            return TRUE;

        case IDC_BUTTON2:   
            FreeLibrary(GetModuleHandle("Dll1"));   //会崩溃,卸载DLL后,还在使用DLL资源代码,所以会C05
            return TRUE;
        }
        break;

    case WM_CLOSE:
        EndDialog(hwndDlg, 0);
        return TRUE;

    }
    

    return FALSE;
}


DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
    DialogBox(GetModuleHandle("Dll1"),MAKEINTRESOURCE(IDD_DIALOG1), NULL,DeleteItemProc);    //实例句柄:资源在哪个模块填哪个模块的实例句柄。

    //崩溃原因:调用FreeLibrary时,dll还有其他代码执行。如果能够等线程运行完了后再调用free,就可解决。
    //新建一个线程来

    FreeLibraryAndExitThread(GetModuleHandle("Dll1"),1);//卸载指定dll,并退出当前线程。

    return 0;
}


BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        HANDLE hThread;
        char szMsg[80];

        hThread = CreateThread(
            NULL,                        // no security attributes 
            0,                           // use default stack size  
            ThreadFunc,                  // thread function 
            NULL,                // argument to thread function 
            0,                           // use default creation flags 
            NULL);                // returns the thread identifier 
    }
        OutputDebugString("DLL_PROCESS_ATTACH: 你被注入了!!!");
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        OutputDebugString("DLL_PROCESS_DETACH: 我走了!!!");
        break;
    }
    return TRUE;
}

注入DLL

// RemoteInject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <windows.h>

/*
步骤:
1. 拿进程句柄
2. 写入参数
    a. 别的进程申请内存,参数选提交 MEM_COMMIT。
    b. 将dll路径写入申请的内存地址
3. 拿取LoadLirabry地址
    GetProcAddress(模块基址,导出函数名称) - 获取模块基址
4. 在目标进程创建新线程
    CreateRemoteThread - 线程函数地址填 获取的LoadLirabry地址。
*/

int main()
{
    HWND hWndCff = FindWindow(NULL, "Dependencies (WoW64)");
    if (hWndCff == NULL)
    {
        return 0;
    }

    DWORD dwProcId;
    GetWindowThreadProcessId(hWndCff, &dwProcId);   //拿进程id

    //拿进程句柄
    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcId);
    if (hProc == NULL)
    {
        return 0;
    }

    //写入参数
    LPVOID pDllPath = VirtualAllocEx(hProc, NULL, MAX_PATH, MEM_COMMIT, PAGE_READWRITE);
    if (pDllPath == NULL)
    {
        return 0;
    }

    char szDllPath[] = {"G:\\CR40C++程序设计\\二阶段\\Windows核心编程\\10\\信号灯\\Debug\\Dll1.dll"};
    BOOL bRet = WriteProcessMemory(hProc, pDllPath, szDllPath, sizeof(szDllPath),NULL); //写入进程内存
    if (!bRet)
    {
        return 0;
    }

    //拿去LoadLirabry地址
    HMODULE hKernel32 = GetModuleHandle("kernel32");    //获取模块基址
    if (hKernel32 == NULL)
    {
        return 0;
    }

    auto pfnLoadLibrary = GetProcAddress(hKernel32,"LoadLibraryA");
    if (pfnLoadLibrary == NULL)
    {
        return 0;
    }

    //在目标进程创建新线程
    HANDLE hMod = CreateRemoteThread(hProc,NULL,0,(LPTHREAD_START_ROUTINE)pfnLoadLibrary, pDllPath,0,NULL);
    if (hMod == NULL)
    {
        std::cout << "注入失败" << std::endl;
        return 0;
    }

    WaitForSingleObject(hMod,INFINITE);
    DWORD dwRetVal;
    GetExitCodeThread(hMod, &dwRetVal);

    std::cout << "Hello World!\n";
}

DLL卸载

调用FreeLibrary函数卸载崩溃的原因:卸载DLL后,还在使用DLL资源代码

FreeLibraryAndExitThread是一个Windows API函数,用于释放通过LoadLibrary函数加载的动态链接库(DLL)模块,并终止当前线程。

函数原型如下:

VOID FreeLibraryAndExitThread(
  HMODULE hModule,
  DWORD   dwExitCode
);

参数解释如下:

  • hModule:要释放的DLL模块的句柄(HMODULE),这个句柄是在调用LoadLibrary函数时返回的。

  • dwExitCode:指定当前线程的退出代码。

该函数没有返回值。

使用FreeLibraryAndExitThread函数可以在释放DLL模块的同时,终止当前线程的执行。该函数会先释放DLL模块,然后调用ExitThread函数终止当前线程。

#include <iostream>
#include <Windows.h>

DWORD WINAPI ThreadProc(LPVOID lpParam) {
    // 加载DLL
    HMODULE hModule = LoadLibrary("mydll.dll");
    if (hModule != NULL) {
        std::cout << "DLL loaded successfully." << std::endl;

        // 使用DLL中的函数...

        // 释放DLL并终止线程
        FreeLibraryAndExitThread(hModule, 0);
    } else {
        std::cout << "Failed to load DLL." << std::endl;
    }

    return 0;
}

int main() {
    // 创建线程
    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
    if (hThread != NULL) {
        // 等待线程结束
        WaitForSingleObject(hThread, INFINITE);
        std::cout << "Thread finished." << std::endl;
        
        // 关闭线程句柄
        CloseHandle(hThread);
    } else {
        std::cout << "Failed to create thread." << std::endl;
    }

    return 0;
}

在上述示例中,我们使用CreateThread函数创建了一个新线程,并在新线程中加载了名为mydll.dll的DLL文件。在DLL加载成功后,我们可以使用DLL中的函数。然后,通过调用FreeLibraryAndExitThread函数来释放DLL模块并终止当前线程。

调试DLL

DLL属性 -> 调试 -> 命令 ->路径(加载dll的程序。谁加载dll就填谁)运行DLL程序,在运行注入程序。下断点。

相关推荐
若亦_Royi37 分钟前
C++ 的大括号的用法合集
开发语言·c++
ue星空4 小时前
Windbg常用命令
windows
ragnwang4 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
lqqjuly7 小时前
特殊的“Undefined Reference xxx“编译错误
c语言·c++
泰勒今天不想展开8 小时前
jvm接入prometheus监控
jvm·windows·prometheus
冰红茶兑滴水8 小时前
云备份项目--工具类编写
linux·c++
刘好念8 小时前
[OpenGL]使用 Compute Shader 实现矩阵点乘
c++·计算机图形学·opengl·glsl
酒鬼猿8 小时前
C++进阶(二)--面向对象--继承
java·开发语言·c++
姚先生979 小时前
LeetCode 209. 长度最小的子数组 (C++实现)
c++·算法·leetcode
易我数据恢复大师9 小时前
怎么设置电脑密码?Windows和Mac设置密码的方法
windows·macos·电脑