65.9K
CodeProject 正在变化。 阅读更多。
Home

Intel® 防篡改工具包有助于保护 Scrypt 加密实用程序免遭逆向工程

2016年5月2日

CPOL

13分钟阅读

viewsIcon

8550

本文介绍了 Intel® Tamper Protection Toolkit 如何帮助保护基于密码的加密工具 (Scrypt 加密工具) [3] 中的关键代码和宝贵数据,抵御静态和动态的逆向工程及篡改。

Intel® Developer Zone 提供跨平台应用程序开发工具和操作指南、平台及技术信息、代码示例以及专家建议,帮助开发者创新并取得成功。加入我们的社区,了解 Android物联网Intel® RealSense™ 技术Windows,下载工具、访问开发套件、与志同道合的开发者交流想法,并参与黑客马拉松、竞赛、路演和本地活动。

引言

本文介绍了 Intel® Tamper Protection Toolkit 如何帮助保护基于密码的加密工具 (Scrypt 加密工具) [3] 中的关键代码和宝贵数据,抵御静态和动态的逆向工程及篡改。Scrypt [4] 是一种现代化的安全基于密码的密钥派生函数,广泛应用于注重安全的软件中。在 [2] 中描述了一个对 Scrypt 的潜在威胁:攻击者可以通过强制使用特定参数来生成弱密钥。Intel® Tamper Protection Toolkit 可用于帮助缓解此威胁。我们将解释如何重构相关代码并将篡改防护应用于该工具。

在本文中,我们将讨论 Intel Tamper Protection Toolkit 的以下组件:

  • Iprot。 一个创建自修改和自加密代码的混淆工具。
  • 加密库。 一个提供 iprot 兼容的基础加密操作实现的库:加密哈希函数、带密钥的哈希消息认证码 (HMAC) 和对称加密算法。

您可以在 https://software.intel.com/en-us/tamper-protection 下载 Intel Tamper Protection Toolkit。

Scrypt 加密工具迁移到 Windows

由于 Scrypt 加密工具的目标平台是 Linux*,而我们希望展示如何在 Windows* 上使用 Intel Tamper Protection Toolkit,因此我们的第一个任务是将 Scrypt 加密工具移植到 Windows。平台相关的代码将用以下条件指令框起来。

#if defined(WIN_TP)
// Windows-specific code
#else
// Linux-specific code
#endif  // defined(WIN_TP)

示例 1:条件指令的基本结构

WIN_TP 预处理符号用于本地化 Windows 特定的代码。对于 Windows 构建,应定义 WIN_TP;否则,将选择参考代码进行构建。

我们使用 Microsoft Visual Studio* 2013 来构建和调试该工具。Windows 和 Linux 在进程、线程、内存、文件管理、基础设施服务和用户界面等方面存在差异。我们必须解决这些差异才能进行迁移,详细描述如下。

  1. 该工具使用 getopt() 来处理命令行参数。程序参数列表请参阅 [2] 中的 Scrypt 加密工具部分。getopt() 函数从 unitstd.h POSIX OS 头文件中访问。我们使用了开源项目 getopt_port [1] 中的 get_opt() 实现。从该项目中获取的两个新文件 getopt.h 和 getopt.c 已添加到我们的源代码树中。
  2. 另一个函数 gettimeofday(),存在于 POSIX API 中,用于帮助该工具测量 salsa opps,即每秒执行的 salsa20/8 操作次数。该工具需要 salsa opps 指标来选择安全的配置 N、r 和 p 作为输入参数,以便 Scrypt 算法执行至少所需的最小数量的 salsa20/8 操作,从而避免暴力破解攻击。我们将 gettimeofday() 的实现 [5] 添加到 scryptenc_cpuperf.c 文件中。
  3. 在工具开始配置算法之前,它会通过调用 POSIX 系统函数 getrlimit(RLIMIT_DATA, …) 来询问操作系统允许用于派生的可用 RAM 量。对于 Windows,进程数据段(已初始化数据、未初始化数据和堆)的最大大小的软硬限制均设置为 4 GB。
    /* ... RLIMIT_DATA... */
    #if defined(WIN_TP)
    rl.rlim_cur = 0xFFFFFFFF;
    rl.rlim_max = 0xFFFFFFFF;
    if((uint64_t)rl.rlim_cur < memrlimit) {
    	memrlimit = rl.rlim_cur;
    }
    #else
    if (getrlimit(RLIMIT_DATA, &rl))
    	return (1);
    if ((rl.rlim_cur != RLIM_INFINITY) &&
         ((uint64_t)rl.rlim_cur < memrlimit))
    	memrlimit = rl.rlim_cur;
    #endif  // defined(WIN_TP)

    示例 2:RLIMIT 数据将进程限制为 4GB

  4. 此外,还添加了 MSVS 编译器指令,用于内联 sysendian.h 中的函数。
    #if defined(WIN_TP)
    static __inline uint32_t
    #else
    static inline uint32_t
    #endif  // WIN_TP
    be32dec(const void *pp);

    示例 3:添加 sysendian.h 内联函数

  5. 我们迁移了 tarsnap_readpass(…) 函数,该函数处理并屏蔽通过终端检索密码。该函数关闭回显并在终端上用空格屏蔽密码。密码存储在内存缓冲区中,并发送到后续函数。
    /* If we're reading from a terminal, try to disable echo. */
    #if defined(WIN_TP)
    if ((usingtty = _isatty(_fileno(readfrom))) != 0) {
    	GetConsoleMode(hStdin, &mode);
    	if (usingtty)
    		mode &= ~ENABLE_ECHO_INPUT;
    	else
    		mode |= ENABLE_ECHO_INPUT;
    	SetConsoleMode(hStdin, mode);
    }
    #else
    if ((usingtty = isatty(fileno(readfrom))) != 0) {
    	if (tcgetattr(fileno(readfrom), &term_old)) {
    		warn("Cannot read terminal settings");
    		goto err1;
    	}
    	memcpy(&term, &term_old, sizeof(struct termios));
    	term.c_lflag = (term.c_lflag & ~ECHO) | ECHONL;
    	if (tcsetattr(fileno(readfrom), TCSANOW, &term)) {
    		warn("Cannot set terminal settings");
    		goto err1;
    	}
    }
    #endif  // defined(WIN_TP)

    示例 4:通过终端进行密码控制

  6. 在原始的 getsalt() 中,盐值由从 Linux 特殊文件 /dev/urandom 读取的伪随机数构成。在 Windows 上,我们建议使用 rdrand() 指令来读取从 Ivy Bridge 微架构开始的 Intel® Xeon® 和 Intel® Core™ 处理器系列上可用的硬件随机数生成器。C 标准伪随机生成器未被使用,因为 getsalt() 与 Intel Tamper Protection Toolkit 混淆工具不兼容。函数 getsalt() 应通过混淆工具进行保护,以抵御静态和动态篡改及逆向工程,因为该函数生成的盐值在 [2] 的 Scrypt 加密工具部分中被归类为敏感数据。下面的示例展示了用于填充盐值的随机数生成代码的原始版本和移植版本。
    #if defined(WIN_TP)
    	uint8_t i = 0;
    
    	for (i = 0; i < buflen; i++, buf++)
    	{
    		_rdrand32_step(buf);
    	}
    #else
    	/* Open /dev/urandom. */
    	if ((fd = open("/dev/urandom", O_RDONLY)) == -1)
    		goto err0;
    	/* Read bytes until we have filled the buffer. */
    	while (buflen > 0) {
    		if ((lenread = read(fd, buf, buflen)) == -1)
    			goto err1;
    		/* The random device should never EOF. */
    		if (lenread == 0)
    			goto err1;
    		/* We're partly done. */
    		buf += lenread;
    		buflen -= lenread;
    	}
    	/* Close the device. */
    	while (close(fd) == -1) {
    		if (errno != EINTR)
    			goto err0;
    	}
    #endif defined(WIN_TP)

    示例 5:原始和移植的随机数生成代码

使用 Intel® Tamper Protection Toolkit 保护工具

现在我们将修改工具的设计和代码,以帮助保护 [2] 中的基于密码的密钥派生部分中威胁模型中已识别的敏感数据。通过使用 Intel Tamper Protection Toolkit 中的混淆编译器 iprot 进行代码混淆来实现对敏感数据的保护。仅混淆那些创建、处理和使用敏感数据的函数是合理的。

从 [2] 的代码混淆部分可知,iprot 以动态库 (.dll) 作为输入,并生成一个二进制文件,其中仅包含命令行中指定的混淆导出函数。因此,我们将所有处理敏感数据的函数放入一个动态库中进行混淆,而将命令行解析和密码读取等其他函数保留在主可执行文件中。

图 1 显示了受保护工具的新设计。该工具分为两部分:主可执行文件和一个待混淆的动态库。主可执行文件负责解析命令行,并将密码和输入文件读取到内存缓冲区中。动态库包含导出函数,如 scryptenc_filescryptdec_file,它们处理敏感数据(N、r、p、salt)。

动态库使用的关键数据结构是 Scrypt 上下文,它存储有关 Scrypt 参数 N、r、p 和 salt 的 HMAC 摘要信息。上下文中的 HMAC 摘要用于确定上下文的最新更改是否由受信任的函数(如 scrypt_ctx_enc_initscrypt_ctx_dec_initscryptenc_filescryptdec_file)完成,这些函数具有用于重新签名和验证上下文的 HMAC 密钥。由于我们打算使用混淆工具混淆这些受信任的函数,因此它们将能够抵抗修改。新增的两个函数 scrypt_ctx_enc_initscrypt_ctx_dec_init 用于在加密和解密模式下初始化 Scrypt 上下文。

图 1:受保护 Scrypt 加密工具的设计。

加密流程

  1. 该工具使用 getopt() 来处理命令行参数。程序参数列表请参阅 [2] 中的基于密码的密钥派生函数部分。
  2. 用于加密的输入文件和密码将被读取到内存缓冲区中。
  3. 主可执行文件调用 scrypt_ctx_enc_init 来初始化 Scrypt 上下文,以便通过 maxmem、maxmemfrac 和 maxtime 等命令行选项,为指定 CPU 时间和 RAM 大小计算安全的 Scrypt 参数(N、r、p 和 salt)进行密钥派生。在此调用结束时,初始化函数会创建一个 HMAC 摘要,包括新更新的状态,以防止在函数返回时被篡改。初始化函数还将返回应用程序必须分配以进行加密的内存量。
  4. 该工具在主可执行文件中根据初始化函数返回的大小动态分配内存。
  5. 可执行文件第二次调用 scrypt_ctx_enc_init。该函数通过哈希 MAC 摘要来验证 Scrypt 上下文的完整性。如果完整性验证通过,该函数会将已分配位置的缓冲区位置设置为上下文,并更新 HMAC。文件读取和动态内存分配在可执行文件中完成,以避免动态库中出现 iprot 不兼容的代码。包含系统调用和 C 标准函数的代码会生成混淆器不支持的间接跳转和重定位。
  6. 可执行文件调用 scryptenc_file 来使用用户提供的密码加密文件。该函数会验证 Scrypt 上下文中用于密钥派生的参数(N、r、p 和 salt)的完整性。如果验证通过,它将调用 Scrypt 算法来派生密钥。然后使用派生出的密钥进行加密。导出函数的输出与原始 Scrypt 工具的输出相同。这意味着输出具有相似的哈希值,用于加密数据的完整性验证和解密过程中的密码正确性。

解密流程

  1. 该工具使用 getopt() 来处理命令行参数。程序参数列表请参阅 [2] 中的基于密码的密钥派生部分。
  2. 用于解密的输入文件和密码将被读取到内存缓冲区中。
  3. 主可执行文件调用 scrypt_ctx_dec_init 来检查加密文件数据中提供的参数是否有效,以及密钥派生函数是否可以在允许的内存和 CPU 时间内计算。
  4. 该工具在主可执行文件中根据初始化函数返回的大小动态分配内存。
  5. 可执行文件第二次调用 scrypt_ctx_dec_init。该函数执行的操作与加密情况相同。
  6. 可执行文件调用 scryptdec_file 来使用密码解密文件。该函数会验证 Scrypt 上下文中用于密钥派生的参数(N、r、p 和 salt)的完整性。如果验证通过,它将调用 Scrypt 算法来派生密钥。该函数使用加密数据中的哈希值来验证密码的正确性和加密数据的完整性。

在受保护的工具中,我们将 Advanced Encryption Standard in CTR 模式密码和密钥哈希函数的 OpenSSL* 实现替换为 Intel Tamper Protection Toolkit 加密库的实现。与 OpenSSL* 不同,该加密库满足 iprot 的所有代码限制,并且可以在不进行进一步修改的情况下从混淆的代码中使用。AES 密码在 scryptenc_filescryptdec_file 中被调用,以使用从密码派生的密钥来加密/解密输入文件。密钥哈希函数由导出函数(scrypt_ctx_enc_initscrypt_ctx_dec_initscryptenc_filescryptdec_file)调用,以在使用 Scrypt 上下文之前验证其数据完整性。在受保护的工具中,动态库的所有导出函数都已使用 iprot 进行混淆。Intel Tamper Protection Toolkit 帮助我们实现了缓解 [2] 中基于密码的密钥派生部分定义的威胁的目标。

我们的解决方案是重新设计的工具,其中包含一个 iprot 混淆的动态库。这能够抵抗上述确定的攻击,并且可以证明 Scrypt 上下文只能由导出函数更新,因为它们拥有重新计算上下文中 HMAC 摘要的 HMAC 私钥。此外,这些函数和 HMAC 密钥已通过混淆器保护,免受篡改和逆向工程。此外,其他敏感数据(如 Scrypt 生成的密钥)也受到保护,因为它们是在混淆的导出函数 scryptenc_filescryptdec_file 中派生的。混淆编译器生成的代码在运行时是自加密的,并能抵抗篡改和调试。

让我们看看 scrypt_ctx_enc_init 如何保护 Scrypt 上下文的代码。主可执行文件在调用 scrypt_ctx_enc_init 的同时通过指针传递 buf_p。如果指针等于 null,则该函数是第一次调用;否则,它是第二次调用。在第一次调用初始化时,它会选择 Scrypt 参数,计算 HMAC 摘要,并返回 Scrypt 计算所需的内存量,如下所示。

// Execute for the first call when it returns memory size required by scrypt
	if (buf_p == NULL) {  		
// Pick parameters for scrypt and initialize the scrypt context
		// <...> 

		// Compute HMAC
		itp_res = itpHMACSHA256Message((unsigned char *)ctx_p, sizeof(scrypt_ctx)-								sizeof(ctx_p->hmac),
							hmac_key, sizeof(hmac_key),
							ctx_p->hmac, sizeof(ctx_p->hmac));

		*buf_size_p = (r << 7) * (p + (uint32_t)N) + (r << 8) + 253;
	}

示例 6:第一次调用保护 Scrypt 上下文的代码

在第二次调用时,指向已分配内存的 buf_p 会被传递给 scrypt_ctx_enc_init 函数。使用上下文中的 HMAC 摘要,该函数验证上下文的完整性,并确保在第一次和第二次调用之间没有人更改它。之后,它使用 buf_p 初始化上下文内的地址,并重新计算 HMAC 摘要,因为上下文已更改,如下所示。

// Execute for the second call when memory for scrypt is allocated
	if (buf_p != NULL) {
		// Verify HMAC
		itp_res = itpHMACSHA256Message(
(unsigned char *)ctx_p, sizeof(scrypt_ctx)-sizeof(ctx_p->hmac),
			hmac_key, sizeof(hmac_key),
			hmac_value, sizeof(hmac_value));
		if (memcmp(hmac_value, ctx_p->hmac, sizeof(hmac_value)) != 0) {
			return -1;
		}

		// Initialize pointers to buffers for scrypt computation:
// ctx_p->addrs.B0 = …

		// Recompute HMAC
		itp_res = itpHMACSHA256Message(
			(unsigned char *)ctx_p, sizeof(scrypt_ctx)-sizeof(ctx_p->hmac),
			hmac_key, sizeof(hmac_key),
			ctx_p->hmac, sizeof(ctx_p->hmac));
	}

示例 7:第二次调用保护 Scrypt 上下文的代码

从 [2] 中我们知道 iprot 对输入代码施加了一些限制,以便能够混淆。它要求没有重定位和没有间接跳转。C 语言中带有全局变量、系统函数和 C 标准函数调用的编码结构可能会生成重定位和间接跳转。示例 7 中的代码调用了一个 C 标准函数 memcmp,这会导致代码与 iprot 不兼容。因此,我们实现了自己的 C 标准函数,如 memcmpmemsetmemmove,供该工具使用。此外,动态库中的所有全局变量都被转换为局部变量,并在堆栈上处理已初始化的数据。

此外,我们在使用双精度浮点数的代码混淆时遇到了一个问题,这个问题没有在教程中涵盖,也没有在 Intel Tamper Protection Toolkit 用户指南中记录。如下所示,在 pickparams 函数中,salsa20/8 的核心操作限制是双精度浮点类型,等于 32768。该值不在堆栈上初始化,编译器将该值放入二进制文件的数据段中,从而在代码中生成重定位。

	double opslimit;
#if defined(WIN_TP)
	// unsigned char d_32768[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x40};
	unsigned char d_32768[sizeof(double)];
	d_32768[0] = 0x00;
	d_32768[1] = 0x00;
	d_32768[2] = 0x00;
	d_32768[3] = 0x00;
	d_32768[4] = 0x00;
	d_32768[5] = 0x00;
	d_32768[6] = 0xE0;
	d_32768[7] = 0x40;
	double *var_32768_p = (double *) d_32768;
#endif

	/* Allow a minimum of 2^15 salsa20/8 cores. */
#if defined(WIN_TP)
	if (opslimit < *var_32768_p)
		opslimit = *var_32768_p;
#else
	if (opslimit < 32768)
		opslimit = 32768;
#endif

示例 8:iprot 兼容的双精度浮点变量的代码

我们通过在堆栈上初始化一个字节序列来解决这个问题,该字节序列带有与此双精度浮点值在内存中的十六进制表示匹配的十六进制转储,并创建一个指向该序列的双精度浮点指针。

要使用 iprot 混淆动态库,我们使用以下命令:

iprot scrypt-dll.dll scryptenc_file scryptdec_file scrypt_ctx_enc_init scrypt_ctx_dec_init -c 512 -d 2600 -o scrypt_obf.dll

受保护工具的接口保持不变。让我们比较未混淆的代码和混淆后的版本。下面显示了两个版本之间存在显著差异的反汇编代码。

# non-obfuscated code
scrypt_ctx_enc_init PROC NEAR
        push    ebp                              ; 10030350 _ 55
        mov     ebp, esp                         ; 10030351 _ 8B. EC
        sub     esp, 100                         ; 10030353 _ 83. EC, 64
        mov     dword ptr [ebp-4H], 0  ; 10030356 _ C7. 45, FC, 00000000
        mov     eax, 1                           ; 1003035D _ B8, 00000001
        imul    ecx, eax, 0                      ; 10030362 _ 6B. C8, 00
        mov     byte ptr [ebp+ecx-1CH], 1 ; 10030365 _ C6. 44 0D, E4, 01
        mov     edx, 1                           ; 1003036A _ BA, 00000001
        shl     edx, 0                           ; 1003036F _ C1. E2, 00
        mov     byte ptr [ebp+edx-1CH], 2 ; 10030372 _ C6. 44 15, E4, 02
        mov     eax, 1                           ; 10030377 _ B8, 00000001
        shl     eax, 1                           ; 1003037C _ D1. E0
        mov     byte ptr [ebp+eax-1CH], 3 ; 1003037E _ C6. 44 05, E4, 03
        mov     ecx, 1                           ; 10030383 _ B9, 00000001
<…>
# obfuscated code with default parameters
scrypt_ctx_enc_init PROC NEAR
        mov     ebp, esp                     ; 1000100E _ 8B. EC
        sub     esp, 100                     ; 10001010 _ 83. EC, 64
        mov     dword ptr [ebp-4H], 0        ; 10001013 _ C7. 45, FC, 00000000
        mov     eax, 1                       ; 1000101A _ B8, 00000001
        imul    ecx, eax, 0                  ; 1000101F _ 6B. C8, 00
        mov     byte ptr [ebp+ecx-1CH], 1    ; 10001022 _ C6. 44 0D, E4, 01
        push    eax                          ; 10001027 _ 50
        pop     eax                          ; 1000102D _ 58
        lea     eax, [eax+3FFFD3H]           ; 1000102E _ 8D. 80, 003FFFD3
        mov     dword ptr [eax], 608469404   ; 10001034 _ C7. 00, 2444819C
        mov     dword ptr [eax+4H], -124000508 ; 1000103A _ C7. 40, 04, F89BE704
        mov     dword ptr [eax+8H], -443981569 ; 10001041 _ C7. 40, 08, E58960FF
        mov     dword ptr [eax+0CH], 1633409 ; 10001048 _ C7. 40, 0C, 0018EC81
        mov     dword ptr [eax+10H], -477560832 ; 1000104F _ C7. 40, 10, E3890000
<…>

示例 9:非混淆和混淆版本的反汇编代码

混淆会降低性能,并且动态库的大小会显著增加。混淆器允许开发者通过单元大小和变异距离来平衡安全性和性能。当前的混淆使用了 512 字节的单元大小和 2600 字节的变异距离。单元是原始二进制文件中的指令子序列。混淆代码中的单元在指令指针即将进入它之前是加密的。当单元完全执行后,它会再次被加密。

Intel Tamper Protection Toolkit 协助保护的工具的源代码将很快在 GitHub 上提供。

致谢

我们感谢 Raghudeep Kannavara 最初提出将 Intel Tamper Protection Toolkit 应用于 Scrypt 加密工具的想法,并感谢 Andrey Somsikov 的多次有益讨论。

参考文献

  1. K. Grasman. GitHub 上的 getopt_port https://github.com/kimgr/getopt_port/
  2. R. Kazantsev, D. Katerinskiy, and L. Thaddeus. Understanding Intel® Tamper Protection Toolkit and Scrypt Encryption Utility, Intel Developer Zone, 2016.
  3. C. Percival. The Scrypt Encryption Utility. http://www.tarsnap.com/scrypt/scrypt-1.1.6.tgz
  4. C. Percival and S. Josefsson (2012-09-17). The Scrypt Password-Based Key Derivation Function. IETF.
  5. W. Shawn. Freebsd sources on GitHub https://github.com/lattera/freebsd

关于作者

Roman Kazantsev 是 Intel Corporation 软件与服务集团的员工。Roman 在软件工程领域拥有 7 年以上的专业经验。他的专业兴趣集中在密码学、软件安全和计算机科学。他目前担任软件工程师一职,其持续的任务是为所有 Intel 平台提供内容保护的加密解决方案和专业知识。他拥有俄罗斯下诺夫哥罗德国立大学计算机科学专业的荣誉学士和硕士学位。

Denis Katerinskiy 是 Intel Corporation 软件与服务集团的员工。他拥有 2 年的软件开发经验。他的主要兴趣包括编程、性能优化、算法开发、数学和密码学。作为一名软件开发工程师,Denis 开发 Intel 架构的软件模拟器。Denis Katerinskiy 目前在托木斯克国立大学攻读计算机科学学士学位。

Thaddeus Letnes 是 Intel Corporation 软件与服务集团的员工。他在软件开发领域拥有 15 年以上的专业经验。他的主要兴趣包括底层系统、语言和工程实践。作为一名开发软件开发工具的软件工程师,Thaddeus 与软件开发者、架构师和项目经理紧密合作,以生产高质量的开发工具。Thaddeus 拥有 Knox College 计算机科学学士学位。

© . All rights reserved.