创建吸附窗口且不在任务栏显示的方法和思路

最近一直在做沙箱项目,在项目快接近结尾的时候,我想给在我们沙箱中运行的程序界面打上一个标记——标识其在我们沙箱中运行的。我大致想法是:在被注入程序的顶层窗口上方显示一个“标题性”窗口,顶层窗口外框外显示一个“异形”的空心窗口。这些窗口如影子般随着其被“吸附”窗口移动而移动,大小变化而变化。(转载请指明出处)以记事本为被注入程序为例:

        我用的注入和HooKApi方案是采用微软的detour库。关于如何HookApi的方法,可以参看我之前的《一种注册表沙箱的思路、实现——Hook Nt函数》。注入的方案,我采用的Detour库的DetourCreateProcessWithDll函数,该函数的W版原型是

[cpp]
BOOL WINAPI DetourCreateProcessWithDllW(LPCWSTR lpApplicationName, 
                                        __in_z LPWSTR lpCommandLine, 
                                        LPSECURITY_ATTRIBUTES lpProcessAttributes, 
                                        LPSECURITY_ATTRIBUTES lpThreadAttributes, 
                                        BOOL bInheritHandles, 
                                        DWORD dwCreationFlags, 
                                        LPVOID lpEnvironment, 
                                        LPCWSTR lpCurrentDirectory, 
                                        LPSTARTUPINFOW lpStartupInfo, 
                                        LPPROCESS_INFORMATION lpProcessInformation, 
                                        LPCSTR lpDllName, 
                                        PDETOUR_CREATE_PROCESS_ROUTINEW pfCreateProcessW) 
        该函数从倒数第二个参数之前的全是CreateProcessW的参数,倒数第二个参数是我们要注入的DLL的路径,最后一个参数是真实的CreateProcessW的函数入口地址。该函数的实现细节是:
        1 以挂起的方式启动被注入程序
        2 在内存中,修改被注入程序的导入表信息,在表中增加一个我们要注入的DLL中的导出函数

        3 恢复被挂起的进程

        该方案通过修改程序导入表,让系统误以为该程序需要调用到我们要注入的DLL中的导出函数,于是将我们注入的DLL加载到该进程内存空间,从而实现注入。这儿有个细节要说明:该方案要求我们注入DLL要至少有一个导出函数,哪怕这个函数什么也不做。

        源码中RegSandBoxMainDialog工程是个MFC工程,它用于启动我们注入的进程并实现注入。我们查看其注入代码的实现

[cpp] 
UpdateData(TRUE); 
 
char chCurrentPath[MAX_PATH] = {0}; 
GetModuleFileNameA( NULL, chCurrentPath, MAX_PATH ); 
 
std::string wszCurrentPath = chCurrentPath; 
size_t nindex = wszCurrentPath.rfind('\\'); 
wszCurrentPath = wszCurrentPath.substr( 0, nindex ); 
 
wszCurrentPath += "\\"; 
 
std::string szDllPath = wszCurrentPath; 
szDllPath += "HookWindow.dll";     // 拼接处注入DLL的完整路径 
 
std::wstring wszFilePath = m_Path; // 被注入进程的路径 
 
STARTUPINFO StartupInfo; 
ZeroMemory(&StartupInfo, sizeof(STARTUPINFO)); 
StartupInfo.cb = sizeof(STARTUPINFO); 
 
PROCESS_INFORMATION ProcessInfo; 
ZeroMemory(&ProcessInfo, sizeof(PROCESS_INFORMATION)); 
 
// FL:使用DetourCreateProcessWithDll需要注入的DLL要有一个导出函数 
BOOL bSuc = DetourCreateProcessWithDll( wszFilePath.c_str(), NULL, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE , 
    NULL, NULL, &StartupInfo, &ProcessInfo, szDllPath.c_str(), CreateProcessW ); 
        HookWindow工程编译连接处HookWIndow.dll,我将在之后一步一步介绍这个DLL的编写。
        HookWindow是一个Win32 dll工程,我们为其定义一个def文件HookWindow.def,其内容为:

[plain]
LIBRARY "HookWindow" 
EXPORTS Notify 
       Notify函数是为了达到DetourCreateProcessWithDll要求:注入DLL必须要至少有一个导出函数(原因已在上面说明过)而设计的,实际这个函数什么也没做,他就是个空壳。
[plain] 
VOID Notify(){ 

        现在得开始考虑窗口的实现了。我们知道windows系统是消息驱动的模型,那么我们的“吸附”窗口的消息模型该是什么样的?当时我思考方案时得出以下两种方案:
        1 Hook进程内窗口消息,在消息链中根据顶层窗口消息而决定我们窗口的创建、显示、隐藏和销毁。这相当于我们窗口的消息循环使用了被注入进程的顶层窗口的消息循环。

        2 注入进程后,启动一个线程,该线程负责创建窗口,同时在该线程中再启动一个监视被注入进程顶层窗口的线程,该线程将根据其得到的被注入进程窗口的位置大小状态等信息告诉我们窗口应该做何种处理。
       这两种方法各有其优缺点,方法1比方法2少1个线程,但是存在一种场景:当点击被注入程序顶层窗口的非客户区时,我们的窗口会被盖掉,因为这个时候还没轮到我们窗口处理该消息(SetWIndowsHookEx WH_CALLWNDPROCRET),此时我们无法让我们窗口显示在被注入进程顶层窗口前面。方法2就是比方法1多出线程数,如果我想创建两个窗口,就多出两个窗口线程,以此类推。如我设想的需求,我将创建一个管理外框异形空心窗口的线程和一个“标题”窗口,那就多出两个线程。

        我觉得我这两个窗口要处理的消息非常简单,同样也想做点与众不同。于是我设计了这样的方案,方案是融合了方案1和方案2的优点:

        SetWindowsHookEx勾住被注入进程的消息,同时设置Hook类型为WH_CALLWNDPROCRET。

[cpp] 
VOID HookWindowsFn() 

    do { 
        g_hhook = SetWindowsHookEx( WH_CALLWNDPROCRET, CallWndRetProc, NULL, GetCurrentThreadId() ); 
        if ( NULL == g_hhook ) { 
            _ASSERT(FALSE); 
        } 
 
        InitializeCriticalSection( &g_cs ); 
    } while (0); 

这样我们将在原程序处理完消息后进行消息处理。

[cpp] 
LRESULT CALLBACK CallWndRetProc( __in int nCode, __in WPARAM wParam, __in LPARAM lParam ) 

    if ( NULL != lParam ) { 
        LPCWPRETSTRUCT lptagCWPRETSTRUCT = (LPCWPRETSTRUCT)lParam; 
        DealMsg( lptagCWPRETSTRUCT->hwnd, lptagCWPRETSTRUCT->message, lptagCWPRETSTRUCT->wParam, lptagCWPRETSTRUCT->lParam ); 
    } 
 
    return CallNextHookEx(NULL, nCode, wParam, lParam); 

当我们收到消息时,我们要判断是否是我们关心的消息,这样将减少我们处理消息的线程的工作量。
[cpp]
BOOL IsNeedDealMsg( UINT uMsg ) 

    return ( IsNeedShowMsg( uMsg )  
        || ( WM_DESTROY == uMsg ) 
        || ( WM_CLOSE == uMsg ) ); 

 
BOOL IsNeedShowMsg( UINT uMsg ) 

    return ( ( WM_SHOWWINDOW == uMsg ) 
        || ( WM_MOVE == uMsg ) 
    || ( WM_MOVING == uMsg ) 
        || ( WM_SIZE == uMsg ) 
        || ( WM_WINDOWPOSCHANGED == uMsg )  
        ); 

[cpp] 
VOID DealMsg( HWND hAttachedWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) 

    if ( FALSE == IsNeedDealMsg( uMsg ) ) { 
        return; 
    } 
其次判断该窗口是否为我们自己创建的“吸附”窗口。如果是我们的“吸附”窗口,我们将不会做任何处理。

[cpp] 
VOID DealMsg( HWND hAttachedWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) 

    if ( IsHelperWindow( hAttachedWnd ) ) { 
        return; 
    } 
[cpp] view plaincopy
BOOL IsHelperWindow( HWND hwnd ) 

    WCHAR wszClassNameBuffer[MAX_PATH] = {0}; 
    int nClassNameLength = GetClassName( hwnd , wszClassNameBuffer, MAX_PATH - 1 ); 
    if ( 0 != nClassNameLength ) { 
        std::wstring wszClassName = wszClassNameBuffer; 
        if ( 0 == wcscmp( wszClassNameBuffer, TITILEWINDOWCLASS ) || 
             0 == wcscmp( wszClassNameBuffer, OUTSIDEWINDOWCLASS ) ) { 
            return TRUE; // 通过类名判断 
        } 
    } 
 
    return FALSE; 

然后我们需要根据消息类型,对窗口句柄做个判断。因为如果我们“宿主”窗口处理完WM_DESTROY后,我们再将不能对其调用GetWindowLong以获取其样式。于是对WM_DESTORY消息,我们只是判断其是否为顶层窗口。如果不是该消息,我们将判断该窗口是否为顶层窗口,且其窗口样式包含WS_SYSMENU(我试验了下,我所遇到的我认为该处理的窗口都有该属性,这个属于经验之谈,不一定准确)。

[cpp] 
if ( WM_DESTROY != uMsg ) { 
        if ( FALSE == IsValibleWindow( hAttachedWnd ) )  { 
            return; 
        } 
    } 
    else { 
        if ( FALSE == IsBaseWindow(hAttachedWnd) ) { 
            return; 
        } 
    } 
 
    if ( FALSE == IsNeedDealMsg( uMsg ) ) { 
        return; 
    } 
[cpp] 
BOOL IsValibleWindow( HWND hWnd ) 

    if ( FALSE == IsBaseWindow( hWnd ) )  
    { 
        return FALSE; 
    } 
 
    DWORD dwStyle = ::GetWindowLong( hWnd, GWL_STYLE ); 
    if ( !( WS_SYSMENU & dwStyle ) ) { 
        return FALSE; 
    } 
 
    return TRUE; 

 
BOOL IsTopWindow( HWND hwnd ) 

    BOOL bTop = FALSE; 
    do { 
        HWND hParentHwnd = NULL; 
        HWND hParenthwnd = GetParent( hwnd ); 
        if ( NULL == hParenthwnd ){ 
            bTop = TRUE; 
        } 
    } while (0); 
    return bTop; 

 
BOOL IsBaseWindow( HWND hWnd ) 

    if ( FALSE == ::IsWindow(hWnd)  
        || FALSE == IsTopWindow(hWnd) )  
    { 
        return FALSE; 
    } 
 
    return TRUE; 

如果是原程序创建的窗口,则判断该句柄是否已经存在一个管理“吸附”窗口的线程(该信息保存在一个Map中)。如果不存在,就创建一个管理两个“吸附”窗口的线程,并将<HWND,HTHREADHANDLE>对保存到Map中。如果存在,则向这个线程管理的窗口发送相应的消息。一个进程可能不止是存在一个顶层窗口,所以我这儿要建立Map信息。

[cpp] 
typedef struct _WindowThreadColloction_{ 
    LPCWindowThread lpTitleWindowThread; 
}WindowThreadColloction, *pWindowThreadColloction; 
 
typedef std::map<HWND,WindowThreadColloction> MapHwndThread; 
typedef MapHwndThread::iterator MapHwndThreadIter; 
[cpp] 
    LPCWindowThread lpWindowThread = NULL; 
    lpWindowThread = GetTitleWindowThread( hAttachedWnd ); 
 
    if ( NULL == lpWindowThread ) { 
        if ( WM_SHOWWINDOW == uMsg ) { 
            lpWindowThread = new CWindowThread(hAttachedWnd); 
            if ( NULL == lpWindowThread ) { 
                _ASSERT(FALSE); 
                return; 
            } 
            UpdateHwndTitleWindowThread( hAttachedWnd, lpWindowThread ); 
        } 
        else { 
            return; 
        } 
    } 
     
    lpWindowThread->NotifyMsg( uMsg ); 

现在我们将看一下我们管理两个“吸附”窗口的线程类。
[cpp] 
class CWindowThread:    
    public CMessageLoop, 
    public CMessageFilter 

public: 
    CWindowThread(void); 
    ~CWindowThread(void); 
public: 
    CWindowThread(HWND hAttachWindow); 
public: 
    VOID NotifyMsg(UINT uMsg); 
    VOID ExitThread(); 
    BOOL PreTranslateMessage(MSG* pMsg); 
private: 
    static DWORD WINAPI ThreadRoutine(LPVOID lpParam); 
private: 
    HWND m_hAttachWindow; 
    HANDLE m_hThread; 
    CWTLTitleWindow* m_pCWTLTitleWindow; 
    CWTLOutSideWindow* m_pCWTLOutSideWindow; 
}; 
typedef CWindowThread* LPCWindowThread; 
消息循环是在该线程中的,于是继承于CMessageLoop;因为我们要让我们窗口屏蔽ATL+F4这类的操作,所以我们要PreTranslateMessage,于是要继承于CMessageFilter。
[cpp] 
BOOL CWindowThread::PreTranslateMessage( MSG* pMsg ) 

    if ( WM_SYSKEYDOWN == pMsg->message ) { 
        return TRUE; 
    } 
    return FALSE; 

[cpp]
DWORD WINAPI CWindowThread::ThreadRoutine( LPVOID lpParam ) 

    CWindowThread* pThis = (CWindowThread*) lpParam; 
    if ( NULL == pThis ){ 
        return 0xFFFFFFFF; 
    } 
    if ( NULL == pThis->m_pCWTLTitleWindow ) { 
        pThis->m_pCWTLTitleWindow = new CWTLTitleWindow( pThis->m_hAttachWindow ); 
        pThis->m_pCWTLTitleWindow->Create( pThis->m_hAttachWindow ); // 注意这儿要设置为父窗口 
        pThis->m_pCWTLTitleWindow->RunWindowMsgLoop(); 
    } 
 
    if ( NULL == pThis->m_pCWTLOutSideWindow ) { 
        pThis->m_pCWTLOutSideWindow = new CWTLOutSideWindow( pThis->m_hAttachWindow ); 
        pThis->m_pCWTLOutSideWindow->Create( pThis->m_hAttachWindow ); // 注意这儿要设置为父窗口 
        pThis->m_pCWTLOutSideWindow->RunWindowMsgLoop(); 
    } 
 
    pThis->Run(); // 启动消息循环 
 
    return 0; 

[cpp] 
VOID CWindowThread::NotifyMsg( UINT uMsg ) 

    if ( m_pCWTLTitleWindow ){ 
        m_pCWTLTitleWindow->DealMsg( uMsg ); 
    } 
    if ( m_pCWTLOutSideWindow ) { 
        m_pCWTLOutSideWindow->DealMsg( uMsg ); 
    } 

下面再来看看窗口的实现。因为我们要做的是“吸附”窗口,该窗口应该不能影响原窗口正常的行为(比如不应该抢焦点,不在任务栏出现),同时考虑到刷新问题,我们要让该窗口具有双缓存。以“标题”窗口为例

[cpp]
class CWTLTitleWindow: 
    public CDoubleBufferWindowImpl< CWTLTitleWindow, CWindow, CWinTraits<WS_POPUP|WS_CLIPSIBLINGS, WS_EX_LEFT|WS_EX_LTRREADING|WS_EX_NOPARENTNOTIFY|WS_EX_NOACTIVATE>> 

public: 
    typedef CWTLTitleWindow _thisClass; 
    typedef CDoubleBufferImpl<_thisClass> _baseDblBufImpl; 
 
    DECLARE_WND_CLASS_EX(TITILEWINDOWCLASS, CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS, COLOR_WINDOW); 
     
    CWTLTitleWindow(void); 
    ~CWTLTitleWindow(void); 
public: 
    CWTLTitleWindow(HWND hAttachWindow); 
 
    BEGIN_MSG_MAP_EX(CWTLTitleWindow) 
        MESSAGE_HANDLER( WM_SHOWWINDOW, OnShow ) 
        MESSAGE_HANDLER( WM_DESTROY, OnDestroy) 
        MESSAGE_HANDLER( WM_QUIT, OnQuit ) 
        MESSAGE_HANDLER( WM_MOUSEACTIVATE, OnMouseActive ) 
        MESSAGE_RANGE_HANDLER( WM_USER, WM_USER + WM_USER, OnDealUserMsg ) 
        CHAIN_MSG_MAP(_baseDblBufImpl) 
    END_MSG_MAP() 
 
    LRESULT OnShow(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); 
    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); 
    LRESULT OnQuit(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); 
    LRESULT OnMouseActive(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); 
    LRESULT OnDealUserMsg(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); 
    void DoPaint(HDC dc); 
 
    VOID Start(); 
    VOID DealMsg( UINT uMsg ); 
     
private: 
    ECalcResult CalcTitleWindowXY( int& x, int& y ); 
    VOID ShowWindow(); 
private: 
    HWND m_hAttachHWnd; 
    HINSTANCE m_hInstance; 
    HBITMAP m_hBitmap; 
    RECT m_hAttachWindowRect; 
};  
首先说一下双缓冲。我继承于CDoubleBufferWindowImpl。在消息映射中,我们要让我们不处理的消息交给基类处理
[cpp] 
typedef CWTLTitleWindow _thisClass; 
typedef CDoubleBufferImpl<_thisClass> _baseDblBufImpl; 
[cpp] view plaincopy
CHAIN_MSG_MAP(_baseDblBufImpl) 
同时实现DoPaint函数。
[cpp]
void CWTLTitleWindow::DoPaint( HDC dc ) 

    CRect rc; 
    GetClientRect(&rc);     
    CMemoryDC MemDc( dc, rc ); 
 
    HBRUSH hBitmapBrush = CreatePatternBrush(m_hBitmap); 
    if ( NULL == hBitmapBrush ) { 
        return; 
    } 
 
    MemDc.FillRect( &rc, hBitmapBrush ); 
    DeleteObject( hBitmapBrush ); 

m_bBitmap是我在资源文件中的一个bmp图片,我们在Start函数中将其载入。在类释放时,将其delete。
[cpp] 
CWTLTitleWindow::~CWTLTitleWindow(void) 

    if ( NULL != m_hBitmap ) { 
        DeleteObject( m_hBitmap ); 
    } 

 
VOID CWTLTitleWindow::Start() 

#pragma warning(push) 
#pragma warning(disable:4312) 
    if ( NULL == m_hInstance ) { 
        m_hInstance = (HINSTANCE)GetWindowLong( GWL_HINSTANCE ); 
    } 
#pragma warning(pop) 
    if ( NULL == m_hBitmap ) { 
        m_hBitmap = LoadBitmap( m_hInstance, MAKEINTRESOURCE(IDB_BITMAP1)); 
    } 

以上基本上算是完成了双缓冲的操作了,但是为了尽量减少刷新的次数,我会多加个判断:改变的位置和大小是否和现在的位置和大小一致,如果一致则不做任何操作,否则刷新。
[cpp] 
ECalcResult CWTLTitleWindow::CalcTitleWindowXY(int& x, int& y ) 

    ECalcResult eResult = EError; 
 
    do { 
        RECT rcAttachWindow; 
        if ( FALSE == ::GetWindowRect( m_hAttachHWnd, &rcAttachWindow ) ) { 
            break; 
        } 
 
        if ( rcAttachWindow.left == m_hAttachWindowRect.left  
            && rcAttachWindow.right == m_hAttachWindowRect.right 
            && rcAttachWindow.top == m_hAttachWindowRect.top  
            && rcAttachWindow.bottom == m_hAttachWindowRect.bottom ) 
        { 
            eResult = ENoChange; 
            break; 
        } 
        else { 
            m_hAttachWindowRect = rcAttachWindow; 
        } 
 
        x = ( rcAttachWindow.left + rcAttachWindow.right - TIPWINDTH ) / 2; 
        y = rcAttachWindow.top; 
 
        eResult = ESuc; 
    } while (0); 
    
    return eResult; 

再说下无焦点窗口的细节。
首先窗口样式要有WS_POPUP,网上有人说还要加上WS_VISIBLE,但是我觉得没必要。其次扩展属性要有WS_EX_NOACTIVATE。再次我们要处理WM_MOUSEACTIVATE消息。

[cpp]
MESSAGE_HANDLER( WM_MOUSEACTIVATE, OnMouseActive ) 
[cpp] view plaincopy
LRESULT CWTLTitleWindow::OnMouseActive( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) 

    return MA_NOACTIVATE; // MA_NOACTIVATEANDEAT亦可 

最后要特别注意下窗口显示和移动对焦点的影响。在窗口显示时,如果我们使用ShowWindow和MoveWindow这类的函数,会导致我们我们窗口还可以获得焦点。我们要使用SetWindowPos,最后一个参数要带上SWP_NOACTIVATE。
[cpp] 
VOID CWTLTitleWindow::ShowWindow() 

    if ( FALSE == IsBaseWindow( m_hAttachHWnd ) ) { 
        return; 
    } 
    int x = 0;  
    int y = 0; 
    ECalcResult eResult = CalcTitleWindowXY( x, y ); 
    if ( EError == eResult ) { 
        ::SetWindowPos( m_hWnd, NULL, x, y, TIPWINDTH, TIPHEIGHT, SWP_NOACTIVATE | SWP_HIDEWINDOW ); 
        return; 
    } 
    else if ( ENoChange == eResult ) { 
        return; 
    } 
     
    ::SetWindowPos( m_hWnd, NULL, x, y, TIPWINDTH, TIPHEIGHT, SWP_NOACTIVATE | SWP_SHOWWINDOW); 

最后说一下业务相关的消息传递。在被注入进程的顶层窗口接受到一些消息后,我们会将这些消息传递给我们的窗口,让其做一些处理。为了区分消息来源于顶层窗口还是自己,我将顶层窗口消息处理为一个用户自定义消息。
[cpp] 
VOID CWTLTitleWindow::DealMsg( UINT uMsg ) 

    ::PostMessage( m_hWnd, WM_USER + uMsg, NULL, NULL ); 

消息映射是这么写的,用于处理整个用户自定义消息(而不会处理顶层窗口传来的其用户自定义消息)
[cpp]
MESSAGE_RANGE_HANDLER( WM_USER, WM_USER + WM_USER, OnDealUserMsg ) 
[cpp] view plaincopy
LRESULT CWTLTitleWindow::OnDealUserMsg( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) 

    UINT uAttachedWindowMsg = uMsg - WM_USER; 
    if ( IsNeedShowMsg(uAttachedWindowMsg) ) { 
        ShowWindow(); 
    } 
    else { 
        ::PostMessage( m_hWnd, uAttachedWindowMsg, wParam, lParam ); 
    } 
    return 1; 

外框窗口和标题窗口基本类似,但是其背景是使用画笔画的,而不是通过贴图。另一个很大的区别就是外框窗口是一个空心的异形窗口。这些区别的主要体现是在DoPaint函数中
[cpp] 
void CWTLOutSideWindow::DoPaint( HDC dc ) 

    CRect rc; www.2cto.com
    GetClientRect(&rc); 
    CMemoryDC MemDc( dc, rc ); 
 
    HRGN RgnInside = CreateRectRgn( rc.left + WIDTHHEIGHTADD, rc.top + WIDTHHEIGHTADD, 
        rc.right - WIDTHHEIGHTADD, rc.bottom - WIDTHHEIGHTADD ); 
     
    HRGN RgnOut = CreateRectRgn( rc.left, rc.top, rc.right, rc.bottom ); 
 
    CombineRgn( RgnOut, RgnOut, RgnInside, RGN_DIFF ); 
 
    MemDc.FillRgn( RgnOut, m_brush ); 
    SetWindowRgn( RgnOut, TRUE ); // 设置异形窗口 
     
    DeleteObject( RgnInside ); 
    DeleteObject( RgnOut ); 
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值