当前位置: 首页 > 科技观察

保护您的应用免受越狱:iOS14AppAttest保护功能

时间:2023-03-18 17:16:02 科技观察

当越狱最初在iOS设备上流行时,iOS开发人员尝试了各种方法来保护他们的应用免受盗版等问题的困扰。有很多方法可以做到这一点,包括检查Cydia是否存在、检测应用程序是否可以读取其自身沙箱之外的文件、在检测到调试器时使应用程序崩溃等等。然而,这些防御措施并没有被证明是那么有效。如果攻击者可以直接访问物理设备,这些措施将不再有效。对于技术娴熟的人来说,他们可以通过让设备看起来没有越狱来有效地绕过这些措施,而且过去和现在都是。同时,对于一些越狱用户来说,他们可能并不想做坏事,只是想要一些很酷的功能,比如可定制的主屏幕。随着最近越狱可能再次兴起,Apple有了自己的解决方案。在iOS14中,新的AppAttestAPI为应用程序提供了一种对服务器请求进行签名的方式,以试图向服务器证明这些请求来自应用程序的合法版本。重要的是要了解AppAttest不会告诉服务器“此设备是否已越狱?”,因为此解决方案已被一次又一次地证明是不可行的。相反,目标是保护服务器请求,使攻击者更难创建非法版本的应用程序来解锁高级功能或植入作弊程序。同样:由于攻击者可以物理访问设备,因此在这种情况下无法完全保护您的应用程序。由于无法信任应用程序来保护自己,因此应用程序证明要求在应用程序的后端完成必要的工作以执行此安全策略。由于是Swift相关的内容,这里就不介绍后端应该怎么处理了,顺便提一下。生成一对密钥来签署请求AppAttest依赖于使用非对称公钥/密钥对来工作。最终目的是让应用程序使用密钥对服务器请求进行签名,然后将数据发送到后端,其中公钥用于确认请求的合法性。如果攻击者拦截了请求,他没有办法改变内容,这样就不会影响后端的验证。生成密钥对,可以引入DeviceCheck框架,调用DCAppAttestService单例对象的generateKey方法:证明将安全地存储在设备的SecurityEnclave中。由于不能直接访问,该方法返回一个keyIdentifier属性,需要时可以通过该属性查找对应的key。我们需要存储它以供以后用于验证对应用程序的请求。值得一提的是,并非所有类型的设备都支持AppAttest。如果你查看Apple的文档,你会发现我们需要先检查它是否支持,然后要求服务器降级以应用异常:ifservice.isSupported{...}但是不要那样做!如前所述,攻击者可以轻松假装设备不支持此操作。Apple也没有相应的应对措施,之所以会出现这种检查,更多是因为部分Macbook没有支持的芯片。根据GuilhermeRambo的调查,大多数iOS设备都支持这个功能,因此对于iOS应用程序来说,这个兼容性检查是不需要的。将公钥发送到后端为了对请求进行签名,后端需要提供一种验证签名的方法。我们需要向后端提供上面生成的公钥的访问权限,以完成验证。但是我们不能简单地创建发送公钥的请求,因为攻击者很容易拦截请求并发送他们自己的公钥,这使他们可以完全控制应用程序向后端发送的内容。此问题的解决方案是让Apple证明我们发送的密钥来自该应用程序的合法版本。这可以通过调用attestKey方法来完成,该方法接收密钥的标识符作为参数:service.attestKey(keyIdentifier,clientDataHash:hash){attestation,erroringguarderror==nilelse{return}letattestationString=attestation?.base64EncodedString()//将证明发送到服务器。Itnowhasaaccesstothepublickey!//Ifitfails,throwtheidentifierawayandstartover.}这个方法会访问远程Apple服务器并返回一个“证明”对象,这个对象不仅包含公钥,还包含很多关于应用程序的信息,以表明它是由Apple合法公钥。客户端收到对象后,必须完整发送给后台,后台需要进行多步验证,确认没有被篡改。如果“证明”对象被验证为有效,后端可以安全地从中提取应用程序的公钥。目前还不清楚苹果是否试图在这个过程中检查用户的设备是否越狱。文档没有提到这一点,但他们也指出AppAttest无法判断设备是否越狱,这至少表明他们尝试过。可以肯定地说,没有办法表明设备是否已越狱,证明一词仅表示请求未被拦截或篡改。证明请求的附加clientDataHash参数与验证过程本身无关,但对安全性至关重要。实际上,这个请求很容易做重放攻击,攻击者可以拦截认证请求并窃取Apple发送的“证明”对象,以便稍后在非法版本的应用程序中“重放”相同的认证请求以进行欺骗服务器。这个问题的解决方案是简单地不允许不分青红皂白地执行身份验证请求。客户端可以提供服务器希望与请求一起使用的一次性令牌(或会话ID)以确保其有效性。如果两次使用相同的令牌,请求将失败。这就是clientDataHash的用途:通过向身份验证请求提供令牌的散列版本,Apple会将其嵌入到最终对象中,并为您的服务器提供提取它的方法。这样,攻击者很难仅通过拦截请求来创建您的应用程序的非法版本。letchallenge=getSessionId().data(using:.utf8)!lethash=Data(SHA256.hash(data:challenge))service.attestKey(keyIdentifier,clientDataHash:hash){...}如前所述,Apple并没有它建议您重复使用密钥,而是为设备中的每个用户帐户执行整个过程。由于此请求依赖于远程Apple服务器,因此可能会失败。如果错误是服务器不可用,苹果说你可以再试一次,但如果不是这样,你应该丢弃密钥标识符并重新开始这个过程。例如,当用户重新安装您的应用程序时,可能会发生这种情况:您生成的密钥在正常的应用程序更新中仍然有效,但在重新安装应用程序、设备迁移或从备份恢复设备时会发生错误。对于这些情况,您的应用程序需要能够重新执行密钥生成过程。在服务器端,还值得一提的是,“证明”对象还包含一个收据,您的服务器可以使用该收据向Apple请求欺诈评估指标。这使您可以检查生成的密钥数量以及与其关联的设备,以检测可能的欺诈案例。苹果公司特别提到了一种攻击的可能性,即用户可能会使用设备向越狱设备提供有效断言,这是一种欺诈性评估,可以通过以异常多的断言请求为目标的用户来检测。加密请求验证密钥有效性后,后端将获得公钥。从现在开始,您每次处理敏感内容时都可以安全地签署请求。用于此目的的generateAssertion方法与密钥的身份验证非常相似,只是这次请求本身需要进行身份验证:letchallenge=getSessionId().data(using:.utf8)!letrequestJSON="{'requestedPremiumLevel':300,'sessionId':'\(challenge)'}".data(using:.utf8)!lethash=Data(SHA256.hash(data:challenge))service.generateAssertion(keyIdentifier,clientDataHash:hash){assertion,erroringguarderror==nilelse{return}letassertionString=assertion?.base64EncodedString()//Sendthesignedassertiontoyourserver.//Theserverwillvalidateit,grabyourrequestandprocessit.}和以前一样,后端必须支持使用一次性令牌来防止重放攻击。这一次,由于请求本身就是我们的clientDataHash,我们将令牌添加到JSON。可以对给定键进行的断言数量没有限制。然而,尽管如此,当应用程序发出请求时,它们通常应该被保留以保护敏感信息,例如下载。在这种情况下,额外的保护来自请求被散列并且只能使用一次的事实。由于整个请求都是由私钥签名的,因此攻击者不能简单地拦截请求并使用它们来制作自己的请求。他们必须弄清楚你请求的参数来自哪里,然后手动尝试对它们进行签名,这比简单地附加代理更具技术性。就像开头说的,想要打破这个保护,也不是不可能,只是需要付出更多的努力。测试和实施AppAttest服务记录您无法重置的标志。为防止这种情况,非生产环境中的应用程序将使用沙盒版本。如果您想在生产环境中进行测试,您应该将com.apple.developer.devicecheck.appattest-environment授权添加到您的应用程序并将其值设置为生产环境。如果您的用户群很大,Apple建议您逐步启用此功能,因为对attestKey的请求会受到网络速度的限制。结论通过在客户端和后端实现此功能,攻击者更难创建应用程序的非法版本。但是请注意,这并不意味着它不可能!如前所述,您无法确定用户是否拥有越狱设备,也无法确定防止他们攻击您的应用程序的方法。与大多数安全措施一样,AppAttest的目标是让这个过程变得足够困难,只有非常熟练和专业的攻击者才能找到侵入您的应用程序的方法——而这样的人很少见。