GUI-Based RunAsEx


Following table is a mapping Token-Elements and how to obtain them:

NameAPI to CallFunction Name in RunAsEx Source <Filename.cpp>
User SIDLookupAccountNamePSID Name2SID(LPCTSTR pszUserName, LPCTSTR pszDomainName) <CoreCode.CPP>
Group SIDsAllocateAndInitializeSid

NetUserGetLocalGroups

NetUserGetGroups

LookupAccountName

PTOKEN_GROUPS CreateTokenGroups(SID_AND_ATTRIBUTES* lpPSIDGroupsAttr)

PSID* QueryLocalGroupSIDs(LPCTSTR pszUserName)

PSID* QueryNetGroupSIDs(LPCTSTR pszUserName, LPCTSTR pszDomainName)

PSID GetEveryoneSID()

PSID GetAuthenticatedUsersSID()

PSID GetInteractiveSID()

...

<CoreCode.CPP>

Logon SID (inside Group SIDs)No Way To Create From Scratch

(but may be able to steal from elsewhere)

always in format S-1-5-5-0-XXXX

SYSTEM process (winlogon, lsass, etc...) does not have this

User processes and other system-level processes (such as svchost running under local_service and network_service) have this member.

A anonymous token token (with authenticationID = 998) does not have this member. Check

http://www.develop.com/.../whatis_alogonsession.html
PrivilegesLookupPrivilegeValuePTOKEN_PRIVILEGES CreateTokenPriv(DWORD& dwPrivGranted, LPCTSTR* lpszPriv, BOOL bGrantEnableAll) <CoreCode.CPP>
Token OwnerLookupAccountNamePSID Name2SID(LPCTSTR pszUserName, LPCTSTR pszDomainName) <CoreCode.CPP>
Token Primary GroupAllocateAndInitializeSidSame as Group SIDs
Token Default DACLAllocateACE, InitializeAcl, AddAce...Importable from caller; RunAsEx uses NULL DACL by default
Token Source An 8-char string; RunAsEx uses "RunAsEx+"
LUID TokenId, LARGE_INTEGER ExpirationTime, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, LUID ModifiedIdAllocateLocallyUniqueIdPTOKEN_STATISTICS CreateTokenStatistics( LUID* lpTokenId, //Optional LUID* lpAuthenticationId, //Optional LARGE_INTEGER* lpExpirationTime, //Optional TOKEN_TYPE* lpTokenType, //Primary SECURITY_IMPERSONATION_LEVEL* lpImpersonationLevel, //Only When ImpersonteToken DWORD* lpDynamicCharged, //Optional DWORD* lpDynamicAvailable, //Optional DWORD* lpGroupCount, //Mandatory DWORD* lpPrivilegeCount, //Mandatory LUID* lpModifiedId //Optional

<CoreCode.CPP>

Note: In the real world, all LUID members here set 0 as HighPart.

RunAsEx will assign random values to these members because they are not critically important.

TOKEN_TYPE TokenType N/A Only Meaningful When Impersonate Token
DWORD DynamicCharged Always 500
DWORD DynamicAvailable Undecided; 420 by default
DWORD GroupCount Checks count of Group SIDs
DWORD PrivilegeCount Checks count of Privileges
LUID AuthenticationIdThe first 1000 LUIDs are reserved (0x3E7 = 999). SYSTEM_LUID { 0x3E7, 0x0 } ANONYMOUS_LOGON_LUID { 0x3e6, 0x0 } LOCALSERVICE_LUID { 0x3e5, 0x0 } NETWORKSERVICE_LUID { 0x3e4, 0x0 }Caller pass. * Check RunAsEx desktop combo box and you will find Service-0x0-3e7$, Service-0x0-3e5$, and Service-0x0-3e4$. These are service logon sessions that get their names from this authenticate ID.

 

Note: This is different from Logon SID S-1-5-5-0-XXXX. I mean AuthenticationId.LowPart != XXXX.

It is the Logon Session LUID. Use the ZTokenMan check system, and you will see its authentication ID is 999. It is just the Logon Session associated with WinStat Service-0x0-3e7$. Use PrccessExplorer to check it!

All Tokens I met AuthenticationId.HighPart = 0.

RunAsEx uses 999 as the default when the caller does not pass into.

This member is closely related to the WinStat name.

Note: AuthenticationId is closely related to the WinStat Name when you pass the token to CreateProcessAsUser with an Empty Desktop Name "". Take this as an example: Configure a NT Service running as a user (instead of the default system); it should be put into a WinStation called "Service-0x0-XXX___FCKpd___0quot;. Go to ZTokenMan and have a look at its authenticationId; it should be the same value.
Note: The word Session is overused indeed. When I mention Logon Session SID, it is a SID embedded in Token. When I say Logon Session LUID, it is a LUID (64bits) indentifying a logon session and shared, usually, by some processes. When I Session ID, when in a WTS environment, it is a 0-based integer showing the current connection's order.

5. Implementation Explanation

Now, let's review what we need to do to RunAs and the corresponding API:

  1. Check If Caller is from Local Admin Group Member—SetTokenInformation, Enumerate Group SID to see if S-1-5-32-544 (alias S-1-5-32-544) there or not
  2. If need a user profile, load userenv.dll now—LoadLibrary, GetProcAddress
  3. Enable All Privileges needed—LookupPrivilegeValue, OpenProcessToken, AdjustTokenPrivileges
  4. Query Current Desktop Name—GetUserObjectInformation
  5. If the Target Desktop in Other WinStat, Yes—SetProcessWinStat (after closing All Windows, Hooks of Current Process)
  6. If the Desktop is non-existing—OpenWindowStation, CreateWindowStation, CreateDesktop
  7. If the user wants to use LogonUser, call it to get Token—LogonUser
  8. If the user has no password, Zw—ZwCreateToken
  9. If you want to launch in another session, change session ID—SetTokenInformation
  10. If the user wants to shoot in NT Service—CreateService, OpenSCManager...
  11. User Token Group SIDs to modify target desktop DACL, get rid of all deny ACE, add positive ACE—AllocateACE, InitializeAcl, AddAce...
  12. CreateProcessAsUser it
  13. If RunAs inside NT Service—DeleteService, ...

Although listing all the code (>3000 lines, compact with reasonable comments) seems daunting, I give the key code here so you can get something instantly without downloading, decompressing, and opening. Please note, you must handle exceptions if something wrong happened, especially in your NT service handler; otherwise, you will be stuck miserably (you can kill process, and modify the Registry manually if you like).

5.1. RunAsUser Function (showing handling of WinStat/Desktop affairs)
BOOL RunAsUser(
  LPTSTR pszEXE, LPTSTR pszCmdLine, //in, Target Program and Command
                                    //Line
  LPTSTR pszDomainName, LPTSTR pszUserName, LPTSTR pszPassword, //in
  LPTSTR pszDesktop, //in, Must Not be NULL, can be "NULL", "EMPTY",
                     //"WinStat/Desktop"
  BOOL bCreateTokenDirectly, //in, True: ZwCreateToken;
                             //FALSE: LogonUser
  DWORD dwSession,   //in, -1: No WTS; n (0-based): Target SessionID
  BOOL bLoadProfile, //in, TRUE: Load User Profile
  BOOL bCopyTokenPropFromCaller, //in, TRUE: All Token Information
                                 //Use Caller Process's
  BOOL bKeepPriv, //in, TRUE: Use Only Privileges User Holding;
                  //FALSE: Set All Privileges
  DWORD dwLogonType, //in, Same As LogonUser
  DWORD dwLogonProvider //in, Reserved
)
{
  //LocalSystem no profile
  if(pszUserName == NULL && bLoadProfile) return FALSE;

  TCHAR szSrcWinStat[MAX_PATH];
  TCHAR szSrcDesktop[MAX_PATH];
  TCHAR szWinStat[MAX_PATH];
  TCHAR szDesktop[MAX_PATH];

  HWINSTA hSrcWinStat = ::GetProcessWindowStation();
  HDESK   hSrcDesktop = ::GetThreadDesktop(::GetCurrentThreadId());
  DWORD dwFakeLen;

  //Get Target User Token --> his.her SID
  HANDLE hToken   = NULL;

  BOOL   fProcess = FALSE;
  BOOL   fSuccess = FALSE;
  PROCESS_INFORMATION pi = {NULL, NULL, 0, 0};
  STARTUPINFO si;

  PSECURITY_DESCRIPTOR pSD = NULL;
  BOOL bRet = FALSE;

  //save current win station and desktop to make return journey
  //smoothly if ...
  HWINSTA hwinstaOld = NULL;
  HDESK hdeskOld     = NULL;
  HWINSTA hwinstaNew = NULL;
  HDESK hdeskNew     = NULL;

  BOOL  bWinStatCreated = FALSE;
  BOOL  bDeskCreated    = FALSE;

  BOOL bSameWinStat = FALSE;
  BOOL bSameDesktop = FALSE;

  HANDLE hTokenSelf = NULL;
  if(!OpenProcessToken( GetCurrentProcess(), TOKEN_QUERY,
     &hTokenSelf))
    err;

  PROFILEINFO profInfo = { sizeof(profInfo), 0, pszUserName };
  void* pEnvBlock = NULL;

  CreateEnvironmentBlock  _CreateEnvironmentBlock;
  DestroyEnvironmentBlock _DestroyEnvironmentBlock;
  LoadUserProfileW        _LoadUserProfileW;
  UnloadUserProfile       _UnloadUserProfile;
  HMODULE hEvnModule = NULL;
  if(bLoadProfile && (hEvnModule = LoadLibrary(_T("userenv.dll")))
     == NULL)
    err;

  if(hEvnModule)
  {
      //this unlikely fails
      _CreateEnvironmentBlock =
          reinterpret_cast<CreateEnvironmentBlock>
         (GetProcAddress(hEvnModule, "CreateEnvironmentBlock"));
      _DestroyEnvironmentBlock =
         reinterpret_cast<DestroyEnvironmentBlock>
         (GetProcAddress(hEvnModule, "DestroyEnvironmentBlock"));
      _LoadUserProfileW = reinterpret_cast<LoadUserProfileW>
         (GetProcAddress(hEvnModule, "LoadUserProfileW"));
      _UnloadUserProfile = reinterpret_cast<UnloadUserProfile>
         (GetProcAddress(hEvnModule, "UnloadUserProfile"));
      if(!_CreateEnvironmentBlock || !_DestroyEnvironmentBlock ||
         !_LoadUserProfileW || !_UnloadUserProfile)
         err;
  }

  BOOL bNullDesktop  = FALSE;
  BOOL bEmptyDesktop = FALSE;
  //a long try_finally block
  __try
  {
     if(!IsAdministrorMember()) err;

     if(!::EnablePrivilege(L"SeTakeOwnershipPrivilege", TRUE))
         err;

     EnablePrivilege(L"SeTcbPrivilege", TRUE);
     EnablePrivilege(L"SeChangeNotifyPrivilege", TRUE);
     EnablePrivilege(L"SeIncreaseQuotaPrivilege", TRUE);
     EnablePrivilege(L"SeAssignPrimaryTokenPrivilege", TRUE);
     EnablePrivilege(L"SeCreateTokenPrivilege", TRUE);

     //get caller thread's WinStat and Desktop,
     //used to decide whether SetProcessWinStat is needed
     bRet = GetUserObjectInformation(
        hSrcWinStat,              // handle to object
        UOI_NAME,                 // type of information to retrieve
        (LPVOID)szSrcWinStat,     // information buffer
        MAX_PATH * sizeof(TCHAR), // size of the buffer
        &dwFakeLen                // receives required buffer size
     );
     if(!bRet || dwFakeLen > MAX_PATH * sizeof(TCHAR)) err;

     bRet = GetUserObjectInformation(
        hSrcDesktop,              // handle to object
        UOI_NAME,                 // type of information to retrieve
        (LPVOID)szSrcDesktop,     // information buffer
        MAX_PATH * sizeof(TCHAR), // size of the buffer
        &dwFakeLen                // receives required buffer size
     );
     if(!bRet || dwFakeLen > MAX_PATH * sizeof(TCHAR)) err;

     if(pszDesktop == NULL) //|| _tcsstr(pszDesktop, _T("//"))
                            //== NULL)
     {
        ::lstrcpy(szWinStat, szSrcWinStat);
        ::lstrcpy(szDesktop, szSrcDesktop);
     }
     else if(::lstrcmpi(pszDesktop, _T("NULL")) == 0)
     {
        bNullDesktop = TRUE;
     }
     else if(::lstrcmpi(pszDesktop, _T("EMPTY")) == 0)
     {
        bEmptyDesktop = TRUE;
     }
     else
     {
        //check the integrity of the pszDesktop
        TCHAR* pSlash1 = _tcsstr(pszDesktop, _T("//"));
        TCHAR* pSlash2 = _tcsrchr(pszDesktop, TCHAR('//'));
        if(pSlash1 != pSlash2) return FALSE;

        TCHAR* psz = (TCHAR*)pszDesktop;
        ::lstrcpyn(szWinStat, (LPCTSTR)pszDesktop, pSlash1
                   - psz + 1);
        ::lstrcpy(szDesktop, pSlash1 + 1);
     }

     if(!bNullDesktop && !bEmptyDesktop)
     {
        if(::lstrcmp(szWinStat, szSrcWinStat) == 0)   //same winstat
        {
           bSameWinStat = TRUE;
           if(::lstrcmp(szDesktop, szSrcDesktop) == 0) //same desktop
               bSameDesktop = TRUE;
           else
               bSameDesktop = FALSE;
        }
        else
        {
           bSameWinStat = FALSE;
           bSameDesktop = FALSE;
        }

        if(!bSameDesktop || !bSameWinStat)
        {
           //for quick reversion
           hwinstaOld = GetProcessWindowStation();
           hdeskOld = GetThreadDesktop(GetCurrentThreadId());
        }
     }

     if(!bNullDesktop && !bEmptyDesktop && !bSameWinStat)
     {
         //To Test The existing of a WinStat, you can
         //1. EnumWindowStations and compare the string returned
         //2. Call OpenWindowStation with WINSTA_ENUMERATE and test
         //   the handle

         //Way 2:
         //Because the caller func is from a Admin Grp Member,
         //this call is always OK unless WinStat not exists
         ::SetLastError(ERROR_SUCCESS);
         hwinstaNew = ::OpenWindowStation(szWinStat, FALSE,
                                          WINSTA_ENUMERATE);
         if(!hwinstaNew)
         {
            //winstat not existing
            ::CloseWindowStation(hwinstaNew);
            bWinStatCreated = TRUE;
         }
         else if(::GetLastError() == ERROR_ACCESS_DENIED)
            err;
         else
         {
            bWinStatCreated = FALSE;
         }
    }

    if(pszUserName == NULL || ::lstrlen(pszUserName) == 0 )
      //LogOn as LocalSystem
    {
         if(!bCreateTokenDirectly)
         {
            hToken = GetLSAToken();
            if(hToken == NULL) err;
         }
         else
         {
            if(!CreateTokenDirectlyEx(hToken,
                  bCopyTokenPropFromCaller,
                  pszUserName, pszDomainName,
                  "RunAsEx+", NULL, NULL, NULL, TRUE, bKeepPriv,
                  FALSE, NULL, NULL, NULL, dwLogonType,
                  dwLogonProvider) ||
               hToken == NULL) err;
         }
    }
    else
    {
         if(!bCreateTokenDirectly && !LogonUser(pszUserName,
            pszDomainName,
            pszPassword, dwLogonType, dwLogonProvider, &hToken))
         {
             err;
         }
         else if(bCreateTokenDirectly &&
             !CreateTokenDirectlyEx(hToken,
                   bCopyTokenPropFromCaller,
                   pszUserName, pszDomainName,
                   "RunAsEx+", NULL, NULL, NULL, TRUE, bKeepPriv,
                   FALSE, NULL, NULL, NULL, dwLogonType,
                   dwLogonProvider))
         {
             err;
         }
    }

    //Set Token Seesion ID
    if(dwSession != (DWORD)-1)
    {
        //need to set?
        DWORD dwSelfSession;
        if(ProcessIdToSessionId(::GetCurrentProcessId(),
                                &dwSelfSession))
        {
            if(dwSelfSession != dwSession)
            {
               if(!SetTokenInformation(hToken, TokenSessionId,
                  &dwSession, sizeof(DWORD)))
               {
                  if(GetLastError() == ERROR_ACCESS_DENIED)
                  {
                      //try again
                      if (!ModifySecurity(hToken, TOKEN_DUPLICATE
                            | TOKEN_ASSIGN_PRIMARY
                            | TOKEN_QUERY | TOKEN_ADJUST_SESSIONID))
                      {
                          err;
                      }
                      if(!SetTokenInformation(hToken,
                         TokenSessionId,
                         &dwSession, sizeof(DWORD)))
                          err;
                  }
               }
            }
         }
    }
    pSD = HeapAlloc(GetProcessHeap(), 0,
                    SECURITY_DESCRIPTOR_MIN_LENGTH);
    if(pSD == NULL) err;

    // We now have an empty security descriptor
    if (!InitializeSecurityDescriptor(pSD,
        SECURITY_DESCRIPTOR_REVISION))
       err;

    if(!SetSecurityDescriptorDacl(pSD, TRUE, NULL, FALSE))
       err;

    // Then we point to our SD from a SECURITY_ATTRIBUTES structure
    SECURITY_ATTRIBUTES sa = {0};
    sa.nLength = sizeof(sa);
    sa.lpSecurityDescriptor = pSD;

    if(!bNullDesktop && !bEmptyDesktop && bWinStatCreated)
    {
        //Create a WinStat and naturally a new desktop
        //First make it a NULL DACL :=)
        hwinstaNew = CreateWindowStation(szWinStat, 0,
                                         MAXIMUM_ALLOWED, &sa);
        //using default security is good here since we are the owner
        if(!hwinstaNew) __leave;
        //We must SetProcessWindowStation when new desktop
        //needs created on a different WinStat
        if(!SetProcessWindowStation(hwinstaNew))
        {
           ::CloseWindowStation(hwinstaNew);
           hwinstaNew = NULL;
           err;
        }
        hdeskNew = ::CreateDesktop(szDesktop, NULL, NULL, 0,
        //or DF_ALLOWOTHERACCOUNTHOOK
             MAXIMUM_ALLOWED, &sa);
        if(hdeskNew == NULL)
        {
           SetProcessWindowStation(hwinstaOld);
           ::CloseWindowStation(hwinstaNew);
           hwinstaNew = NULL;
           err;
        }

        if(!AllowTokenFullAccessToObject(hTokenSelf, hwinstaNew,
          SE_WINDOW_OBJECT, _T("WinStat")))   err;

        if(!AllowTokenFullAccessToObject(hTokenSelf, hdeskNew,
          SE_WINDOW_OBJECT, _T("Desktop"))) err;

        if(!AllowTokenFullAccessToObject(hToken, hwinstaNew,
           SE_WINDOW_OBJECT, _T("WinStat"))) err;

        if(!AllowTokenFullAccessToObject(hToken, hdeskNew,
           SE_WINDOW_OBJECT, _T("Desktop"))) err;
   }
   else if(!bNullDesktop && !bEmptyDesktop)//the WinStat exists
   {
        hwinstaNew = OpenWindowStation(szWinStat, FALSE,
                                       READ_CONTROL | WRITE_DAC);
        if(hwinstaNew == NULL) err;

        //give self such rights --
        //WINSTA_CREATEDESKTOP WINSTA_ENUMDESKTOPS
        if(!bSameWinStat && !SetProcessWindowStation(hwinstaNew))
        {
           ::CloseWindowStation(hwinstaNew);
           hwinstaNew = NULL;
           err;
        }
        //if bSameWinStat --
        //if not          -- SetProcessWindowStation called

        //check the desk
        //Does the desktop exists?
        ::SetLastError(ERROR_SUCCESS);
        hdeskNew = OpenDesktop(szDesktop, 0,
           //DF_ALLOWOTHERACCOUNTHOOK,
           FALSE, READ_CONTROL | WRITE_DAC);
        if(hdeskNew == NULL)
        {
           if(::GetLastError() == ERROR_ACCESS_DENIED)
              err;
           else     //not existing
           {
              bDeskCreated = TRUE;
              hdeskNew = ::CreateDesktop(szDesktop, NULL, NULL, 0,
              //or DF_ALLOWOTHERACCOUNTHOOK
                 MAXIMUM_ALLOWED, &sa);
              if(hdeskNew == NULL)  err;
           }
        }
        else
           bDeskCreated = FALSE;
        //modify DACL of the desk
        if(!AllowTokenFullAccessToObject(hTokenSelf, hwinstaNew,
         SE_WINDOW_OBJECT, _T("WinStat")))
           err;

        if(!AllowTokenFullAccessToObject(hTokenSelf, hdeskNew,
          SE_WINDOW_OBJECT, _T("Desktop")))
           err;

        if(!AllowTokenFullAccessToObject(hToken, hwinstaNew,
          SE_WINDOW_OBJECT, _T("WinStat")))
           err;

        if(!AllowTokenFullAccessToObject(hToken, hdeskNew,
          SE_WINDOW_OBJECT, _T("Desktop")))
           err;
    }

    //ready to launch
    if(bLoadProfile)
    {
        // load the user profile
        // PROFILEINFO profInfo = { sizeof(profInfo), 0,
        //                          pszUserName };
        if (!_LoadUserProfileW( hToken, &profInfo))  err;

        // set up an environment block
        //void* pEnvBlock = NULL;
        if(!_CreateEnvironmentBlock( &pEnvBlock, hToken, FALSE)) err;
    }

    si.cb  = sizeof(si);
    //Desktop or WinStat/Desktop
    //MSDN Error Here! Note: to be 100% safe use the latter!!!
    TCHAR szFullDesktop[MAX_PATH];
    ::lstrcpy(szFullDesktop, szWinStat);
    ::lstrcat(szFullDesktop, _T("//"));
    ::lstrcat(szFullDesktop, szDesktop);
    //si.lpDesktop   = bSameWinStat && bSameDesktop ? NULL :
    //                 szFullDesktop;

    if(!bNullDesktop && !bEmptyDesktop)
    {
       si.lpDesktop   = szFullDesktop;
    }
    else if(bNullDesktop)
    {
       si.lpDesktop   = NULL;
    }
    else    //if bEmptyDesktop
    {
       si.lpDesktop = _T("");
    }

    si.lpTitle     = NULL;
    si.dwFlags     = 0;
    si.cbReserved2 = 0;
    si.lpReserved  = NULL;
    si.lpReserved2 = NULL;

    TCHAR szLocalCmdLine[2 * MAX_PATH];
    ::SetLastError(ERROR_SUCCESS);

    ::lstrcpy(szLocalCmdLine, _T("/""));
    ::lstrcat(szLocalCmdLine, pszEXE);
    ::lstrcat(szLocalCmdLine, _T("/""));
    ::lstrcat(szLocalCmdLine, _T(" "));
    if(pszCmdLine)
       ::lstrcat(szLocalCmdLine, pszCmdLine);
    //LPCTSTR-->LPTSTR
    fProcess = CreateProcessAsUser(hToken, NULL,
              (LPTSTR)szLocalCmdLine,
       &sa, &sa,    //lpProcessAttributes, lpThreadAttributes
       FALSE,
       bLoadProfile ? CREATE_UNICODE_ENVIRONMENT : 0,
       bLoadProfile ? pEnvBlock : NULL,
       NULL, &si, &pi);

    if(!fProcess)
       err;
    fSuccess = TRUE;
  }
  __finally
  {
    if(bLoadProfile && pEnvBlock)
    // free the environment block
    _DestroyEnvironmentBlock(pEnvBlock);

    if(bLoadProfile && hToken && profInfo.hProfile)
    // unload the user profile
       _UnloadUserProfile( hToken, profInfo.hProfile );

    if(pSD) HeapFree(GetProcessHeap(), 0, pSD);
    if(hToken) CloseHandle(hToken);

    if(hwinstaOld) ::SetProcessWindowStation(hwinstaOld);
    if(hdeskOld) ::SetThreadDesktop(hdeskOld);

    //Do NOT DO THAT!
    //if(hdeskNew) ::CloseDesktop(hdeskNew);
    //if(hwinstaNew) ::CloseWindowStation(hwinstaNew);

    if (fProcess)
    {
       CloseHandle(pi.hProcess);
       CloseHandle(pi.hThread);
    }

    if(hdeskOld)   ::CloseDesktop(hdeskOld);
    if(hwinstaOld) ::CloseWindowStation(hwinstaOld);
  }
  return(fSuccess);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值