windows消息处理过程及消息钩子

应用层发消息: 发送消息过程 SendMessage(user32.dll)->SendMessageWorker,先检查有没有hook消息钩子,有的话调用CsSendMessage,进入消息钩子过滤函数。

没有的话,看是不是系统消息,是的话在Message表中找到对应msg id的索引值,通过索引值在在gapfnScSendMessage数组中找到对应的消息处理函数

如果是NtUserMessageCall的话,则进驱动处理消息。

 

进驱动层发消息: 进入NtUserMessageCall后,如果是系统消息,同上,在gapfnMessageCall数组中找到对应的处理函数。比如msg id为0x112,最小化窗口消息,

调用过程为NtUserfnNCDESTORY->xxxWrapSendMessage->xxxSendMessageTimeout->xxxInterSendMsgEx,在这个函数中,先通过AllocSMS分配一个消息结构体,

在将消息加入到目标线程的pSMSReceiveList链表中,代表目标线程有要接受的消息,然后调用SetWakeBit唤醒目标线程,激发目标线程的EventQueueServer,然后等待

自己的EventQueueServer被激发。然后调用xxxSleepThread是自己处于等待状态。在等待前,调用xxxReceiveMessage检查是否有hook消息钩子,如果有的话,回调CallClientProc

->xxxHkCallHook 分发处理消息钩子函数。 自己会setevent,while死循环阻塞在KeWaitForSingleObject等待。在此函数中,先检查是哪种互斥对象,如果对象被激活,则调用处理函数。否则KiSwapThread通知cpu切换为别的线程运行。调用KiComputeWaitInterval继续等待。

 

安装钩子过程:进入驱动调用NtUserSetWindowsHookEx->zzzSetWindowsHookEx修改目标线程的ptiThread->aphkStart[nFilterType + 1]钩子数组。

 

键盘按键:ioapic寄存器接收,读出index和哪个cpu处理,local apic发给键盘中断处理例程,再传给键盘端口驱动,端口类驱动。==win32k的RawInputThread读顶层类设备状态,读出按键扫描码,经过底层键盘钩子处理,匹配是win,alt+tab键等对应处理,再匹配普通字符,发给顶层窗口的线程,进入wm_char处理

 

 

 

消息优先级:QS_SENDMESSAGE > QS_POSTMESSAGE > QS_QUIT > QS_INPUT > QS_PAINT > QS_TIMER

 

msg:   112

user消息用的是win32k自己的一套通信方式,最多就用了个event来同步

跨线程的WM_COPYDATA没有使用共享内存,反而复制了两次数据

发送者SendMessage->xxxSendMessageTimeout->xxxInterSendMsgEx(UserAllocPoolWithQuota分配内核内存,将用户数据复制到内核空间)->SetWakeBit唤醒接受者->SetWakeBit等待应答

接受者xxxReceiveMessage->XXXSENDMESSAGETOCLIENT(宏)->ScSendMessageSMS(也是宏)->SfnCOPYDATA(sender side)->CaptureCallbackData(把数据从内核空间复制到用户空间)->KeUserModeCallback(转到用户模式)->SfnCOPYDATA(receiver side)->窗口过程->回到内核模式,应答发送者

 

 

回调函数什么时候被调用, 每个回调函数的情况都不一样, 就如你老板说的, 是系统定义好的, 对方(系统/库)对这个回调函数的文档说明一般只讲个大概不会讲细节, 你想弄清楚"触发机制", 那就去看对方的源码, 具体在哪些情况下哪些代码会call你的callback函数. 如果对方文档不详源码不给, 那就要靠自己的经验去猜去反汇编.

1) comp

comp可算是最简单的callback, 你调用qsort时把comp(你/crt库之间的callback)作为参数, 告诉对方(crt库)比较大小时call你这个comp. 接下去qsort立即触发comp, 反复调用comp直至qsort返回.

 

[] Vc7\crt\src\qsort.c

void __cdecl qsort (

    void *base,

    size_t num,

    size_t width,

    int (__cdecl *comp)(const void *, const void *)

    )

{

        for (;;) {

            if (mid > loguy) {

                do  {

                    loguy += width;

                } while (loguy < mid && comp(loguy, mid) <= 0);

            }

            if (mid <= loguy) {

                do  {

                    loguy += width;

                } while (loguy <= hi && comp(loguy, mid) <= 0);

            }

        }

}

 

2) ThreadProc

你调用AfxBeginThread时把ThreadProc(你/mfc库之间的callback)作为参数, 告诉对方(mfc库)线程开始执行时call你这个ThreadProc.

 

AfxBeginThread把ThreadProc作为CWinThread内部参数保存起来, 调用成员函数CWinThread::CreateThread. CWinThread::CreateThread调用_beginthreadex时把_AfxThreadEntry(mfc库/crt库之间的callback)作为参数.

 

_beginthreadex把_AfxThreadEntry放在_ptiddata ptd, 调用Windows API CreateThread时把_threadstartex(crt库/系统之间的callback)作为参数.

 

CreateThread按原参数(加上hProcess)调用CreateRemoteThread. CreateRemoteThread把lpStartAddress(AfxBeginThread是_threadstartex, 如果你直接用Windows API CreateThread, 则是你的ThreadProc)放在CONTEXT ThreadContext. 并把ThreadContext.Eip设置为BaseThreadStartThunk(子系统/子系统之间的callback).

 

AfxBeginThread (pfnThreadProc = ThreadProc, pParam = Param)

 CWinThread::CWinThread (m_pfnThreadProc = pfnThreadProc = ThreadProc, m_pThreadParams = pParam = Param)

 CWinThread::CreateThread (startup.pThread = this)

  _beginthreadex (ptd->_initaddr = initialcode = _AfxThreadEntry, ptd->_initarg = argument = startup, CREATE_SUSPENDED)

   CreateThread (lpStartAddress = _threadstartex, lpParameter = ptd) (user mode/kernel mode切换)

    CreateRemoteThread (ThreadContext.Eip = BaseThreadStartThunk, ThreadContext.Eax = lpStartAddress, ThreadContext.Ebx = lpParameter)

     BaseCreateStack

     BaseInitializeContext

     NtCreateThread (ThreadContext = &ThreadContext, CreateSuspended = TRUE)

      PspCreateThread (ThreadContext = ThreadContext, StartRoutine = NULL, StartContext = NULL, Thread->StartAddress = ThreadContext->Eip)

       ObCreateObject 创建线程对象

       MmCreateKernelStack

       KeInitializeThread (SystemRoutine = PspUserThreadStartup, StartRoutine = NULL, StartContext = ThreadContext->Eip, ContextFrame = ThreadContext)

        KeInitializeApc (SuspendApc, KernelRoutine = KiSuspendNop, NormalRoutine = KiSuspendThread, NormalContext = NULL, ApcMode = KernelMode)

        KeInitializeSemaphore (SuspendSemaphore, Semaphore->Header.SignalState = 0, Semaphore->Limit = 2)

        KiInitializeContextThread (Thread->PreviousMode = TrFrame->PreviousPreviousMode = UserMode, SwitchFrame->RetAddr = KiThreadStartup)

         KeContextToKframes

       KeSuspendThread (if CreateSuspended)

        KiInsertQueueApc (SuspendApc)

       KeReadyThread

        KiReadyThread 把线程对象转到就绪状态

     CsrClientCallServer

     NtResumeThread (if !CREATE_SUSPENDED)

      KeResumeThread (Thread->SuspendSemaphore.Header.SignalState += 1)

       KiWaitTest (if Thread->SuspendSemaphore.Header.WaitListHead not empty)

        KiWaitSatisfyAny (Thread->SuspendSemaphore.Header.SignalState -= 1)

        KiUnwaitThread

         KiReadyThread (Thread->State = Ready)

  ResumeThread

  WaitForSingleObject (startup.hEvent)

  SuspendThread (if CREATE_SUSPENDED)

  SetEvent (startup.hEvent2)

 

|_

| SwitchFrame

|_

| PSystemRoutine (PspUserThreadStartup)

|_

| PStartRoutine (NULL)

|_

| PStartContext (BaseThreadStartThunk)

|_

| PUserContextFlag (1)

|_

| TrFrame (copy from ThreadContext)

|_

| NpxFrame

|_

  Thread->InitialStack

 

AfxBeginThread到达KiReadyThread之后就逐级返回. 线程切换轮到新线程, 新线程先处理SuspendApc, 等待SuspendSemaphore.

如果在处理SuspendApc之前, CreateRemoteThread已调用NtResumeThread, 那么SuspendSemaphore立即满足, 无需等待, 立即执行KiThreadStartup.

反之, 则转到等待状态, 直到CreateRemoteThread调用NtResumeThread之后, 又转到就绪状态. 线程切换又轮到新线程, 开始执行KiThreadStartup.

 

AfxBeginThread创建的线程总是CREATE_SUSPENDED, 在_beginthreadex返回后再ResumeThread, 在_AfxThreadEntry callback后再SuspendThread(如果你给AfxBeginThread参数有CREATE_SUSPENDED).

 

KiSuspendNop

KiSuspendThread

 KeWaitForSingleObject (SuspendSemaphore, WaitMode = KernelMode, Alertable = FALSE, Timeout = NULL)

  KiWaitSatisfyOther (if Semaphore->Header.SignalState > 0, Semaphore->Header.SignalState -= 1)

  KiSwapThread (else, Thread->State = Waiting)

   SwapContext

 

KiThreadStartup

 PspUserThreadStartup

  KeInitializeApc (StartApc, KernelRoutine = PspNullSpecialApc, NormalRoutine = PspSystemDll.LoaderInitRoutine, NormalContext = NULL, ApcMode = UserMode)

  KeInsertQueueApc (StartApc, SystemArgument1 = PspSystemDll.DllBase)

   KiInsertQueueApc (StartApc)

  DbgkCreateThread

 KiServiceExit2 (TFrame = &TrFrame, Thread->Tcb.Alerted = 0)

  KiDeliverApc (StartApc, PreviousMode = UserMode, ExceptionFrame = NULL, TrapFrame = TFrame, Thread->TrapFrame = TFrame)

   PspNullSpecialApc

    ExFreePool

   KiInitializeUserApc (TrapFrame = TFrame, TFrame->Eip = (ULONG)KeUserApcDispatcher, TFrame->HardwareEsp = UserStack)

    KeContextFromKframes

  iretd (esp = &TFrame->Eip)

 

|_UserStack

| NormalRoutine (PspSystemDll.LoaderInitRoutine)

|_

| NormalContext (NULL)

|_

| SystemArgument1 (PspSystemDll.DllBase)

|_

| SystemArgument2 (NULL)

|_

| ContextFrame (copy from ThreadContext)

|_

  ThreadContext.Esp

 

KeUserApcDispatcher (UserStartApc) 

KiUserApcDispatcher

 PspSystemDll.LoaderInitRoutine

 LdrInitializeThunk (NormalContext = &ContextFrame)

  LdrpInitialize (Context = NormalContext)

   LdrpInitializeThread

    LdrpAllocateTls

    LdrpCallInitRoutine (DLL_THREAD_ATTACH)

    LdrpCallTlsInitializers (DLL_THREAD_ATTACH)

 NtContinue (ContextRecord = &ContextFrame, TestAlert = TRUE) (user mode/kernel mode切换)

  KiContinue (TrFrame->Eip = ContextRecord->Eip)

   KiContinuePreviousModeUser

    KeContextToKframes

  KeTestAlertThread

  KiServiceExit2

 

BaseThreadStartThunk (eax = ThreadContext.Eax = lpStartAddress = _threadstartex, ebx = ThreadContext.Ebx = lpParameter = ptd)

 BaseThreadStart (lpStartAddress = _threadstartex, lpParameter = ptd)

  CsrNewThread

   NtRegisterThreadTerminatePort

  _threadstartex (ptd = ptd)

   _callthreadstartex (ptd->_initaddr = _AfxThreadEntry, ptd->_initarg = startup)

    _AfxThreadEntry (pStartup = startup, pThread = pStartup->pThread, pThread->m_pfnThreadProc = ThreadProc, pThread->m_pThreadParams = Param)

     AfxInitThread

      SetWindowsHookEx (WH_MSGFILTER)

     SetEvent (pStartup->hEvent)

     WaitForSingleObject (pStartup->hEvent2)

     ThreadProc (lpParameter = Param) 你/mfc库之间的callback绕了一大圈终于被调用

     AfxEndThread

    _endthreadex

  ExitThread

 

3) WndProc

WndProc的情况就复杂多了, 前几天我只是想弄清楚窗口移动时几个消息的先后顺序和作用, 花了好几天时间才管见一斑. 要是把WndProc被调用(几百个消息各种情况下)的"触发机制"全弄清楚了, 也就是对Windows的窗口子系统了如指掌了.

 

xxxMoveWindow/xxxSetWindowPlacement

 xxxSetWindowPos

 

xxxSetWindowPos/xxxMinMaximize

 InternalBeginDeferWindowPos

 _DeferWindowPos

 xxxEndDeferWindowPosEx

  xxxCalcValidRects

   WM_WINDOWPOSCHANGING (if !SWP_NOSENDCHANGING)

    xxxDefWindowProc

     xxxAdjustSize (if !SWP_NOSIZE)

      xxxInitSendValidateMinMaxInfo

       WM_GETMINMAXINFO

   WM_NCCALCSIZE (if !SWP_NOSIZE || SWP_FRAMECHANGED)

    xxxDefWindowProc

     xxxCalcClientRect

      GetCaptionHeight

      GetWindowBorders

      xxxMenuBarCompute

  xxxDoSyncPaint

  xxxSendChangedMsgs

   WM_WINDOWPOSCHANGED (if !SWP_NOCHANGE)

    xxxDefWindowProc

     xxxHandleWindowPosChanged

      WM_MOVE (if !SWP_NOCLIENTMOVE)

      xxxSendSizeMessage (if !SWP_NOCLIENTSIZE)

       WM_SIZE

 

WM_NCLBUTTONDOWN

 xxxDefWindowProc

  xxxDWP_NCMouse

   WM_SYSCOMMAND (SC_SIZE + ht - HTSIZEFIRST + WMSZ_SIZEFIRST)

 

WM_NCLBUTTONUP

 xxxDefWindowProc

  xxxDWP_NCMouse

   xxxHandleNCMouseGuys

    WM_SYSCOMMAND

 

WM_SYSCOMMAND (SC_SIZE + ht - HTSIZEFIRST + WMSZ_SIZEFIRST)

 xxxDefWindowProc

  xxxSysCommand

 

xxxSysCommand (SC_RESTORE/SC_MINIMIZE/SC_MAXIMIZE)

 xxxShowWindow

  xxxMinMaximize

   CkptRestore

   InternalBeginDeferWindowPos

   _DeferWindowPos

   xxxEndDeferWindowPosEx (SWP_DRAWFRAME | SWP_NOCOPYBITS | SWP_STATECHANGE)

 

xxxSysCommand (SC_SIZE/SC_MOVE)

 xxxMoveSize

  xxxInitSendValidateMinMaxInfo

   WM_GETMINMAXINFO

  xxxMS_FlushWigglies

  bSetDevDragWidth

  xxxDrawDragRect

  WM_ENTERSIZEMOVE

  xxxCapture

  xxxMS_TrackMove

 

xxxMS_TrackMove (WM_MOUSEMOVE)

 xxxTM_MoveDragRect

  WM_SIZING

  xxxDrawDragRect

 

xxxMS_TrackMove (WM_LBUTTONUP)

 xxxTM_MoveDragRect

 xxxDrawDragRect

 xxxReleaseCapture

 xxxSetWindowPos

 WM_EXITSIZEMOVE

 

3.1) SendMessage

SendMessage的消息SDK文档称之为nonqueued messages, 没放到消息队列(mlPost/mlInput)里面, GetMessage不会得到SendMessage的消息.

mlPost是PostMessage的消息队列, 比如WM_HOTKEY, WM_TIMER, WM_LOGONNOTIFY. WM_QUIT一般没存放在队列(Desktop/console窗口例外), 是直接放到xxxReadPostMessage的参数.

mlInput是键盘鼠标和系统事件的消息队列. psmsReceiveList是异线程SendMessage的SMS(SendMessage Structure)队列. GetMessage只取回mlPost/mlInput的消息, SMS是另外处理的.

 

xxxInternalGetMessage

 while (TRUE) {

 while (QS_SENDMESSAGE) xxxReceiveMessage (pti->psmsReceiveList)

 if (QS_POSTMESSAGE) xxxReadPostMessage (pti->mlPost) break

 if (QS_INPUT|QS_EVENT) xxxScanSysQueue (pti->pq->mlInput) break

 xxxSleepThread

 }

 

#define QS_ALLINPUT (QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY | QS_SENDMESSAGE)

 

typedef struct tagTHREADINFO { ...

    PQ              pq;                 // keyboard and mouse input queue

    PSMS            psmsReceiveList;    // SMSs to be processed

    MLIST           mlPost;             // posted message list.

} THREADINFO;

 

* Message Queue structure.

typedef struct tagQ { ...

    MLIST       mlInput;            // raw mouse and key message list.

} Q, *PQ;

 

 

 

SendMessage如果同线程, 是直接call WndProc. 如果异线程, 一般是调用NtUserMessageCall(特殊的消息是调用fnCOPYGLOBALDATA/fnEMGETSEL这些系统函数). NtUserMessageCall又根据不同的消息分别调用45个NtUserfn*系统函数. 比如WM_SETTEXT是调用NtUserfnINSTRINGNULL, 进而调用xxxInterSendMsgEx.

 

xxxInterSendMsgEx构建一个sms(SendMessage Structure), 把消息复制到sms, 如果消息有指针参数, 比如WM_SETTEXT的lParam是字符指针, 就分配系统内存把指针所指的内容复制过来, 把sms加到系统gpsmsList队列首, 再把sms加到ptiReceiver->psmsReceiveList队列尾. 接着设置目标线程的QS_SENDMESSAGE标志, 激发目标线程的EventQueueServer, 然后等待自己的EventQueueServer.  

 

目标线程一般多在消息循环的GetMessage中等待自己的EventQueueServer(如果正在处理消息, 处理完仍是回到GetMessage), EventQueueServer激发, 导致KeWaitForSingleObject返回, xxxSleepThread返回, 进入xxxReceiveMessage, 把sms从ptiReceiver->psmsReceiveList队列(从头到尾逐个处理)移除, 调用xxxSendMessageToClient进入usermode call WndProc再返回kernel mode, 接着设置源线程的QS_SMSREPLY标志, 激发源线程的EventQueueServer, 然后继续自己的消息循环.

 

 

 

源线程的EventQueueServer激发, 导致KeWaitForSingleObject返回, xxxSleepThread返回到xxxInterSendMsgEx, 把sms从系统gpsmsList队列移除, 释放系统内存和sms, 逐级返回到SendMessage.

 

SendMessage (同线程)

 SendMessageWorker

  UserCallWinProcCheckWow

   InternalCallWinProc

    WndProc

 

SendMessage (异线程)

 SendMessageWorker

  NtUserMessageCall (user mode/kernel mode切换)

   EnterCrit

   NtUserfnINSTRINGNULL (WM_SETTEXT)

    RtlInitLargeUnicodeString

    xxxWrapSendMessage (xParam = 0)

     xxxSendMessageTimeout (fuFlags = SMTO_NORMAL, uTimeout = 0, lpdwResult = NULL)

      xxxInterSendMsgEx (pism = 0, lRet = psms->lRet)

       AllocSMS (psms->spwnd = pwnd, psms->ptiCallBackSender = NULL, psms->flags = 0)

       HeavyAllocPool (dwFlags = DAP_USEQUOTA)

       HMAssignmentLock (psms->spwnd)

       SetWakeBit (pti = ptiReceiver, wWakeBit = QS_SENDMESSAGE)

        KeSetEvent (pti->pEventQueueServer, PriorityIncrement = 2, WaitImmediatelyFollowed = FALSE)

       KeSetKernelStackSwapEnable (Enable = FALSE)

       xxxSleepThread (fsWakeMask = QS_SMSREPLY, Timeout = 0, fInputIdle = FALSE)

        ClearQueueServerEvent

         KeClearEvent(ptiCurrent->pEventQueueServer)

        LeaveCrit 

        KeWaitForSingleObject (ptiCurrent->pEventQueueServer)

        EnterCrit

       KeSetKernelStackSwapEnable (Enable = OldState)

       SetWakeBit (pti = ptiSender, wWakeBit = QS_SMSREPLY)

       UnlinkSendListSms (ppsmsUnlink = NULL)

        HMAssignmentUnlock (psms->spwnd)

        UserFreePool

        FreeSMS

   LeaveCrit

 

xxxInternalGetMessage

 xxxSleepThread (fsWakeMask = QS_ALLINPUT | QS_EVENT | QS_ALLPOSTMESSAGE, Timeout = 0, fInputIdle = TRUE)

  ClearQueueServerEvent

   KeClearEvent(ptiCurrent->pEventQueueServer) 

  LeaveCrit

  KeWaitForSingleObject (ptiCurrent->pEventQueueServer, WaitReason = WrUserRequest, WaitMode = UserMode, Alertable = FALSE, Timeout = NULL)

  EnterCrit

 xxxReceiveMessage

  xxxSendMessageToClient

   SfnINSTRINGNULL

    AllocCallbackMessage

    CaptureCallbackData

    LeaveCrit

    KeUserModeCallback (ApiNumber = 0x1B)

     KiGetUserModeStackAddress

     KiCallUserMode ([esp].TsEip = KiUserCallbackDispatcher)

     KiServiceExit

     KiSystemCallExitBranch

     KiSystemCallExit2

     sysexit (kernel mode进入user mode)

     KiUserCallbackDispatcher

      __fnINSTRING

       FixupCallbackPointers 

       DispatchClientMessage

        UserCallWinProcCheckWow

         InternalCallWinProc

          WndProc

       XyCallbackReturn

       int 2b (user mode返回kernel mode)

     KiCallbackReturn

   EnterCrit

  SetWakeBit(pti = ptiSender, wWakeBit = QS_SMSREPLY)

   KeSetEvent (pti->pEventQueueServer)

 

----写于2013-11-3

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
本实例由VS2008开发,在提供了一套驱动开发框架的同时,又演示了如何获取Shadow SSDT表函数原始地址的办法。 主要函数:ULONG GetShadowSSDT_Function_OriAddr(ULONG index); 原理说明: 根据特征码搜索导出函数KeAddSystemServiceTable来获取Shadow SSDT基址,以及通过ZwQuerySystemInformation()函数获取win32k.sys基址,然后解析PE定位到Shadow SSDT在win32k.sys的偏移地址,并通过进一步计算来得到Shadow SSDT表函数的原始地址。 这里只测试了三个函数:(460)NtUserMessageCall、(475)NtUserPostMessage和(502)NtUserSendInput,具体使用时可以举一反三,网上完整的源代码实例并不太多,希望可以帮到真正有需要的朋友。 系统环境: 在WinXP SP3系统 + 瑞星杀毒软件 打印输出: [ LemonInfo : Loading Shadow SSDT Original Address Driver... ] [ LemonInfo : 创建“设备”值为:0 ] [ LemonInfo : 创建“设备”成功... ] [ LemonInfo : 创建“符号链接”状态值为:0 ] [ LemonInfo : 创建“符号链接”成功... ] [ LemonInfo : 驱动加载成功... ] [ LemonInfo : 派遣函数(DispatchRoutine) IRP 开始... ] [ LemonInfo : 派遣函数(DispatchRoutine) IRP Enter IRP_MJ_DEVICE_CONTROL... ] [ LemonInfo : 获取ShadowSSDT表 (460)NtUserMessageCall 函数的“当前地址”为:0xB83ECFC4,“起源地址”为:0xBF80EE6B ] [ LemonInfo : 获取ShadowSSDT表 (475)NtUserPostMessage 函数的“当前地址”为:0xB83ECFA3,“起源地址”为:0xBF8089B4 ] [ LemonInfo : 获取ShadowSSDT表 (502)NtUserSendInput 函数的“当前地址”为:0xBF8C31E7,“起源地址”为:0xBF8C31E7 ] [ LemonInfo : 派遣函数(DispatchRoutine) IRP_MJ_DEVICE_CONTROL 成功执行... ] [ LemonInfo : 派遣函数(DispatchRoutine) IRP 结束... ] [ LemonInfo : UnLoading Shadow SSDT Original Address Driver... ] [ LemonInfo : 删除“符号链接”成功... ] [ LemonInfo : 删除“设备”成功... ] [ LemonInfo : 驱动卸载成功... ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值