C++实现DLL注入的完整过程

1 简介

网上确实有关于DLL注入的过程,但是很多写的都不全,或者内容有点老旧。

  • DLL文件注入的原理是:接管被注入应用的控制权,并在应用程序运行的内存中开辟一条线程运行DLL文件中的入口函数的代码。
  • 项目需求:向一个.txt文件注入dll,然后会自动弹出一个窗口
  • 工具:VS2019
  • 本文主要讲解的是对普通程序的注入操作,如果要注入系统进程当中,3、4中的方法是行不通的。需要突破Session0后进行注入。

2 DLL动态链接库的编写

  • 在VS2019中新建项目-选择【动态链接库(DLL)】
    • 不要勾选:【将解决方案和项目放在同一目录中】
  • 创建完项目后,会有四个文件
    • framework.h
    • pch. h
    • dllmain.cpp
    • pch.cpp
  • 都不要动,进入dllmain.cpp文件,进行改写
    • 因为dllmain.cpp中有DLL文件的入口函数,当注入dll文件后,一定会运行该函数,在该函数中有一个switch判断,这是因为这个函数不光是在被注入后会被调用,后续还会被调用到,大家可以自行查查对应的宏是什么意思。这里只用到了DLL_PROCESS_ATTACH这个宏定义,它代表刚注入的时候第一次调用该函数。
    • 在入口函数中,我们写了两条代码,即当注入成功后,会弹出窗口。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "windows.h"
#include <iostream>
#include <fstream>
using namespace std;

 BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {

    case DLL_PROCESS_ATTACH: {
        // 当DLL被进程 <<第一次>> 调用时,导致DllMain函数被调用,
        // 同时ul_reason_for_call的值为DLL_PROCESS_ATTACH,
        // 如果同一个进程后来再次调用此DLL时,操作系统只会增加DLL的使用次数,
        // 不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
        // 获取窗口对象
        HWND hwnd = GetActiveWindow();
        MessageBox(hwnd, L"DLL已进入目标进程。", L"信息", MB_ICONINFORMATION);

    }
    }
    return TRUE;
}
  • 在生成解决方案前需要注意,需要修改配置
  • x64还是x86需要根据操作的软件的版本确定,如果注入失败,很可能是这里选择错误了。后面写的MFC窗口程序也要注意这个地方。

在这里插入图片描述

  • 我用的notepad++是x64的,所以这里选择x64

在这里插入图片描述

  • 右键该项目-【生成】
  • 在输出目录下(默认在解决方案/x64/Release目录下),找到dll文件,该文件就是我们要注入的

3 MFC 窗口程序编写

  • 这里就很好办了

  • 创建MFC 应用,注意,如果没有的话,点击【工具】-【获取工具和功能】

    • 选择【使用C++的桌面开发】,还没完,右侧的MFC安装选项可能不会被选中,你需要勾选一下
    • 下载完后就好了
      在这里插入图片描述
  • MFC 应用程序的使用方法这里不详细说明了。

  • 建立好MFC应用程序后,打开资源文件

    • 放置按钮Button,即其他控件,大家自己试试,挺简单的
    • 双击Button按钮,会跳到一个代码段,这个代码段与Button的点击事件绑定,我们在这个函数当中编写注入程序就好了
      在这里插入图片描述在这里插入图片描述
  • 在注入之前,你需要获得被注入程序的相关信息。通过SPY++获取(VS的内置工具)

    • 打开【工具】-【SPY++】
    • 查找窗口
      在这里插入图片描述
  • 拖拽【查找程序窗口工具】到记事本窗口窗顶

  • 自动显示相关的内容

  • 这里面的信息我们接下来要用到
    在这里插入图片描述

  • 以下是注入程序的内容(写在与Button绑定的事件函数中)

    • 需要引用#include<windows.h>
    • 网上其他的代码可能会在求字符串长度那里出问题!下文中使用的是CString类型的所以长度需要按照文中的方式求解。
      • 指的是m_StrDLLPath 长度的求解方式
    • 如果是wchar_t * wStr类型的字符串,可通过 size_t wSize = (wcslen(wStr) + 1) * sizeof(wchar_t) 得到字节数,如果字节数给少了的话,依然是无法注入的
// m_StrDLLPath 为DLL文件的绝对路径,例如CString m_StrDLLPath(L"D:\\桌面\\testDLL.DLL")
if (m_StrDLLPath.IsEmpty()) {
	MessageBox(_T("请选择DLL模块"));
	return;
}
// :: 代表全局查找,后面的内容可以通过SPY++获取
// 【参数1】类
// 【参数2】标题
HWND hwnd = ::FindWindow(L"Notepad++", L"D:\\桌面\\新建文本文档 (2).txt - Notepad++");
if (hwnd == NULL) {
	MessageBox(L"没有找到");
	return;
}
// 获取窗口所在的PID
DWORD dwPID = 0;
GetWindowThreadProcessId(hwnd, &dwPID);
if (dwPID == 0) {
	MessageBox(L"获取PID失败");
	return;
}
// 通过PID获取进程的句柄
// 获取句柄后,可以完全控制进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);
if (hProcess == NULL) {
	MessageBox(L"进程的句柄获取失败");
	return;
}
// TerminateProcess(hProcess, 0);//关闭句柄对象
// 实现注入
// 1.首先要提升权限,打开进程的访问令牌
// 【参数1】当前程序
// 【参数2】权限,可添加的权限|可查询的权限
HANDLE hToken;
if (FALSE == OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
	// 权限修改失败
	MessageBox(L"权限修改失败");
	return;
}
//2.查看与进程相关的特权信息
LUID luid;
if (FALSE == LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {
	// 特权信息查询失败
	MessageBox(L"特权信息查询失败");
	return;
};
//3.调节进程的访问令牌的特权属性
// 这几行代码固定不变
TOKEN_PRIVILEGES tkp;
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // 打开特权
// 【参数1】访问令牌
// 【参数2】是否禁用特权
// 【参数3】新特权所占的字节数
// 【参数4】原来的特权是否需要保存
// 【参数5】原特权的长度
if (FALSE == AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL)) {
	// 提升特权失败
	MessageBox(L"提升特权失败");
	return;
};
	
//在远程进程中申请内存空间
// 【参数1】程序的句柄对象
// 【参数2】申请的内存地址,由系统分配,所以为NULL
// 【参数3】申请的内存长度
// 【参数4】调用物理存储器
// 【参数5】这块内存可读可写,可执行
// 【返回】申请到的地址
LPVOID lpAddr = VirtualAllocEx(hProcess, NULL, m_StrDLLPath.GetLength() * 2+2,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
if (lpAddr == NULL) {
	// 在远程进程中申请内存失败
	MessageBox(L"在远程进程中申请内存失败");
	return;
}
// 把DLL路径写入到远程进程中
// 强行修改程序的内存
// 【参数1】程序的句柄
// 【参数2】申请到的内存首地址
// 【参数3】写入的内容
// 【参数4】要写入的字节数
// 【参数5】
if (FALSE == WriteProcessMemory(hProcess, lpAddr, m_StrDLLPath, m_StrDLLPath.GetLength() * 2, NULL)) {
	// 在远程进程中写入数据失败
	MessageBox(L"在远程进程中写入数据失败");
	return;
};


// 调用Kernel32.dll中的LoadLibraryW方法用以加载DLL文件
PTHREAD_START_ROUTINE pfnStartAssr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"Kernel32.dll"), "LoadLibraryW");

// 在远程进程中开辟线程
// 【参数1】远程线程的句柄
// 【参数2】线程属性。NULL表示使用默认属性
// 【参数3】堆栈大小。0代表默认
// 【参数4】加载DLL文件的对象
// 【参数5】加载文件的路径
// 【参数6】延迟时间。0代表立即启动
// 【参数7】线程ID。为NULL就行了
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnStartAssr, lpAddr, 0, NULL);
if (hThread == NULL) {
	// 创建远程线程失败
	MessageBox(L"创建远程线程失败");
	return;
}
MessageBox(L"注入成功");
  • 然后以Release ,x64 的方式运行该应用程序。
  • 打开被注入的应用程序,点击注入按钮,如果弹出窗口,则代表注入成功!

4 更普遍的例子

  • 3中使用的是MFC程序,下面咱们直接使用控制台程序编写。
  • 这里的逻辑是这样的:先通过任务管理器,找到要注入的程序,右键属性,得到要注入的进程的名称EditPlus.exe。然后通过这个名称找到PID值,通过PID再进行后续的注入操作
#include <windows.h>
#include <iostream>
#include <Tlhelp32.h>
#include <stdio.h>
using namespace std;

/// <summary>
/// 根据进程名称获取进程信息
/// </summary>
/// <param name="info"></param>
/// <param name="processName"></param>
/// <returns></returns>
BOOL getProcess32Info(PROCESSENTRY32* info, const TCHAR processName[])
{
    HANDLE handle; //定义CreateToolhelp32Snapshot系统快照句柄
    handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);//获得系统快照句柄
    //PROCESSENTRY32 结构的 dwSize 成员设置成 sizeof(PROCESSENTRY32)
    info->dwSize = sizeof(PROCESSENTRY32);
    //调用一次 Process32First 函数,从快照中获取进程列表
    Process32First(handle, info);
    //重复调用 Process32Next,直到函数返回 FALSE 为止
    while (Process32Next(handle, info) != FALSE)
    {
        if (wcscmp(processName, info->szExeFile) == 0)
        {
            return TRUE;
        }
    }
    return FALSE;
}

/// <summary>
/// 注入DLL文件
/// </summary>
/// <param name="DllFullPath">DLL文件的全路径</param>
/// <param name="dwRemoteProcessId">要注入的程序的PID</param>
/// <returns></returns>
BOOL InjectDLL(const wchar_t* DllFullPath, const DWORD dwRemoteProcessId)
{
    // 计算路径的字节数
    int pathSize = (wcslen(DllFullPath) + 1) * sizeof(wchar_t);
    // 获取句柄后,可以完全控制进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwRemoteProcessId);
    if (hProcess == NULL) {
        cout << "获取句柄失败" << endl;
        return FALSE;
    }
    // TerminateProcess(hProcess, 0);//关闭句柄对象
    // 实现注入
    // 1.首先要提升权限,打开进程的访问令牌
    // 【参数1】当前程序
    // 【参数2】权限,可添加的权限|可查询的权限
    HANDLE hToken;
    if (FALSE == OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES |
        TOKEN_QUERY, &hToken)) {
        // 权限修改失败
        cout << "权限修改失败" << endl;
        return FALSE;
    }
    //2.查看与进程相关的特权信息
    LUID luid;
    if (FALSE == LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {
        // 特权信息查询失败
        cout << "特权信息查询失败" << endl;
        return FALSE;
    };
    //3.调节进程的访问令牌的特权属性
    // 这几行代码固定不变
    TOKEN_PRIVILEGES tkp;
    tkp.PrivilegeCount = 1;
    tkp.Privileges[0].Luid = luid;
    tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // 打开特权
    // 【参数1】访问令牌
    // 【参数2】是否禁用特权
    // 【参数3】新特权所占的字节数
    // 【参数4】原来的特权是否需要保存
    // 【参数5】原特权的长度
    if (FALSE == AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL))
    {
        // 提升特权失败
        cout << "提升特权失败" << endl;
        return FALSE;
    };

    //在远程进程中申请内存空间
    // 【参数1】程序的句柄对象
    // 【参数2】申请的内存地址,由系统分配,所以为NULL
    // 【参数3】申请的内存长度
    // 【参数4】调用物理存储器
    // 【参数5】这块内存可读可写,可执行
    // 【返回】申请到的地址
    LPVOID lpAddr = VirtualAllocEx(hProcess, NULL, pathSize, MEM_COMMIT, 
PAGE_EXECUTE_READWRITE);
    if (lpAddr == NULL) {
        // 在远程进程中申请内存失败
        cout << "在远程进程中申请内存失败" << endl;
        return FALSE;
    }
    // 把DLL路径写入到远程进程中
    // 强行修改程序的内存
    // 【参数1】程序的句柄
    // 【参数2】申请到的内存首地址
    // 【参数3】写入的内容
    // 【参数4】要写入的字节数
    // 【参数5】
    if (FALSE == WriteProcessMemory(hProcess, lpAddr, DllFullPath,
        pathSize, NULL)) {
        // 在远程进程中写入数据失败
        cout << "在远程进程中写入数据失败" << endl;
        return FALSE;
    };


    // 调用Kernel32.dll中的LoadLibraryW方法用以加载DLL文件
    PTHREAD_START_ROUTINE pfnStartAssr =
        (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"Kernel32.dll"), 
"LoadLibraryW");

    // 在远程进程中开辟线程
    // 【参数1】远程线程的句柄
    // 【参数2】线程属性。NULL表示使用默认属性
    // 【参数3】堆栈大小。0代表默认
    // 【参数4】加载DLL文件的对象
    // 【参数5】加载文件的路径
    // 【参数6】延迟时间。0代表立即启动
    // 【参数7】线程ID。为NULL就行了
    HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, pfnStartAssr, lpAddr, 0,
        NULL);
    if (hRemoteThread == NULL) {
        // 创建远程线程失败
        cout << "创建远程线程失败" << endl;
        // 释放内存
        VirtualFreeEx(hProcess, lpAddr, 0, MEM_FREE);
        return FALSE;
    }
    cout << "注入成功" << endl;
    // 等待线程结束
    WaitForSingleObject(hRemoteThread, -1);
    // 关闭线程
    CloseHandle(hRemoteThread);
    // 释放内存
    VirtualFreeEx(hProcess, lpAddr, 0, MEM_FREE);
}


int main()
{
    PROCESSENTRY32 info;
    if (getProcess32Info(&info, L"EditPlus.exe"))
    {
        // 24
        InjectDLL(L"F:\\Dll1.dll", info.th32ProcessID);//这个dll你所要注入的dll文件,这个"数字"是你想注入的进程的PID号
    }
    else {
        cout << "查找失败" << endl;
    }
    return 0;
}

5 突破Session0进行注入

  • 这种方式可以注入到系统进程当中
#include <iostream>
#include <windows.h>
#include <Tlhelp32.h>
#include <stdio.h>
using namespace std;



/// <summary>
/// 根据进程名称获取进程信息
/// </summary>
/// <param name="info"></param>
/// <param name="processName"></param>
/// <returns></returns>
BOOL getProcess32Info(PROCESSENTRY32* info, const TCHAR processName[])
{
        HANDLE handle; //定义CreateToolhelp32Snapshot系统快照句柄
        handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);//获得系统快照句柄
        //PROCESSENTRY32 结构的 dwSize 成员设置成 sizeof(PROCESSENTRY32)
        info->dwSize = sizeof(PROCESSENTRY32);
        //调用一次 Process32First 函数,从快照中获取进程列表
        Process32First(handle, info);
        //重复调用 Process32Next,直到函数返回 FALSE 为止
        while (Process32Next(handle, info) != FALSE)
        {
               if (wcscmp(processName, info->szExeFile) == 0)
               {
                       return TRUE;
               }
        }
        return FALSE;
}

BOOL ZwCreateThreadExInjectDll(DWORD dwProcessId, const wchar_t* pszDllFileName)
{
        int pathSize = (wcslen(pszDllFileName) + 1) * sizeof(wchar_t);
        // 1.打开目标进程
        HANDLE hProcess = OpenProcess(
               PROCESS_ALL_ACCESS, // 打开权限
               FALSE, // 是否继承
               dwProcessId); // 进程PID
        if (NULL == hProcess)
        {
               cout << L"打开目标进程失败!" << endl;
               return FALSE;
        }
        // 2.在目标进程中申请空间
        LPVOID lpPathAddr = VirtualAllocEx(
               hProcess, // 目标进程句柄
               0, // 指定申请地址
               pathSize, // 申请空间大小
               MEM_RESERVE | MEM_COMMIT, // 内存的状态
               PAGE_READWRITE); // 内存属性
        if (NULL == lpPathAddr)
        {
               cout << L"在目标进程中申请空间失败!" << endl;
               CloseHandle(hProcess);
               return FALSE;
        }
        // 3.在目标进程中写入Dll路径
        if (FALSE == WriteProcessMemory(
               hProcess, // 目标进程句柄
               lpPathAddr, // 目标进程地址
               pszDllFileName, // 写入的缓冲区
               pathSize, // 缓冲区大小
               NULL)) // 实际写入大小
        {
               cout << L"目标进程中写入Dll路径失败!" << endl;
               CloseHandle(hProcess);
               return FALSE;
        }

        //4.加载ntdll.dll
        HMODULE hNtdll = LoadLibraryW(L"ntdll.dll");
        if (NULL == hNtdll)
        {
               cout << L"加载ntdll.dll失败!" << endl;
               CloseHandle(hProcess);
               return FALSE;
        }

        //5.获取LoadLibraryA的函数地址
        //FARPROC可以自适应32位与64位
        FARPROC pFuncProcAddr = GetProcAddress(GetModuleHandle(L"Kernel32.dll"),
               "LoadLibraryW");
        if (NULL == pFuncProcAddr)
        {
               cout << L"获取LoadLibrary函数地址失败!" << endl;
               CloseHandle(hProcess);
               return FALSE;
        }

        //6.获取ZwCreateThreadEx函数地址,该函数在32位与64位下原型不同
        //_WIN64用来判断编译环境 ,_WIN32用来判断是否是Windows系统
#ifdef _WIN64
        typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
               PHANDLE ThreadHandle,
               ACCESS_MASK DesiredAccess,
               LPVOID ObjectAttributes,
               HANDLE ProcessHandle,
               LPTHREAD_START_ROUTINE lpStartAddress,
               LPVOID lpParameter,
               ULONG CreateThreadFlags,
               SIZE_T ZeroBits,
               SIZE_T StackSize,
               SIZE_T MaximumStackSize,
               LPVOID pUnkown
               );
#else
        typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
               PHANDLE ThreadHandle,
               ACCESS_MASK DesiredAccess,
               LPVOID ObjectAttributes,
               HANDLE ProcessHandle,
               LPTHREAD_START_ROUTINE lpStartAddress,
               LPVOID lpParameter,
               BOOL CreateSuspended,
               DWORD dwStackSize,
               DWORD dw1,
               DWORD dw2,
               LPVOID pUnkown
               );
#endif 
        typedef_ZwCreateThreadEx ZwCreateThreadEx =
               (typedef_ZwCreateThreadEx)GetProcAddress(hNtdll, "ZwCreateThreadEx");
        if (NULL == ZwCreateThreadEx)
        {
               cout << L"获取ZwCreateThreadEx函数地址失败!" << endl;
               CloseHandle(hProcess);
               return FALSE;
        }
        //7.在目标进程中创建远线程
        HANDLE hRemoteThread = NULL;
        DWORD dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL,
               hProcess,
               (LPTHREAD_START_ROUTINE)pFuncProcAddr, lpPathAddr, 0, 0, 0, 0, NULL);
        if (NULL == hRemoteThread)
        {
               cout << L"目标进程中创建线程失败!" << endl;
               CloseHandle(hProcess);
               return FALSE;
        }

        // 8.等待线程结束
        WaitForSingleObject(hRemoteThread, -1);
        // 9.清理环境
        VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
        CloseHandle(hRemoteThread);
        CloseHandle(hProcess);
        FreeLibrary(hNtdll);
        return TRUE;
}

const wchar_t dllPath[] = L"F:\\Dll1.dll";
int main()
{
        PROCESSENTRY32 info;
        // 查找指定进程名的PID
        if (getProcess32Info(&info, L"EditPlus.exe"))
        {
               // 进程标识符PID。
               ZwCreateThreadExInjectDll(info.th32ProcessID, dllPath);
               cout << "注入成功" << endl;
        }
        else {
               cout << "注入失败" << endl;
        }
}

后话

  • 这是最简单的DLL注入的实现,在此基础,你可以完善MFC程序,想办法让获取DLL文件的路径和获取被注入程序的信息变得更加智能
  • 你还可以修改DLL文件中的注入程序,执行其他的代码
  • 22
    点赞
  • 135
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
c hook注入dll是一种在Windows系统中实现函数钩子的技术。下面给出一个完整的例子: 首先,创建一个c文件,命名为hookdll.c,代码如下: #include <Windows.h> // 定义要hook的目标函数 typedef bool (WINAPI* ORIGINAL_FUNCTION)(LPCTSTR); ORIGINAL_FUNCTION OriginalFunction; // 定义hook的替代函数 bool WINAPI HookFunction(LPCTSTR lpFileName) { // 在这里编写你的hook函数逻辑 // 可以在这个函数中修改传入参数或返回值,实现钩子的目的 // ... // 调用原始函数 return OriginalFunction(lpFileName); } // Dll入口函数 BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { if (ul_reason_for_call == DLL_PROCESS_ATTACH) { // 加载kernel32.dll HMODULE kernel32 = LoadLibrary("kernel32"); if (kernel32 != NULL) { // 获取目标函数地址 OriginalFunction = (ORIGINAL_FUNCTION)GetProcAddress(kernel32, "函数名"); if (OriginalFunction != NULL) { // 修改函数的内存属性为可执行和可写 DWORD oldProtect; VirtualProtect(OriginalFunction, sizeof(ORIGINAL_FUNCTION), PAGE_EXECUTE_READWRITE, &oldProtect); // 修改函数的指针为hook函数的指针 *OriginalFunction = &HookFunction; // 还原函数的内存属性 VirtualProtect(OriginalFunction, sizeof(ORIGINAL_FUNCTION), oldProtect, &oldProtect); } // 释放kernel32.dll内存 FreeLibrary(kernel32); } } return TRUE; } 编译这个项目,得到一个dll文件,命名为hookdll.dll。 然后,创建一个使用目标dll的示例程序,例如使用了kernel32.dll中的某个方法。然后按照以下步骤实现hook注入: 1. 打开示例程序的源代码,编辑代码,添加以下代码段: #pragma comment(lib, "hookdll.lib") 2. 将hookdll.dll拷贝到示例程序的目录下。 3. 使用LoadLibrary函数在程序中动态加载hookdll.dll。 4. 调用示例程序中使用kernel32.dll中方法的代码,此时会执行被hook函数的替代函数。 通过以上步骤,我们就实现了c hook注入dll完整例子。在hook函数中可以对传入的参数或返回值进行修改,实现我们想要的钩子效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值