
大家好,我是良许。
最近几年,随着物联网设备的爆发式增长,嵌入式系统的安全问题越来越受到重视。
我记得几年前做汽车电子项目的时候,客户对安全的要求还比较宽松,但现在不一样了,几乎每个项目都会被问到"你们的加密方案是什么?
""如何防止固件被破解?"这些问题。
今天就和大家聊聊嵌入式安全和加密技术这个话题。
嵌入式设备不像服务器那样有专人维护,它们往往部署在各种环境中,面临的威胁也更加多样化。
我在做汽车电子的时候就遇到过这样的案例:有黑客通过CAN总线注入恶意指令,导致车辆的某些功能异常。
这还只是冰山一角,实际上嵌入式系统面临的威胁包括:
物理攻击:攻击者可以直接接触到设备,通过JTAG、UART等调试接口读取内存数据,甚至可以用显微镜观察芯片内部结构进行侧信道攻击。
我见过有人用热风枪把Flash芯片拆下来,直接用编程器读取固件。
网络攻击:现在的嵌入式设备大多联网,黑客可以通过网络漏洞入侵设备。
比如很多智能家居设备使用弱密码或者默认密码,很容易被攻破。
固件逆向:攻击者获取固件后,可以通过反汇编、反编译等手段分析代码逻辑,找出加密算法和密钥,甚至可以修改固件植入后门。
供应链攻击:在生产、运输、维护等环节,设备可能被植入恶意代码或硬件木马。
对于我们做嵌入式开发的来说,安全不再是可选项,而是必选项。
特别是在汽车、医疗、工控等关键领域,一旦出现安全问题,后果不堪设想。
我之前在外企做项目的时候,有一次因为安全测试没通过,整个项目延期了三个月,损失了上百万。
所以现在我做项目,都会在设计阶段就把安全考虑进去,而不是等到最后才想起来打补丁。
对称加密是最常用的加密方式,加密和解密使用同一个密钥。
在嵌入式系统中,AES(高级加密标准)是最流行的对称加密算法。
AES加密原理:AES支持128位、192位、256位三种密钥长度,加密过程包括字节替换、行移位、列混淆、轮密钥加等步骤。
对于嵌入式系统来说,AES-128通常就足够了,因为它在安全性和性能之间取得了很好的平衡。
下面是一个使用STM32的硬件AES加密的示例代码:
#include "stm32f4xx_hal.h"
CRYP_HandleTypeDef hcryp;
// 初始化AES硬件加密模块
void AES_Init(void)
{
__HAL_RCC_CRYP_CLK_ENABLE();
hcryp.Instance = CRYP;
hcryp.Init.DataType = CRYP_DATATYPE_8B;
hcryp.Init.KeySize = CRYP_KEYSIZE_128B;
hcryp.Init.Algorithm = CRYP_AES_ECB;
if (HAL_CRYP_Init(&hcryp) != HAL_OK)
{
Error_Handler();
}
}
// AES加密函数
HAL_StatusTypeDef AES_Encrypt(uint8_t *plaintext, uint8_t *key,
uint8_t *ciphertext, uint16_t length)
{
HAL_StatusTypeDef status;
// 设置密钥
hcryp.Init.pKey = (uint32_t *)key;
if (HAL_CRYP_Init(&hcryp) != HAL_OK)
{
return HAL_ERROR;
}
// 执行加密
status = HAL_CRYP_Encrypt(&hcryp, (uint32_t *)plaintext,
length/4, (uint32_t *)ciphertext, 1000);
return status;
}
// AES解密函数
HAL_StatusTypeDef AES_Decrypt(uint8_t *ciphertext, uint8_t *key,
uint8_t *plaintext, uint16_t length)
{
HAL_StatusTypeDef status;
hcryp.Init.pKey = (uint32_t *)key;
if (HAL_CRYP_Init(&hcryp) != HAL_OK)
{
return HAL_ERROR;
}
status = HAL_CRYP_Decrypt(&hcryp, (uint32_t *)ciphertext,
length/4, (uint32_t *)plaintext, 1000);
return status;
}这段代码展示了如何使用STM32的硬件加密模块进行AES加密和解密。
使用硬件加密的好处是速度快、功耗低,而且密钥不会暴露在软件中,安全性更高。
我在实际项目中,只要芯片支持硬件加密,都会优先使用。
非对称加密使用一对密钥:公钥和私钥。
公钥用于加密,私钥用于解密。
最常用的非对称加密算法是RSA和ECC(椭圆曲线加密)。
RSA vs ECC:在嵌入式系统中,我更倾向于使用ECC而不是RSA,原因很简单:ECC在相同安全强度下,密钥长度更短,计算速度更快,占用的存储空间也更小。
比如256位的ECC密钥,安全强度相当于3072位的RSA密钥。
下面是一个使用mbedTLS库进行ECC签名验证的示例:
#include "mbedtls/ecdsa.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
// ECC签名验证函数
int verify_firmware_signature(uint8_t *firmware, size_t fw_len,
uint8_t *signature, size_t sig_len)
{
int ret;
mbedtls_ecdsa_context ctx;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
uint8_t hash[32];
// 初始化
mbedtls_ecdsa_init(&ctx);
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&ctr_drbg);
// 计算固件的SHA256哈希
mbedtls_sha256(firmware, fw_len, hash, 0);
// 加载公钥(这里假设公钥已经存储在设备中)
// 实际项目中,公钥通常烧录在OTP区域或者安全存储区
ret = load_public_key(&ctx);
if (ret != 0)
{
goto cleanup;
}
// 验证签名
ret = mbedtls_ecdsa_read_signature(&ctx, hash, sizeof(hash),
signature, sig_len);
cleanup:
mbedtls_ecdsa_free(&ctx);
mbedtls_entropy_free(&entropy);
mbedtls_ctr_drbg_free(&ctr_drbg);
return ret;
}这个函数用于验证固件的数字签名,确保固件没有被篡改。
在实际项目中,我会在Bootloader阶段就进行签名验证,只有验证通过的固件才允许运行。
哈希算法用于生成数据的"指纹",常用的有SHA-256、SHA-3等。
哈希算法有两个重要特性:单向性(不可逆)和抗碰撞性(很难找到两个不同的输入产生相同的输出)。
在嵌入式系统中,哈希算法主要用于:
#include "mbedtls/sha256.h"
// 计算固件的SHA256哈希值
void calculate_firmware_hash(uint8_t *firmware, size_t length,
uint8_t *hash_output)
{
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0); // 0表示SHA-256
mbedtls_sha256_update(&ctx, firmware, length);
mbedtls_sha256_finish(&ctx, hash_output);
mbedtls_sha256_free(&ctx);
}
// 验证固件完整性
int verify_firmware_integrity(uint8_t *firmware, size_t length,
uint8_t *expected_hash)
{
uint8_t calculated_hash[32];
calculate_firmware_hash(firmware, length, calculated_hash);
// 比较计算出的哈希值和预期的哈希值
if (memcmp(calculated_hash, expected_hash, 32) == 0)
{
return 0; // 验证通过
}
else
{
return -1; // 验证失败
}
}密钥的安全存储是整个加密系统的基础。
如果密钥泄露,再强的加密算法也没用。
在嵌入式系统中,密钥存储有几种方案:
硬编码在代码中:这是最不安全的方式,但我见过很多项目都这么做。
攻击者只要反编译固件,就能轻松找到密钥。
千万不要这样做!
存储在Flash中:比硬编码稍好一点,但Flash的内容可以被读取出来。
如果一定要存在Flash中,至少要对密钥进行加密存储,使用一个主密钥来加密其他密钥。
存储在OTP区域:OTP(One-Time Programmable)区域只能写入一次,写入后无法修改,而且通常有读保护功能。
这是比较安全的方式,我在项目中经常使用。
使用安全芯片:最安全的方式是使用专门的安全芯片(如TPM、SE等)来存储密钥。
这些芯片有防篡改机制,即使攻击者物理接触到芯片,也很难提取出密钥。
在实际应用中,我们通常不会直接使用原始密钥,而是通过密钥派生函数(KDF)从主密钥派生出多个子密钥。
这样做的好处是:
#include "mbedtls/hkdf.h"
// 使用HKDF派生密钥
int derive_key(uint8_t *master_key, size_t master_key_len,
uint8_t *info, size_t info_len,
uint8_t *derived_key, size_t derived_key_len)
{
const mbedtls_md_info_t *md_info;
md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
return mbedtls_hkdf(md_info, NULL, 0,
master_key, master_key_len,
info, info_len,
derived_key, derived_key_len);
}
// 示例:派生不同用途的密钥
void generate_session_keys(uint8_t *master_key)
{
uint8_t encryption_key[16];
uint8_t authentication_key[32];
// 派生加密密钥
derive_key(master_key, 32,
(uint8_t *)"encryption", 10,
encryption_key, 16);
// 派生认证密钥
derive_key(master_key, 32,
(uint8_t *)"authentication", 14,
authentication_key, 32);
}安全启动是嵌入式系统安全的第一道防线。
它的核心思想是:在系统启动的每个阶段,都验证下一阶段代码的完整性和真实性,形成一条信任链。
信任链的建立:
下面是一个简化的安全启动流程示例:
#define APP_START_ADDR 0x08010000
#define APP_SIZE 0x00070000
#define SIGNATURE_ADDR 0x08080000
typedef void (*app_func_t)(void);
// 安全启动主函数
void secure_boot(void)
{
uint8_t *app_code = (uint8_t *)APP_START_ADDR;
uint8_t *signature = (uint8_t *)SIGNATURE_ADDR;
int ret;
// 1. 验证应用程序签名
ret = verify_firmware_signature(app_code, APP_SIZE,
signature, 64);
if (ret != 0)
{
// 签名验证失败,进入安全模式
enter_safe_mode();
return;
}
// 2. 验证应用程序哈希值
uint8_t expected_hash[32];
uint8_t calculated_hash[32];
// 从安全存储区读取预期的哈希值
read_expected_hash(expected_hash);
calculate_firmware_hash(app_code, APP_SIZE, calculated_hash);
if (memcmp(expected_hash, calculated_hash, 32) != 0)
{
// 哈希验证失败
enter_safe_mode();
return;
}
// 3. 验证通过,跳转到应用程序
app_func_t app = (app_func_t)(*(uint32_t *)(APP_START_ADDR + 4));
// 设置应用程序的栈指针
__set_MSP(*(uint32_t *)APP_START_ADDR);
// 跳转到应用程序
app();
}
// 安全模式处理
void enter_safe_mode(void)
{
// 记录错误日志
log_security_error();
// 可以尝试恢复出厂固件
// 或者等待通过安全通道更新固件
while(1)
{
// 闪烁LED指示错误
HAL_GPIO_TogglePin(ERROR_LED_GPIO_Port, ERROR_LED_Pin);
HAL_Delay(500);
}
}对于需要网络通信的嵌入式设备,使用TLS(传输层安全)或DTLS(数据报传输层安全)协议是必须的。
TLS用于TCP连接,DTLS用于UDP连接。
在嵌入式Linux系统中,我通常使用mbedTLS库来实现TLS通信。
下面是一个简单的TLS客户端示例:
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
int tls_client_connect(const char *server_addr, const char *server_port)
{
int ret;
mbedtls_net_context server_fd;
mbedtls_ssl_context ssl;
mbedtls_ssl_config conf;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
// 初始化
mbedtls_net_init(&server_fd);
mbedtls_ssl_init(&ssl);
mbedtls_ssl_config_init(&conf);
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&ctr_drbg);
// 初始化随机数生成器
ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func,
&entropy, NULL, 0);
if (ret != 0)
{
return -1;
}
// 连接到服务器
ret = mbedtls_net_connect(&server_fd, server_addr,
server_port, MBEDTLS_NET_PROTO_TCP);
if (ret != 0)
{
return -1;
}
// 设置SSL配置
mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT);
mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg);
// 设置SSL上下文
mbedtls_ssl_setup(&ssl, &conf);
mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send,
mbedtls_net_recv, NULL);
// 执行SSL握手
while ((ret = mbedtls_ssl_handshake(&ssl)) != 0)
{
if (ret != MBEDTLS_ERR_SSL_WANT_READ &&
ret != MBEDTLS_ERR_SSL_WANT_WRITE)
{
return -1;
}
}
// 握手成功,可以开始安全通信
return 0;
}对于一些资源受限的设备,可能无法使用完整的TLS协议。
这时候可以使用消息认证码(MAC)来保证消息的完整性和真实性。
常用的MAC算法有HMAC(基于哈希的消息认证码)。
#include "mbedtls/md.h"
// 计算HMAC
int calculate_hmac(uint8_t *key, size_t key_len,
uint8_t *message, size_t msg_len,
uint8_t *mac_output)
{
const mbedtls_md_info_t *md_info;
md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
return mbedtls_md_hmac(md_info, key, key_len,
message, msg_len, mac_output);
}
// 验证消息的HMAC
int verify_message_hmac(uint8_t *key, size_t key_len,
uint8_t *message, size_t msg_len,
uint8_t *received_mac)
{
uint8_t calculated_mac[32];
calculate_hmac(key, key_len, message, msg_len, calculated_mac);
if (memcmp(calculated_mac, received_mac, 32) == 0)
{
return 0; // 验证通过
}
else
{
return -1; // 验证失败
}
}即使使用了加密技术,如果攻击者能够轻松逆向你的代码,找出加密算法的实现细节,安全性还是会大打折扣。
因此,代码混淆和加固也很重要。
代码混淆技术:
防调试技术:
再安全的系统也可能存在漏洞,因此需要有安全的固件更新机制。
我在项目中实现OTA(Over-The-Air)更新时,会遵循以下原则:
双区更新:Flash分为两个区域,一个运行区,一个备份区。
更新时先把新固件写入备份区,验证通过后再切换。
这样即使更新失败,也能回退到旧版本。
增量更新:对于大型固件,可以只传输变化的部分,减少传输时间和流量消耗。
安全通道:固件传输必须使用加密通道,防止中间人攻击。
签名验证:新固件必须经过签名验证,确保来自可信的源。
// OTA更新流程
int ota_update(uint8_t *new_firmware, size_t fw_size,
uint8_t *signature)
{
// 1. 验证签名
if (verify_firmware_signature(new_firmware, fw_size,
signature, 64) != 0)
{
return -1; // 签名验证失败
}
// 2. 写入备份区
if (write_to_backup_area(new_firmware, fw_size) != 0)
{
return -2; // 写入失败
}
// 3. 验证写入的数据
if (verify_backup_area(new_firmware, fw_size) != 0)
{
return -3; // 验证失败
}
// 4. 设置启动标志,下次启动时切换到新固件
set_boot_flag(BOOT_FROM_BACKUP);
// 5. 重启系统
NVIC_SystemReset();
return 0;
}安全事件的记录和审计也很重要。我在项目中会记录以下事件:
这些日志可以帮助我们及时发现安全问题,分析攻击手段。
当然,日志本身也需要保护,防止被篡改。
嵌入式安全是一个系统工程,需要从硬件、软件、通信等多个层面来考虑。
我这些年做项目的经验告诉我,安全不是事后补救,而应该在设计阶段就融入到系统中。
虽然增加安全机制会带来一些额外的开发工作量和成本,但相比于出现安全问题后的损失,这些投入是完全值得的。
最后给大家几点建议:第一,不要自己发明加密算法,使用经过验证的标准算法。
第二,密钥管理是重中之重,一定要妥善保护。
第三,保持学习,安全技术在不断发展,攻击手段也在不断进化,我们需要持续关注最新的安全动态。
希望这篇文章能够帮助大家更好地理解嵌入式安全和加密技术,在实际项目中应用这些知识,开发出更安全可靠的嵌入式系统。
更多编程学习资源
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。