PE文件结构分析及应用二

写于2012年,现只做参考使用

例子3。2:获取一个PE文件里的所有位图并保存在磁盘里

关于位图结构分析请参考“VC API常用函数简单例子大全”中的第八十九个GetDIBits函数

资源节其实就是一个由IMAGE_RESOURCE_DIRECTORY和IMAGE_RESOURCE_DIRECTORY_ENTRY结构组合起来而构成的目录树,而我们要做的就是顺着这一目录树一层一层的找下去,直到找到IMAGE_RESOURCE_DATA_ENTRY结构。

这里的PE文件还是以前面的"abc.exe"为例,(因为这个PE文件在我们的控制范围之内,其它的可能会有各种各样的意外情况,这里只是最理想的状态,比如PE文件有没有加壳,压缩等,毕竟是基础,先从最理想状态开始,后面慢慢会有介绍的)

好了先用文字描述一下这个过程,只要仔细顺着这一层一层的看下去,就一定会成功的!
整个过程如下:
先获取资源节在PE文件的位置,然后读取PE文件资源表第一层IMAGE_RESOURCE_DIRECTORY结构,
再遍历其下的IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组,找出哪个元素代表位图类型,
然后再根据找出的元素,获取下一层的IMAGE_RESOURCE_DIRECTORY结构位置,
再遍历其下的IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组,
此时这层IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组每一个元素就代表一副位图,
再获取第一个元素指向的IMAGE_RESOURCE_DIRECTORY结构,此时就进了语言ID层,
语言ID层其下IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组大小已知为1,
而且这个IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组元素的指向不是IMAGE_RESOURCE_DIRECTORY结构,
而是具体包含位图信息,数据的IMAGE_RESOURCE_DATA_ENTRY结构,获取这个结构后,
就根据这个结构提供的信息读取实际位图数据。这里由于要把所有的位图都读取出来,
所以要对第二层的每个元素进行同样的操作

代码如下:(位图保存在E盘下)

#include<stdio.h>
#include<windows.h>
void SaveBmp(char *bmpPath,BYTE *bmData,int Size);
int main()
{
HANDLE hFile=CreateFile("e:\\abc.exe",GENERIC_READ|GENERIC_WRITE,NULL,
      NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
IMAGE_DOS_HEADER dosHeader;
DWORD dwSize;
ReadFile(hFile,(void *)&dosHeader,sizeof(IMAGE_DOS_HEADER),&dwSize,NULL);//读取DOS文件头
SetFilePointer(hFile,dosHeader.e_lfanew+4,NULL,FILE_BEGIN);//移到文件头位置,加4略过PE标志
IMAGE_FILE_HEADER fileHeader;
ReadFile(hFile,(void *)&fileHeader,sizeof(IMAGE_FILE_HEADER),&dwSize,NULL);//读取文件头
int NumSection=fileHeader.NumberOfSections;//获取节数量
SetFilePointer(hFile,dosHeader.e_lfanew+248,NULL,FILE_BEGIN);//移到节表位置
IMAGE_SECTION_HEADER Section;
for(int i=0;i<NumSection;i++)//依次读出IMAGE_SECTION_HEADER数组的每个元素
{
 
 ReadFile(hFile,(void *)&Section,sizeof(IMAGE_SECTION_HEADER),&dwSize,NULL);
 if(strcmp((char *)Section.Name,".rsrc")==0)//如果是资源节头
 break;
}
SetFilePointer(hFile,Section.PointerToRawData,NULL,FILE_BEGIN);//移到资源表
IMAGE_RESOURCE_DIRECTORY resDirectory;
//读取第一层目录IMAGE_RESOURCE_DIRECTORY结构
ReadFile(hFile,(void *)&resDirectory,sizeof(IMAGE_RESOURCE_DIRECTORY),&dwSize,NULL);
//计算其后IMAGE_RESOURCE_DIRECTORY结构数组大小
int dirEntryCount=resDirectory.NumberOfIdEntries+resDirectory.NumberOfNamedEntries;
IMAGE_RESOURCE_DIRECTORY_ENTRY resDirEntry;
for(i=0;i<dirEntryCount;i++)//依次读出第一目录下的IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组元素
{
 ReadFile(hFile,(void *)&resDirEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwSize,NULL);
   if(resDirEntry.NameIsString==0&&resDirEntry.Id==2)
    break;
 
}
SetFilePointer(hFile,Section.PointerToRawData+resDirEntry.OffsetToDirectory,
      NULL,FILE_BEGIN);//移到第二层目录
//读取第二层目录IMAGE_RESOURCE_DIRECTORY结构
ReadFile(hFile,(void *)&resDirectory,sizeof(IMAGE_RESOURCE_DIRECTORY),&dwSize,NULL);
//计算其后IMAGE_RESOURCE_DIRECTORY结构数组大小
int bmpCount=resDirectory.NumberOfIdEntries+resDirectory.NumberOfNamedEntries;

IMAGE_RESOURCE_DIRECTORY_ENTRY *pResDirEntry;
pResDirEntry=new IMAGE_RESOURCE_DIRECTORY_ENTRY[bmpCount];
for(i=0;i<bmpCount;i++)//依次读出第二目录下的IMAGE_RESOURCE_DIRECTORY_ENTRY数组元素
{
 ReadFile(hFile,(void *)&pResDirEntry[i],sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwSize,NULL);
 
}
for(i=0;i<bmpCount;i++)
{
 //移到第三层(语言ID),并读取第三层的IMAGE_RESOURCE_DIRECTORY结构
SetFilePointer(hFile,Section.PointerToRawData+pResDirEntry[i].OffsetToDirectory,NULL,FILE_BEGIN);
ReadFile(hFile,(void *)&resDirectory,sizeof(IMAGE_RESOURCE_DIRECTORY),&dwSize,NULL);
//已知第三层的IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组大小为1
ReadFile(hFile,(void *)&resDirEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwSize,NULL);
SetFilePointer(hFile,Section.PointerToRawData+resDirEntry.OffsetToDirectory,
      NULL,FILE_BEGIN);//移到IMAGE_RESOURCE_DATA_ENTRY结构处
IMAGE_RESOURCE_DATA_ENTRY resDataEntry;
ReadFile(hFile,(void *)&resDataEntry,sizeof(IMAGE_RESOURCE_DATA_ENTRY),&dwSize,NULL);
//移到位图存储位置
SetFilePointer(hFile,resDataEntry.OffsetToData,NULL,FILE_BEGIN);
BYTE *bmData=new BYTE[resDataEntry.Size];
ReadFile(hFile,(void *)bmData,resDataEntry.Size,&dwSize,NULL);//读取位图数据
char bmpPath[15];
sprintf(bmpPath,"e:\\%d.bmp",pResDirEntry[i].Id);
SaveBmp(bmpPath,bmData,resDataEntry.Size);//保存位图
}
return 0;
}
void SaveBmp(char *bmpPath,BYTE *bmData,int Size)//该函数用于保存位图到磁盘
{
 DWORD dwSize;
BITMAPFILEHEADER bfh;//定义位图文件头
bfh.bfType=0x4d42;
bfh.bfSize=Size+sizeof(BITMAPFILEHEADER);
bfh.bfReserved1=0;
bfh.bfReserved2=0;
bfh.bfOffBits=54;
HANDLE hFBmp=CreateFile(bmpPath,GENERIC_WRITE,0,NULL,CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,NULL);
WriteFile(hFBmp,(void *)&bfh,sizeof(BITMAPFILEHEADER),&dwSize,NULL);//写入位图文件头
WriteFile(hFBmp,(void *)bmData,Size,&dwSize,NULL);//写入PE文件中的位图数据
}

例子3。3:获取PE文件图标并保存在磁盘

相对于位图在PE文件的储存方式,图标并没有像位图那样去掉了位图文件头,图标所有的数据都存储在PE文件里,但图标跟位图的区别不只这一点,图标在PE文件是分开来储存的,也就是图标被分为了两部分存储在PE文件里,这一点从“Id(资源类型ID)成员的取值”那里可以看出,3代表Icon(图标)14代表图标组(图标组)。

先来分析一下图标图片结构:(一个图标文件里可能有多幅图片,16X16,32X32大小等。)

图标文件开头是下面这样一个结构

typedef struct   
{
unsigned short Reserved;
unsigned short ResourceType;
unsigned short ImageCount;//指出这个图标文件包含多少图片。
} GroupIcon;

然后是下面是这样一个结构数组,大小由GroupICon结构的ImageCount成员指出。

typedef struct   
{
   unsigned char  Width;
   unsigned char  Height;
   unsigned char  Colors;
   unsigned char  Reserved;
   unsigned short Planes;
   unsigned short BitsPerPixel;
   unsigned long  ImageSize;
   unsigned short ResourceID;
} IconDirResEntry;

接下来就是图标具体数据了。

由于这里我们只是读取PE文件里的图标,所以不对每个成员做具体分析,只要了解一下IconDirResEntry结构的ResourceID成员就行了,这个成员的意思具体取决于它在PE文件中,还是图标文件中,在PE文件中,这个成员对应每副图片数据ID,而在图标文件中则代表每副图片数据在图标文件的位置(偏移)。

前面说了,PE文件中的图标是分两部分存储的,到底是哪两部分呢?GroupICon, IconDirResEntry结构作为一部分(对应图标组14),

最后的所有图片数据作为一部分(对应图标3)。

我们要怎么获得图标在PE文件里的数量和ID号呢,比如我导入了三个图标,是依据(图标3)第二层IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组大小,还是依据(图标组14)。

答案是依据图标组14,也就是说把前面那个"例子3.1:获取一个PE文件里的位图数量以及它们的ID号"里的(位图2)改成(图标14)就可以了。

如有一个PE文件,我导入了三个图标,运行下面代码:

#include<stdio.h>
#include<windows.h>
int main()
{
HANDLE hFile=CreateFile("e:\\abc.exe",GENERIC_READ|GENERIC_WRITE,NULL,
      NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
IMAGE_DOS_HEADER dosHeader;
DWORD dwSize;
ReadFile(hFile,(void *)&dosHeader,sizeof(IMAGE_DOS_HEADER),&dwSize,NULL);//读取DOS文件头
SetFilePointer(hFile,dosHeader.e_lfanew+4,NULL,FILE_BEGIN);//移到文件头位置,加4略过PE标志
IMAGE_FILE_HEADER fileHeader;
ReadFile(hFile,(void *)&fileHeader,sizeof(IMAGE_FILE_HEADER),&dwSize,NULL);//读取文件头
int NumSection=fileHeader.NumberOfSections;//获取节数量
SetFilePointer(hFile,dosHeader.e_lfanew+248,NULL,FILE_BEGIN);//移到节表位置
IMAGE_SECTION_HEADER Section;
for(int i=0;i<NumSection;i++)//依次读出IMAGE_SECTION_HEADER数组的每个元素
{
 
 ReadFile(hFile,(void *)&Section,sizeof(IMAGE_SECTION_HEADER),&dwSize,NULL);
 if(strcmp((char *)Section.Name,".rsrc")==0)//如果是资源节头
 break;
}
SetFilePointer(hFile,Section.PointerToRawData,NULL,FILE_BEGIN);//移到资源表
IMAGE_RESOURCE_DIRECTORY resDirectory;
//读取第一层目录IMAGE_RESOURCE_DIRECTORY结构
ReadFile(hFile,(void *)&resDirectory,sizeof(IMAGE_RESOURCE_DIRECTORY),&dwSize,NULL);
//计算其后IMAGE_RESOURCE_DIRECTORY结构数组大小
int dirEntryCount=resDirectory.NumberOfIdEntries+resDirectory.NumberOfNamedEntries;
IMAGE_RESOURCE_DIRECTORY_ENTRY resDirEntry;
for(i=0;i<dirEntryCount;i++)//依次读出第一目录下的IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组元素
{
 ReadFile(hFile,(void *)&resDirEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwSize,NULL);
  if(resDirEntry.NameIsString==0&&resDirEntry.Id==14) //改成图标组14
    break;
 
}
SetFilePointer(hFile,Section.PointerToRawData+resDirEntry.OffsetToDirectory,
      NULL,FILE_BEGIN);//移到第二层目录
//读取第二层目录IMAGE_RESOURCE_DIRECTORY结构
ReadFile(hFile,(void *)&resDirectory,sizeof(IMAGE_RESOURCE_DIRECTORY),&dwSize,NULL);
//计算其后IMAGE_RESOURCE_DIRECTORY结构数组大小
int bmpCount=resDirectory.NumberOfIdEntries+resDirectory.NumberOfNamedEntries;
printf("该PE文件里有%d个图标\n它们的ID号如下:\n",bmpCount);
for(i=0;i<bmpCount;i++)//依次读出第二目录下的IMAGE_RESOURCE_DIRECTORY_ENTRY数组元素
{
 ReadFile(hFile,(void *)&resDirEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwSize,NULL);
 if(resDirEntry.NameIsString==0)
  printf("%d\n",resDirEntry.Id);
}
return 0;
}

运行效果:


但如果把图标组14改成图标3会有什么结果了(也就是资源类型为3(图标)对应的第二层的IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组代表什么)。

这里我改了,结果如下:

很明显,我只导入了三个图标,如果读取3的话,肯定是错的,但这个结构数组代表什么呢?这个结构数组代表PE文件里所有图标的图片数量。

一个图标里面可能有4张图片,或者2张,比如PE文件有三个图标文件,第一个图标文件有9张图片,第二个有2张,第三个只有一张。总共有12张图片,也就是图标的数据。这一点前面已经介绍过了,但我要怎么知道那几副图片是属于哪个图标组的呢?请转到IconDirResEntry结构和GroupIcon结构,看一下是怎么描述 ImageCount,和ResourceID成员的。ImageCount确定图标的数量,ResourceID在PE文件用于确定图标的ID,也就是上面的1,2,3,4,5。。。的ID号。

而我们所要做的,就是把这两部分正确的合并在一起,并计算每个图片数据在PE文件的偏移,并把这个偏移记录在对应的ResourceID里。

接下来我们就可以读取PE文件的图标了。

由于PE文件显示的图标总是第一个,也就是14图标组第二层IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组的第一个元素对应的图标。它的ID号也在最前,这也就是为什么MFC设置应用程序图标的时候,把ID号往前定义就可以了。

所以我们只要读取第一个图标组和它对应的那些ID图片数据就行了。

 代码就跳过吧,最终以失败而告终,那个 IconDirResEntry结构在PE文件中是14大小的,在图标文件中却变成了16大小了。我实在是力不从心了,也没参考资料,摸索不出来了。、

下面最后一个是未成功代码,参考使用。

//其实如果先把PE文件映射到内存,会比直接在磁盘上读取PE文件方便很多。
//但如果想更好的了解PE文件的话,那就直接分析磁盘上的PE文件。
#include<stdio.h>
#include<windows.h>
//自定义图标组结构
typedef struct   
{
unsigned short Reserved;
unsigned short ResourceType;
unsigned short ImageCount;
} GroupIcon;
typedef struct   
{
   unsigned char  Width;
   unsigned char  Height;
   unsigned char  Colors;
   unsigned char  Reserved;
   unsigned short Planes;
   unsigned short BitsPerPixel;
   unsigned long  ImageSize;
   unsigned short ResourceID;
} IconDirResEntry;
typedef struct
{
 BYTE *pirData;
 DWORD size;
}IconData;
BYTE *GetData(HANDLE hFile,IMAGE_RESOURCE_DIRECTORY_ENTRY *pResDirEntry,DWORD *pdwSize);
int main()
{
 
HANDLE hFile=CreateFile("e:\\abc.exe",GENERIC_READ|GENERIC_WRITE,NULL,
      NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
IMAGE_DOS_HEADER dosHeader;
DWORD dwSize;
ReadFile(hFile,(void *)&dosHeader,sizeof(IMAGE_DOS_HEADER),&dwSize,NULL);//读取DOS文件头
SetFilePointer(hFile,dosHeader.e_lfanew+4,NULL,FILE_BEGIN);//移到文件头位置,加4略过PE标志
IMAGE_FILE_HEADER fileHeader;
ReadFile(hFile,(void *)&fileHeader,sizeof(IMAGE_FILE_HEADER),&dwSize,NULL);//读取文件头
int NumSection=fileHeader.NumberOfSections;//获取节数量
SetFilePointer(hFile,dosHeader.e_lfanew+248,NULL,FILE_BEGIN);//移到节表位置
IMAGE_SECTION_HEADER Section;
for(int i=0;i<NumSection;i++)//依次读出IMAGE_SECTION_HEADER数组的每个元素
{
 
 ReadFile(hFile,(void *)&Section,sizeof(IMAGE_SECTION_HEADER),&dwSize,NULL);
 if(strcmp((char *)Section.Name,".rsrc")==0)//如果是资源节头
 break;
}
SetFilePointer(hFile,Section.PointerToRawData,NULL,FILE_BEGIN);//移到资源表
IMAGE_RESOURCE_DIRECTORY resDirectory;
//读取第一层目录IMAGE_RESOURCE_DIRECTORY结构
ReadFile(hFile,(void *)&resDirectory,sizeof(IMAGE_RESOURCE_DIRECTORY),&dwSize,NULL);
//计算其后IMAGE_RESOURCE_DIRECTORY结构数组大小
int dirEntryCount=resDirectory.NumberOfIdEntries+resDirectory.NumberOfNamedEntries;
IMAGE_RESOURCE_DIRECTORY_ENTRY AresDirEntry[2];//存储图标3和图标数组14
IMAGE_RESOURCE_DIRECTORY_ENTRY resDirEntry;
for(i=0;i<dirEntryCount;i++)//依次读出第一目录下的IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组元素
{
 ReadFile(hFile,(void *)&resDirEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwSize,NULL);
   if(resDirEntry.NameIsString==0&&resDirEntry.Id==14)
    AresDirEntry[0]=resDirEntry;
   else if(resDirEntry.NameIsString==0&&resDirEntry.Id==3)
    AresDirEntry[1]=resDirEntry;
   }
SetFilePointer(hFile,Section.PointerToRawData+AresDirEntry[0].OffsetToDirectory,
      NULL,FILE_BEGIN);//移到第二层目录
//读取第二层目录IMAGE_RESOURCE_DIRECTORY结构
ReadFile(hFile,(void *)&resDirectory,sizeof(IMAGE_RESOURCE_DIRECTORY),&dwSize,NULL);
//读取第二层目录IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组第一个元素
ReadFile(hFile,(void *)&resDirEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwSize,NULL);
DWORD GIdataSize=Section.PointerToRawData;
//获取第一个图标组数据
BYTE *HData=GetData(hFile,&resDirEntry,&GIdataSize);
GroupIcon *pGIcon=(GroupIcon *)HData;
//不判断对应图标ID,只获取图标对应图片数量就可以了,都是从1,2,3...开始
SetFilePointer(hFile,Section.PointerToRawData+AresDirEntry[1].OffsetToDirectory,
      NULL,FILE_BEGIN);//移到图标3第二层目录
//读取第二层目录IMAGE_RESOURCE_DIRECTORY结构
ReadFile(hFile,(void *)&resDirectory,sizeof(IMAGE_RESOURCE_DIRECTORY),&dwSize,NULL);
IMAGE_RESOURCE_DIRECTORY_ENTRY *pResDirEntry=
new IMAGE_RESOURCE_DIRECTORY_ENTRY[pGIcon->ImageCount];
int picPOS=sizeof(GroupIcon)+pGIcon->ImageCount*sizeof(IconDirResEntry);//图片偏移
IconData *iconData=new IconData[pGIcon->ImageCount];
HANDLE iconFile=CreateFile("e:\\aa.ico",GENERIC_WRITE,0,NULL,CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,NULL);
WriteFile(iconFile,(void *)pGIcon,sizeof(GroupIcon),&dwSize,NULL);
for(i=0;i<pGIcon->ImageCount;i++)
{
 DWORD IDSize=Section.PointerToRawData;
 //重新计算位置,因为GetData会改变文件位置
SetFilePointer(hFile,Section.PointerToRawData+AresDirEntry[1].OffsetToDirectory
+sizeof(IMAGE_RESOURCE_DIRECTORY)+sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY)*i,NULL,FILE_BEGIN);
ReadFile(hFile,(void *)pResDirEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwSize,NULL);
iconData[i].pirData=GetData(hFile,pResDirEntry,&IDSize);
iconData[i].size=IDSize;
IconDirResEntry *pIconDrResEntry=(IconDirResEntry *)
(HData+sizeof(GroupIcon)+i*14);//指向下一层IconDirResEntry结构
pIconDrResEntry->ResourceID=picPOS;
WriteFile(iconFile,(void *)pIconDrResEntry,sizeof(IconDirResEntry),&dwSize,NULL);
//printf("%d\n",picPOS);
picPOS+=IDSize;//计算下一个图标在图标文件中的偏移
}
for(i=0;i<pGIcon->ImageCount;i++)
WriteFile(iconFile,(void *)iconData[i].pirData,iconData[i].size,&dwSize,NULL);
return 0;
}
//这个函数根据第二层的IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组元素获得其对应的数据。
BYTE *GetData(HANDLE hFile,IMAGE_RESOURCE_DIRECTORY_ENTRY *pResDirEntry,DWORD *pdwSize)
{
DWORD dwSize;
SetFilePointer(hFile,*pdwSize+pResDirEntry->OffsetToDirectory,NULL,FILE_BEGIN);
IMAGE_RESOURCE_DIRECTORY resDirectory;
//读取第三层目录IMAGE_RESOURCE_DIRECTORY结构
ReadFile(hFile,(void *)&resDirectory,sizeof(IMAGE_RESOURCE_DIRECTORY),&dwSize,NULL);
//读取第三层目录IMAGE_RESOURCE_DIRECTORY_ENTRY结构
ReadFile(hFile,(void *)pResDirEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&dwSize,NULL);
//移到IMAGE_RESOURCE_DATA_ENTRY结构处
SetFilePointer(hFile,*pdwSize+pResDirEntry->OffsetToDirectory,NULL,FILE_BEGIN);
IMAGE_RESOURCE_DATA_ENTRY resDataEntry;
//读取IMAGE_RESOURCE_DATA_ENTRY结构
ReadFile(hFile,(void *)&resDataEntry,sizeof(IMAGE_RESOURCE_DATA_ENTRY),&dwSize,NULL);
//移到数据储存处
SetFilePointer(hFile,resDataEntry.OffsetToData,NULL,FILE_BEGIN);
BYTE *HData=new BYTE[resDataEntry.Size];
ReadFile(hFile,(void *)HData,resDataEntry.Size,&dwSize,NULL);//读取数据
*pdwSize=resDataEntry.Size;//数据大小
return HData;
}

 

/*未写

。。。。。。。

*/

//本文参考了网上若干资料

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bczheng1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值