Skip to content

Latest commit

 

History

History
184 lines (126 loc) · 21.2 KB

第20章-调试.md

File metadata and controls

184 lines (126 loc) · 21.2 KB

调试

我至今仍记得刚开始使用Z80汇编语言编程时的情景:有好几次,仅仅是为了得到回应,我会吹牛说我能一次就写出没有问题的代码。只需要写出来,编译,瞧,多么完美的程序!说实话,在30多年的工作经历中,结果是我经常被毫无非议地嘲笑了,我从来没有实现过之前吹过的牛皮,很可能永远也不会了

Will Arthur,在写作本章时的回忆。

某个地方的某个人可能写了一个很重要的程序,并且在第一次就成功运行没有任何问题,如果真得存在这种可能,那它也就是存在于统计学上。因为编写程序的过程也可以认为是编写bug的过程,在编程世界中调试需求是永恒不变的规则。

这一章的目的是给你介绍一些用于TPM2.0程序调试和通用调试的特殊工具和方法。我们将主要讨论两个方面的调试:与TPM直接通信的底层软件和使用FAPI和TPM通信的高层次软件。底层应用包含使用ESAPI和SAPI,特殊定制的TSS软件层(如SAPI层),TAB,以及RM的软件。大多数的底层应用调试描述都是直接来自于对应部分作者的底层应用调试经验,这包括SAPI,TAB,RM,以及TPM2.0设备驱动,和所有底层相关的软件。因为现在没有TSS软件栈中FAPI和ESAPI的实现(注:ESAPI已经可以用了哦),所以没有这些方面的调试知识。也正因为如此,这一章的内容都是基于TSS1.2的调试经验,因为我们认为许多问题都是类似的,与版本关系不大。

底层应用软件调试

因为我们在TPM2.0方面仅在底层软件上有实际经验,我们先来讨论这些内容。

问题

当TPM出现一个错误的时候,它会返回一个错误代码。TPM2.0规范的编写者用心设计了这些有助于调试的错误代码。很多时候,错误代码会告诉你确切的错误原因,但是因为对规范不熟悉,你可能会感到一团糟。在少数情况下,错误代码仅仅提示查找问题的大方向,但是并没有定位错误的具体粒度信息。不论是上面的哪种情况,缺乏具体信息或者是对规范不熟悉,你作为一个经验不多的程序员都会感到不知所措。

这一小节描述了一小部分不同的错误条件,并说明了一个用于调试这些问题的技术框架,由简到繁地介绍如下:

  • 错误代码分析:许多简单的错误完全可以通过这种方式调试。
  • 分析调试跟踪信息:这就需要检查底层驱动来将发送到TPM的命令字节流和由TPM收到的命令响应数据流。经常发生的情况是,对比正常的跟踪数据和有问题的数据就可以很快定位问题。
  • 更加负责的问题:这些问题将需要更多的工作来调试。一个HMAC授权错误可以很快产生一个错误代码,但是调试这个错误需要详细检查计算HMAC的所有步骤。
  • 最难调试的错误:这些错误需要深入到TPM2.0模拟器中,并使用调试器单步调试从而理解TPM具体在哪里产生错误。通常情况下,调试TPM模拟器的代码之后,问题的答案就显而易见了。当然,更好地理解规范也有助于发现问题。人类的本性是,我们经常因为森林而分心,从而忘记应该砍下哪一棵树(意思就是我们常常迷失在上千页的规范中,不知所踪)。因此,使用调试器来调试模拟器的技术还是需要的。在Intel开发TPM2.0相关代码的过程中,本小节的作者曾经使用这个方法为自己和同事调试了许多错误。

分析错误代码

当你拿到一个错误代码时首先要做的就是解析它。解析的过程在TPM2.0规范第1部分的“Response Code Details”一节有介绍。这一小节中的“Response Code Evaluation”流程图特别有用。

第一个错误案例与TPM2_Startup命令相关。以下的代码片段会产生一个错误(这个代码是第7章中TPM2_Startup测试代码简单修改的版本):

rval = Tss2_Sys_Startup( sysContext, 03 );
CheckPassed(rval);

代码在执行CheckPassed时会失败,因为TPM2_Startup命令返回了错误代码0x000001C4而不是TPM2_RC_SUCCESS。根据之前提到的流程图,你可以按照以下方式解析这个错误代码:

Bit 8: 1
Bit 7: 1
Bit 6: 1

上述这些比特位表示错误码位于比特5:0,并且错误参数在比特11:8中,根据TPM2.0规范第1部分的“TPM_RC Values”小节的描述,具体的错误值是TPM_RC_VALUE。关于这个错误的文字描述是“值超出范围或者在当前上下文中这个值不正确”。这就意味着命令的参数1是错误的。查看TPM2_Startup命令的描述之后,很容易发现,这个命令允许的参数是TPM_SU_CLEAR(0x0000)和TPM_SU_STATE(0x0001)。显然,使用0x3作为命令参数是错误的来源。

我们强烈鼓励大家使用一个解析错误的工具(tpm2-tools工程下已经有很好用的错误解析工具了),因为手动解析错误在反复的调试中很伤脑筋。一个工具的示例输出如下:

>tpm2decoderring /e 1c4
ERROR: PARAM #1, TPM_RC_VALUE: value is out of range or is not correct for the context

调试跟踪分析

很多时候,因为缺乏TPM知识,或者有时候错误比较晦涩难懂,仅仅分析错误代码是不够的。另外,如果一个程序以前能工作,那用这个程序的运行输出与当前出问题程序的输出做比较,这有可能更快地发现错误。基于这个原因,我们强烈推荐为TPM设备驱动增加打印命令数据以及命令响应数据的代码。这个工具在过去两年的TPM2.0开发工作中帮我节省了几个星期的时间。

针对之前的代码示例,一个正常运行的程序中命令的跟踪信息转储(dump)如下:

Cmd sent: TPM2_Startup
Locality = 3
80 01 00 00 00 0c 00 00 01 44 00 00
Response Received:
80 01 00 00 00 0a 00 00 00 00
passing case: PASSED!

一个不正常的程序的跟踪信息转储如下:

cmd sent: TPM2_Startup
Locality = 3
80 01 00 00 00 0c 00 00 01 44 00 03
Response Received:
80 01 00 00 00 0a 00 00 01 c4
passing case: FAILED! TPM Error -- TPM Error: 0x1c4

使用一个好用的表工具就能够很快地发现出错命令数据中不正确的值,00 03。

上述方法的一个警告是, TPM的很多输出都是随机的,并且这些随机的输出又常常反过来作为其他命令的输入。这就意味着,在用可视化工具比较不同时,你需要忽略这些转储中的这些随机数据。经验将会帮助你快速定位哪些内容需要忽略,哪些应该集中关注。这实际上并没有听起来这么难。

另外一种使用跟踪信息转储的方式是,比较多种来自不同软件层级的跟踪信息。比方说,你可能会有一个针对ESAPI层的跟踪转储,一个来自于驱动层的跟踪转储,甚至是一个来自于TPM模拟器的跟踪转储。有时候将这几种转储信息同步起来就很有挑战性。因为会话nonce是随机的,唯一的,并且无论它们出现在哪一层软件中,值都是一样的,因此会话nonce可以用于同步这几种转储信息。首先在一个跟踪转储信息中找到一个nonce值,然后在其他跟踪转储中搜索这个唯一的nonce值。

更加复杂的错误

一个更加复杂的错误例子是关于HMAC授权的错误。这个错误是由错误代码TPM_RC_AUTH_FAIL来指示的。这个错误代码的描述是“HMAC授权检查失败,并且DA计数器加1”,错误代码中的高比特位指明了是哪一个会话中出现了这个HMAC错误。

不幸的是,调试这个错误并不容易。计算HMAC需要很多步骤:秘钥生成,输入输出参数做哈希,以及HMAC计算。上述步骤中又有很多输入参数:nonce,秘钥,以及授权会话类型等。这些数据中的任何一个出错,或者生成HMAC的步骤出错,都将导致HMAC不正确。

我们发现,唯一能够高效调试这些错误的方式是,增强代码的调试跟踪能力,将所有用于米哟啊生成,哈希,以及HMAC计算的输入输出信息打印出来。然后小心仔细地分析这些数据并与TPM规范仔细对比,这样通常就能发现失败的原因。这种类型的调试要求知道TPM2.0规范的细节——尤其是HMAC计算的细微差别。

最后的手段

最后一种类型的错误包括那些在调试时上述方法都不好用的错误。通常情况下这些错误在实现一个TPM新命令或者新功能的时候发生。此时没有之前可以工作的程序的调试跟踪信息可供对比,并且分析错误代码并不能定位问题。幸运的是,这种情况并不总是让人感到绝望;可以通过TPM模拟器很容易地调试这些错误。

这种类型的错误中一个常见的类型是方案错误,TPM_RC_SCHEME。这个错误表示密钥方案有问题,这个可能发生在创建密钥或者使用密钥的时候。密钥方案(机制)通常是一些结构体构成的联合,每个结构体都由多个数据域构成。许多关于怎么设置密钥方案的解释都不够明了,尤其是对于TPM2.0新手。

通常情况下,调试这些错误的最好方式是使用之前提到的调试技巧,在TPM2.0模拟器上运行代码,并单步调试。这种方式提供了一种从内部观察TPM所接收数据的方式,并了解TPM返回错误的原因。当然,这还要假设你能够访问到模拟器源码(现在这个已经不是问题了,随便google一下就好)。有了TPM的源码,你就可以单步调试模拟器并逐步找到错误的根源。

涉及的步骤如下:

  1. 在windows的VS中构建并启动TPM2.0模拟器(linux+gdb也可以哦)。参考第6章中相关的内容。选择“Debug”下拉菜单,选择“Start Debugging”来以调试模式启动模拟器。

  2. 将失败的程序转到模拟器上运行。最简单的方式是使用SAPI创建一个子程序,然后将子程序添加到SAPI测试程序列表中。这样一来,因为SAPI默认会使用模拟器来执行命令,所以你就不用额外开发TPM2.0的驱动来与模拟器通信,或者处理模拟器相关的平台命令,比如开启模拟器,设置Locality等等。同样还可以免费使用TAB和RM。如果不这样做,那你就得必须得自己完成这些工作。

  3. 在你选择的调试器中启动运行失败的程序,单步运行到失败的命令,然后停止。这可以通过单步运行和断点来实现。

  4. 在VS的模拟器运行实例中选择“Debug”下拉菜单中的“Break All”。

  5. 在模拟器中设置一个你一定会运行到的断点。如果你对模拟器不是很熟,那就在TPMCmdp.c文件的_rpc_Send_Command函数的位置设置断点。

  6. 选择Debug下拉菜单中的Continue再次启动模拟器。

  7. 在测试程序的调试器中执行合适的命令来让程序之前设置的断点位置继续执行。

  8. 模拟器会在你刚刚设置的断点处停下。从这里开始你就可以调试模拟器内部的子程序,并且最终找到TPM出错的原因。

常见的问题

现在我们已经讨论了TPM2.0程序的调试技术,下面我们将介绍一下常见的问题来源。再次强调一下,需要记住的是,这些内容全是我们在调试很底层的程序时获得的经验。这些问题是底层TPM2.0程序员有可能遇到的问题。这些问题可以分为以下几个类别:大小端,序列化、反序列化错误,错误的参数(包括之前提到的秘钥方案错误),以及授权错误。

当在小端系统上开发程序时(比如x86系统),在序列化和反序列化数据期间需要合理地修改大小端。这是一种很常见的错误,通常情况下可以通过仔细分析调试跟踪信息就可以发现这个问题。

序列化和反序列化错误与大小端关系比较密切,并且同样可以通过观察跟踪信息来调试。当然这需要理解TPM2.0规范的细节,特别是第2,3部分。

错误的参数,包括算法方案中的错误内容,有时候更难发现。调试这些问题需要详细地理解TPM2.0规范的所有三个部分内容。正因为如此,调试这些问题通常需要单步运行模拟器。

最后一种错误——授权错误,HMAC或者Policy——要求详细分析整个用于生成授权信息的软件。之前也提到了,增加额外的跟踪信息来显示被授权命令相关操作的所有输入输出信息,可以加速调试。

高层应用软件的调试

调试应用层软件,尤其是使用TSS-FAPI的应用,与调试底层软件如TSS本身所需的方法不同。这是因为这两种应用产生的预期错误是不同的。一个使用TSS的应用程序开发者不用处理由参数标准化和反标准化,命令和命令响应数据解析,以及不正确的数据流引起的错误。理由很简单:TSS软件库已经处理过这些问题。因此,正常情况下是不需要跟踪或者解析命令及命令响应数据字节流的。

从我们开发TPM1.2应用的经验来看(我们预期这种经验可以推广到TPM2.0),你应该从模拟器开始。并且这里我们不是指“当你遇到问题时从使用模拟器开始解决”,而是,一开始开发应用程序时就使用模拟器,而不是使用TPM硬件设备。这样可以有以下好处:

  • 首先,至少硬件TPM2.0可能比较匮乏,但是模拟器总是可用的。
  • 模拟器应该比硬件TPM运行要快,这在跑软件回归测试时很重要。性能的差异会在以下场景中显现出来:测试程序产生一个很大的RSA密钥,或者当NV空间被快速读写并且硬件TPM可能会限制写操作以防止写坏。
  • 模拟器和TSS通过TCP/IP套接字接口通信。这就允许你在一种操作系统上开发应用(这个系统上可能都没有TPM驱动),同时在另外一个平台上运行模拟器。
  • 通过删除状态文件就可以很容易地将TPM模拟器恢复出厂设置。但是取消一个硬件TPM的配置就困难很多:你不得不写(并且调试)一个取消配置的应用。
  • 正常的TPM安全保护并没有起作用(比如对平台组织架构的限制)。
  • 在不启动平台的情况下可以很容易地“启动”TPM模拟器。这就简化了持续性和电源管理相关问题的测试。同时也加速调试过程。
  • 最后,模拟器环境很容易搭建。

我们使用TPM1.2的经验是,一旦应用程序可以在模拟器上正常运行,它就可以在不做修改的情况下在硬件TPM上运行。

调试过程

与IBM的TPM1.2模拟器不同的是,微软的TPM2.0模拟器(TCG成员可以使用),没有跟踪功能。你不能简单地运行这个应用,然后读取模拟器的输出。TSS的实现是否会包含跟踪功能也不太清楚。TPM1.2的TSS,Trousers,除了命令和命令响应转储之外也没有更多的其他功能。

但是,模拟器的源码是可以看到的。所以,调试的过程与任何应用程序调试无二:

  1. 运行应用程序,产生错误后找到出错的命令。
  2. 在调试器中运行模拟器,在相应的命令设置断点。每个TPM2.0命令在模拟器代码中都有一个和TPM规范第3部分一样的C函数。
  3. 单步运行到命令内部,直到错误产生。(这里还是需要一些功夫的,因为即使是单行执行,有时候可能需要运行很多代码,花费大量时间)
  4. 有时候可能需要重新运行,然后单步调试到规范第4部分的子函数中,但是我们的经验是,通常不需要这样做。

典型的问题

这一小节展示了一些TPM1.2应用的一些问题,我们希望这些经验也适用于TPM2.0。我们也列出了对应TPM2.0预期的错误可能。

授权

TPM2.0的明文口令授权比较直接,所以比较容易调试。但是,HMAC授权错误是很常见的。调试的方法就是“分而治之”。跟踪命令(或者命令响应的)参数的哈希,HMAC密钥,以及HMAC值。如果参数哈希不同,那就说明计算哈希使用的参数与发送到TPM的参数有差异。如果HAMC密钥不同,很可能是使用了错误的口令或者选择了错误的实体。如果HMAC值不同,那有可能是在传递时出现错误或者是使用的salt和bind值不正确。

被禁用的函数

或者TPM1.2中最常出现的错误是,试图使用被禁用的函数。TPM1.2有禁用和关闭标志位,同时TPM2.0也包含这样的功能。

TPM2.0有一个额外的HMAC错误情况:实体被创建时,可能禁用了HMAC授权。具体请参考TPM2.0规范第1部分中的userWithAuth和adminWithPolicy属性。

缺失对象

一个典型的TPM1.2认知错误是,创建一个密钥就会返回被父密钥加密的密钥。但是实际上,创建密钥并不会加载密钥;而是有一个单独的命令来做密钥加载。

TPM2.0的一个额外情况是。TPM1.2SRK天生就是持续性的。TPM2.0的主密钥则都是临时性的,必须被显示地设置成持续性的。因为重启之后主密钥可能会丢失。

最后,一个对象可能已经被加载,但是可能(无意中已经被清除,或者是RM)不在TPM中了。 你可以在flush操作和失败的命令设置断点,然后看看对象是在什么时候被清除的。

类似的情况,TSS1.2的一个常见错误是资源泄露——对象(或者会话)被加载以后忘记清除,因此TPM最终会填满响应的存储槽。跟踪加载和清除这一对操作可能会发现缺少资源清除操作,这也使用于TSS2.0。

错误的类型

在TPM1.2中,密钥基本上都是签名密钥,或者加解密/存储密钥。单步调试到检查类型的函数中就应该可以发现相关的错误。

TPM2.0增加了限制性密钥的概念,这也就引入了两个新的可能的错误。首先是,一个限制性的密钥可能被错误地用于只有非限制性密钥可以使用的地方。第二,用户可能试图修改密钥算法,但是限制性密钥的算法在创建时就不能被修改了。

TPM1.2中仅有很少的算法填充机制,但是TPM2.0在算法上的变化要比1.2版本的多得多。在TPM2.0中,甚至PCR都有多种算法,这就有可能因此在扩展操作中出现错误。

另外, TPM2.0的NV空间有四种类型(普通,位域,扩展,和计数器)。这无疑将会导致更多的错误,比如试图将一个普通的数值写入一个位域NV索引中。

错误的大小

非对称秘钥操作对数据的大小有限制。一个常见的问题是,试图对超出秘钥和算法对应大小的数据签名或者解密(Unseal)。举例来说,一个RSA2048位的秘钥可以操作稍微小于256字节的数据。这里的“稍微”是因为填充以及OID(Object Identifier)也包括在内。

Policy

TPM2.0引入了Policy授权,Policy授权非常灵活但是可能也很难调试。幸运的是,TPM本身针对Policy有相应的调试辅助,TPM2_PolicyGetDigest。尽管在通常情况下你不会深入到TPM内部或者转储TPM内部数据结构的内容,但是这个命令是个例外,它就是用来干这个的。

回忆一下,假设我们有一个实体需要Policy授权,其内部有Policy摘要,这个摘要是预先计算好的并且在实体创建的时候就指定好了。Policy摘要的值是通过扩展每一个Policy声明来计算的。当Policy命令执行时,会话的摘要会被扩展。如果一切正常,会话中的摘要最终与Policy摘要匹配,然后这个实体就会被授权访问。

但是,在这一章我们的假设是,并不都是一切正常。最终的摘要并不相等,因此授权会失败。我们预计,调试的过程又将是“分而治之”。首先确认是哪一个Policy命令失败了,然后再找失败的原因。

有一些Policy命令,比如说TPM2_PolicySecret,是很直接的,因为一旦授权错误它们会立即返回错误。而剩下的命令——也就是延后的授权比如说TPM2_PolicyCommandCode——就比较难调试了,因为由这些命令产生的错误只有在后面真正授权时才会产生错误。

为了确定是哪一个Policy命令失败了,我们建议你将计算Policy哈希的过程保存下来。也就是说保存的这些值中,第一个值是0,然后是针对每一次Policy操作的中间哈希值,以及最后有一个最终的值(也就是Policy哈希)。然后,在Policy做授权验证的时候,每一次Policy命令之后都使用TPM2_PolicyGetDigest命令获取TPM中的Policy中间值。然后将获取的值(TPM)和期望的值(计算Policy时保存的值)做对比。第一个不匹配的地方就是问题所在。

一个作者的梦想是,一个优质的TSS FAPI的实现将会包含上述的步骤。它有一个Policy,一个XML文档,用于存储中间哈希值。它可以隐式地在每一次Policy命令之后执行一个TPM2_PolicyGetDigest命令。这样一来,错误消息会在每一个Policy命令之后立即出现,而不是直到真正使用的时候,并且这个时候只能返回一个通用的错误消息:“失败了,但是我不知道具体是哪里”。

找到Policy命令失败的原因要依赖具体的Policy命令。调试相应命令失败的原因就留给读者自己发挥吧。

总结

这一章描述了许多广为人知的TPM应用调试方法。希望这些内容能帮你在调试路上开个好头,但愿你能继续发掘更好的调试技术。