武.林.外.传 主进程 hook 相关 delphi 代码

elementclient HOOK 文档.doc

 

修改 elementclient.exe 文件步骤

一.

1.patcher.dll   中的 hook_p();    函数的主要功能是修改 elementclient.exe 文件,让其启动时,自动加载我们的插件文件 sougoupy.dll

2.hook_p 函数首先备份 elementclient.exe  防止我们的程序修改后,游戏不能运行,方便用户修复使用。

3.hook_p 修改 elementclient.exe 文件,把插件 sougoupy.dll 的全路径写到其代码段的零区域,此处文件地址定位:0x797801

4.执行文件 hook 的地址如下:

   

 (图 1.1)

00B97729     0000           add byte ptr ds:[eax],al

00B9772B     0000           add byte ptr ds:[eax],al

00B9772D     0000           add byte ptr ds:[eax],al

00B9772F     0000           add byte ptr ds:[eax],al

00B97731     0000           add byte ptr ds:[eax],al

00B97733     0000           add byte ptr ds:[eax],al

00B97735     0000           add byte ptr ds:[eax],al

00B97737     0000           add byte ptr ds:[eax],al

00B97739     0000           add byte ptr ds:[eax],al

00B9773B     0000           add byte ptr ds:[eax],al

PS:以上的地址将会写入具体的 hook 代码,然后跳转回 (图 1.1)所指定的地址。 

修改后的 hook  code 代码为:

00B97729     60             pushad

00B9772A     68 0178B900    push elementc.00B97801       ; ASCII "C:\win\hell\1.dll"

00B9772F     FF15 9482B900  call dword ptr ds:[<&KERNEL32.LoadLibraryA>] ; kernel32.LoadLibraryA

00B97735     61             popad

00B97736     8965 E8        mov dword ptr ss:[ebp-18],esp

00B97739     33DB           xor ebx,ebx

00B9773B   ^ E9 4898F7FF    jmp elementc.00B10F88

//二进制为:

60 68 01 78 B9 00 FF 15 94 82 B9 00 61 89 65 E8 33 DB E9 48 98 F7 FF

 

patcher HOOK 文档.doc

 

修改 Patcher.exe 文件步骤

一.

1.在文件偏移 $64100 处,写入外部dll 所在的全路径。(ps:此处的外部 dll,功能为拦截住用户点“开始”游戏时,保证 elementclient.exe 文件被正确改正。)

此处外部 dll 名称定义为:patcher.dll  

2.我们的启动程序做的 inline hook :

  

               (图 1.1

上图画红框的地址,为我们需要inline  hook 的地址。

00464035     0000            add byte ptr ds:[eax],al   //上图 hook 后会跳转到此处做

00464037     0000            add byte ptr ds:[eax],al   //加载外部 dll 的工作,然后跳转

00464039     0000            add byte ptr ds:[eax],al  // 回到地址:0x443723

0046403B     0000            add byte ptr ds:[eax],al

0046403D     0000            add byte ptr ds:[eax],al

0046403F     0000            add byte ptr ds:[eax],al

       

                      (图 1.2  patcher.exe IAT )

Ps: iat 表用于汇编写入时,调用系统动态库。

3.Hook

A.

0044371E    /E9 12090200     jmp patcher.00464035   //已经修改的地址

//二进制代码:E9 12 09 02 00

对应的文件地址为 : 0044371E - 400000 = 4371E

修改长度:5 字节

B.

00464035    > \60            pushad

00464036    .  68 00414600   push patcher1.00464060

0046403B       FF15 DC504600 call dword ptr ds:[<&KERNEL32.LoadLibraryA>]

00464041       61            popad

00464042       E8 6047FEFF   call patcher1.004487A7

00464047     ^ E9 D7F6FDFF   jmp patcher1.00443723

//二进制代码:60 68 00 4146 00 FF 15 DC 50 46 00 61 E8 60 47 FE FF E9 D7 F6 FD FF

对应的文件地址为 : 00464035 - 400000 = 64035

修改长度:23 字节

4.备份 patcher.exe 文件细节。

 A.我们的启动程序首先判断是否存在配置文件,不存在则直接拷贝一份命名为:patcher.exe.bak

B.当存在备份文件时,修改代码之前,对比我们记录在配置文件中的 lastmd5 值(此值为修改后的 patcher.exe 文件的 md5),如果此时的 patcher.exe 的 md5 值跟 lastmd5  不同时,则说明游戏对 patcher.exe 文件有更新,此时我们再拷贝一份新的 patcher.exe 为 patcher.exe.bak

C.这样有效的保证了当我们软件修改 patcher.exe 后导致无法运行游戏,用户可以通过我们的软件修复 patcher.exe 文件。

二.

Patcher.dll  做的工作如下:

1.在 CreateProcessW 处进行跳转 hook.

CreateProcessW:

7C802336 k>  8BFF           mov edi,edi

7C802338     55             push ebp

7C802339     8BEC           mov ebp,esp

7C80233B     6A 00          push 0

7C80233D     FF75 2C        push dword ptr ss:[ebp+2C]

7C802340     FF75 28        push dword ptr ss:[ebp+28]

修改为:

7C802336 k>- E9 151DC683    jmp patcher.00464050

7C80233B     6A 00          push 0

7C80233D     FF75 2C        push dword ptr ss:[ebp+2C]

7C802340     FF75 28        push dword ptr ss:[ebp+28]

7C802343     FF75 24        push dword ptr ss:[ebp+24]

Hook 代码处添加为:

00464050     60             pushad

00464051     68 F0404600    push patcher.004640F0

00464056     FF15 DC504600  call dword ptr ds:[<&KERNEL32.LoadLibraryA>]  ; kernel32.LoadLibraryA

0046405C     68 E8404600    push patcher.004640E8

00464061     50             push eax

00464062     B8 40AE807C    mov eax,kernel32.GetProcAddress

00464067     FFD0           call eax

00464069     FFD0           call eax

0046406B     61             popad

0046406C     B8 3623807C    mov eax,kernel32.CreateProcessW

00464071     83C0 05        add eax,5

00464074     8BFF           mov edi,edi

00464076     55             push ebp

00464077     8BEC           mov ebp,esp

00464079     FFE0           jmp eax

PS:由于 IAT 表,对于 GetProcAddressCreateProcessW 两个函数地址的记录不完全,所以需要在 patcher.dll 模块中,计算函数地址,然后再次写入回去即可。

//***************************************************

同时添加字符串("hook_p","patcher.dll"):

004640E8     68 6F6F6B5F    push 5F6B6F6F

004640ED     70 00          jo short patcher.004640EF

004640EF     0070 61        add byte ptr ds:[eax+61],dh

004640F2     74 63          je short patcher.00464157

004640F4     68 65722E64    push 642E7265

004640F9     6C             ins byte ptr es:[edi],dx

004640FA     6C             ins byte ptr es:[edi],dx

PS:此时,当玩家点击游戏的 “开始”按钮时,就会首先执行我们的 hook_p 函数

 

 

library patcher;

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls,Registry,shellapi,MD5,IniFiles;

//-------------------  patcher.exe ---------------
const PAGE_EXECUTE_READWRITE = $40;
const hook_array_jmp:array[0..4] of byte = ($E9,$15,$1D,$C6,$83);
const hook_array_string:array[0..19] of Byte =($68,$6F,$6F,$6B,$5F,$70,$00,
                                              $00,$70,$61,$74,$63,$68,$65,
                                              $72,$2E,$64,$6C,$6C,$00);
const hook_array_code:array[0..42] of byte = ($60,$68,$F0,$40,$46,$00,$FF,
                                              $15,$DC,$50,$46,$00,$68,$E8,
                                              $40,$46,$00,$50,$B8,$40,$AE,
                                              $80,$7C,$FF,$D0,$FF,$D0,$61,
                                              $B8,$36,$23,$80,$7C,$83,$C0,
                                              $05,$8B,$FF,$55,$8B,$EC,$FF,
                                              $E0);
 //-------------------- elementclient.exe  ---------
const element_hook_array_jmp:array[0..4] of byte = ($E9,$A1,$67,$08,$00);
const element_hook_array_code:array[0..22] of byte = ($60,$68,$01,$78,$B9,
                                              $00,$FF,$15,$94,$82,$B9,$00,
                                              $61,$89,$65,$E8,$33,$DB,$E9,
                                              $48,$98,$F7,$FF);
var
  thisDllIni:TIniFile;
  ini_fileName:string = 'config.ini';                                           //配置文件名称
  //-------------------  patcher.exe ---------------
  iat_patcher_createProcessW_add:DWORD;                                         // iat 表中 CreateProcessW 函数地址
  iat_patcher_virtualProtectEx_add:DWORD;                                       //iat 表中  VirtualProtectEx 函数地址
  getprocaddress_add:dword;
  patcher_dll_name:string = 'patcher.dll';                                      //patcher 中的 dll 名称
  plugin_dll_name:string = 'sogoupy.dll';                                       //插件的名称
  work_dirpath:string;                                                          //工作目录
  current_dirpath:string;                                                       //当前模块所在路径
  elementclient_path:string;                                                    //游戏客户端路径
  elementclient_file_name:string = 'elementclient.exe';                         //游戏客户端名字
  hook_jmp_add:dword;                                                           //hook 代码跳转的地址
  hook_code_add:dword = $464050;                                                //hook 代码的地址
  hook_add_string_addr:DWORD = $4640e8;                                         //添加字符串到更新程序中
  plugin_pathAdd_toWrite_inPatcher:dword = $64100;                              //插件全路径在游戏中的记录
 //-------------------- elementclient.exe  ---------
  element_plugin_full_path_add:dword = $00B97801 - $400000;                     //在游戏文件中,插件的全路径
  element_jmp_file_hook_add:dword = $00B10F83 - $400000;                        //在文件中 hook 的地址
  element_code_file_hook_add:dword = $00B97729 - $400000;                       //在文件中写入 hook code 的地址
{$R *.res}
//***********************************************
procedure getFuncAdd();                                                         //获取函数地址
var
  h:dword;
begin
  h:=LoadLibraryA('kernel32.dll');
  iat_patcher_createProcessW_add:=DWORD(GetProcAddress(h,'CreateProcessW'));
  iat_patcher_virtualProtectEx_add:=DWORD(GetProcAddress(h,'VirtualProtectEx'));
  getprocaddress_add:=DWORD(GetProcAddress(h,'GetProcAddress'));
  FreeLibrary(h);
end;

//************************************************                              //修改内存属性
function modifyMemory(startAdd,attr,size:dword):Boolean;
var
  retValue:dword;
  hProcess:dword;
  num:dword;
  c:pointer;
begin
  hProcess:=GetCurrentProcess();
  result:=false;
  retValue:=0;
  c:=@num;                                                                      //此处一定要用指针,否则函数执行失败
   asm
      pushad
      mov ecx,c
      push ecx
      mov ebx,attr
      push ebx
      mov ebx,size
      push ebx
      mov ebx,startAdd
      push ebx
      mov ebx,hProcess
      push ebx
      mov ebx,iat_patcher_virtualProtectEx_add
      call ebx
      mov retValue,eax
      popad
   end;
   if(retValue <> 0) then result:=true;
end;
//***********************************************
procedure hook_process_modify();                                                //hook CreateProcess
begin

  getFuncAdd();
  hook_jmp_add:= iat_patcher_createProcessW_add;

  modifyMemory(hook_jmp_add,PAGE_EXECUTE_READWRITE,5);                          //修改内存属性
  modifyMemory(hook_code_add,PAGE_EXECUTE_READWRITE,42);                        //修改内存属性
  modifyMemory(hook_add_string_addr,PAGE_EXECUTE_READWRITE,20);                 //修改内存属性

  asm
    pushad                                                                      //执行内存写入  jmp
     lea eax,hook_array_jmp
     xor ecx,ecx
     mov ebx,hook_jmp_add
     @back:
     mov dl,Byte ptr[eax]
     mov Byte ptr[ebx],dl
     inc eax
     inc ebx
     inc ecx
     cmp ecx,5
     jnz @back
    popad
  end;

  asm
    pushad                                                                      //执行内存写入  code
     lea eax,hook_array_code
     xor ecx,ecx
     mov ebx,hook_code_add
     @back:
     mov dl,Byte ptr[eax]
     mov Byte ptr[ebx],dl
     inc eax
     inc ebx
     inc ecx
     cmp ecx,43
     jnz @back
    popad
  end;

  asm
    pushad                                                                      //执行内存写入 string
     lea eax,hook_array_string
     xor ecx,ecx
     mov ebx,hook_add_string_addr
     @back:
     mov dl,Byte ptr[eax]
     mov Byte ptr[ebx],dl
     inc eax
     inc ebx
     inc ecx
     cmp ecx,20
     jnz @back
    popad
  end;

  asm                                                                           //再次修改函数地址
    mov eax,hook_code_add
    add eax,19
    mov ebx,getprocaddress_add
    mov [eax],ebx

    mov eax,hook_code_add
    add eax,29
    mov ebx,iat_patcher_createProcessW_add
    mov [eax],ebx
  end;
end;
//***********************************************                               //修改 elementclient.exe 文件
function modify_elementclient_file(gm_path,plug_full_path:string):boolean;
var
  f:THANDLE;
  ret:Boolean;
  num,zero:dword;
  ret_offset:dword;
  md5_file_exe:string;
  md5_last_modify:string;
  ini_dll_full_path:string;
begin
  result:=false;
  md5_file_exe:=MD5DigestToStr(MD5File(gm_path));
  md5_last_modify:=thisDllIni.ReadString('elementclient','lastmd5','');
  ini_dll_full_path:= thisDllIni.ReadString('elementclient','dllpath','');
  if(not FileExists(gm_path + '.bak')) then
  begin
    copyfile(PChar(gm_path),PChar(gm_path + '.bak'),false);
  end
  else
  begin
    if(FileExists(work_dirpath + ini_fileName)) then                            //如果配置文件存在
     begin
       if(Trim(md5_last_modify) <> '') then                                     //如果配置文件中 lastmd5 节点不为空
       begin
         if(md5_file_exe <> md5_last_modify) then
          begin
           copyfile(PChar(gm_path),PChar(gm_path + '.bak'),false);
          end;
       end;
     end;
  end;

if(md5_last_modify = '') or (md5_last_modify <> md5_file_exe) or
   (ini_dll_full_path = '') or
   (ini_dll_full_path <> MD5DigestToStr(MD5String((plug_full_path)))) then
begin
 f:=CreateFileA(
                 PChar(gm_path),
                 GENERIC_ALL,
                 0,
                 nil,
                 OPEN_EXISTING,
                 FILE_ATTRIBUTE_NORMAL,
                 0);

   if(f = INVALID_HANDLE_VALUE) then                                            //打开文件失败
   begin
      ShowMessage('插件无法加载,请关闭所有正在运行的游戏再重试');
      Exit;
   end;

  ret_offset:=SetFilePointer(f,element_plugin_full_path_add,nil,FILE_BEGIN);
  if(ret_offset <> element_plugin_full_path_add) then                           //在文件中移动指针失败
  begin
    ShowMessage('移动文件指针失败');
    Exit;
  end;

  //将插件的全路径写入目标程序中
  ret:= writefile(f,dword(Pointer(plug_full_path)^),length(plug_full_path),num,nil);
  if(not ret) then Exit;
  zero:=0;
  writefile(f,zero,1,num,nil);
  //执行 hook 文件的写入 ---- element_jmp_file_hook_add

  ret_offset:=SetFilePointer(f,element_jmp_file_hook_add,nil,FILE_BEGIN);
  if(ret_offset <> element_jmp_file_hook_add) then                           //在文件中移动指针失败
  begin
    ShowMessage('移动文件指针失败');
    Exit;
  end;
  ret:= writefile(f,element_hook_array_jmp,5,num,nil);
  if(not ret) then Exit;


  ret_offset:=SetFilePointer(f,element_code_file_hook_add,nil,FILE_BEGIN);
  if(ret_offset <> element_code_file_hook_add) then                           //在文件中移动指针失败
  begin
    ShowMessage('移动文件指针失败');
    Exit;
  end;
  ret:= writefile(f,element_hook_array_code,23,num,nil);
  if(not ret) then Exit;
 
  CloseHandle(f);
  thisDllIni.WriteString('elementclient','lastmd5',MD5DigestToStr(MD5File(gm_path)));
  thisDllIni.WriteString('elementclient','dllpath',MD5DigestToStr(MD5String(plug_full_path)));
 end;
 result:=True;
end;
//***********************************************                               //导出函数,主要用于修改 elementclient.exe 程序
procedure hook_p();
var
  ret:Boolean;
  dllPath:string;
  plugin_dll_full_path:string;
  position:integer;
  gameClientFullPath:string;
begin
  dllPath:= PChar(plugin_pathAdd_toWrite_inPatcher + $400000);
  if(dllPath = '') then
  begin
   showmessage('获取工作目录路径失败');
   Exit;
  end;

  dllPath:=ExtractFilePath(dllPath);
  if(dllPath = '') then
  begin
    ShowMessage('获取的工作目录参数错误');
    Exit;
  end;

  work_dirpath:=dllPath;
  plugin_dll_full_path:=dllPath + plugin_dll_name;                              //插件的全路径
  current_dirpath:=ExtractFilePath(Application.ExeName);
  position:=Pos('patcher',current_dirpath);                                     //获得上层目录字符串

  elementclient_path:=Copy(current_dirpath,1,position - 1);
  elementclient_path:= elementclient_path + 'element\';                         //拼接路径
  gameClientFullPath:=elementclient_path + elementclient_file_name;             //游戏客户端全路径
  thisDllIni:=TIniFile.Create(work_dirpath + ini_fileName);
  ret:=modify_elementclient_file(gameClientFullPath,plugin_dll_full_path);
  thisDllIni.Free;
end;
//***********************************************
exports
  hook_p;
//***********************************************
begin
  hook_process_modify();
end.
 

 

 

 

unit StartGame;

interface
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls,Registry,shellapi,MD5,IniFiles;
//------------------------------------------------
procedure modify_file();                                                        //修改 patcher 文件

//------------------------------------------------
const hook_array_jmp:array[0..4] of byte = ($E9,$12,$09,$02,$00);               //hook 跳转代码
const hook_array_code:array[0..22] of byte = (                                  //hook 修改的代码
      $60,$68,$00,$41,$46,$00,$FF,$15,$DC,$50,$46,$00,
      $61,$E8,$60,$47,$FE,$FF,$E9,$D7,$F6,$FD,$FF);
var
  thisSoftIni:TIniFile;
  update_class_Name:string = 'ZPerfectWorldPatcherClass';
  patcher_path:string;                                                          //game patcher.exe 文件的全路径
  gm_path_registry:string = 'Software\PWRD\zhuxian2';                           //在注册表中保存游戏路径的键
  gm_registry_key:string = 'launcher';                                          //键值
  plugin_name:string = 'patcher.dll';                                           //外部动态库插件名称
  hook_add_jmp_file_offset:DWORD = $4371E;                                      //hook修改文件偏移
  hook_add_jmp_code_offset:DWORD = $64035;                                      //hook修改文件偏移
  plugin_pathAdd_toWrite_inPatcher:dword = $64100;                              //pathcer 文件偏移,用于写入插件的工作目录
  this_soft_ini_fileName:string = 'config.ini';                                 //本软件的配置文件名称
implementation
uses Unit1;

//**********  从注册表中读取游戏路径   **********
function getGamePathFromRegistry():string;
var
  ARegistry : TRegistry;
begin
  ARegistry := TRegistry.Create;
  result:='';
   with ARegistry do
   begin
     RootKey:=HKEY_CURRENT_USER;
      if OpenKey(gm_path_registry,false) then
      begin
        result:=ReadString(gm_registry_key);
      end;
      CloseKey;
      Destroy;
   end;
end;

//**************   修改文件 ***********************
procedure modify_patcher();
var
  plugin_dir:string;
  str_to_write:string;
  f:THANDLE;
  ret_offset:dword;
  ret:boolean;
  num:dword;
  zero:Byte;
  md5_file_exe,md5_file_exe_bak:string;
  md5_last_modify:string;
  ini_dll_full_path:string;
begin
   patcher_path:= Trim(getGamePathFromRegistry());
   if(patcher_path = '') then Exit;
   //备份 patcher 文件
   if(not FileExists(PChar(patcher_path+'.bak'))) then
   begin
    copyfile(PChar(patcher_path),PChar(patcher_path + '.bak'),false);
   end
   else
   begin
     if(FileExists(this_soft_ini_fileName)) then                                //如果配置文件存在
     begin
       md5_last_modify:=thisSoftIni.ReadString('patcher','lastmd5','');
       if(Trim(md5_last_modify) <> '') then                                     //如果配置文件中lastmd5节点不为空
       begin
         md5_file_exe:=MD5DigestToStr(MD5File(patcher_path));
         if(md5_file_exe <> md5_last_modify) then
          begin
           copyfile(PChar(patcher_path),PChar(patcher_path + '.bak'),false);
          end;
       end;
      
     end;
   end;
   plugin_dir:= ExtractFilePath(Application.ExeName);
   str_to_write:= plugin_dir + plugin_name;                                     //需要加载 dll 的全路径

   ini_dll_full_path:=thisSoftIni.ReadString('patcher','dllpath','');

   if(ini_dll_full_path = '') or (ini_dll_full_path <> MD5DigestToStr(MD5File(str_to_write))) then
   begin
   f:=CreateFileA(
                 PChar(patcher_path),
                 GENERIC_ALL,
                 0,
                 nil,
                 OPEN_EXISTING,
                 FILE_ATTRIBUTE_NORMAL,
                 0);

   if(f = INVALID_HANDLE_VALUE) then Exit;                                      //打开文件失败

   ret_offset:=SetFilePointer(f,plugin_pathAdd_toWrite_inPatcher,nil,FILE_BEGIN);
   if(ret_offset <> plugin_pathAdd_toWrite_inPatcher) then Exit;                //在文件中移动指针失败

   ret:= writefile(f,dword(Pointer(str_to_write)^),length(str_to_write),num,nil);
   if(not ret) then Exit;                                                       //写入外部dll路径失败
   zero:=0;
   writefile(f,zero,1,num,nil);
   thisSoftIni.WriteString('patcher','dllpath',MD5DigestToStr(MD5File(str_to_write)));                    //写入配置文件
   CloseHandle(f);
   end;
end;

//*****  修改代码,让其主动加载外部 dll  **********
function modify_code_for_load_dll():boolean;
var
  f:THANDLE;
  ret:boolean;
  last_md5_str:string;
  ret_offset,num:dword;
begin
   result:=false;
   last_md5_str:=thisSoftIni.ReadString('patcher','lastmd5','');
   if (last_md5_str = '') or (MD5DigestToStr(MD5File(patcher_path)) <> last_md5_str) then
   begin
   f:=CreateFileA(
                 PChar(patcher_path),
                 GENERIC_ALL,
                 0,
                 nil,
                 OPEN_EXISTING,
                 FILE_ATTRIBUTE_NORMAL,
                 0);
                
  if(f = INVALID_HANDLE_VALUE) then Exit;                                      //打开文件失败
  ret_offset:=SetFilePointer(f,hook_add_jmp_file_offset,nil,FILE_BEGIN);
  if(ret_offset <> hook_add_jmp_file_offset) then Exit;                        //在文件中移动指针失败
  ret:= writefile(f,hook_array_jmp,5,num,0);
 
   ret_offset:=SetFilePointer(f,hook_add_jmp_code_offset,nil,FILE_BEGIN);
  if(ret_offset <> hook_add_jmp_code_offset) then Exit;                        //在文件中移动指针失败
  ret:= writefile(f,hook_array_code,23,num,0);
  CloseHandle(f);
  thisSoftIni.WriteString('patcher','lastmd5',MD5DigestToStr(MD5File(patcher_path)));
  end;
  Result:=True;
end;
//*************************************************                             //修改文件入口函数
procedure modify_file();
var
  h:hwnd;
begin
  h:=FindWindow(PChar(update_class_Name),nil);
  if(h <> 0) then
  begin
    ShowMessage('你已经运行了一个更新程序');
    Exit;
  end;
 thisSoftIni:= TIniFile.Create(ExtractFilePath(Application.ExeName)+this_soft_ini_fileName);
 modify_patcher();
 modify_code_for_load_dll();
 thisSoftIni.Free;
end;

//**************************************************
end.

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值