神泣逆向工程

原始链接: http://bbs.pediy.com/showthread.php?threadid=35832
标 题: 【原创】神泣逆向工程—我的处女破文
作 者: Bughoho
时 间: 2006-12-03,17:53
链 接: http://bbs.pediy.com/showthread.php?threadid=35832

【文章标题】: 神泣逆向工程—我的处女破文
【文章作者】: BUG
【作者邮箱】: ********
【作者QQ号】: ********
【软件名称】: 神泣
【下载地址】: 自己搜索下载
【加壳方式】: Asprotect
【编写语言】: VC++
【使用工具】: PEID,OD,IDA 5.0
【操作平台】: WINDOWS XP
【软件介绍】: 神泣是光通代理的一款韩国泡菜式游戏
【作者声明】: 最近国内对游戏破解开发搞得有点严,本来我也不想发布的,但是这是我的第一次:),不管怎样也得记录一下嘛,失误之处还请各位高手指正,谢谢!
--------------------------------------------------------------------------------
【前言】
  破解此游戏并非用在商业用途,只是想自己脱机外挂升升级,让电脑自己去跟电脑说话,我就可以静心研究了,不过也是通过这一次逆向,让我感觉进步了不少。
  这是我第一写破文,还请大家批评指正!
【详细过程】
    首先是脱壳,这个游戏是Asprotect的壳子,我现在还是新手,要不是有Asprotect的脱壳机,我想我在这一关上又得研究好几天了。脱掉它的"外衣"之后,就开始对它逆向工程了,要分析发送数据的来源,就得对send下断点,然后输入帐号密码,登陆。这时断点断在了WS_32模块,回到用户领空,
到达这里:
0051B740 /$ B8 00100000   MOV EAX,1000
0051B745 |. E8 A68C0200   CALL _game.005443F0
0051B74A |. 53         PUSH EBX
0051B74B |. 8B9C24 0C1000>MOV EBX,DWORD PTR SS:[ESP+100C]
0051B752 |. 8BCB       MOV ECX,EBX
0051B754 |. 55         PUSH EBP
0051B755 |. 56         PUSH ESI
0051B756 |. 8BB424 101000>MOV ESI,DWORD PTR SS:[ESP+1010]
0051B75D |. 8BC1       MOV EAX,ECX
0051B75F |. 57         PUSH EDI
0051B760 |. 8D6B 02     LEA EBP,DWORD PTR DS:[EBX+2]
0051B763 |. 8D7C24 12   LEA EDI,DWORD PTR SS:[ESP+12]
0051B767 |. C1E9 02     SHR ECX,2
0051B76A |. 66:896C24 10 MOV WORD PTR SS:[ESP+10],BP
0051B76F |. 53         PUSH EBX
0051B770 |. F3:A5       REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS>
0051B772 |. 8BC8       MOV ECX,EAX
0051B774 |. 83E1 03     AND ECX,3
0051B777 |. F3:A4       REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[>
0051B779 |. 8D4C24 16   LEA ECX,DWORD PTR SS:[ESP+16]
0051B77D |. 51         PUSH ECX
0051B77E |. E8 2DB5FFFF   CALL _game.00516CB0               ; 加密函数
0051B783 |. A1 A0661002   MOV EAX,DWORD PTR DS:[21066A0]
0051B788 |. 83C4 08     ADD ESP,8
0051B78B |. 83C3 02     ADD EBX,2
0051B78E |. 8D5424 10   LEA EDX,DWORD PTR SS:[ESP+10]
0051B792 |. 6A 00       PUSH 0                       ; /Flags = 0
0051B794 |. 53         PUSH EBX                       ; |DataSize
0051B795 |. 52         PUSH EDX                       ; |Data
0051B796 |. 50         PUSH EAX                       ; |Socket => EC
0051B797 |. FF15 D8135B00 CALL DWORD PTR DS:[<&ws2_32.send>]     ; /send

这里还挺近的,PUSH ECX就是原始数据,
这以后我就用IDA看了,动态调试时用OD,静态分析还是IDA强大,
跟进_game.00516CB0到达:

.text:00516CB0 sub_516CB0     proc near           ; CODE XREF: sub_51B740+3Ep
.text:00516CB0
.text:00516CB0 arg_0       = dword ptr 4
.text:00516CB0 arg_4       = dword ptr 8
.text:00516CB0
.text:00516CB0           mov   al, ds:byte_2100685
.text:00516CB5           test   al, al
.text:00516CB7           jz     short locret_516CEB
.text:00516CB7
.text:00516CB9           mov   eax, ds:dword_210067C
.text:00516CBE           test   eax, eax
.text:00516CC0           jnz   short loc_516CD7
.text:00516CC0
.text:00516CC2           mov   eax, [esp+arg_4]
.text:00516CC6           mov   ecx, [esp+arg_0]
.text:00516CCA           push   eax
.text:00516CCB           push   ecx
.text:00516CCC           mov   ecx, offset unk_20FF4D8
.text:00516CD1           call   sub_517CC0                 ;先不管那2个全

局变量地址判断了什么,发送登陆封包是通过了这个函数
.text:00516CD1
.text:00516CD6           retn
.text:00516CD6
.text:00516CD7 ; -----------------------------------------------------------------------

----
.text:00516CD7
.text:00516CD7 loc_516CD7:                   ; CODE XREF: sub_516CB0+10j
.text:00516CD7           mov   edx, [esp+arg_4]
.text:00516CDB           mov   eax, [esp+arg_0]
.text:00516CDF           push   edx
.text:00516CE0           push   eax
.text:00516CE1           mov   ecx, offset unk_20FF628
.text:00516CE6           call   sub_518000
.text:00516CE6
.text:00516CEB
.text:00516CEB locret_516CEB:                 ; CODE XREF: sub_516CB0+7j
.text:00516CEB           retn
.text:00516CEB
.text:00516CEB sub_516CB0     endp

分析可知arg_4 = 长度,arg_0为原始内容,unk_20FF4D8就是一个偏移量,从代码上看来,我猜测它是this指针,类是一个全局变量,所以直接赋值了一个偏移量,在后面的代码上看来我的分析是正确的.

.........
以上是水话.现在说点实在的.
最终到达sub_517B20:
.text:00517B20 sub_517B20     proc near           ; CODE XREF: sub_517CC0+Bp
.text:00517B20
.text:00517B20 dest         = dword ptr 10h
.text:00517B20 src         = dword ptr 14h
.text:00517B20 datalen       = dword ptr 18h
.text:00517B20
.text:00517B20           push   ebx
.text:00517B21           push   ebp
.text:00517B22           push   esi
.text:00517B23           mov   esi, ecx     ; esi = this指针
.text:00517B25           xor   ebp, ebp
.text:00517B27           push   edi
.text:00517B28           mov   edx, [esi+104h]
.text:00517B2E           cmp   edx, ebp
.text:00517B30           jle   else         ; 如果 esi+104h <= 0 就跳到另一个分支
.text:00517B30
.text:00517B36           mov   ecx, [esp+4+datalen]
.text:00517B3A           mov   eax, 10h
.text:00517B3F           sub   eax, edx
.text:00517B41           cmp   ecx, eax
.text:00517B43           jg     short _len_jg_else
.text:00517B43
.text:00517B45           mov   eax, ecx
.text:00517B47           dec   ecx
.text:00517B48           test   eax, eax
.text:00517B4A           jz     short _small_return
.text:00517B4A
.text:00517B4C           mov   eax, [esp+4+dest]
.text:00517B50           lea   edi, [ecx+1]
.text:00517B53           mov   ecx, [esp+4+src]
.text:00517B53
.text:00517B57
.text:00517B57 _loc_for:                     ; CODE XREF: sub_517B20+5Aj
.text:00517B57           mov   edx, [esi+104h]
.text:00517B5D           mov   bl, [ecx]
.text:00517B5F           mov   dl, [esi+edx+108h]
.text:00517B66           xor   dl, bl
.text:00517B68           mov   [eax], dl
.text:00517B6A           mov   ebx, [esi+104h]
.text:00517B70           inc   ebx
.text:00517B71           inc   eax
.text:00517B72           inc   ecx
.text:00517B73           dec   edi
.text:00517B74           mov   [esi+104h], ebx
.text:00517B7A           jnz   short _loc_for
.text:00517B7A
.text:00517B7C
.text:00517B7C _small_return:                 ; CODE XREF: sub_517B20+2Aj
.text:00517B7C           cmp   dword ptr [esi+104h], 10h
.text:00517B83           jl     _loc_return
.text:00517B83
.text:00517B89           mov   [esi+104h], ebp
.text:00517B8F           pop   edi
.text:00517B90           pop   esi
.text:00517B91           pop   ebp
.text:00517B92           pop   ebx
.text:00517B93           retn   0Ch
.text:00517B93
.text:00517B96 ; -----------------------------------------------------------------------

----
.text:00517B96
.text:00517B96 _len_jg_else:                   ; CODE XREF: sub_517B20+23j
.text:00517B96           mov   ebx, [esp+4+src]
.text:00517B9A           mov   edi, [esp+4+dest]
.text:00517B9E           sub   ecx, eax
.text:00517BA0           cmp   edx, 10h
.text:00517BA3           mov   [esp+4+datalen], ecx
.text:00517BA7           jge   short loc_517BD0
.text:00517BA7
.text:00517BA9
.text:00517BA9 _loc_for_1:                   ; CODE XREF: sub_517B20+AEj
.text:00517BA9           mov   eax, [esi+104h]
.text:00517BAF           mov   dl, [esi+eax+108h]
.text:00517BB6           mov   al, [ebx]
.text:00517BB8           xor   dl, al
.text:00517BBA           mov   [edi], dl
.text:00517BBC           mov   eax, [esi+104h]
.text:00517BC2           inc   eax
.text:00517BC3           inc   edi
.text:00517BC4           inc   ebx
.text:00517BC5           cmp   eax, 10h
.text:00517BC8           mov   [esi+104h], eax
.text:00517BCE           jl     short _loc_for_1
.text:00517BCE
.text:00517BD0
.text:00517BD0 loc_517BD0:                   ; CODE XREF: sub_517B20+87j
.text:00517BD0           mov   [esi+104h], ebp
.text:00517BD6           jmp   short loc_517BE4
.text:00517BD6
.text:00517BD8 ; -----------------------------------------------------------------------

----
.text:00517BD8
.text:00517BD8 else:                       ; CODE XREF: sub_517B20+10j
.text:00517BD8           mov   ebx, [esp+4+src]
.text:00517BDC           mov   edi, [esp+4+dest]
.text:00517BE0           mov   ecx, [esp+4+datalen]
.text:00517BE0
.text:00517BE4
.text:00517BE4 loc_517BE4:                   ; CODE XREF: sub_517B20+B6j
.text:00517BE4           cmp   ecx, 10h
.text:00517BE7           jl     short loc_517C58
.text:00517BE7
.text:00517BE9           mov   ebp, ecx
.text:00517BEB           shr   ebp, 4
.text:00517BEE           mov   eax, ebp
.text:00517BF0           neg   eax
.text:00517BF2           shl   eax, 4
.text:00517BF5           add   ecx, eax
.text:00517BF7           mov   [esp+4+datalen], ecx
.text:00517BF7
.text:00517BFB
.text:00517BFB loc_517BFB:                   ; CODE XREF: sub_517B20+132j
.text:00517BFB                           ; 这个循环就是加密了.
.text:00517BFB           lea   eax, [esi+0F4h] ; DWORD m_key1[4]
.text:00517C01           mov   ecx, esi     ; KeyCode函数里面会压入this指针进入真正执行操作的函数
.text:00517C03           push   eax
.text:00517C04           lea   eax, [esi+108h] ; DWORD m_key2[4]
.text:00517C0A           push   eax
.text:00517C0B           call   KeyCode       ; 这个函数是通过循环将edx,eax,ecx(ecx这里虽然是一个指针,其实是指向第一个成员变量)
.text:00517C0B                           ; 这个函数太长,我写程序时是用的内嵌汇编,我写这篇文章时正在还原这个函数.
.text:00517C0B
.text:00517C10           mov   ecx, esi
.text:00517C12           call   convKey       ; 呵呵,小动作.把一个KEY数组的值全部加1
.text:00517C12
.text:00517C17           mov   ecx, [ebx]
.text:00517C19           mov   edx, [esi+108h]
.text:00517C1F           xor   ecx, edx
.text:00517C21           add   ebx, 10h
.text:00517C24           mov   [edi], ecx
.text:00517C26           mov   edx, [ebx-0Ch]
.text:00517C29           mov   eax, [esi+10Ch]
.text:00517C2F           add   edi, 10h
.text:00517C32           xor   edx, eax
.text:00517C34           mov   [edi-0Ch], edx
.text:00517C37           mov   eax, [ebx-8]
.text:00517C3A           xor   eax, [esi+110h]
.text:00517C40           mov   [edi-8], eax
.text:00517C43           mov   ecx, [ebx-4]
.text:00517C46           mov   eax, [esi+114h]
.text:00517C4C           xor   ecx, eax
.text:00517C4E           dec   ebp
.text:00517C4F           mov   [edi-4], ecx
.text:00517C52           jnz   short loc_517BFB
.text:00517C52
.text:00517C54           mov   ecx, [esp+4+datalen]
.text:00517C54
.text:00517C58
.text:00517C58 loc_517C58:                   ; CODE XREF: sub_517B20+C7j
.text:00517C58           test   ecx, ecx
.text:00517C5A           jle   short _loc_return
.text:00517C5A
.text:00517C5C           lea   edx, [esi+0F4h]
.text:00517C62           lea   eax, [esi+108h]
.text:00517C68           push   edx
.text:00517C69           push   eax
.text:00517C6A           mov   ecx, esi
.text:00517C6C           call   KeyCode
.text:00517C6C
.text:00517C71           mov   ecx, esi
.text:00517C73           call   convKey
.text:00517C73
.text:00517C78           mov   ecx, [esp+4+datalen]
.text:00517C7C           mov   eax, [esi+104h]
.text:00517C82           cmp   eax, ecx
.text:00517C84           jge   short _loc_return
.text:00517C84
.text:00517C86
.text:00517C86 loc_517C86:                   ; CODE XREF: sub_517B20+18Cj
.text:00517C86           mov   edx, [esi+104h]
.text:00517C8C           mov   al, [esi+edx+108h]
.text:00517C93           mov   dl, [ebx]
.text:00517C95           xor   al, dl
.text:00517C97           mov   [edi], al
.text:00517C99           mov   ebp, [esi+104h]
.text:00517C9F           inc   ebp
.text:00517CA0           inc   edi
.text:00517CA1           mov   eax, ebp
.text:00517CA3           inc   ebx
.text:00517CA4           cmp   eax, ecx
.text:00517CA6           mov   [esi+104h], ebp
.text:00517CAC           jl     short loc_517C86
.text:00517CAC
.text:00517CAE
.text:00517CAE _loc_return:                   ; CODE XREF: sub_517B20+63j
.text:00517CAE                           ; sub_517B20+13Aj
.text:00517CAE                           ; sub_517B20+164j
.text:00517CAE           pop   edi
.text:00517CAF           pop   esi
.text:00517CB0           pop   ebp
.text:00517CB1           pop   ebx
.text:00517CB2           retn   0Ch
.text:00517CB2
.text:00517CB2 sub_517B20     endp

代码太多,我又很懒,我主要还原一下 loc_517BFB 加密这个循环部分吧。
要逆向这个游戏涉及的地方太多,又不能一一贴出,我讲解一下我分析出来的结果好了

class CCode//20FF4D8
{
  BYTE m_keydata[240];//这是一个KEY表,加密的条件之1
  //+F0
  int m_num;//这个还不知道它的完全用途
  //+F4
  DWORD m_key1[4];//这是个DWORD数组,加密的条件之1
  //+104
  DWORD m_nopnum;//现在仅知道通过它来判断加密还是解密,不过从值上来看不像是它的完全用途
  //+108
  DWORD m_key2[4];//这是个DWORD数组,加密的条件之1
//----------------------------------------------------------
这里是loc_517BFB 还原出来的代码

  else//加密
  {
    if( len >= 10 )
    {
    int nnum;
    __asm
    {
      mov   eax,len;
      shr   eax,4
      mov   nnum,eax
      neg   eax
      shl   eax,4
      add   len,eax
    }
    for( ; nnum > 0; nnum-- )//一次循环加密16字节,所以上面会shr eax,4
    {
      KeyCode(m_key2,m_key1);//每一次循环都会变换m_key1,m_key2和m_keydata的值(m_keydata的值是否变化有待深入考究)
      convKey();
      DWORD tmp;
      memcpy(&tmp,( src ),sizeof(DWORD));
      tmp ^= m_key2[0];
      *(DWORD*)( dest ) = tmp;

      memcpy(&tmp,( src + 4 ),sizeof(DWORD));
      tmp ^= m_key2[1];
      *(DWORD*)( dest + 4) = tmp;

      memcpy(&tmp,( src + 8 ),sizeof(DWORD));
      tmp ^= m_key2[2];
      *(DWORD*)( src + 8) = tmp;

      memcpy(&tmp,( src + 12 ),sizeof(DWORD));
      tmp ^= m_key2[3];
      *(DWORD*)( dest + 12) = tmp;

      src += 16;
      dest += 16;
    }
    }

这就是加密的全过程了,解密部分我也还原出来了,但是这里还是不贴的好,以免某些人搞破坏。
虽然这里加密就完了,其实远远还没这么简单,上面提到加密需要的三个数据m_key1,m_key2,m_keydata。m_keydata和m_key1会在游戏开始时初始化,首先,连接游戏服务器,连接成功后服务端会主动热情的发送给你一个KEY封包,它的结构是这样子的:
//消息头结构
typedef struct _MsgHeader
{
  WORD len;
  WORD cmd;
  _MsgHeader()
  {
    len = 0;
    cmd = 0x0;
  }
}MsgHeader;

//接收密钥结构
typedef struct _MsgKey
{
  BYTE d1;
  BYTE d2;
  BYTE d3;
  BYTE key1[0x40];
  BYTE key2[0x80];
}MsgKey;

MsgKey的长度 = C7-MsgHeader

然后通过这个KEY就可以变形得出m_keydata和key1的值

我的表达能力不是很好,写到这里的时候,已经用了我1个多小时的时间了,深切的感到自己语文成绩之差,这里我已经不好描述了,所以我还是贴代码描述好了,抱歉!

从 DeCodeKey 函数开始

DWORD dword_210067C;// =dekey2 中的 d1,只有这次写入操作;
DWORD dword_2100688 = 0x456FC070;//本来是通过时间来生成一个KEY,只有这次写入操作,应该不会有副本做判断,直接赋值常量可能也能成功

CCode::CCode()
{
  //m_num = 0x0a;
  //memcpy((void*)m_key1,"/x81/x28/x59/x9E/x00/xBD/xC7/xB7/xD1/xBB/x9F/xF1/xA8/x37/xD8/xE2",16);
  //m_nopnum = 0;
  //strcpy((char*)m_key2,"");
}
CCode::~CCode()
{
}

//设置KEY
//data : 接收的密钥封包去掉头2字节
void CCode::DeCodeKey(BYTE* data)//接受到的KEY封包
{
  MsgKey msgkey;
  msgkey.d1 = (BYTE)data[0];
  msgkey.d2 = (BYTE)data[2];
  msgkey.d3 = (BYTE)data[1];
  memcpy( msgkey.key1, &data[0x3],0x40 );//复制到结构里去
  memcpy( msgkey.key2, &data[0x43],0x80 );
  dekey1(msgkey.d1,msgkey.d3,msgkey.d2,msgkey.key1,msgkey.key2 );
}

void CCode::dekey1( BYTE d1,BYTE d2,BYTE d3,BYTE* key1,BYTE* key2 )//dekey1在游戏程序中是一个虚函数
{
  DWORD var_84 = 0;
  BYTE var_80[0x80];

  dekey2(var_80,&var_84,d1 & 0xFF,key1,d2 & 0xFF,key2,d3 & 0xFF);
}
//一些无关痛痒的结构
typedef struct _Unstname//暂时不知道干什么的,dekey2中把这个结构进行变形
{
  BYTE var_8C[0x7F];
  BYTE var_D;
  _Unstname()
  {
    memset(var_8C,0,0x80);//一次性把var_D也刷掉
  }
}Unstname;

DWORD d_dword_210065C[4];
void CCode::dekey2(OUT BYTE* var_80,OUT DWORD* var_84,BYTE d1,BYTE* key1,BYTE d2,BYTE* key2,BYTE d3)
{
  //DWORD var_C4 = 0;
  //DWORD var_C0 = 0;
  //DWORD var_BC = 0;
  //DWORD var_B8 = 0;
  //DWORD var_B4 = 0;
  //DWORD var_B0 = 0;
  //DWORD var_AC = 0;
  //DWORD var_A8 = 0;
  DWORD var_C4[8] = {0};
  BYTE var_A4[0x24];
  Unstname unstname;
  BYTE var_C[0x8];
  DWORD var_4;

  dword_210067C = d1;
  outkey80((BYTE*)&unstname,sizeof(Unstname)/*0x80*/);//这个函数通过了dword_2100688变

量来生成一个80字节的数据,那个变量是跟时间有关的数据变形得到的,不过我用常量好象也能使用,待考究
 
  unstname.var_D &= 0x7F;//还原代码而已,还没看到用途

  SHA256(var_C4,(unsigned char *)&unstname,0x80,key2,d3);//这里就通过SHA算法生成一个数据
                                          //SHA256函数里面我就直接按游戏中的加密顺序套用了标准函数了

  if( dword_210067C == 0 )
  {
    //快到InitKey函数了...快打通了..
    InitKey(var_C4,0x10);//变形得到m_keydata.
    d_dword_210065C[0] = var_C4[4];
    d_dword_210065C[1] = var_C4[5];
    d_dword_210065C[2] = var_C4[6];
    d_dword_210065C[3] = var_C4[7];
    setkey1(d_dword_210065C);//设置m_key1的值
  }
  else
  {
  }
}
void CCode::setkey1(DWORD key[4])
{
  m_key1[0] = key[0];
  m_key1[1] = key[1];
  m_key1[2] = key[2];
  m_key1[3] = key[3];
  //memcpy(m_key1,key,0x10);
  m_nopnum = 0;
}
void CCode::SHA256(DWORD* var_C4,unsigned char *input,DWORD len,BYTE* key2,BYTE d3)
{
  //BYTE var_114[0x68];//可能是0x28+0x40
  sha256_context ctx;
  BYTE sdata1[0x40];
  BYTE sdata2[0x40];
  BYTE var_sha_outstr[32];
  BYTE var_C[8];//跟异常有关
  DWORD var_4;//完全没用到

  //call   sub_517E80 //关于var_114
  var_4 = 0;

  BYTE* s = NULL;
  if(len > 0x40)
  {
    sha256_starts(&ctx);
    sha256_update(&ctx,input,len);
    sha256_finish(&ctx,var_sha_outstr);

    s = var_sha_outstr;
  }
  else
  {
    s = input;
  }
  memset(sdata1,0,0x40);
  memset(sdata2,0,0x40);

  memcpy(sdata1, s, 32 );
  memcpy(sdata2, s, 32 );
 
  for(int idx = 0; idx < 0x40; idx++)
  {
    sdata1[idx] = sdata1[idx] ^ 0x36;
    sdata2[idx] = sdata2[idx] ^ 0x5C;
  }
 
  sha256_starts( &ctx );
  sha256_update( &ctx, (uint8 *) sdata1, 0x40 );
  sha256_update( &ctx, (uint8 *) key2, d3 );
  sha256_finish( &ctx, (uint8 *)var_C4);

  sha256_starts( &ctx );        
  sha256_update( &ctx, (uint8 *) sdata2, 0x40 );
  sha256_update( &ctx, (uint8 *) var_C4, 0x20 );
  sha256_finish( &ctx, (uint8 *)var_C4);

}

//初始化KEY表
//addr:可能是通过密钥封包运算出来的
void CCode::InitKey(DWORD* addr,int num)
{
  InitKeys(addr,num*8,this);//汇编代码.....太长了加我太懒了..
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值