挂钩Windows API

挂钩Windows API

 

1. 内容

2. 介绍

3. 挂钩方法

  3.1 运行前挂钩

  3.2 运行时挂钩

    3.2.1 使用IAT挂钩本进程

    3.2.2 改写入口点挂钩本进程

    3.2.3 保存原始函数

    3.2.4 挂钩其它进程

      3.2.4.1 DLL注入

      3.2.4.2 独立的代码

      3.2.4.3 原始修改

4. 结束语

 

 

=====[ 2. 介绍 ]====================================================

 

    这篇文章是有关在OS Windows下挂钩API函数的方法。所有例子都在基于NT技术的Windows版本NT 4.0及以上有效(Windows NT 4.0, Windows 2000, Windows XP)。可能在其它Windows系统也会有效。

    你应该比较熟悉Windows下的进程、汇编器、PE文件结构和一些API函数,才能明白这篇文章里的内容。

    这里使用"Hooking API"这个术语表示对API的完全修改。当调用被挂钩的API时,我们的代码能立刻被执行。我将写下完全的挂钩过程。

 

 

=====[ 3. 挂钩方法 ]==============================================

 

    一般来说我们的目的是用我们的代码取代一些函数里的代码。这些问题有时可以在进程运行前解决。这些大多数时候可以用我们运行的用户级进程来完成,目的可以是修改程序的行为。举个例子应用程序的破解,比方说有些程序会在启动时需要原光盘,我们想要不用光盘就启动它。如果我们修改获取驱动类型的函数我们就可以让程序从硬盘启动。

    当我们挂钩系统进程时(比如说服务)这些不可能做到或者我们不打算这么做,或者在这个例子里我们不知道哪个进程才是目标。这时我们就要用到动态挂钩(在运行时挂钩)的技术。使用的例子有rootkit或者病毒里的反杀毒软件的技术。

 

 

=====[ 3.1 运行前挂钩 ]===========================================

 

    这里修改我们想要修改函数来自的物理模块(大多数时候是.exe或.dll)。在这里我们至少有3种可能的做法。

    第一种可能是找到函数的入口点然后重写它的代码。这会因为函数的大小而受限制,但我们能动态加载其它一些模块(API LoadLibrary),所以应该足够了。

    内核函数(kernel32.dll)是通用的因为Windows中每个进程都有这个模块的拷贝。另一个好处是如果我们知道哪些模块在某版本中会修改,我们可以在一些API如LoadLibraryA中使用直接的指针。这是因为kernel模块在内存中地址在相同Windows版本中是固定的。我们同样也能用动态加载的模块的作用。在这里它的初始化部分在加载进内存后立刻就运行。在新模块的初始化部分我们不受限制。

    第二种可能是在模块中被代替的函数只是原函数的扩展。然后我们选择要么修改开始的5个字节为跳转指令或者改写IAT。如果改为跳转指令,那么将会改变指令执行流程转为执行我们的代码。如果调用了IAT记录被修改的函数,我们的代码能在调用结束后被执行。但模块的扩展没那么容易,因为我们必须注意DLL首部。

    下一个是修改整个模块。这意味着我们创建自己的模块版本,它能够加载原始的模块并调用原始的函数,当然我们对这个不感兴趣,但重要的函数都是被更新的。这种方法对于有的模块过大有几百个导出函数的很不方便。

 

 

=====[ 3.2 运行时挂钩 ]==========================================

 

    在运行前挂钩通常都非常特殊,并且是在内部面向具体的应用程序(或模块)。如果我们更换了kernel32.dll或ntdll.dll里的函数(只在NT操作系统里),我们就能完美地做到在所有将要运行的进程中替换这个函数。但说来容易做起来却非常难,因为我们不但得考虑精确性和需要编写比较完善的新函数或新模块,但主要问题是只有将要运行的进程才能被挂钩(要挂钩所有进程只能重启电脑)。另一个问题是如何进入这些文件,因为NT操作系统保护了它们。比较好的解决方法在进程正在运行时挂钩。这需要更多的有关知识,但最后的结果相当不错。在运行中挂钩只对能够写入它们的内存的进程能成功。为了能写入它自己我们使用API函数WriteProcessMemory。现在我们开始运行中挂钩我们的进程。

 

 

=====[ 3.2.1 使用IAT挂钩本进程 ]===================================

 

    这里有很多种可能性。首先介绍如何用改写IAT挂钩函数的方法。接下来这张图描述了PE文件的结构:

 

     +-------------------------------+     - offset 0

     | MS DOS标志("MZ") 和 DOS块     |

     +-------------------------------+    

     |      PE 标志 ("PE")           |

     +-------------------------------+

     |             .text             |     - 模块代码

     |           程序代码            |

     |                               |

     +-------------------------------+

     |             .data             |     - 已初始化的(全局静态)数据

     |          已初始化的数据       |

     |                               |

     +-------------------------------+

     |            .idata             |     - 导入函数的信息和数据

     |            导入表             |      

     |                               |

     +-------------------------------+

     |            .edata             |     - 导出函数的信息和数据

     |            导出表             |      

     |                               |

     +-------------------------------+

     |           调试符号            |

     +-------------------------------+

 

    这里对我们比较重要的是.idata部分的导入地址表(IAT)。这个部分包含了导入的相关信息和导入函数的地址。有一点很重要的是我们必须知道PE文件是如何创建的。当在编程语言里间接调用任意API(这意味着我们是用函数的名字来调用它,而不是用它的地址),编译器并不直接把调用连接到模块,而是用jmp指令连接调用到IAT,IAT在系统把进程调入内存时时会由进程载入器填满。这就是我们可以在两个不同版本的Windows里使用相同的二进制代码的原因,虽然模块可能会加载到不同的地址。进程载入器会在程序代码里调用所使用的IAT里填入直接跳转的jmp指令。所以我们能在IAT里找到我们想要挂钩的指定函数,我们就能很容易改变那里的jmp指令并重定向代码到我们的地址。完成之后每次调用都会执行我们的代码了。这种方法的缺点是经常有很多函数要被挂钩(比方说如果我们要在搜索文件的API中改变程序的行为我们就得修改函数FindFirstFile和FindNextFile,但我们要知道这些函数都有ANSI和WIDE版本,所以我们不得不修改FindFirstFileA、FindFirstFileW、FindNextFileA和FileNextFileW的IAT地址。但还有其它类似的函数如FindFirstFileExA和它的WIDE版本FindFirstFileExW,也都是由前面提到的函数调用的。我们知道FindFirstFileW调用FindFirstFileExW,但这是直接调用,而不是使用IAT。再比如说ShellAPI的函数SHGetDesktopFolder也会直接调用FindFirstFilwW或FindFirstFileExW)。如果我们能获得它们所有,结果就会很完美。

    我们通过使用imagehlp.dll里的ImageDirectoryEntryToData来很容易地找到IAT。

 

    PVOID ImageDirectoryEntryToData(

        IN LPVOID Base,   

        IN BOOLEAN MappedAsImage,   

        IN USHORT DirectoryEntry,   

        OUT PULONG Size   

    );

 

在这里Base参数可以用我们程序的Instance(Instance通过调用GetModuleHandle获得):

 

    hInstance = GetModuleHandleA(NULL);

 

DirectoryEntry我们可以使用恒量IMAGE_DIRECTORY_ENTRY_IMPORT。

 

    #define IMAGE_DIRECTORY_ENTRY_IMPORT 1

 

    函数的结果是指向第一个IAT记录指针。IAT的所有记录是由IMAGE_IMPORT_DESCRIPTOR定义的结构。所以函数结果是指向IMAGE_IMPORT_DESCRIPTOR的指针。

 

    typedef struct _IMAGE_THUNK_DATA {

        union {

            PBYTE ForwarderString;

            PDWORD Function;

            DWORD Ordinal;

            PIMAGE_IMPORT_BY_NAME AddressOfData;

        } ;

    } IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;

 

    typedef struct _IMAGE_IMPORT_DESCRIPTOR {

        union {

            DWORD Characteristics;

            PIMAGE_THUNK_DATA OriginalFirstThunk;

        } ;

        DWORD TimeDateStamp;

        DWORD ForwarderChain;

        DWORD Name;

        PIMAGE_THUNK_DATA FirstThunk;

    } IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;

 

    IMAGE_IMPORT_DESCRIPTOR里的Name成员变量是模块名字的指针。如果我们想要挂钩某个函数比如是来自kernel32.dll我们就在导入表里找属于名字kernel32.dll的描述符号。我们先调用ImageDirectoryEntryToData然后找到名字是"kernel32.dll"的描述符号(可能不只一个描述符号是这个名字),最后我们在这个模块的记录里所有函数的列表里找到我们想要的函数(函数地址通过GetProcAddress函数获得)。如果我们找到了就必须用VirtualProtect函数来改变内存页面的保护属性,然后就可以在内存中的这些部分写入代码了。在改写了地址之后我们要把保护属性改回来。在调用VirtualProtect之前我们还要先知道有关页面的信息,这通过VirtualQuery来实现。我们可以加入一些测试以防某些函数会失败(比方说如果第一次调用VirtualProctect就失败了,我们就没办法继续)。

 

    PCSTR pszHookModName = "kernel32.dll",pszSleepName = "Sleep";

    HMODULE hKernel = GetModuleHandle(pszHookModName);

    PROC pfnNew = (PROC)0x12345678,       //这里存放新地址

        pfnHookAPIAddr = GetProcAddress(hKernel,pszSleepName);

 

    ULONG ulSize;

    PIMAGE_IMPORT_DESCRIPTOR pImportDesc =

        (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(

            hKernel,

            TRUE,

            IMAGE_DIRECTORY_ENTRY_IMPORT,

            &ulSize

        );

 

    while (pImportDesc->Name)

    {

        PSTR pszModName = (PSTR)((PBYTE) hKernel + pImportDesc->Name);

        if (stricmp(pszModName, pszHookModName) == 0)

        break;  

        pImportDesc++;

    }

 

    PIMAGE_THUNK_DATA pThunk =

    (PIMAGE_THUNK_DATA)((PBYTE) hKernel + pImportDesc->FirstThunk);

 

    while (pThunk->u1.Function)

    {

        PROC* ppfn = (PROC*) &pThunk->u1.Function;

        BOOL bFound = (*ppfn == pfnHookAPIAddr);

 

        if (bFound)

        {

            MEMORY_BASIC_INFORMATION mbi;

            VirtualQuery(

                ppfn,

                &mbi,

                sizeof(MEMORY_BASIC_INFORMATION)

            );

            VirtualProtect(

                mbi.BaseAddress,

                mbi.RegionSize,

                PAGE_READWRITE,

                &mbi.Protect)

            )

 

            *ppfn = *pfnNew;

 

            DWORD dwOldProtect;

            VirtualProtect(

                mbi.BaseAddress,

                mbi.RegionSize,

                mbi.Protect,

                &dwOldProtect

            );

            break;

        }

        pThunk++;

    }

 

调用Sleep(1000)的结果如例子所示:

 

    00407BD8: 68E8030000    push 0000003E8h

    00407BDD: E812FAFFFF    call Sleep

 

    Sleep:     ;这是跳转到IAT里的地址

    004075F4: FF25BCA14000    jmp dword ptr [00040A1BCh]

 

    原始表:

    0040A1BC: 79 67 E8 77 00 00 00 00

     

    新表:

    0040A1BC: 78 56 34 12 00 00 00 00

 

所以最后会跳转到0x12345678。

         

 

=====[ 3.2.2 改写入口点挂钩本进程 ]==================

 

    改写函数入口点开始的一些字节这种方法相当简单。就象改变IAT里的地址一样,我们也要先修改页面属性。在这里对我们想要挂钩的函数是一开始的5个字节。为了之后的使用我们用动态分配MEMORY_BASIC_INFORMATION结构。函数的起始地址也是用GetProcAddress来获得。我们在这个地址里插入指向我们代码的跳转指令。接下来程序调用Sleep(5000)(所以它会等待5秒钟),然后Sleep函数被挂钩并重定向到new_sleep,最后它再次调用Sleep(5000)。因为新的函数new_sleep什么都不做并直接返回,所以整个程序只需要5秒钟而不是10秒种。

 

 

.386p

.model flat, stdcall

 

includelib lib/kernel32.lib

Sleep            PROTO :DWORD

GetModuleHandleA    PROTO :DWORD

GetProcAddress        PROTO :DWORD,:DWORD

VirtualQuery        PROTO :DWORD,:DWORD,:DWORD

VirtualProtect        PROTO :DWORD,:DWORD,:DWORD,:DWORD

VirtualAlloc        PROTO :DWORD,:DWORD,:DWORD,:DWORD

VirtualFree        PROTO :DWORD,:DWORD,:DWORD

FlushInstructionCache    PROTO :DWORD,:DWORD,:DWORD

GetCurrentProcess    PROTO

ExitProcess         PROTO :DWORD

 

 

.data

 

kernel_name         db "kernel32.dll",0

sleep_name        db "Sleep",0

old_protect        dd ?

 

MEMORY_BASIC_INFORMATION_SIZE    equ 28

 

PAGE_READWRITE        dd 000000004h

PAGE_EXECUTE_READWRITE dd 000000040h

MEM_COMMIT        dd 000001000h

MEM_RELEASE        dd 000008000h

 

 

.code

start:

    push    5000

    call    Sleep

 

do_hook:

    push    offset kernel_name

    call    GetModuleHandleA

    push    offset sleep_name

    push    eax

    call    GetProcAddress

    mov    edi,eax            ;最后获得Sleep地址

 

    push    PAGE_READWRITE

    push    MEM_COMMIT

    push    MEMORY_BASIC_INFORMATION_SIZE

    push     0

    call    VirtualAlloc

    test    eax,eax

    jz     do_sleep

    mov    esi,eax            ;为MBI结构分配内存

 

    push    MEMORY_BASIC_INFORMATION_SIZE

    push    esi

    push    edi

    call    VirtualQuery        ;内存页的信息

    test    eax,eax

    jz    free_mem

 

    call    GetCurrentProcess

    push    5

    push    edi

    push    eax

    call    FlushInstructionCache    ;只是为了确定一下:)

 

    lea    eax,[esi+014h]

    push    eax

    push    PAGE_EXECUTE_READWRITE

    lea    eax,[esi+00Ch]

    push    [eax]

    push    [esi]

    call    VirtualProtect          ;我们要修改保护属性,这样才能够写入代码

    test    eax,eax

    jz    free_mem       

 

    mov    byte ptr [edi],0E9h    ;写入跳转指令

    mov    eax,offset new_sleep

    sub    eax,edi

    sub    eax,5

    inc    edi

    stosd                ;这里是跳转地址

 

    push    offset old_protect

    lea    eax,[esi+014h]

    push    [eax]

    lea    eax,[esi+00Ch]

    push    [eax]

    push    [esi]

    call    VirtualProtect        ;恢复页保护属性

 

free_mem:

    push    MEM_RELEASE

    push    0

    push    esi

    call    VirtualFree        ;释放内存

do_sleep:

    push    5000

    call    Sleep

    push    0

    call    ExitProcess

new_sleep:               

    ret    004h

end start

 

 

第二次调用Sleep的结果是这样:

 

    004010A4: 6888130000    push 000001388h

    004010A9: E80A000000    call Sleep

 

 

    Sleep:     ;这里是跳转到IAT里的地址

    004010B8: FF2514204000    jmp dword ptr [000402014h]

 

    tabulka:

    00402014: 79 67 E8 77 6C 7D E8 77

     

    Kernel32.Sleep:

    77E86779: E937A95788    jmp 0004010B5h

 

    new_sleep:

    004010B5: C20400    ret 004h   

 

 

=====[ 3.2.3 保存原始函数 ]=====================================

 

    更多时候我们需要的不仅仅是挂钩函数。比方说也许我们并不想取代给定的函数而只是想检查一下它的结果,或者也许我们只是想在函数被使用特定的参数来调用时才取代原函数。比较好的例子有前面提过的通过取代FindXXXFile函数来完成隐藏文件。所以如果我们想要隐藏指定的文件并且不想被注意的话,就得对其它所有文件只调用没有被修改过的原始函数。这对使用修改IAT的方法时是很简单的,为调用原始函数我们可以用GetProcAddress获得它的原始地址,然后直接调用。但修改入口点的方法就会有问题,因为修改了函数入口点的5个字节,使我们破坏了原函数。所以我们必须保存开始的那些指令。这将用到以下的技术。

    我们知道我们要修改开始的5个字节但不知道里面包含多少条指令以及指令的长度。我们得为开始那些指令保留足够的内存空间。16个字节应该足够了,因为函数开始时通常没有多长的指令,很可能根本就用不到16个字节。整个被保留的内存用0x90(0x90=nop)来填满。下一个5个字节预留给将在之后填入的跳转指令。

 

old_hook:        db 090h,090h,090h,090h,090h,090h,090h,090h

            db 090h,090h,090h,090h,090h,090h,090h,090h

            db 0E9h,000h,000h,000h,000h

 

 

    现在我们已准备好拷贝开始的指令。为获得指令长度的代码相当麻烦,这就是我们得使用已完成的引擎的原因。它是由Z0MBiE写的。传入参数是我们要获得长度的指令的地址。输出参数在eax里。

 

 

; LDE32, Length-Disassembler Engine, 32-bit, (x) 1999-2000 Z0MBiE

; special edition for REVERT tool

 

; version 1.05

 

C_MEM1                  equ     0001h       ; |

C_MEM2                  equ     0002h       ; |may be used simultaneously

C_MEM4                  equ     0004h       ; |

C_DATA1                 equ     0100h       ; |

C_DATA2                 equ     0200h       ; |may be used simultaneously

C_DATA4                 equ     0400h       ; |

C_67                    equ     0010h       ; used with C_PREFIX

C_MEM67                 equ     0020h       ; C_67 ? C_MEM2 : C_MEM4

C_66                    equ     1000h       ; used with C_PREFIX

C_DATA66                equ     2000h       ; C_66 ? C_DATA2 : C_DATA4

C_PREFIX                equ     0008h       ; prefix. take opcode again

C_MODRM                 equ     4000h       ; MODxxxR/M

C_DATAW0                equ     8000h       ; opc&1 ? C_DATA66 : C_DATA1

 

                        p386

                        model   flat

                        locals  @@

 

                        .code

 

public                  disasm_main

public                  _disasm_main

public                  @disasm_main

public                  DISASM_MAIN

 

disasm_main:

_disasm_main:

@disasm_main:

DISASM_MAIN:

 

 

; __fastcall            EAX

; __cdecl               [ESP+4]

 

;这是我的第一处修改,它只是这个函数的声明

get_instr_len:

 

                        mov     ecx, [esp+4]    ; ECX = opcode ptr

 

                        xor     edx, edx        ; 标志

                        xor     eax, eax

 

@@prefix:               and     dl, not C_PREFIX

 

                        mov     al, [ecx]

                        inc     ecx

 

                        or      edx, table_1[eax*4]

 

                        test    dl, C_PREFIX

                        jnz     @@prefix

 

                        cmp     al, 0F6h

                        je      @@test

                        cmp     al, 0F7h

                        je      @@test

 

                        cmp     al, 0CDh

                        je      @@int

 

                        cmp     al, 0Fh

                        je      @@0F

@@cont:

                        test    dh, C_DATAW0 shr 8

                        jnz     @@dataw0

@@dataw0done:

                        test    dh, C_MODRM shr 8

                        jnz     @@modrm

@@exitmodrm:

                        test    dl, C_MEM67

                        jnz     @@mem67

@@mem67done:

                        test    dh, C_DATA66 shr 8

                        jnz     @@data66

@@data66done:

                        mov     eax, ecx

                        sub     eax, [esp+4]

 

                        and     edx,C_MEM1+C_MEM2+C_MEM4+C_DATA1+C_DATA2+C_DATA4

                        add     al, dl

                        add     al, dh

 

;这里是我的第二处修改,只有在原始版本这里是retn

@@exit:                 ret     00004h  

 

@@test:                 or      dh, C_MODRM shr 8

                        test    byte ptr [ecx], 00111000b  ; F6/F7 -- test

                        jnz     @@cont

                        or      dh, C_DATAW0 shr 8

                        jmp     @@cont

 

@@int:                  or      dh, C_DATA1 shr 8

                        cmp     byte ptr [ecx], 20h

                        jne     @@cont

                        or      dh, C_DATA4 shr 8

                        jmp     @@cont

 

@@0F:                   mov     al, [ecx]

                        inc     ecx

                        or      edx, table_0F[eax*4]

 

                        cmp     edx, -1

                        jne     @@cont

 

@@error:                mov     eax, edx

                        jmp     @@exit

 

@@dataw0:               xor     dh, C_DATA66 shr 8

                        test    al, 00000001b

                        jnz     @@dataw0done

                        xor     dh, (C_DATA66+C_DATA1) shr 8

                        jmp     @@dataw0done

 

@@mem67:                xor     dl, C_MEM2

                        test    dl, C_67

                        jnz     @@mem67done

                        xor     dl, C_MEM4+C_MEM2

                        jmp     @@mem67done

 

@@data66:               xor     dh, C_DATA2 shr 8

                        test    dh, C_66 shr 8

                        jnz     @@data66done

                        xor     dh, (C_DATA4+C_DATA2) shr 8

                        jmp     @@data66done

 

@@modrm:                mov     al, [ecx]

                        inc     ecx

 

                        mov     ah, al  ; ah=mod, al=rm

 

                        and     ax, 0C007h

                        cmp     ah, 0C0h

                        je      @@exitmodrm

 

                        test    dl, C_67

                        jnz     @@modrm16

 

@@modrm32:              cmp     al, 04h

                        jne     @@a

 

                        mov     al, [ecx]       ; sib

                        inc     ecx

                        and     al, 07h

 

@@a:                    cmp     ah, 40h

                        je      @@mem1

                        cmp     ah, 80h

                        je      @@mem4

 

                        cmp     ax, 0005h

                        jne     @@exitmodrm

 

@@mem4:                 or      dl, C_MEM4

                        jmp     @@exitmodrm

 

@@mem1:                 or      dl, C_MEM1

                        jmp     @@exitmodrm

 

@@modrm16:              cmp     ax, 0006h

                        je      @@mem2

                        cmp     ah, 40h

                        je      @@mem1

                        cmp     ah, 80h

                        jne     @@exitmodrm

 

@@mem2:                 or      dl, C_MEM2

                        jmp     @@exitmodrm

 

                        endp

 

                        .data

 

;0F      -- 在代码中分析,不需要标志(也就是标志(flag)必须为0)

;F6,F7   -- --//-- (ttt=000 -- 3 字节, 否则为2字节)

;CD      -- --//-- (如果为 CD 20 为6字节, 否则为2字节)

 

table_1                 label   dword   ; 一般的指令

 

dd C_MODRM              ; 00

dd C_MODRM              ; 01

dd C_MODRM              ; 02

dd C_MODRM              ; 03

dd C_DATAW0             ; 04

dd C_DATAW0             ; 05

dd 0                    ; 06

dd 0                    ; 07

dd C_MODRM              ; 08

dd C_MODRM              ; 09

dd C_MODRM              ; 0A

dd C_MODRM              ; 0B

dd C_DATAW0             ; 0C

dd C_DATAW0             ; 0D

dd 0                    ; 0E

dd 0                    ; 0F

dd C_MODRM              ; 10

dd C_MODRM              ; 11

dd C_MODRM              ; 12

dd C_MODRM              ; 13

dd C_DATAW0             ; 14

dd C_DATAW0             ; 15

dd 0                    ; 16

dd 0                    ; 17

dd C_MODRM              ; 18

dd C_MODRM              ; 19

dd C_MODRM              ; 1A

dd C_MODRM              ; 1B

dd C_DATAW0             ; 1C

dd C_DATAW0             ; 1D

dd 0                    ; 1E

dd 0                    ; 1F

dd C_MODRM              ; 20

dd C_MODRM              ; 21

dd C_MODRM              ; 22

dd C_MODRM              ; 23

dd C_DATAW0             ; 24

dd C_DATAW0             ; 25

dd C_PREFIX             ; 26

dd 0                    ; 27

dd C_MODRM              ; 28

dd C_MODRM              ; 29

dd C_MODRM              ; 2A

dd C_MODRM              ; 2B

dd C_DATAW0             ; 2C

dd C_DATAW0             ; 2D

dd C_PREFIX             ; 2E

dd 0                    ; 2F

dd C_MODRM              ; 30

dd C_MODRM              ; 31

dd C_MODRM              ; 32

dd C_MODRM              ; 33

dd C_DATAW0             ; 34

dd C_DATAW0             ; 35

dd C_PREFIX             ; 36

dd 0                    ; 37

dd C_MODRM              ; 38

dd C_MODRM              ; 39

dd C_MODRM              ; 3A

dd C_MODRM              ; 3B

dd C_DATAW0             ; 3C

dd C_DATAW0             ; 3D

dd C_PREFIX             ; 3E

dd 0                    ; 3F

dd 0                    ; 40

dd 0                    ; 41

dd 0                    ; 42

dd 0                    ; 43

dd 0                    ; 44

dd 0                    ; 45

dd 0                    ; 46

dd 0                    ; 47

dd 0                    ; 48

dd 0                    ; 49

dd 0                    ; 4A

dd 0                    ; 4B

dd 0                    ; 4C

dd 0                    ; 4D

dd 0                    ; 4E

dd 0                    ; 4F

dd 0                    ; 50

dd 0                    ; 51

dd 0                    ; 52

dd 0                    ; 53

dd 0                    ; 54

dd 0                    ; 55

dd 0                    ; 56

dd 0                    ; 57

dd 0                    ; 58

dd 0                    ; 59

dd 0                    ; 5A

dd 0                    ; 5B

dd 0                    ; 5C

dd 0                    ; 5D

dd 0                    ; 5E

dd 0                    ; 5F

dd 0                    ; 60

dd 0                    ; 61

dd C_MODRM              ; 62

dd C_MODRM              ; 63

dd C_PREFIX             ; 64

dd C_PREFIX             ; 65

dd C_PREFIX+C_66        ; 66

dd C_PREFIX+C_67        ; 67

dd C_DATA66             ; 68

dd C_MODRM+C_DATA66     ; 69

dd C_DATA1              ; 6A

dd C_MODRM+C_DATA1      ; 6B

dd 0                    ; 6C

dd 0                    ; 6D

dd 0                    ; 6E

dd 0                    ; 6F

dd C_DATA1              ; 70

dd C_DATA1              ; 71

dd C_DATA1              ; 72

dd C_DATA1              ; 73

dd C_DATA1              ; 74

dd C_DATA1              ; 75

dd C_DATA1              ; 76

dd C_DATA1              ; 77

dd C_DATA1              ; 78

dd C_DATA1              ; 79

dd C_DATA1              ; 7A

dd C_DATA1              ; 7B

dd C_DATA1              ; 7C

dd C_DATA1              ; 7D

dd C_DATA1              ; 7E

dd C_DATA1              ; 7F

dd C_MODRM+C_DATA1      ; 80

dd C_MODRM+C_DATA66     ; 81

dd C_MODRM+C_DATA1      ; 82

dd C_MODRM+C_DATA1      ; 83

dd C_MODRM              ; 84

dd C_MODRM              ; 85

dd C_MODRM              ; 86

dd C_MODRM              ; 87

dd C_MODRM              ; 88

dd C_MODRM              ; 89

dd C_MODRM              ; 8A

dd C_MODRM              ; 8B

dd C_MODRM              ; 8C

dd C_MODRM              ; 8D

dd C_MODRM              ; 8E

dd C_MODRM              ; 8F

dd 0                    ; 90

dd 0                    ; 91

dd 0                    ; 92

dd 0                    ; 93

dd 0                    ; 94

dd 0                    ; 95

dd 0                    ; 96

dd 0                    ; 97

dd 0                    ; 98

dd 0                    ; 99

dd C_DATA66+C_MEM2      ; 9A

dd 0                    ; 9B

dd 0                    ; 9C

dd 0                    ; 9D

dd 0                    ; 9E

dd 0                    ; 9F

dd C_MEM67              ; A0

dd C_MEM67              ; A1

dd C_MEM67              ; A2

dd C_MEM67              ; A3

dd 0                    ; A4

dd 0                    ; A5

dd 0                    ; A6

dd 0                    ; A7

dd C_DATA1              ; A8

dd C_DATA66             ; A9

dd 0                    ; AA

dd 0                    ; AB

dd 0                    ; AC

dd 0                    ; AD

dd 0                    ; AE

dd 0                    ; AF

dd C_DATA1              ; B0

dd C_DATA1              ; B1

dd C_DATA1              ; B2

dd C_DATA1              ; B3

dd C_DATA1              ; B4

dd C_DATA1              ; B5

dd C_DATA1              ; B6

dd C_DATA1              ; B7

dd C_DATA66             ; B8

dd C_DATA66             ; B9

dd C_DATA66             ; BA

dd C_DATA66             ; BB

dd C_DATA66             ; BC

dd C_DATA66             ; BD

dd C_DATA66             ; BE

dd C_DATA66             ; BF

dd C_MODRM+C_DATA1      ; C0

dd C_MODRM+C_DATA1      ; C1

dd C_DATA2              ; C2

dd 0                    ; C3

dd C_MODRM              ; C4

dd C_MODRM              ; C5

dd C_MODRM+C_DATA1      ; C6

dd C_MODRM+C_DATA66     ; C7

dd C_DATA2+C_DATA1      ; C8

dd 0                    ; C9

dd C_DATA2              ; CA

dd 0                    ; CB

dd 0                    ; CC

dd 0                    ; CD

dd 0                    ; CE

dd 0                    ; CF

dd C_MODRM              ; D0

dd C_MODRM              ; D1

dd C_MODRM              ; D2

dd C_MODRM              ; D3

dd C_DATA1              ; D4

dd C_DATA1              ; D5

dd 0                    ; D6

dd 0                    ; D7

dd C_MODRM              ; D8

dd C_MODRM              ; D9

dd C_MODRM              ; DA

dd C_MODRM              ; DB

dd C_MODRM              ; DC

dd C_MODRM              ; DD

dd C_MODRM              ; DE

dd C_MODRM              ; DF

dd C_DATA1              ; E0

dd C_DATA1              ; E1

dd C_DATA1              ; E2

dd C_DATA1              ; E3

dd C_DATA1              ; E4

dd C_DATA1              ; E5

dd C_DATA1              ; E6

dd C_DATA1              ; E7

dd C_DATA66             ; E8

dd C_DATA66             ; E9

dd C_DATA66+C_MEM2      ; EA

dd C_DATA1              ; EB

dd 0                    ; EC

dd 0                    ; ED

dd 0                    ; EE

dd 0                    ; EF

dd C_PREFIX             ; F0

dd 0                    ; F1

dd C_PREFIX             ; F2

dd C_PREFIX             ; F3

dd 0                    ; F4

dd 0                    ; F5

dd 0                    ; F6

dd 0                    ; F7

dd 0                    ; F8

dd 0                    ; F9

dd 0                    ; FA

dd 0                    ; FB

dd 0                    ; FC

dd 0                    ; FD

dd C_MODRM              ; FE

dd C_MODRM              ; FF

 

table_0F                label   dword   ; 0F为前缀的指令

 

dd C_MODRM              ; 00

dd C_MODRM              ; 01

dd C_MODRM              ; 02

dd C_MODRM              ; 03

dd -1                   ; 04

dd -1                   ; 05

dd 0                    ; 06

dd -1                   ; 07

dd 0                    ; 08

dd 0                    ; 09

dd 0                    ; 0A

dd 0                    ; 0B

dd -1                   ; 0C

dd -1                   ; 0D

dd -1                   ; 0E

dd -1                   ; 0F

dd -1                   ; 10

dd -1                   ; 11

dd -1                   ; 12

dd -1                   ; 13

dd -1                   ; 14

dd -1                   ; 15

dd -1                   ; 16

dd -1                   ; 17

dd -1                   ; 18

dd -1                   ; 19

dd -1                   ; 1A

dd -1                   ; 1B

dd -1                   ; 1C

dd -1                   ; 1D

dd -1                   ; 1E

dd -1                   ; 1F

dd -1                   ; 20

dd -1                   ; 21

dd -1                   ; 22

dd -1                   ; 23

dd -1                   ; 24

dd -1                   ; 25

dd -1                   ; 26

dd -1                   ; 27

dd -1                   ; 28

dd -1                   ; 29

dd -1                   ; 2A

dd -1                   ; 2B

dd -1                   ; 2C

dd -1                   ; 2D

dd -1                   ; 2E

dd -1                   ; 2F

dd -1                   ; 30

dd -1                   ; 31

dd -1                   ; 32

dd -1                   ; 33

dd -1                   ; 34

dd -1                   ; 35

dd -1                   ; 36

dd -1                   ; 37

dd -1                   ; 38

dd -1                   ; 39

dd -1                   ; 3A

dd -1                   ; 3B

dd -1                   ; 3C

dd -1                   ; 3D

dd -1                   ; 3E

dd -1                   ; 3F

dd -1                   ; 40

dd -1                   ; 41

dd -1                   ; 42

dd -1                   ; 43

dd -1                   ; 44

dd -1                   ; 45

dd -1                   ; 46

dd -1                   ; 47

dd -1                   ; 48

dd -1                   ; 49

dd -1                   ; 4A

dd -1                   ; 4B

dd -1                   ; 4C

dd -1                   ; 4D

dd -1                   ; 4E

dd -1                   ; 4F

dd -1                   ; 50

dd -1                   ; 51

dd -1                   ; 52

dd -1                   ; 53

dd -1                   ; 54

dd -1                   ; 55

dd -1                   ; 56

dd -1                   ; 57

dd -1                   ; 58

dd -1                   ; 59

dd -1                   ; 5A

dd -1                   ; 5B

dd -1                   ; 5C

dd -1                   ; 5D

dd -1                   ; 5E

dd -1                   ; 5F

dd -1                   ; 60

dd -1                   ; 61

dd -1                   ; 62

dd -1                   ; 63

dd -1                   ; 64

dd -1                   ; 65

dd -1                   ; 66

dd -1                   ; 67

dd -1                   ; 68

dd -1                   ; 69

dd -1                   ; 6A

dd -1                   ; 6B

dd -1                   ; 6C

dd -1                   ; 6D

dd -1                   ; 6E

dd -1                   ; 6F

dd -1                   ; 70

dd -1                   ; 71

dd -1                   ; 72

dd -1                   ; 73

dd -1                   ; 74

dd -1                   ; 75

dd -1                   ; 76

dd -1                   ; 77

dd -1                   ; 78

dd -1                   ; 79

dd -1                   ; 7A

dd -1                   ; 7B

dd -1                   ; 7C

dd -1                   ; 7D

dd -1                   ; 7E

dd -1                   ; 7F

dd C_DATA66             ; 80

dd C_DATA66             ; 81

dd C_DATA66             ; 82

dd C_DATA66             ; 83

dd C_DATA66             ; 84

dd C_DATA66             ; 85

dd C_DATA66             ; 86

dd C_DATA66             ; 87

dd C_DATA66             ; 88

dd C_DATA66             ; 89

dd C_DATA66             ; 8A

dd C_DATA66             ; 8B

dd C_DATA66             ; 8C

dd C_DATA66             ; 8D

dd C_DATA66             ; 8E

dd C_DATA66             ; 8F

dd C_MODRM              ; 90

dd C_MODRM              ; 91

dd C_MODRM              ; 92

dd C_MODRM              ; 93

dd C_MODRM              ; 94

dd C_MODRM              ; 95

dd C_MODRM              ; 96

dd C_MODRM              ; 97

dd C_MODRM              ; 98

dd C_MODRM              ; 99

dd C_MODRM              ; 9A

dd C_MODRM              ; 9B

dd C_MODRM              ; 9C

dd C_MODRM              ; 9D

dd C_MODRM              ; 9E

dd C_MODRM              ; 9F

dd 0                    ; A0

dd 0                    ; A1

dd 0                    ; A2

dd C_MODRM              ; A3

dd C_MODRM+C_DATA1      ; A4

dd C_MODRM              ; A5

dd -1                   ; A6

dd -1                   ; A7

dd 0                    ; A8

dd 0                    ; A9

dd 0                    ; AA

dd C_MODRM              ; AB

dd C_MODRM+C_DATA1      ; AC

dd C_MODRM              ; AD

dd -1                   ; AE

dd C_MODRM              ; AF

dd C_MODRM              ; B0

dd C_MODRM              ; B1

dd C_MODRM              ; B2

dd C_MODRM              ; B3

dd C_MODRM              ; B4

dd C_MODRM              ; B5

dd C_MODRM              ; B6

dd C_MODRM              ; B7

dd -1                   ; B8

dd -1                   ; B9

dd C_MODRM+C_DATA1      ; BA

dd C_MODRM              ; BB

dd C_MODRM              ; BC

dd C_MODRM              ; BD

dd C_MODRM              ; BE

dd C_MODRM              ; BF

dd C_MODRM              ; C0

dd C_MODRM              ; C1

dd -1                   ; C2

dd -1                   ; C3

dd -1                   ; C4

dd -1                   ; C5

dd -1                   ; C6

dd -1                   ; C7

dd 0                    ; C8

dd 0                    ; C9

dd 0                    ; CA

dd 0                    ; CB

dd 0                    ; CC

dd 0                    ; CD

dd 0                    ; CE

dd 0                    ; CF

dd -1                   ; D0

dd -1                   ; D1

dd -1                   ; D2

dd -1                   ; D3

dd -1                   ; D4

dd -1                   ; D5

dd -1                   ; D6

dd -1                   ; D7

dd -1                   ; D8

dd -1                   ; D9

dd -1                   ; DA

dd -1                   ; DB

dd -1                   ; DC

dd -1                   ; DD

dd -1                   ; DE

dd -1                   ; DF

dd -1                   ; E0

dd -1                   ; E1

dd -1                   ; E2

dd -1                   ; E3

dd -1                   ; E4

dd -1                   ; E5

dd -1                   ; E6

dd -1                   ; E7

dd -1                   ; E8

dd -1                   ; E9

dd -1                   ; EA

dd -1                   ; EB

dd -1                   ; EC

dd -1                   ; ED

dd -1                   ; EE

dd -1                   ; EF

dd -1                   ; F0

dd -1                   ; F1

dd -1                   ; F2

dd -1                   ; F3

dd -1                   ; F4

dd -1                   ; F5

dd -1                   ; F6

dd -1                   ; F7

dd -1                   ; F8

dd -1                   ; F9

dd -1                   ; FA

dd -1                   ; FB

dd -1                   ; FC

dd -1                   ; FD

dd -1                   ; FE

dd -1                   ; FF

 

                        end

 

 

    现在我们可以获取任意地址的指令长度。我们重复调用这个函数直到读取了5个字节。完成后把这些字节拷贝到old_hook。我们知道了开始这些指令的长度,所以我们可以在原始函数的下条指令填入跳转地址。

 

.386p

.model flat, stdcall

 

...

 

.data

 

kernel_name         db "kernel32.dll",0

sleep_name        db "Sleep",0

 

...

 

MEM_RELEASE        dd 000008000h

 

;16 nops + 一个跳转指令

old_sleep        db 090h,090h,090h,090h,090h,090h,090h,090h,

               090h,090h,090h,090h,090h,090h,090h,090h,

               0E9h,000h,000h,000h,000h

 

 

.code

start:

    push    5000

    call    Sleep

 

do_hook:

    push    offset kernel_name

    call    GetModuleHandleA

    push    offset sleep_name

        push    eax

    call    GetProcAddress

        push    eax

    mov    esi,eax

 

    xor    ecx,ecx

    mov    ebx,esi

get_five_bytes:

    push    ecx

    push    ebx

    call    get_instr_len        ;调用LDE32

    pop    ecx

    add    ecx,eax

    add    ebx,eax

    cmp    ecx,5

    jb    get_five_bytes

    mov    edi,offset old_sleep    ;计算跳转地址

    mov    [edi+011h],ebx

    sub    [edi+011h],edi

    sub    dword ptr [edi+011h],015h

    rep    movsb

    pop    edi

 

;下面的代码都是前面有的,所以不需要注解了

 

    push    PAGE_READWRITE

    push    MEM_COMMIT

    push    MEMORY_BASIC_INFORMATION_SIZE

    push     0

    call    VirtualAlloc

    test    eax,eax

    jz     do_sleep

    mov    esi,eax

 

    push    MEMORY_BASIC_INFORMATION_SIZE

    push    esi

    push    edi

    call    VirtualQuery

    test    eax,eax

    jz    free_mem

 

    call    GetCurrentProcess

    push    5

    push    edi

    push    eax

    call    FlushInstructionCache   

 

    lea    eax,[esi+014h]

    push    eax

    push    PAGE_EXECUTE_READWRITE

    lea    eax,[esi+00Ch]

    push    [eax]

    push    [esi]

    call    VirtualProtect

    test    eax,eax

    jz    free_mem

 

    mov    byte ptr [edi],0E9h

    mov    eax,offset new_sleep

    sub    eax,edi

    sub    eax,5

    inc    edi

    stosd

 

    push    offset old_protect

    lea    eax,[esi+014h]

    push    [eax]

    lea    eax,[esi+00Ch]

    push    [eax]

    push    [esi]

    call    VirtualProtect

 

free_mem:

    push    MEM_RELEASE

    push    0

    push    esi

    call    VirtualFree

do_sleep:

    push    5000

    call    Sleep

    push    0

    call    ExitProcess

new_sleep:

    mov    eax,dword ptr [esp+004h]       

    add    eax,eax                ;重复延时

    push    eax

    mov    eax,offset old_sleep            ;调用原函数

    call    eax

    ret    004h

 

 

挂钩后看起来想这样:

 

    004010CC: 6888130000    push 000001388h

    004010D1: E818090000    call Sleep

 

 

    Sleep:     ;跳转到IAT里的地址

    004019EE: FF2514204000    jmp dword ptr [000402014h]

 

    tabulka:

    00402014: 79 67 E8 77 6C 7D E8 77

     

    Kernel32.Sleep:

    77E86779: E95FA95788    jmp 0004010DDh

 

    new_sleep:

    004010DD: 8B442404    mov eax,dword ptr [esp+4]

    004010E1: 03C0        add eax,eax

    004010E3: 50        push eax

    004010E4: B827304000    mov eax,000403027h

    004010E9: FFD0        call eax

 

    old_sleep:

    00403027: 6A00        push 0

    00403029: FF742408    push dword ptr [esp+8]

    0040302D: 90        nop

    0040302E: 90        nop

    0040302F: 90        nop

    00403030: 90        nop

    00403031: 90        nop

    00403032: 90        nop

    00403033: 90        nop

    00403034: 90        nop

    00403035: 90        nop

    00403036: 90        nop

    00403037: E94337A877    jmp Kernel32.77E8677F

 

;这个指令在Kernel32.Sleep(77E86779)后1个字节

 

    Kernel32.77E8677F:   

    77E8677F: E803000000    call Kernel32.SleepEx

    ...            ;后面的已不重要

 

为了让这些看起来更清楚,这是原始版本的Kernel32.Sleep:

 

    Kernel32.Sleep:

    77E86779: 6A00        push 0

    77E8677B: FF742408    push dword ptr [esp+8]

    77E8677F: E803000000    call Kernel32.SleepEx

    77E86784: C20400    ret 00004h

 

 

    就象你看到的,在我们已经拷贝了第1和第2个指令(这里总共6个字节)和指向下一条指令的跳转指令后应该是怎样。这里我们得假设跳转指令并不象函数开始的指令那么放置,否则我们就会遇到问题。下一个问题就是诸如ntdll.DbgBreakPoint这样的API,它们太短了,所以不能用这种挂钩的方法。并且它是由Kernel32.DebugBreak调用,所以也不能通过修改IAT来挂钩。虽然说没有谁会去挂钩这个只有int 3的函数,但没有什么是做不到的,只要认真想想就能找到解决的方法。我的方法是挂钩它之后的那个函数(它可能会因为修改了前一个函数的开始5个字节而被破坏)。DbgBreakPoint函数长度为2个字节,所以这里我们可以设置一些标志,然后试着在第二个函数的开始写入条件跳转指令...但这不是我们现在的问题。

    保存原始函数的问题已经叙述完了,就到解除挂钩(unhook)。解除挂钩就是把被修改的字节恢复为原始状态。修改IAT的方法里,如果你想解除挂钩的话,你就需要在表里恢复原始的地址。修改入口点的方法里,你要做的就是把原始函数的开始指令拷贝回去。两种做法都很简单,所以不需要再讲了。

 

 

=====[ 3.2.4 挂钩其它进程 ]========================================

   

    现在我们来实践一下运行中挂钩。试想,谁会想只挂钩自己进程?这显然是非常不实用的。

    我来演示3种不同的挂钩其它进程的方法。其中两种都使用了CreateRemoteThread这个API,它只在使用了NT技术的Windows版本里有效。对我来说在较老的Windows里挂钩没那么有趣。忘了说我将介绍的这3个方法我都没有实践过,所以可能会出点问题。

    先介绍CreateRemoteThread。就象帮助里说的,这个函数可以在任意进程里创建新线程并运行它的代码。

 

    HANDLE CreateRemoteThread(

        HANDLE hProcess,

        LPSECURITY_ATTRIBUTES lpThreadAttributes,

        DWORD dwStackSize,

        LPTHREAD_START_ROUTINE lpStartAddress,

        LPVOID lpParameter,

        DWORD dwCreationFlags,

        LPDWORD lpThreadId

    );

 

    句柄hProcess可以通过OpenProcess获得。这里我们必须获得足够权限。lpStartAddress是指向目标进程地址空间里存放新线程第一条指令地址的指针,因为新线程是在目标进程里创建,所以它存在于目标进程的地址空间里。lpParameter是指向提交给新线程的参数的指针。

 

 

=====[ 3.2.4.1 DLL注入 ]==================================================

 

    我们可以在目标进程地址空间里任意地方运行我们的新线程。这看起来没什么用,除非在里面有我们完整的代码。第一种方法就是这么实现。它调用GetProcAddress获取LoadLibrary地址。然后把LoadLibrary赋值给参数lpStartAddress。LoadLibrary函数只有一个参数,就和目标进程里新线程的函数一样。

 

    HINSTANCE LoadLibrary(

        LPCTSTR lpLibFileName

    );

 

    我们可以使用这点相似性,把lpParameter参数赋为我们的DLL库的名字。在新线程运行后lpParameter的位置就是lpLibFileName的位置。这里最重要的东西前面已经讲过了。在加载了新的模块到目标进程后就开始执行初始化部分。如果我们在这里放置了能够挂钩其它函数的特殊函数就OK了。在执行了初始化部分后,这个线程就什么都不做并被关闭,但我们的模块仍然在地址空间中。这种方法很不错而且很容易实现,它的名字叫DLL注入。但如果你和我一样不喜欢还得多个DLL库的话,请看下面的方法。但如果不介意多个DLL库的话这确实是最快的方法(从程序员的角度来看)。

 

 

=====[ 3.2.4.2 独立的代码 ]===============================================

 

    实现独立的代码比较困难,但也容易给人深刻印象。独立的代码是不需要任何静态地址的代码。它里面所有东西都是互相联系地指向代码里面某些特定的地方。如果我们不知道这段代码开始执行的地址它也能自己完成。当然,也有可能先获得地址然后重新链接我们的代码这样它可以完全正常地在新地址工作,但这比编写独立的代码更困难。这类型代码的例子比方说病毒的代码。病毒通过这种方法感染可执行文件,它把它自己的代码加入到可执行文件中的某个地方。在不同的可执行文件中放置病毒代码的位置也不一样,这取决于比方说文件结构的长度。

    首先将我们的代码插入目标进程,然后CreateRemoteThread函数就会负责运行我们的代码。所以第一步我们要做的就是通过OpenProcess函数获取目标进程的信息和句柄,接着调用VirtualAllocEx在目标进程地址空间里分配一些内存给我们的代码,最后调用WriteProcessMemory把我们的代码写入分配的内存里并运行它。调用CreateRemoteThread的参数lpStartAddress设置为分配的内存地址,lpParameter可以随便设置。因为我不喜欢附加什么不必要的文件所以我使用了这种方法。

 

 

=====[ 3.2.4.3 原始修改 ]=====================================================

 

    在非NT内核的老版本Windows里是没有CreateRemoteThread函数的,所以我们不能用以上的方法。可能会有比我现在介绍的这种方法好很多的方法,事实上我的这种方法还没有经过实践,但理论上来说是可行的。

    我们其实根本不需要把我们代码放到目标进程里来挂钩它的函数。有两个函数WriteProcessMemory和OpenProcess,它们在所有版本的Windows中都有效。我们还需要的函数是VirtualProtectEx,用来修改进入目标进程的内存页。我找不到任何不直接从我们的进程挂钩目标进程的的理由...

 

 

=====[ 4. 结束语 ]================================================

 

    我欢迎任何人提出更多的这里没有提到的挂钩方法,我肯定那会有很多。同样欢迎补充我介绍得不是很详细的方法。也可以把我懒得写的代码部分完成,把源代码发给我。这篇文档的目的是演示挂钩技术的细节,我希望我做到了。

    特别感谢Z0MBiE的代码,为我节省了很多宝贵的时间。

 

============================[ End ]========================

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值