调用InitializeSecurityContext(Negotiate)时要使用的TargetName是什么?

Ian*_*oyd 18 security winapi ntlm kerberos

问题

在调用时InitializeSecurityContext,我传递给TargetName参数的值是多少?

修订背景

我正在调用这个函数InitializeSecurityContext:

InitializeSecurityContextA(
      @pAS.hcred,           //[in] credentials
      phContext,            //[in] optional] Context handle structure
      pszTargetName,        //[in, optional] Target name
      0,                    //[in] context requirements
      0,                    //[in] reserved1, must be zero
      SECURITY_NATIVE_DREP, //[in] target data representation
      pInput,               //[in] optional] SecBufferDescription
      0,                    //[in] reserved2, must be zero
      @pAS.hctxt,           //[in, out] pointer to context handle structure
      @OutBuffDesc,         //[in, out] pointer to SecBufferDesc
      ContextAttributes,    //[out] context attributes
      @lifetime);           //[out] expiration timestamp
Run Code Online (Sandbox Code Playgroud)

我传递给pszTargetName谁?

我试过了

  • null: InitializeSecurityContextA(@pAS.hcred, phContext, null, ...);
  • "": InitializeSecurityContextA(@pAS.hcred, phContext, "", ...);
  • "spn/HOSTNAME": InitializeSecurityContextA(@pAS.hcred, phContext, "spn/HOSTNAME", ...);
  • spn/HOSTNAME.DOMAIN.COM: InitializeSecurityContextA(@pAS.hcred, phContext, "spn/HOSTNAME.DOMAIN.COM", ...);
  • "cargocult/PROGRAMMING": InitializeSecurityContextA(@pAS.hcred, phContext, "cargocult/PROGRAMMING", ...);
  • "http/TFS.DOMAIN.COM": InitializeSecurityContextA(@pAS.hcred, phContext, "http/TFS.DOMAIN.COM", ...);
  • "http/HOSTNAME": InitializeSecurityContextA(@pAS.hcred, phContext, "http/HOSTNAME", ...);
  • "qwertyasdf": InitializeSecurityContextA(@pAS.hcred, phContext, "qwertyasdf", ...);

  • "AuthSamp": InitializeSecurityContextA(@pAS.hcred, phContext, "AuthSamp", ...);

他们都要么失败,要么降级到NTLM.

注意:我的计算机已加入域,但域名命名domain.com,甚至hostname.domain.com甚至没有qwertyasdf.所以我对这些尝试失败并不感到惊讶.但人们说尝试的东西http/HOSTNAME,所以我投入http/HOSTNAME.

背景

InitializeSecurityContext(协商)函数具有一个可选 TargetName参数:

pszTargetName [in,optional]

指向以null结尾的字符串的指针,该字符串指示服务主体名称(SPN)或目标服务器的安全上下文.
应用程序必须提供有效的SPN以帮助缓解重放攻击.

这应该是什么?

更多背景

我正在尝试验证一组用户的凭据,例如:

Boolean ValidateCredentials(String username, String password, String domain)
{
   ...
}
Run Code Online (Sandbox Code Playgroud)

验证一组用户凭据需要使用SSPI API.第一个要调用的函数是InitializeSecurityContext.其中一个参数InitializeSecurityContext"TargetName"字符串.

我尝试将其保留为null,但Application Verifier触发断点,写出错误:

VERIFIER STOP 00005003:pid 0xF08:
InitializeSecurityContext对Kerberos服务使用NULL目标或格式错误的目标.
请参阅pszTargetName获取目标值.
00000000:未使用.
00000000:没有

在这一点上,记住Negotiate提供者将尝试使用Kerberos,但回退到将有所帮助NTLM.在的情况下Negotiate,Kerberos或者NTLM,所述TargetName的参数被记录为:

服务主体名称(SPN)或目标服务器的安全上下文.

但那我应该通过什么?

我尝试过做SSPI知识库文章的内容,没有任何内容(即通过NULL):

如何在Microsoft操作系统上验证用户凭据

ss = _InitializeSecurityContext(
        &pAS->hcred,
        pAS->fInitialized ? &pAS->hctxt : NULL, 
        NULL,        //<-------pszTargetName
        0, 
        0,
        SECURITY_NATIVE_DREP, 
        pAS->fInitialized ? &sbdIn : NULL,
        0, 
        &pAS->hctxt, 
        &sbdOut, 
        &fContextAttr, 
        &tsExpiry);
Run Code Online (Sandbox Code Playgroud)

但没有(ie NULL)不起作用.

注意:知识库文章在2007年大规模改写.在其最初的1999年版本中,它们"AuthSamp"作为目标传递,但也失败了.

奖金Chatter:

服务主体名称
(SPN)客户端唯一标识服务实例的名称.如果在整个林中的计算机上安装多个服务实例,则每个实例都必须具有自己的SPN.如果客户端可能使用多个名称进行身份验证,则给定的服务实例可以具有多个SPN

安全上下文
当前有效的安全属性或规则.例如,当前用户登录到计算机或智能卡用户输入的个人识别码.对于SSPI,安全上下文是不透明的数据结构,其包含与连接相关的安全数据,例如会话密钥或会话持续时间的指示.

奖金Chatter 2

从应用程序验证程序文档:

Verifier插件检测到以下错误:

  • 在调用AcquireCredentialsHandle(或更高级别的包装器API)时直接指定NTLM包.

  • InitializeSecurityContext调用中的目标名称为NULL.

  • 调用InitializeSecurityContext时的目标名称不是正确形成的SPN,UPN或NetBIOS样式的域名.

  • 后两种情况将迫使Negotiate直接(第一种情况)或间接地回退到NTLM(在第二种情况下,域控制器将返回"未找到主体"错误,导致Negotiate回退).

  • 插件在检测到降级到NTLM时也会记录警告; 例如,域控制器找不到SPN时.这些仅作为警告记录,因为它们通常是合法的情况 - 例如,在对未加入域的系统进行身份验证时.

在我的情况下,我对验证域是null(因为我不知道机器的域名,或者即使有一个域).但是如果硬编码我的开发机器的域名,结果是一样的.

更新3

pszTargetName的值,它触发AppVerifier错误,但登录成功:

  • null
  • ""
  • "AuthSamp"
  • "qwertyasdf"
  • *我正在验证的域名(例如"avatopia.com")
  • *机器加入的域名(例如"avatopia.com")
  • *用户帐户所在域的名称(例如"avatopia.com")

pszTargetName的值不会触发AppVerifier错误,但登录失败:

  • "http/HOSTNAME"
  • "http/TFS.DOMAIN.COM"
  • "frob/GROBBER"
  • "cargocult/PROGRAMMING"
  • "spn/HOSTNAME"
  • "spn/HOSTNAME.DOMAIN.COM"

pszTargetname的值不会触发AppVerifier错误,并且登录成功:

  • 没有

更新4

我想做的是:弄清楚用户名/密码是否有效.

  • 我有一个用户名:例如 "ian"
  • 我有一个密码:例如 "pass1"

现在,该帐户ian可能是本地帐户或帐户的进一步皱纹.ian在您提出要求之前,您需要决定是否是本地或域帐户.这是因为ian可以有两个帐户:

  • ian 在域上 stackoverflow.com
  • ian 在本地机器上

所以我需要指定是否我想:

  • 问一个特定的域名(例如stackoverflow.com),或
  • 问本地机器(我将表示".")

现在我们可以提出一个交叉参考:

Username  Password  Domain             Machine on domain?  Validate as
========  ========  =================  ==================  ==============
iboyd     pass1     .                  No                  Local account
iboyd     pass1     (empty)            No                  Local account
iboyd     pass1     stackoverflow.com  No                  Domain account

iboyd     pass1     .                  Yes                 Local account
iboyd     pass1     (empty)            Yes                 Domain account
iboyd     pass1     stackoverflow.com  Yes                 Domain account
Run Code Online (Sandbox Code Playgroud)

更新5

它可能有助于解释我正在尝试做什么,然后也许如何做它将变得更容易.让我说我走进市中心随意的办公大楼,走进一个随机的小隔间,输入一个随机的用户名和密码:

在此输入图像描述

我打算尝试登录域名TURBOENCABULATOR.我指定我想尝试TURBOENCABULATOR通过为我的用户名添加前缀来对域进行身份验证:

TURBOENCABULATOR\ian
Run Code Online (Sandbox Code Playgroud)

注意:我非常怀疑该网络有一个名为turboencabulator的域名,因为该名称本身仅来自罗克韦尔自动化.登录尝试几乎肯定会失败.但Windows如何检查它们?

Windows如何尝试验证这些凭据?Windows如何验证凭据:

  • 用户名:ian
  • 密码:pass1
  • 领域:TURBOENCABULATOR

Windows是否使用安全支持包界面假设 Windows使用NegotiateKerberos进行身份验证,Windows传递的pszTarget参数是什么?几乎可以肯定,我输入的凭证将无效.Windows将如何确定它们是否有效?Windows将调用哪些API 来验证凭证?

Windows能够验证凭证.还要验证凭据.

也许我没有尝试连接到TURBOENCABULATOR域,而是尝试通过将turboencabulator.com域名添加到我的用户名来连接到域turboencabulator.com\ian:

在此输入图像描述

同样的问题适用.Windows如何验证凭据?我想做Windows所做的事情.假设Windows使用kerberos进行授权,那么Windows pszTargetName在SSPI中作为参数传递了什么?

也许turboencabulator.com我尝试连接到域,而不是尝试连接到turboencabulator.net域:

在此输入图像描述

请注意,在此示例中,我已将域名附加到我的用户名,而不是在其前面添加.

也许我没有尝试连接到turboencabulator.net域,而是尝试通过在我的用户名前加上.\as 来验证用户是否为本地(机器)帐户:

在此输入图像描述

Windows如何根据本地帐户数据库验证用户名和密码?它是否将SSPI与Negotiate包一起使用?如果是这样,它通过什么价值pszTargetName呢?

人们在谈论Web服务器,http,团队基础服务器.我真的不知道他们从哪里得到的.或者他们谈论在活动目录中编辑用户以确保存在某些东西 - 我不明白为什么我需要编辑任何东西:Windows不编辑任何东西.

TargetName在调用InitializeSecurityContext以验证一组凭据时我使用了什么?

奖金Chatter

以下是Application Verifier文档中的一章,说明如果有人错误地使用NTLM,他们为什么要进行测试:

为什么需要NTLM插件

NTLM是一种过时的身份验证协议,其缺陷可能会危及应用程序和操作系统的安全性.最重要的缺点是缺乏服务器身份验证,这可能允许攻击者诱骗用户连接到欺骗性服务器.作为缺少服务器身份验证的必然结果,使用NTLM的应用程序也可能容易受到一种称为"反射"攻击的攻击.后者允许攻击者将用户的身份验证会话劫持到合法服务器,并使用它来验证攻击者对用户计算机的身份.NTLM的漏洞和利用它们的方式是增加安全社区研究活动的目标.

虽然Kerberos已经存在多年,但仍然编写许多应用程序以仅使用NTLM.这不必要地降低了应用程序的安全性.但是,Kerberos无法在所有场景中替换NTLM - 主要是客户端需要对未加入域的系统(家庭网络可能是其中最常见的)进行身份验证的场景.Negotiate安全包允许尽可能使用Kerberos的向后兼容折衷,并且在没有其他选项时仅恢复为NTLM.切换代码以使用Negotiate而不是NTLM将显着提高我们客户的安全性,同时引入很少或没有应用程序兼容性.谈判本身并不是一个灵丹妙药 - 有些情况下,攻击者可以强制降级到NTLM,但这些更难以利用.但是,立即改进的是,为正确使用Negotiate而编写的应用程序自动免受NTLM反射攻击.

通过对使用NTLM的最后警告:在Windows的未来版本中,可以在操作系统中禁用NTLM.如果应用程序对NTLM具有硬依赖性,则在禁用NTLM时,它们将无法进行身份验证.

插件如何工作

Verifier插件检测到以下错误:

  • 在调用AcquireCredentialsHandle(或更高级别的包装器API)时直接指定NTLM包.

  • InitializeSecurityContext调用中的目标名称为NULL.

  • 调用InitializeSecurityContext时的目标名称不是正确形成的SPN,UPN或NetBIOS样式的域名.

后两种情况将迫使Negotiate直接(第一种情况)或间接地回退到NTLM(在第二种情况下,域控制器将返回"未找到主体"错误,导致Negotiate回退).

插件在检测到降级到NTLM时也会记录警告; 例如,域控制器找不到SPN时.这些仅作为警告记录,因为它们通常是合法的情况 - 例如,在对未加入域的系统进行身份验证时.

NTLM停止

5000 - 应用程序已明确选择NTLM程序包

严重性 - 错误

应用程序或子系统在调用AcquireCredentialsHandle时显式选择NTLM而不是Negotiate.尽管客户端和服务器可能使用Kerberos进行身份验证,但这可以通过显式选择NTLM来防止.

如何解决此错误

此错误的修复是选择Negotiate包代替NTLM.如何完成此操作取决于客户端或服务器使用的特定网络子系统.下面给出一些例子.您应该查阅您正在使用的特定库或API集的文档.

APIs(parameter) Used by Application    Incorrect Value  Correct Value  
=====================================  ===============  ========================
AcquireCredentialsHandle (pszPackage)  “NTLM”           NEGOSSP_NAME “Negotiate”
Run Code Online (Sandbox Code Playgroud)

也可以看看

Har*_*wok 9

伊恩,我想我们仍然不明白你要做什么.为了帮助您向我们提供有关您尝试执行的操作的更多信息,这里有一些关于SSPI的背景知识.您可能已经知道这一点,但只是为了确保我们在同一页面上.

SSPI通常用于通过网络验证用户.客户端调用AcquireCredentialsHandle获取凭证句柄,然后通过调用创建安全上下文InitializeSecurityContext.将安全缓冲区传递给服务器.请注意,SSPI并未规定如何通过安全缓冲区.您可以随意使用http,tcp,命名管道.一旦服务器收到安全缓冲区.同样,它称之为第AcquireCredentialsHandle一个.然后它将收到的安全缓冲区传递到AcceptSecurityContext并生成新的安全缓冲区.在某些情况下,新生成的安全缓冲区需要发送回客户端,客户端将其传递给InitializeSecurityContext并再次生成另一个新的安全上下文.这SSPI握手过程一直持续到InitializeSecurityContextAcceptSecurityContext这两个回报SEC_E_OK

尽管SSPI设计用于通过网络进行身份验证,但许多应用程序实际上正在进行环回SSPI握手,这意味着客户端和服务器都在同一个盒子上.这实际上只是网络身份验证的一个特例.环回SSPI握手的最终结果是经过身份验证的SSPI安全上下文.有了这个经过身份验证的SSPI,应用程序可以做QueryContextAttributesImpersonateSecurityContext.既然你似乎不知道是什么targetName意思,我猜你正试图做回环握手.我可能错了,但你需要告诉我们你想做什么.

要了解targetNameKerberos中需要的原因而不是NTLM中的原因,您需要了解更多底层实现.

获取凭证句柄有两种不同的方法.通常,人们指定使用当前的安全上下文.(即您用于登录计算机的帐户).您还可以提供另一组用户名/密码.不同的安全包对术语有不同的含义credentials.NTLM意味着它将保存密码的哈希值.Kerberos意味着它将保存票证授予票证(TGT).对于SSPI程序员,您无需担心这一点.

现在,当客户端将获取的凭证句柄传入时InitializeSecurityContext,同样地,不同的安全包将执行不同的操作.NTLM将在第一次InitializeSecurityContext调用时生成NEGOTIATE数据包.在生成NEGOTITATE数据包的过程中没有涉及其他机器.Kerberos包非常不同.它将与KDC交谈以请求所请求服务的服务票证.该服务由Kerberos中的服务主体名称(SPN)标识.我不能在这里详述所有细节.净网络是针对NTLM的服务请求是无目标的,同时针对Kerberos的服务请求.您可以使用NTLM身份验证方法将相同的NTLM NEGOTIATE数据包用于不同的服务.但是,您需要使用Kerberos身份验证方法为不同的服务使用不同的Kerberos服务票证.这就是为什么在调用InitializeSecurityContextKerberos/Negotiate时,你需要提供targetName.

当KDC收到服务票证的请求时,它会搜索其LDAP数据库并找出与指定帐户关联的帐户servicePrincipalName.该帐户可以是AD用户帐户或AD计算机帐户.KDC将使用目标服务帐户的主密钥(由帐户密码生成)来加密会话密钥.此加密的会话密钥稍后将从客户端传递到服务器.

现在,记得我说服务器也需要做AcquireCredentialsHandle,我说有两种主要的方法来获取凭证?我想你正在使用第一种方法来获取凭证句柄.这意味着它将使用当前的安全上下文.在正常的网络认证情况下,这可以通过以下示例来说明.如果您的服务器是HTTP服务器,它将是您的IIS服务器的服务帐户.IIS服务器将使用其服务帐户主密钥来解密加密的会话密钥.一旦获得会话密钥,客户端和服务器就使用会话密钥继续通信以进行加密和解密.

如果它是一个环回SSPI场景,那就更棘手了.如果您正在运行domain\jane并自行循环.您需要为domain\jane指定SPN.什么是域\ jane的SPN.如果检查AD用户对象,则默认情况下没有.您需要手动修复它.

曾经有一件事对我有用,但它没有记载.您可以将用户的UPN(即jane@domain.com)指定为SPN.这适合我.你可以尝试一下.

如果这不起作用,另一种解决方法是使用第二种方法来执行服务器部分AcquireCredentialsHandle.domain\jane您可以指定其他服务帐户凭据,而不是使用凭据句柄.您可以确保服务帐户具有正确的SPN集.然后,您可以在您的服务帐户中使用该服务帐户的SPN InitializeSecurityContext.当然,这也意味着您需要在代码中对服务帐户的密码进行硬编码.您需要小心并确保完全锁定此服务帐户,以便即使密码被盗,您的AD环境也不会有很大的风险.


Ian*_*oyd 8

简答

的TargetName是,用户名“服务器”代码将被运行的。

  • 我登录为 ian@stackoverflow.com
  • 我想证明我的身份 steve@stackoverflow.com
  • 设置的TargetNamesteve@stackoverflow.com

背景

Negotiate验证包将尝试使用Kerberos。如果不能,它将尝试回退到NTLM.

  • 希望使用NTLM; 它是旧的、已弃用的、弱的、损坏的,不应使用。
  • 使用Kerberos.
  • 为了使用Kerberos必须提供一个TargetName
  • 没有TargetNameKerberos根本无法实现功能

考虑到所有相关方,问题变成了:

  • 我(伊恩
  • 史蒂夫进行身份验证

我指定什么TargetName

在此处输入图片说明

这是了解TargetName对 Kerberos 意味着什么很重要的地方:

  • 我想证明我的身份 steve@stackoverflow.local
  • 域控制器交给我一个加密的 blob 来证明我的身份
  • blob 已加密,因此steve@stackoverflow.local是唯一能够解密它的人
  • 域控制器知道加密它,steve@stackoverflow.local因为我steve@stackoverflow.localTargetName 中指定
  • 史蒂夫目标

这就是史蒂夫如何知道 blob 是有效的,它是加密的,所以只有他才能解密。

我必须告诉 Kerberos我会将加密的 blob 提供给,以便域控制器知道为谁加密。

所以在上面的可能名称列表中,三个值起作用:

在此处输入图片说明

InitializeSecurityContext(credHandle, context, "steve@stackoverflow.local", ...);    
InitializeSecurityContext(credHandle, context, "stackoverflow.local\steve", ...);    
InitializeSecurityContext(credHandle, context, "steve", ...); //if we're in the same forest
Run Code Online (Sandbox Code Playgroud)

所以你可以看到为什么我之前尝试调用InitializeSecurityContext都失败了:

InitializeSecurityContextA(credHandle, context, null, ...);
InitializeSecurityContextA(credHandle, context, "", ...);
InitializeSecurityContextA(credHandle, context, "spn/HOSTNAME", ...);
InitializeSecurityContextA(credHandle, context, "spn/HOSTNAME.DOMAIN.COM", ...);
InitializeSecurityContextA(credHandle, context, "cargocult/PROGRAMMING", ...);
InitializeSecurityContextA(credHandle, context, "http/TFS.DOMAIN.COM", ...);
InitializeSecurityContextA(credHandle, context, "http/HOSTNAME", ...);
InitializeSecurityContextA(credHandle, context, "qwertyasdf", ...);
InitializeSecurityContextA(credHandle, context, "AuthSamp", ...);
Run Code Online (Sandbox Code Playgroud)

因为我没有将Steve指定为TargetName;我在指定一些毫无意义的东西:

spn/HOSTNAME
Run Code Online (Sandbox Code Playgroud)

公平地说,人们确实一直告诉我通过"spn/HOSTNAME"

额外的混乱,因为灵活性(欢迎使用 SPN)

SPN 短版

  • 使用 Kerberos 时,您必须指定一个用户名作为您的TargetName
  • SPN 是用户名的“别名”
  • 因此您可以指定一个 SPN 作为您的TargetName

SPN 长版

在上述情况下,我必须知道“服务器”代码将作为steve@stackoverflow.local.

那是一种痛苦。我的意思是,当我知道是史蒂夫时就好了。但是如果我正在与远程机器交谈,我必须找出代码运行的用户帐户

  • 我必须弄清楚 IIS 正在运行iisagent@stackoverflow.local
  • 我必须弄清楚 SQL Server 正在运行sqldaemon@stackoverflow.local
  • 如果服务作为本地服务运行怎么办?那根本就不是域用户?

幸运的是(?),Kerberos 创建了别名(称为服务原则名称- 或 SPN):

  • 如果我需要向 Web 服务器进行身份验证 http://bugtracker.stackoverflow.local
  • 并且 IIS 服务在域帐户下运行 iisagent@stackoverflow.local
  • 而不是必须指定目标名称 iisagent@stackoverflow.local
  • 我可以指定 HTTP/bugtracker.stackoverflow.local
  • 那是因为 IIS 向域控制器注册了一个别名
  • HTTP/bugtracker.stackoverflow.local ? iisagent@stackoverflow.local

如果您希望将其用作TargetName ,则所有这些都要求您知道 SPN 。各种标准 Microsoft 产品在安装时都会注册 SPN:

  • 信息系统HTTP/[servername]
  • SQL 服务器MSSQLSvc/[servername]:1433
  • 邮件发送SMTPSVC/[servername]
  • 文件共享HOST/[servername]

这些都是无证的,如果配置不正确,会让你的生活陷入困境。

但绝非必须提供SPN。SPN 只是一个别名,旨在让您的生活更轻松更困难。

这大致相当于尝试指定"stackoverflow.com",而不是简单地使用"35.186.238.101"

Bonus Chatter - SSPI 如何工作?

SSPI 被设计为围绕不同安全算法的通用包装器。使用 API 的方法非常简单:

  • 客户端:调用InitializeSecurityContext并获得一个 blob
  • 客户端将该 blob 发送到服务器
  • 服务器:调用AcceptSecurityContext(blob),并返回一个 blob
  • 服务器将该 blob 发送回客户端
  • 客户端:调用InitializeSecurityContext(blob),并返回一个 blob
  • 客户端将该 blob 发送回服务器
  • 服务器:调用AcceptSecurityContext(blob),并返回一个 blob
  • ...继续重复直到被告知停止...

双方不断来回,直到函数停止返回需要发送到另一方的 blob:

在此处输入图片说明

因此,使用 SSPI,您可以来回进行这种乒乓操作,直到您被告知停止为止。因此,他们能够将每个身份验证方案硬塞进那个乒乓直到被告知停止的高级抽象。

我如何传输斑点?

您可以通过您使用的任何通信渠道传输 blob。

如果您通过 TCP/IP 与远程服务器通话,那么您可能会使用它:

// Open connection to server
sockConnect(162.210.196.166, 1433);

blob = null;

Boolean bContinue = InitializeSecurityContext(ref blob);

while (bContinue)
{
   sockWrite(blob); //send the blob to the server

   blob = sockRead(); //wait for the server to return a blob 

   bContinue = InitializeSecurityContext(ref blob);
}
Run Code Online (Sandbox Code Playgroud)

如果您通过 http 进行操作:

blob = null;
Boolean bContinue = InitializeSecurityContext(ref blob);

while (bContinue)
{
    http = new HttpRequest("http://4chan.org/default.aspx");
    http.AddHeader("X-SSPI-Blob", blob.ToBase64());
    http.Send();

    blob = http.ReasponseHeader["X-SSPI-Blob"];
    if (blob.IsEmpty())
       break;

    bContinue = InitializeSecurityContext(ref blob);
}
Run Code Online (Sandbox Code Playgroud)

SSPI API 不关心来回传输 blob - 只是您必须来回传输它。

  • 使用连接到 SQL Server,SQL 客户端驱动程序通过数据库连接进行传输
  • 使用 http,浏览器在请求和响应头中发送 blob

如果您愿意,您甚至可以使用运营商 pidgeon、Skype 或电子邮件。