บทที่ 1 ในการเขียนโปรแกรมที่ปลอดภัย: อย่าใช้ IV ของคุณซ้ำ

May 10 2023
ฉันเขียนบทความเกี่ยวกับช่องโหว่ล่าสุดของ Samsung [ที่นี่] และความคิดเห็นหนึ่งกล่าวว่า … “มันเป็นบั๊กเก่า การใช้ IV (Initialisation Vectors) ซ้ำดูเป็นปัญหาพื้นฐานมาก” จากความคิดเห็นนี้ ความคิดเห็นอาจไม่ลงรายละเอียดเพียงพอ ดังนั้นฉันจะพยายามอธิบาย "จุดบกพร่อง" และหวังว่าจะแสดงให้เห็นว่าเป็นการเขียนโค้ดที่แย่อย่างน่าตกใจ … เกือบจะประมาทเลินเล่อในแง่ของการป้องกัน และอาจมองเห็นได้ด้วยซ้ำ เป็นประตูลับโดยเจตนา
ภาพถ่ายโดย Med Badr Chemmaoui บน Unsplash

ฉันเขียนบทความเกี่ยวกับช่องโหว่ล่าสุดของ Samsung [ ที่นี่ ] และความคิดเห็นหนึ่งกล่าวว่า … “มันเป็นบั๊กเก่า การใช้ IV (Initialisation Vectors) ซ้ำดูเหมือนเป็นปัญหาพื้นฐานมาก” จากความคิดเห็นนี้ ความคิดเห็นอาจไม่ลงรายละเอียดเพียงพอ ดังนั้นฉันจะพยายามอธิบาย "จุดบกพร่อง" และหวังว่าจะแสดงให้เห็นว่าเป็นการเขียนโค้ดที่แย่อย่างน่าตกใจเกือบจะประมาทเลินเล่อในแง่ของการป้องกัน และอาจมองเห็นได้ด้วยซ้ำ เป็นประตูลับโดยเจตนา

และสำหรับ "ปัญหาพื้นฐานมาก" นั้นควรเป็น "การเข้ารหัสที่แย่มาก" และไม่ควรพบเห็น "ข้อบกพร่อง" นี้ ในสภาพแวดล้อมที่เชื่อถือได้ แสดงให้เห็นถึงการขาดความรู้เกือบทั้งหมดเกี่ยวกับวิธีการทำงานของการเข้ารหัสด้วยช่องโหว่สำหรับผู้เริ่มต้น กระดาษอยู่ที่นี่ [1]:

ในความเป็นจริง มันเหมือนกับ WEP อีกครั้ง และที่วิธี WEP Wifi มี IV ขนาดเล็ก (Initialisation Vector) และเมื่อเปิดตัว เป็นไปได้ที่เพียงแค่ XOR cipher streams และค้นพบข้อความธรรมดา โปรแกรมสลีปสามารถแคร็กจุดเชื่อมต่อของ Cisco ได้ในเวลาน้อยกว่าหนึ่งวัน โชคดีที่ตอนนี้เราใช้ WPA-2 และไม่มี IV ที่ใช้ซ้ำ

ฉันหวังว่าจะแสดงให้เห็นว่าเราควรกังวลหากรหัสเช่นนี้เข้าใกล้อุปกรณ์ของผู้ใช้ อันที่จริง ถ้าเคยมีประตูหลังในโทรศัพท์มือถือ อาจเป็นประตูหลังก็ได้

หากคุณต้องการอ่านเกี่ยวกับ "จุดบกพร่อง" ลองที่นี่:

Crypto Bug ในอุปกรณ์ Samsung Galaxy: การทำลายสภาพแวดล้อมการดำเนินการที่เชื่อถือได้ (TEE)

"ข้อผิดพลาด" ที่ไม่ดี

ตอนนี้ฉันจะอธิบายว่า "จุดบกพร่อง" นี้เลวร้ายเพียงใด หากคุณสนใจเรื่องความปลอดภัยในโลกไซเบอร์ หวังว่าคุณจะรู้ว่า AES GCM เป็นรหัสสตรีม ด้วยวิธีนี้ เราจะใช้ค่าคีย์ลับและค่าเกลือ (IV — Initialisation Vector) และสร้างคีย์สตรีมหลอกที่ไม่มีที่สิ้นสุด ข้อความธรรมดาของเราเป็นเพียง XOR-ed พร้อมคีย์สตรีมเพื่อสร้างข้อความรหัสของเรา:

ค่าเกลือควรเป็นค่าสุ่มเสมอ เนื่องจากค่าเกลือคงที่จะสร้างคีย์สตรีมเดียวกันสำหรับข้อความธรรมดาเดียวกันเสมอ และตำแหน่งที่เราสามารถเปิดเผยคีย์สตรีมโดยสตรีมรหัส XOR-ing และในที่สุดก็เปิดเผยข้อความธรรมดา ในกรณีของการรวมคีย์ ข้อความธรรมดาเป็นคีย์เข้ารหัส ดังนั้นคีย์เข้ารหัสที่ใช้โดย TEE จะถูกเปิดเผย

หากเราใช้ IV ซ้ำ อีฟจะสามารถสตรีมรหัส XOR ร่วมกันและเปิดเผยคีย์สตรีม (K) จากที่นั่นเธอสามารถถอดรหัสสตรีมรหัสทุกรายการ แต่เพียงแค่ XOR-ing สตรีมรหัสด้วย K

การเข้ารหัส

AES GCM (โหมดเคาน์เตอร์ Galois) เป็นโหมดเข้ารหัสสตรีมสำหรับ AES มันขึ้นอยู่กับโหมด CTR แต่แปลงเป็นรหัสสตรีม ซึ่งให้เวลาแฝงต่ำในกระบวนการเข้ารหัส/ถอดรหัสและประมวลผลได้รวดเร็ว นอกจากนี้ยังผสานรวมโหมด AEAD สำหรับการรับรองความถูกต้อง แต่เนื่องจาก GCM เป็นโหมดการเข้ารหัสแบบสตรีม จึงเปิดให้ ใช้ การโจมตีIV ซ้ำได้ ด้วยเหตุนี้ IV (Initialization Vector) ของรหัสจะเหมือนกันสำหรับข้อความเข้ารหัสสองข้อความ จากนั้นเราสามารถ XOR ไปยังสตรีมรหัสทั้งสองพร้อมกันเพื่อเปิดเผยรหัสสตรีมรหัส ( K ) จากนั้นเราสามารถเปิดเผยข้อความธรรมดาโดย XOR-ing สตรีมรหัสใด ๆด้วยK

มาลองโค้ดเพื่อทำสิ่งนี้กัน ในกรณีนี้ ฉันจะใช้ Golang เพื่อแสดงหลักการพื้นฐานของวิธีการ ฉันจะใช้คีย์แบบคงที่ในกรณีนี้ (เนื่องจากจะไม่เปลี่ยนแปลงภายใน TEE) ของ “0123456789ABCDEF” (16 ไบต์ — คีย์ 128 บิต) และไม่มีค่าคงที่ของ “0123456789AB” (12 ไบต์ — 96 บิต) [ ที่นี่ ]:

package main
import (
 "crypto/aes"
 "crypto/cipher"
 "fmt"
 "os"
)
func xor(a, b []byte, length int) []byte {
 c := make([]byte, len(a))
 for i := 0; i < length; i++ {
  c[i] = a[i] ^ b[i]
 }
 return (c)
}
func main() {
 nonce := []byte("0123456789AB")
 key := []byte("0123456789ABCDEF")
 block, err := aes.NewCipher(key)
 if err != nil {
  panic(err.Error())
 }
 msg1 := "hello"
 msg2 := "Hello"
 argCount := len(os.Args[1:])
 if argCount > 0 {
  msg1 = (os.Args[1])
 }
 if argCount > 1 {
  msg2 = (os.Args[2])
 }
 plaintext1 := []byte(msg1)
 plaintext2 := []byte(msg2)
 aesgcm, err := cipher.NewGCM(block)
 if err != nil {
  panic(err.Error())
 }
 ciphertext1 := aesgcm.Seal(nil, nonce, plaintext1, nil)
 ciphertext2 := aesgcm.Seal(nil, nonce, plaintext2, nil)
 xor_length := len(ciphertext1)
 if len(ciphertext1) > len(ciphertext2) {
  xor_length = len(ciphertext2)
 }
 ciphertext_res := xor(ciphertext1, ciphertext2, xor_length)
 fmt.Printf("Message 1:\t%s\n", msg1)
 fmt.Printf("Message 2:\t%s\n", msg2)
 fmt.Printf("Cipher 1:\t%x\n", ciphertext1)
 fmt.Printf("Cipher 2:\t%x\n", ciphertext2)
 fmt.Printf("Key:\t\t%x\n", key)
 fmt.Printf("Nonce:\t\t%x\n", nonce)
 fmt.Printf("XOR:\t\t%x\n", ciphertext_res)
 plain1, _ := aesgcm.Open(nil, nonce, ciphertext1, nil)
 plain2, _ := aesgcm.Open(nil, nonce, ciphertext2, nil)
 fmt.Printf("Decrypted:\t%s\n", plain1)
 fmt.Printf("Decrypted:\t%s\n", plain2)
}

Message 1:	hello
Message 2:	Hello
Cipher 1:	7fcbe7378c2b87a5dfb2803d4fcaca8d5cde86dbfa
Cipher 2:	5fcbe7378cf8c68b82a2b8d705354e8d6c0502cef2
Key:		30313233343536373839414243444546
Nonce:		303132333435363738394142
XOR:		2000000000d3412e5d1038ea4aff840030db841508
Decrypted:	hello
Decrypted:	Hello

Message 1: hello
Message 2: Cello
Cipher 1: 7fcbe7378c2b87a5dfb2803d4fcaca8d5cde86dbfa
Cipher 2: 54cbe7378c5638db82df34a46172abed62b887aa48
Key:  30313233343536373839414243444546
Nonce:  303132333435363738394142
XOR:  2b000000007dbf7e5d6db4992eb861603e660171b2
Decrypted: hello
Decrypted: Cello

บทสรุป

นี่เป็นการเขียนโค้ดที่แย่มากและฉันไม่คิดว่าเด็กจบใหม่จะใช้งานในระดับนี้ หากทีมพัฒนาที่สร้างโค้ดภายใน TEE ไม่เข้าใจการโจมตีแบบใช้ซ้ำ IV พวกเขาจำเป็นต้องเข้าคอร์สอบรมการเขียนโค้ดที่ปลอดภัยก่อนที่จะสัมผัสกับโค้ดที่เชื่อถือได้อีก หากนี่เป็นการลับหลังโดยเจตนา นั่นก็เป็นอีกเรื่องหนึ่ง ฉันหวังว่ามันเป็นแค่ข้อบกพร่อง แต่เราจำเป็นต้องปรับปรุงความรู้ของเราในการสร้าง TEE เนื่องจากสิ่งเหล่านี้ทำงานภายในระบบบนคลาวด์ด้วย

อ้างอิง

[1] Shakevsky, A., Ronen, E., & Wool, A. (2022) Trust Dies in Darkness: ฉายแสงให้กับการออกแบบ TrustZone Keymaster ของ Samsung เอกสารลับ ePrint วิทยาการเข้ารหัสลับ