用Delphi在2000和XP/2003下从Ring3进入Ring0的无驱动解决方法

该博客介绍了使用Delphi实现Ring0函数调用及虚拟地址到物理地址转换的方法。需要JEDI Win32 API库支持,定义了多个常量和类型,实现了如设置调试权限、打开物理内存等多个函数,并给出读取CMOS时钟和调用NTOSKrnl.exe中Ring0函数的使用示例。

by LYSoft LiuYang 


 

需要JEDI Win32 APIJWA)库支持

 

uses

  Windows, Dialogs, SysUtils, NTDDK,

  JwaWinNT, JwaWinType, JwaNtStatus, JwaAccCtrl, JwaAclApi, ntdll;

 

const

  KGDT_NULL     = 0;

  KGDT_R0_CODE  = 8;

  KGDT_R0_DATA  = 16;

  KGDT_R3_CODE  = 24;

  KGDT_R3_DATA  = 32;

  KGDT_TSS      = 40;

  KGDT_R0_PCR   = 48;

  KGDT_R3_TEB   = 56;

  KGDT_VDM_TILE = 64;

  KGDT_LDT      = 72;

  KGDT_DF_TSS   = 80;

  KGDT_NMI_TSS  = 88;

 

type

  TGDT = record

    Limit,

    BaseLow,

    BaseHigh : Word;

  end;

 

  PHYSICAL_ADDRESS = Large_Integer;

  CALLGATE_DESCRIPTOR = record

    Offset_0_15, Selector: Word;

    GateDescriptor:Word;

    Offset_16_31: Word;

  end;

 

implementation

 

function ZwOpenSection; external 'ntdll.dll';

function ZwClose; external 'ntdll.dll';

 

function SetDebugPrivilege(CanDebug: boolean): Boolean;

 

  function EnablePrivilege(hToken: Cardinal; PrivName: string; bEnable: Boolean): Boolean;

  var

    TP: Windows.TOKEN_PRIVILEGES;

    Dummy: Cardinal;

  begin

    TP.PrivilegeCount := 1;

    LookupPrivilegeValue(nil, pchar(PrivName), TP.Privileges[0].Luid);

    if bEnable then

      TP.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED

    else TP.Privileges[0].Attributes := 0;

    AdjustTokenPrivileges(hToken, False, TP, SizeOf(TP), nil, Dummy);

    Result := GetLastError = ERROR_SUCCESS;

  end;

var

  hToken: Cardinal;

begin

  OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES, hToken);

  Result := EnablePrivilege(hToken, SE_DEBUG_NAME, CanDebug);

  CloseHandle(hToken);

end;

 

function SetPhyscialMemorySectionCanBeWrited(hSection: THandle): boolean;

label CleanUp;

var

  pDacl, pNewDacl: JwaWinNT.PACL;

  pSD: JwaWinNT.PSECURITY_DESCRIPTOR;

  dwRes: DWORD;

  ea: EXPLICIT_ACCESS;

begin

  Result := false;

  pDacl := nil; pNewDacl := nil; pSD := nil;

  dwRes := GetSecurityInfo(hSection, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION,

    nil, nil, @pDacl, nil, pSD);

  if dwRes <> ERROR_SUCCESS then

    begin

      MessageDlg(Format('GetSecurityInfo Error %d', [dwRes]), mtError, [mbOK], 0);

      goto CleanUp;

    end;

  ZeroMemory(@ea, sizeof(EXPLICIT_ACCESS));

  ea.grfAccessPermissions := SECTION_MAP_WRITE;

  ea.grfAccessMode := GRANT_ACCESS;

  ea.grfInheritance := NO_INHERITANCE;

  ea.Trustee.TrusteeForm := TRUSTEE_IS_NAME;

  ea.Trustee.TrusteeType := TRUSTEE_IS_USER;

  ea.Trustee.ptstrName := 'CURRENT_USER';

  dwRes := SetEntriesInAcl(1, @ea, pDacl, pNewDacl);

  if dwRes <> ERROR_SUCCESS then

     begin

       MessageDlg(Format('SetEntriesInAcl Error : %d', [dwRes]), mtError, [mbOK], 0);

       goto CleanUp;

     end;

  dwRes := SetSecurityInfo(hSection, SE_KERNEL_OBJECT,

    DACL_SECURITY_INFORMATION, nil, nil, pNewDacl, nil);

  if dwRes <> ERROR_SUCCESS then

     begin

       MessageDlg(Format('SetSecurityInfo Error : %d', [dwRes]), mtError, [mbOK], 0);

       goto CleanUp;

     end;

  Result := true;

  CleanUp:

  if pSD<>nil then LocalFree(Cardinal(pSD));

  if pNewDacl<>nil then LocalFree(Cardinal(pNewDacl));

end;

 

function OpenPhysicalMemory: THandle;

var

  hSection : THandle;

  status: NTSTATUS;

  objName: UNICODE_STRING;

  objectAttributes: OBJECT_ATTRIBUTES;

begin

  Result := 0;

  RtlInitUnicodeString(@objName, '/Device/PhysicalMemory');

  InitializeObjectAttributes(@objectAttributes, @objName,

    OBJ_CASE_INSENSITIVE or OBJ_KERNEL_HANDLE, 0, nil);

  status := ZwOpenSection(hSection, SECTION_MAP_READ or SECTION_MAP_WRITE, @objectAttributes);

  if (status = STATUS_ACCESS_DENIED) then

     begin

       status := ZwOpenSection(hSection, READ_CONTROL or WRITE_DAC, @objectAttributes);

       if status = STATUS_SUCCESS then  SetPhyscialMemorySectionCanBeWrited(hSection);

       ZwClose(hSection);

       status := ZwOpenSection(hSection, SECTION_MAP_READ or SECTION_MAP_WRITE, @objectAttributes);

     end;

  if status = STATUS_SUCCESS then Result :=hSection;

end;

 

procedure ClosePhysicalMemory(hPhysicalMemorySection: THandle);

begin

  ZwClose(hPhysicalMemorySection);

end;

 

function AddressIn4MBPage(Address: ULONG): Boolean;

begin

  Result := (Address > 0) and ($80000000<=Address) and (Address<$A0000000)

end;

 

function MiniMmGetPhysicalAddress(vAddress: ULONG): ULONG;

begin

  if AddressIn4MBPage(vAddress)

     then Result := vAddress - $80000000

     else Result := $FFFFFFFF;

end;

 

function MiniMmGetPhysicalPageAddress(VirtualAddress: ULONG): ULONG;

begin

  if AddressIn4MBPage(VirtualAddress)

     then Result := VirtualAddress and $1FFFF000

     else Result := $FFFFFFFF;

end;

 

function ExecRing0Proc(ProcEntryPoint: Pointer; SegmentLength: ULONG): boolean;

var

  GDT : TGDT; mapAddr: ULONG;

  hSection : THandle;

  cg: ^CALLGATE_DESCRIPTOR;

  farcall : array [0..2] of Word;

  BaseAddress: Pointer;

  setcg: boolean;

  i: Cardinal;

begin

  Result := false;

  asm SGDT GDT end;

  i := (gdt.BaseHigh shl 16) or gdt.BaseLow;

  mapAddr := MiniMmGetPhysicalPageAddress(i);

  if mapAddr=$FFFFFFFF then

     begin

       MessageDlg(Format('Can not convert GDT virtual address of [Base = %s  Limit = %s]',

         [IntToHex(i, 8), IntToHex(GDT.Limit, 4)]), mtError, [mbOK], 0);

       Exit;

     end;

  hSection := OpenPhysicalMemory;

  if hSection=0 then

     begin

       MessageDlg('Error in open physical memory.', mtError, [mbOK], 0);

       Exit;

     end;

  BaseAddress := MapViewOfFile(hSection, FILE_MAP_READ or FILE_MAP_WRITE, 0, mapAddr,    //low part

                     (gdt.Limit+1));

  if BaseAddress = nil then

     begin

       ZwClose(hSection);

       MessageDlg(Format('MapViewOfFile Error : %s%sGDT : Address = %s   Limit = %s',

         [SysErrorMessage(GetLastError), #13#10, IntToHex(mapAddr, 8), IntToHex(GDT.Limit, 4)]), mtError, [mbOK], 0);

       Exit;

     end;

  setcg := false;

  i := Cardinal(BaseAddress)+8;  // skip first empty entry

  while i < Cardinal(BaseAddress)+(gdt.Limit and $FFF8) do

    begin

      cg:=Ptr(i);

      with cg^ do

        begin

          if IntToHex(GateDescriptor, 4)[2] = '0' then  // call gate not present

             begin   // install callgate

               Offset_0_15 := LOWORD(Integer(ProcEntryPoint));

               Selector := KGDT_R0_CODE; // ring 0 code

               // [Installed flag=1] [Ring 3 code can call=11] 0 [386 call gate=1100] 00000000

               GateDescriptor := $EC00;

               Offset_16_31 := HIWORD(Integer(ProcEntryPoint));

               setcg := TRUE;

               Break;

             end;

        end;

      Inc(i, 8);

    end;

  if not setcg then

     begin

       UnMapViewOfFile(BaseAddress);

       ZwClose(hSection);

       MessageDlg('Can not install CallGate in your system GDT', mtError, [mbOK], 0);

       Exit;

     end;

  farcall[0] := 0;  farcall[1] := 0;

  farcall[2] := (short(ULONG(cg)-ULONG(BaseAddress))) or 3;  //Ring 3 callgate;

  if not VirtualLock(ProcEntryPoint, SegmentLength) then

     begin

       MessageDlg(SysErrorMessage(GetLastError), mtError, [mbOK], 0);

       Exit;

     end;

  try

    SetThreadPriority(GetCurrentThread, THREAD_PRIORITY_TIME_CRITICAL);

    Sleep(0);

    asm  // call callgate

      //  push arg1 ... argN  // call far fword ptr [farcall]

      LEA EAX, farcall  // load to EAX

      DB 0FFH, 018H  // hardware code, means call fword ptr [eax]

    end;

    SetThreadPriority(GetCurrentThread, THREAD_PRIORITY_NORMAL);

    Result := true;

  except

    on e: Exception do MessageDlg(e.Message, mtError, [mbOK], 0);

  end;

  VirtualUnlock(ProcEntryPoint, SegmentLength);

  // Clear callgate

  FillChar(cg^, 8, 0);

  UnMapViewOfFile(BaseAddress);

  ClosePhysicalMemory(hSection);

end;

 

使用示例,读取CMOS时钟:

unit NTRing0_Unit;

 

interface

 

uses

  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

  Dialogs, StdCtrls, Buttons;

 

type

  TForm1 = class(TForm)

    BitBtn1: TBitBtn;

    procedure BitBtn1Click(Sender: TObject);

  private

    { Private declarations }

  public

    { Public declarations }

  end;

 

var

  Form1: TForm1;

  tHour, tMin, tSec: byte;

 

implementation

 

{$R *.dfm}

 

uses NTRing0;

 

procedure Ring0Proc; stdcall;

begin

  asm            // ring0 prolog

    PUSHAD // push eax,ecx,edx,ebx,ebp,esp,esi,edi onto the stack

    PUSHFD // decrement stack pointer by 4 and push EFLAGS onto the stack

    CLI    // disable interrupt

    // execute your ring0 code here ...

    MOV AH,0

    MOV DX,$70

    MOV AL ,AH

    OUT DX , AL

    INC DX

    IN AL,DX

    MOV tSec, AL

    //

    MOV AH,2

    MOV DX,$70

    MOV AL ,AH

    OUT DX , AL

    INC DX

    IN AL,DX

    MOV tMin, AL

    //

    MOV AH,4

    MOV DX,$70

    MOV AL ,AH

    OUT DX , AL

    INC DX

    IN AL,DX

    MOV tHour, AL

    // ring0 epilog

    POPFD // restore registers pushed by pushfd

    POPAD // restore registers pushed by pushad

    RETF  // you may retf <sizeof arguments> if you pass arguments

  end;

end;

 

procedure TForm1.BitBtn1Click(Sender: TObject);

begin

  //  execute ring 0

  if ExecRing0Proc(@Ring0Proc, 100) then

  ShowMessage(Format('CMOS Time is %d:%d:%d',

    [10*(tHour shr 4) + tHour and $F,

     10*(tMin shr 4) + tMin and $F,

     10*(tSec shr 4) + tSec and $F]));

end;

 

end.

调用NTOSKrnl.exe中的Ring0函数实现VA->PA(虚拟地址到物理地址)的转换
type
  TMemoryAddress = record
    PhysicalAddress : PHYSICAL_ADDRESS;  //*000
    VirtualAddress : DWord;  //*008
  end;

var
  MemoryAddress : TMemoryAddress;
  _MmGetPhysicalAddress : Cardinal;

  NTOSBaseAddr : Cardinal;
// NTOSkern.exe的加载地址,2003系统默认是$804DE000

procedure Ring0Func; stdcall;
begin
  asm
    pushad
    pushf
    cli

    mov esi, MemoryAddress.VirtualAddress
    push esi
    call _MmGetPhysicalAddress
    mov MemoryAddress.PhysicalAddress.LowPart, eax  // save low part of LARGE_INTEGER
    mov MemoryAddress.PhysicalAddress.HighPart, edx       // save high part of LARGE_INTEGER

    popf
    popad
    retf
  end;
end;


procedure MmGetPhysicalAddress;
var hNTDll: THandle;
begin
  _MmGetPhysicalAddress := 0;
  hNTDll := LoadLibrary('ntoskrnl.exe');
  if hNTDll <> 0 then
     begin
       _MmGetPhysicalAddress := NTOSBaseAddr + Cardinal(GetProcAddress(hNTDll, 'MmGetPhysicalAddress')) - hNTDll;
       FreeLibrary(hNTDll);
//       ShowMessage(Format('Virtual address of MmGetPhysicalAddress in Kernel Mode  : %s', [IntToHex(_MmGetPhysicalAddress, 8)]));
     end;
  if _MmGetPhysicalAddress > 0 then ExecRing0Proc(@Ring0Func, 32);
end;

......
MemoryAddress.VirtualAddress := StrToInt64Def(Edit1.Text, $806AB000);
  MmGetPhysicalAddress;
  Memo1.Lines.Add(Format('(Ring 0 Mode) Virtual address : $%s  = Physical address : $%s',
    [IntToHex(MemoryAddress.VirtualAddress, 8),
     IntToHex(MemoryAddress.PhysicalAddress.LowPart, 8)]));

powered by LYSoft LiuYang
http://lysoft.7u7.net

program Project1; //{$APPTYPE CONSOLE} uses windows, SysUtils, tlhelp32, accctrl, aclapi; procedure SetPrivilege; var OldTokenPrivileges, TokenPrivileges: TTokenPrivileges; ReturnLength: dword; hToken: THandle; Luid: int64; begin OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES, hToken); LookupPrivilegeValue(nil, &#39;SeDebugPrivilege&#39;, Luid); TokenPrivileges.Privileges[0].luid := Luid; TokenPrivileges.PrivilegeCount := 1; TokenPrivileges.Privileges[0].Attributes := 0; AdjustTokenPrivileges(hToken, False, TokenPrivileges, SizeOf(TTokenPrivileges), OldTokenPrivileges, ReturnLength); OldTokenPrivileges.Privileges[0].luid := Luid; OldTokenPrivileges.PrivilegeCount := 1; OldTokenPrivileges.Privileges[0].Attributes := TokenPrivileges.Privileges[0].Attributes or SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, False, OldTokenPrivileges, ReturnLength, PTokenPrivileges(nil)^, ReturnLength); end; function GetProcessID(EXE_Name: PChar): THandle; var s: string; ok: Bool; ProcessListHandle: THandle; ProcessStruct: TProcessEntry32; begin Result := 0; //获得进程列表句柄 ProcessListHandle := CreateToolHelp32Snapshot(TH32CS_SNAPPROCESS, 0); try ProcessStruct.dwSize := SizeOf(ProcessStruct); //获得第一个进程句柄 ok := Process32First(ProcessListHandle, ProcessStruct); while ok do begin s := ExtractFileName(ProcessStruct.szExeFile);//获取进程的可执行文件名称 if AnsiCompareText(Trim(s), EXE_Name)=0 then//如果是HL程序名,表示找到游戏进程。 begin Result := ProcessStruct.th32ProcessID;//保留游戏进程句柄 break; end; ok := Process32Next(ProcessListHandle, ProcessStruct);//获取下一个进程信息。 end; finally CloseHandle(ProcessListHandle);//关闭进程列表句柄 end; end; ///////////////////////////////////////////////////////////////// Function CreateSystemProcess(szProcessName: LPTSTR): BOOL; Var hProcess: THANDLE; hToken, hNewToken: THANDLE; dwPid: DWORD; pOldDAcl: PACL; pNewDAcl: PACL; bDAcl: BOOL; bDefDAcl: BOOL; dwRet: DWORD; pSacl: PACL; pSidOwner: PSID; pSidPrimary: PSID; dwAclSize: DWORD; dwSaclSize: DWORD; dwSidOwnLen: DWORD; dwSidPrimLen: DWORD; dwSDLen: DWORD; ea: EXPLICIT_ACCESS; pOrigSd: PSECURITY_DESCRIPTOR; pNewSd: PSECURITY_DESCRIPTOR; si: STARTUPINFO; pi: PROCESS_INFORMATION; bError: BOOL; Label Cleanup; begin pOldDAcl:= nil; pNewDAcl:= nil; pSacl:= nil; pSidOwner:= nil; pSidPrimary:= nil; dwAclSize:= 0; dwSaclSize:= 0; dwSidOwnLen:= 0; dwSidPrimLen:= 0; pOrigSd:= nil; pNewSd:= nil; SetPrivilege; // 选择 WINLOGON 进程 dwPid := GetProcessId(&#39;WINLOGON.EXE&#39;); If dwPid = High(Cardinal) Then begin bError := TRUE; Goto Cleanup; end; hProcess := OpenProcess(PROCESS_QUERY_INFORMATION,FALSE,dwPid); If hProcess = 0 Then begin bError := TRUE; Goto Cleanup; end; If not OpenProcessToken(hProcess,READ_CONTROL or WRITE_DAC,hToken) Then begin bError := TRUE; Goto Cleanup; end; // 设置 ACE 具有所有访问权限 ZeroMemory(@ea, Sizeof(EXPLICIT_ACCESS)); BuildExplicitAccessWithName(@ea, &#39;Everyone&#39;, TOKEN_ALL_ACCESS, GRANT_ACCESS, 0); If not GetKernelObjectSecurity(hToken, DACL_SECURITY_INFORMATION, pOrigSd, 0, dwSDLen) Then begin {第一次调用给出的参数肯定返回这个错误,这样做的目的是 为了得到原安全描述符 pOrigSd 的长度} // HEAP_ZERO_MEMORY = 8;HEAP_GENERATE_EXCEPTIONS = &H4 If GetLastError = ERROR_INSUFFICIENT_BUFFER Then begin pOrigSd := HeapAlloc(GetProcessHeap(), $00000008, dwSDLen); If pOrigSd = nil Then begin bError := TRUE; Goto Cleanup; end; // 再次调用才正确得到安全描述符 pOrigSd If not GetKernelObjectSecurity(hToken, DACL_SECURITY_INFORMATION, pOrigSd, dwSDLen, dwSDLen) Then begin bError := TRUE; Goto Cleanup; end; end Else begin bError := TRUE; Goto Cleanup; end; end;//GetKernelObjectSecurity() // 得到原安全描述符的访问控制列表 ACL If not GetSecurityDescriptorDacl(pOrigSd,bDAcl,pOldDAcl,bDefDAcl) Then begin bError := TRUE; goto Cleanup; end; // 生成新 ACE 权限的访问控制列表 ACL dwRet := SetEntriesInAcl(1,@ea,pOldDAcl,pNewDAcl); If dwRet ERROR_SUCCESS Then begin pNewDAcl := nil; bError := TRUE; goto Cleanup; end; If not MakeAbsoluteSD(pOrigSd, pNewSd, dwSDLen, pOldDAcl^, dwAclSize, pSacl^, dwSaclSize, pSidOwner, dwSidOwnLen, pSidPrimary, dwSidPrimLen) Then begin {第一次调用给出的参数肯定返回这个错误,这样做的目的是 为了创建新的安全描述符 pNewSd 而得到各项的长度} If GetLastError = ERROR_INSUFFICIENT_BUFFER Then begin pOldDAcl := HeapAlloc(GetProcessHeap(), $00000008, dwAclSize); pSacl := HeapAlloc(GetProcessHeap(), $00000008, dwSaclSize); pSidOwner := HeapAlloc(GetProcessHeap(), $00000008, dwSidOwnLen); pSidPrimary := HeapAlloc(GetProcessHeap(), $00000008, dwSidPrimLen); pNewSd := HeapAlloc(GetProcessHeap(), $00000008, dwSDLen); If (pOldDAcl = nil) or (pSacl = nil) or (pSidOwner = nil) or (pSidPrimary = nil) or (pNewSd = nil) Then begin bError := TRUE; goto Cleanup; end; {再次调用才可以成功创建新的安全描述符 pNewSd 但新的安全描述符仍然是原访问控制列表 ACL} If not MakeAbsoluteSD(pOrigSd, pNewSd, dwSDLen, pOldDAcl^, dwAclSize, pSacl^, dwSaclSize, pSidOwner, dwSidOwnLen, pSidPrimary, dwSidPrimLen) Then begin bError := TRUE; goto Cleanup; end; end Else begin bError := TRUE; goto Cleanup; end; end; {将具有所有访问权限的访问控制列表 pNewDAcl 加入到新的 安全描述符 pNewSd 中} If not SetSecurityDescriptorDacl(pNewSd,bDAcl,pNewDAcl,bDefDAcl) Then begin bError := TRUE; goto Cleanup; end; // 将新的安全描述符加到 TOKEN 中 If not SetKernelObjectSecurity(hToken,DACL_SECURITY_INFORMATION,pNewSd) Then begin bError := TRUE; goto Cleanup; end; // 再次打开 WINLOGON 进程的 TOKEN,这时已经具有所有访问权限 If not OpenProcessToken(hProcess,TOKEN_ALL_ACCESS,hToken) Then begin bError := TRUE; goto Cleanup; end; // 复制一份具有相同访问权限的 TOKEN If not DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, nil, SecurityImpersonation, TokenPrimary, hNewToken) Then begin bError := TRUE; goto Cleanup; end; ZeroMemory(@si,Sizeof(STARTUPINFO)); si.cb := Sizeof(STARTUPINFO); {不虚拟登陆用户的话,创建新进程会提示 1314 客户没有所需的特权错误} ImpersonateLoggedOnUser(hNewToken); {我们仅仅是需要建立高权限进程,不用切换用户 所以也无需设置相关桌面,有了新 TOKEN 足够} // 利用具有所有权限的 TOKEN,创建高权限进程 If not CreateProcessAsUser(hNewToken, nil, szProcessName, nil, nil, FALSE, 0, nil, nil, si, pi) Then begin bError := TRUE; goto Cleanup; end; bError := FALSE; Cleanup: If pOrigSd = nil Then HeapFree(GetProcessHeap(),0,pOrigSd); If pNewSd = nil Then HeapFree(GetProcessHeap(),0,pNewSd); If pSidPrimary = nil Then HeapFree(GetProcessHeap(),0,pSidPrimary); If pSidOwner = nil Then HeapFree(GetProcessHeap(),0,pSidOwner); If pSacl = nil Then HeapFree(GetProcessHeap(),0,pSacl); If pOldDAcl = nil Then HeapFree(GetProcessHeap(),0,pOldDAcl); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); CloseHandle(hToken); CloseHandle(hNewToken); //CloseHandle(hProcess); If bError Then Result := FALSE Else Result := True; end; begin CreateSystemProcess(&#39;test.exe&#39;); { TODO -oUser -cConsole Main : Insert code here } end.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值