【技术分享】如何解密TFS的秘密变量

阅读量125243

|

发布时间 : 2017-08-02 18:20:44

x
译文声明

本文是翻译文章,文章来源:lowleveldesign.org

原文地址:https://lowleveldesign.org/2017/07/04/decrypting-tfs-secret-variables/

译文仅供参考,具体内容表达以及含义原文为准。

https://p4.ssl.qhimg.com/t015bb548c944192eec.png

译者:興趣使然的小胃

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

什么是 TFS(Team Foundation Server

Team Foundation Server 提供一组协作软件开发工具,它与你现有的 IDE 或编辑器集成,从而使你的跨功能团队可以在所有大小的软件项目上高效工作。

引用自https://www.visualstudio.com/zh-hans/tfs/

一、前言

TFS的每个生成(build)及发布(release)定义中都包含一组自定义变量,并且会在随后的build/release管道中使用这些变量作为参数用于PowerShell/批处理脚本、配置文件转换或者其他子任务。从一个任务中访问这些变量类似于访问进程环境变量。由于TFS使用了非常详细的日志记录,因此我们经常可以在build日志中发现明文形式的具体变量值。这也是微软使用秘密变量的原因之一。

TFS的生成配置面板示例如下所示,其中有个秘密变量amiprotected(注意图中高亮的小锁图标):

 http://p1.qhimg.com/t01a09be111c0d0f715.png

当秘密变量被保存之后,我们再也无法通过网页面板读取这个值(当你点击小锁图标时,文本框会被清空)。

当我们将秘密变量传递给PowerShell脚本并打印这个变量时,对应的输出日志如下所示:

 http://p1.qhimg.com/t011ec13bdc5ecd120a.png

现在让我们来探索一下这些秘密变量具体存储的位置及过程。


二、秘密变量存储的过程

为了分析TFS存储秘密变量的具体过程,我一开始检查的是TFS的数据库服务器。不同的团队集合都有独立的数据库与之对应。每个数据库会将表(table)分组为名字易于理解的schema,比如Build、Release或者TfsWorkItemTracking。此外,默认的dbo schema中也包含一些表。我们的秘密变量属于生成定义的一部分,因此我们可以来检查一下Build这个schema。tbl_Definition看上去像是个可行的方法。这个表的前几列如下所示:

 http://p2.qhimg.com/t01bb8824339dde018b.png

其中,Variables这一列以JSON格式列出了所有的变量。不幸的是,我无法了解关于我们的秘密变量的更多信息(除了得知它是个秘密变量之外)。因此,我开始搜索是否存在名字为“tbl_Variables”或者“tbl_Secrets”的表,但一无所获。抱着最后一丝希望,我使用了procmon这个工具,这个工具也是我在实在没办法的时候一直使用的一个工具。利用这个工具,我跟踪了秘密变量保存时的路径,在这个路径中发现了一些有趣的信息:

 http://p5.qhimg.com/t01ebf920985d3e4726.png

我选择其中一条记录,检查它的调用栈:

 http://p8.qhimg.com/t01eb791d5731329e93.png

现在我需要使用一个小技巧,来解码procmon跟踪路径中的frame信息。最后一个frame指向的是TeamFoundationStrongBoxService.AddStream方法。这个类的名称非常特别,因此我特意在数据库中查找了一下相关信息,很快就找到了一些较为有趣的表。

tbl_StrongBoxDrawer表:

 http://p2.qhimg.com/t016524bb444b7fd55b.png

tbl_StrongBoxItem表:

 http://p6.qhimg.com/t019dabebf83f1f61f4.png

以及tbl_SigningKey表:

 http://p3.qhimg.com/t0195190b0da429d0ac.png

正如你所看到的那样,tbl_StrongBoxDrawer表中的某一行引用了build的定义,同时对秘密变量进行了分组。tbl_StrongBoxItem表以加密形式存储了秘密变量所对应的所有值(包括我们当前的秘密变量)。最后,tbl_SigningKey表存储了某些私钥,这些私钥可能会在加密过程中使用。

现在是启动dnspy工具、开始更为细致分析的时候了。AddStream方法包含将敏感信息添加到TFS数据库的逻辑处理流程。首先,秘密变量值经过AES加密(使用了自动生成的密钥以及随机初始化的向量)并保存在一个byte数组中。其次,AES密钥经过签名密钥的加密,并被保存到另一个byte数组中。最后,这两个表以及初始化向量会被保存为与秘密向量有关的加密值。对于本文使用的例子来说,这个加密值如下所示:

 http://p1.qhimg.com/t01068c45fdd6d73c5c.png

而签名密钥会以RSA CSP数据块的形式保存,如下所示:

 http://p0.qhimg.com/t019119885bddd5dfb4.png

当你从数据库中提取这段数据后,你可以使用如下示例代码进一步解压这段数据:

byte[] aesKey;
 
using (RSACryptoServiceProvider rsaCryptoServiceProvider = new RSACryptoServiceProvider(2048,
    new CspParameters { Flags = CspProviderFlags.UseMachineKeyStore })) {
    var cspBlob = "...hex string...";
    rsaCryptoServiceProvider.ImportCspBlob(Hex.FromHexString(cspBlob));
 
    var encryptedAesKey = "...hex string...";
    aesKey = rsaCryptoServiceProvider.Decrypt(Hex.FromHexString(encryptedAesKey), true);
}
 
var iv = Hex.FromHexString("...hex string...");
var cipher = Hex.FromHexString("...hex string...");
 
using (var aes = Aes.Create()) {
    var transform = aes.CreateDecryptor(aesKey, iv);
    using (var encryptedStream = new MemoryStream(cipher)) {
        using (var cryptoStream = new CryptoStream(encryptedStream, transform, CryptoStreamMode.Read)) {
            using (var decryptedStream = new MemoryStream()) {
                cryptoStream.CopyTo(decryptedStream);
 
                Console.WriteLine(Hex.PrettyPrint(decryptedStream.ToArray()));
            }
        }
    }
}

如上所述,tbl_SigningKey表包含非常敏感的数据,因此你必须确保只有经过认证的用户才能访问这个表。TFS在这里竟然没有使用任何DPAPI或者其他加密机制,这让我非常惊讶。


三、揭开秘密变量的神秘面纱

前文中,我们利用存在数据库中相关数据成功解密了秘密变量,但实际上还有更为简单的方法。如果你可以修改build/release任务管道,那么你就可以很方便地创建一个PowerShell任务,如下所示:

 http://p0.qhimg.com/t011efc805da2f1decd.png

由于字符串中第一个字符与剩余字符之间有一个空格,因此输出不会被屏蔽,你可以以在日志中看到明文形式的密码:

 http://p1.qhimg.com/t014cbe51a1a8e5bd7f.png

因此,你可以得出结论,如果人们能够修改build/release任务管道,或者人们可以修改任务管道中使用的脚本内容,那么他们就能读取秘密变量的值。


四、从TFS内存中提取秘密变量

最后,让我们研究一下TFS服务器进程(w3wp.exe)的内存,看看我们能从中获得什么信息。我将这个进程的全部内存导出到一个文件中,然后使用WinDbg打开这个文件(WinDbg已事先加载了SOSEX以及SOS扩展)。先来列出StrongBoxItemInfo的所有实例信息:

0:000> !mx *.StrongBoxItemInfo
AppDomain 0000000001eb8b50 (/LM/W3SVC/2/ROOT/tfs-1-131555514123625000)
---------------------------------------------------------
module: Microsoft.TeamFoundation.Framework.Server
  class: Microsoft.TeamFoundation.Framework.Server.StrongBoxItemInfo
module: Microsoft.TeamFoundation.Client
  class: Microsoft.TeamFoundation.Framework.Client.StrongBoxItemInfo
…
 
0:000>  .foreach (addr {!DumpHeap -short -mt 000007fe92560f80}) { !mdt addr }
...
0000000203419dc8 (Microsoft.TeamFoundation.Framework.Server.StrongBoxItemInfo)
    <drawerid>k__BackingField:(System.Guid) {c2808c4d-0c0d-43e5-b4b2-e743f5121cdd} VALTYPE (MT=000007feef467350, ADDR=0000000203419df8)
    <itemkind>k__BackingField:0x00 (String) (Microsoft.TeamFoundation.Framework.Server.StrongBoxItemKind)
    <lookupkey>k__BackingField:000000020341a1f8 (System.String) Length=24, String="9/variables/amiprotected"
    <signingkeyid>k__BackingField:(System.Guid) {c2808c4d-0c0d-43e5-b4b2-e743f5121cdd} VALTYPE (MT=000007feef467350, ADDR=0000000203419e08)
    <expirationdate>k__BackingField:(System.Nullable`1[[System.DateTime, mscorlib]]) VALTYPE (MT=000007feef4c13b8, ADDR=0000000203419e18)
    <credentialname>k__BackingField:NULL (System.String)
    <encryptedcontent>k__BackingField:000000020341a3f0 (System.Byte[], Elements: 312)
    <value>k__BackingField:NULL (System.String)
    <fileid>k__BackingField:0xffffffff (System.Int32)
…

LookupKey这个字段包含变量的名称。为了将其与build/release定义结合起来,我们需要找到Id等于c2808c4d-0c0d-43e5-b4b2-e743f5121cdd的StrongBoxDrawerName实例。但我们的主要任务是解密EncryptedContent字段的值,为了完成这个任务,我们需要得到签名密钥(id: c2808c4d-0c0d-43e5-b4b2-e743f5121cdd)。TFS会在缓存中保存最近使用的签名密钥,因此如果我们运气不错的话,我们还是有可能在缓存中找到这个签名密钥。这个过程稍显漫长,首先,我们需要列出TeamFoundationSigningService+SigningServiceCache类的所有实例:

0:000> !mx *.TeamFoundationSigningService+SigningServiceCache
module: Microsoft.TeamFoundation.Framework.Server
  class: Microsoft.TeamFoundation.Framework.Server.TeamFoundationSigningService+SigningServiceCache
 
0:000> !DumpHeap -mt 000007fe932d99e0
         Address               MT     Size
...
0000000203645ce0 000007fe932d99e0       96

然后,一路跟踪对象树,找到与我们有关的那个私钥:

Microsoft.TeamFoundation.Framework.Server.TeamFoundationSigningService+SigningServiceCache
|-m_cacheData (00000002036460a0)
|-m_cache (0000000203646368)
|-m_dictionary (0000000203646398)
 
0:000> !mdt 0000000203646398 -e:2 -start:0 -count:2
…
[1] (…) VALTYPE (MT=000007fe969d7160, ADDR=0000000100d6d510)
key:(System.Guid) {c2808c4d-0c0d-43e5-b4b2-e743f5121cdd} VALTYPE (MT=000007feefdc7350, ADDR=0000000100d6d520)
 value:000000020390d440 
 
|-Value (000000020390d410)
|-<Value>k__BackingField (000000020390d3f0)
|-m_Item1 (000000020390d2b0)
 
0:000> !mdt 000000020390d2b0
000000020390d2b0 (Microsoft.TeamFoundation.Framework.Server.SigningServiceKey)
    <keydata>k__BackingField:000000020390ce00 (System.Byte[], Elements: 1172)
    <keytype>k__BackingField:0x0 (RSAStored) (Microsoft.TeamFoundation.Framework.Server.SigningKeyType)

其实一开始我们可以直接搜索SigningServiceKey类的实例。但如果这么做,我们需要打印出所有的GC根,逐一检查,核实其Id是否就是我们正在查找的那个Id。我发现这种自上而下的方法会更为容易一些(但这个方法还是稍显乏味)。


五、总结

希望这篇文章能对读者有所帮助。根据本文分析,我们认为还是有办法能够安全地保存TFS秘密变量中的敏感数据,但首先要做到以下几点:

1、只有授权用户可以修改build/release管道(包括使用秘密变量的任务的内容)。

2、只有授权用户可以访问TFS数据库中的tbl_SigningKey表。

此外,我们也可以使用精心配置的TFS代理,配合密钥库或者某些本地DPAPI加密块来执行所有的敏感任务。

本文翻译自lowleveldesign.org 原文链接。如若转载请注明出处。
分享到:微信
+10赞
收藏
興趣使然的小胃
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66