
تحليل CVE-2016-3959 وهجوم إثبات المفهوم ضد خادم SSH مكتوب بلغة Go.
Alex Mullins
9 أبريل 2016
مؤخرًا، تم اكتشاف خطأ في مكتبة خوارزمية التوقيع الرقمي (DSA) البرمجية في لغة البرمجة Go. في هذه المقالة سنستعرض تفاصيل هذا الخطأ وكيف يمكن للمهاجم استغلاله لشن هجوم حجب خدمة ضد خادم SSH قياسي مكتوب بلغة Go يستخدم مكتبة DSA الأساسية لمصادقة العملاء.
الذكر الأول لهذه الثغرة ظهر في منشور على القائمة البريدية لأمن المصادر المفتوحة (oss-sec) على الرابط http://seclists.org/oss-sec/2016/q2/11.
تحتوي لغة Go على حلقة لا نهائية في العديد من إجراءات الأعداد الصحيحة الكبيرة مما يجعل برامج Go عرضة لهجمات حجب الخدمة عن بُعد. البرامج التي تستخدم مصادقة عملاء HTTPS أو مكتبات خادم ssh في Go معرّضة لهذه الثغرة. تتم معالجة هذا في التغيير التالي: https://golang.org/cl/21533
-- Jason Buberel
باختصار، إذا تم استغلال هذه الثغرة، فقد تؤدي إلى حلقة لا نهائية في الكود الأساسي لمكتبة BigNum. سيؤدي ذلك إلى استنزاف موارد النظام من حيث وحدة المعالجة المركزية والذاكرة وقد يتسبب في النهاية في توقف البرنامج أو النظام نفسه عن الاستجابة.
البيان أعلاه يقول إن SSH وكذلك مصادقة عملاء HTTPS متأثران، ولكن بعد النظر إلى حزمتي crypto/tls و net/http في Go يبدو أن ذلك غير صحيح. مصادقة عملاء HTTPS يمكنها استخدام مخططي توقيع RSA أو ECDSA فقط، وليس DSA. انظر أدناه. إذا كنت مخطئًا بشأن هذا، فيرجى إخباري.
https://golang.org/pkg/crypto/tls/#Certificate
type Certificate struct {
Certificate [][]byte
// PrivateKey contains the private key corresponding to the public key
// in Leaf. For a server, this must implement crypto.Signer and/or
// crypto.Decrypter, with an RSA or ECDSA PublicKey. For a client
// (performing client authentication), this must be a crypto.Signer
// with an RSA or ECDSA PublicKey.
PrivateKey crypto.PrivateKey
... other fields
}
بعد فترة وجيزة من ظهور ذلك المنشور على القائمة البريدية oss-sec، تم إصدار رقم CVE: CVE-2016-3959. مطورو Go لديهم إصلاح جاهز لهذه الثغرة وسيظهر في الإصدارين 1.5.4 و 1.6.1 المقرر إصدارهما يوم الأربعاء 13 أبريل 2016؛ https://groups.google.com/forum/#!topic/golang-nuts/MmSbFHLPo8g.
لمتابعة أمثلة الكود في هذه المقالة، ستحتاج إلى تثبيت Go الإصدار 1.6. اتبع التعليمات على https://golang.org/doc/install. إذا كنت تريد تنزيل هذا المستند وأمثلة الكود، فستحتاج أيضًا إلى تثبيت Git. اتبع التعليمات على https://git-scm.com/book/en/v2/Getting-Started-Installing-Git. لاستنساخ المستودع، نفّذ الأمر التالي في الطرفية:
$ go get github.com/alexmullins/dsa
سيؤدي هذا إلى استنساخ المستودع في مساحة عمل Go الخاصة بك.
سيغطي القسم التالي تفاصيل الثغرة.
إذن ما هو الخطأ بالضبط؟ للإجابة على ذلك، يجب أن نعود إلى الإعلان الأصلي على القائمة البريدية oss-sec. لا توجد معلومات كثيرة هناك بخلاف شرح عام للمشكلة ورابط إلى إصلاح الكود على https://golang.org/cl/21533. رسالة الالتزام لذلك التغيير تحتوي على ما يلي:
crypto/dsa: eliminate invalid PublicKey early
For PublicKey.P == 0, Verify will fail. Don't even try.
--- Robert Griesemer
والكود المُصحح:
https://github.com/golang/go/blob/master/src/crypto/dsa/dsa.go#L247
// Verify verifies the signature in r, s of hash using the public key, pub. It
// reports whether the signature is valid.
//
// Note that FIPS 186-3 section 4.6 specifies that the hash should be truncated
// to the byte-length of the subgroup. This function does not perform that
// truncation itself.
func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool {
// FIPS 186-3, section 4.7
// Code fix added to check if the key parameters are sensible.
if pub.P.Sign() == 0 {
return false
}
if r.Sign() < 1 || r.Cmp(pub.Q) >= 0 {
return false
}
if s.Sign() < 1 || s.Cmp(pub.Q) >= 0 {
return false
}
w := new(big.Int).ModInverse(s, pub.Q)
n := pub.Q.BitLen()
if n&7 != 0 {
return false
}
z := new(big.Int).SetBytes(hash)
u1 := new(big.Int).Mul(z, w)
u1.Mod(u1, pub.Q)
u2 := w.Mul(r, w)
u2.Mod(u2, pub.Q)
v := u1.Exp(pub.G, u1, pub.P)
u2.Exp(pub.Y, u2, pub.P)
v.Mul(v, u2)
v.Mod(v, pub.P)
v.Mod(v, pub.Q)
return v.Cmp(r) == 0
}
لتلخيص رسالة الالتزام وإصلاح الكود أعلاه: في Go 1.6 والإصدارات السابقة، يوجد خطأ في دالة Verify في حزمة crypto/dsa. إذا استدعى أي شخص Verify مع تعيين معامل المفتاح العام P إلى 0، فسيؤدي ذلك إلى حلقة لا نهائية في إحدى العبارات في الأسفل داخل دالة Verify.
استطراد قصير لشرح DSA. DSA هي خوارزمية توقيع رقمي تستخدم التشفير غير المتماثل لتوقيع رسالة يمكن استخدامها لاحقًا لضمان أن الرسالة أُرسلت بالفعل من قبل المرسل/حامل المفتاح الخاص. مثال بسيط: ترسل أليس رسالة إلى بوب تخبره فيها بمكان وزمان لقائهما لتناول الغداء. لكن بوب يريد التأكد من أن أليس هي فعلاً من أرسلت له الرسالة وليس شخصًا آخر. لكي يعمل هذا، ستوقّع أليس الرسالة بمفتاحها الخاص، ويمكن لبوب التحقق من توقيع أليس باستخدام مفتاحها العام الذي يعرفه بوب. لا يمكن لأي شخص آخر غير حامل المفتاح الخاص توقيع رسالة يمكن التحقق منها لاحقًا بالمفتاح العام المقابل (هذه هي الفكرة على الأقل).
لكي يعمل DSA، يحتاج إلى 5 أرقام كبيرة. الأرقام الثلاثة الأولى تُعرف بمعاملات DSA P و Q و G. تحدد هذه المجموعة الأساسية ومولّد المجموعة. يمكن إنشاء هذه الأرقام بشكل صحيح باستدعاء dsa.GenerateParameters().
type Parameters struct {
P, Q, G *big.Int
}
آخر رقمين مطلوبين لـ DSA هما المفتاح الخاص X والمفتاح العام المقابل Y. يمكن أيضًا إنشاء هذين الرقمين باستدعاء dsa.GenerateKey().
type PrivateKey struct {
PublicKey
X *big.Int
}
type PublicKey struct {
Parameters
Y *big.Int
}
هذا كل ما تحتاج لمعرفته عن DSA للمتابعة. لمزيد من المعلومات، راجع معيار NIST: http://csrc.nist.gov/publications/fips/fips186-3/fips_186-3.pdf أو صفحة ويكيبيديا: https://en.wikipedia.org/wiki/Digital_Signature_Algorithm.
نعود إلى الموضوع؛ أين تقع الحلقة اللانهائية في دالة Verify؟ مع مزيد من البحث ستجد أن الكود يتعطل عند:
v := u1.Exp(pub.G, u1, pub.P)
هذا هو تعليق دالة Exp():
// Exp sets z = x**y mod |m| (i.e. the sign of m is ignored), and returns z.
// If y <= 0, the result is 1 mod |m|; if m == nil or m == 0, z = x**y.
// See Knuth, volume 2, section 4.6.3.
func (z *Int) Exp(x, y, m *Int) *Int {
مما سبق، يمكنك أن ترى أن التعليق يحدد أنه عندما تكون m == 0 فإنها سترفع إلى الأس دون اختزال معياري. عندما تأخذ رقمًا كبيرًا وترفعه إلى أس رقم كبير آخر، ستكون النتيجة أيضًا رقمًا ضخمًا حقًا. لست معتادًا جدًا على كيفية عمل math/big، لكن أعتقد أن هذا ما يحدث هنا. تعمل Exp() بجد على هذا الرفع إلى الأس الذي سيستغرق وقتًا طويلاً جدًا لإكماله (قد يكون بمثابة ما لا نهاية).
فيما يلي بعض الأرقام المستخدمة في استدعاء dsa.Verify() لدالة Exp() والتي تم جمعها من كود الاختبار أدناه:
x = 87134495734400160760614045850064869082246125869475484226357302998590334523718907040547736115253396811403341841812955872027275698952059512800196447089300992352859585665865224989740
07948775031938554271780506269767106717359222697821209685947889925442133804051298762702245652821695254167558015585995918548052076 (307 digits)
y = 751336012463178371212581620103057049388105279629 (48 digits)
z = x ^ y
باستخدام Wolfram Alpha، يمكنك الحصول على فكرة عن حجم هذا الرقم z. هناك "113406800566837208055789635448879116719378036793444 أو 1.13407x10^50 رقمًا عشريًا" في الرقم الناتج z. (ملاحظة: يمكن استخدام أول 150 رقمًا فقط من x مرفوعًا إلى y في مربع الإدخال على الويب في Wolfram Alpha، لذا فإن العدد الفعلي للأرقام أكبر حتى!) لإعطائك فكرة عن المقياس: يقدر العلماء عدد الذرات في الكون بحوالي 10^78 إلى 10^82 http://www.universetoday.com/36302/atoms-in-the-universe/.
مثال اختبار لتوقيع/تحقق DSA:
func generatePrivKey(t *testing.T) *dsa.PrivateKey {
// Create the DSA parameters
params := dsa.Parameters{}
err := dsa.GenerateParameters(¶ms, rand.Reader, dsa.L1024N160)
if err != nil {
t.Fatalf("failed to generate dsa parameters: %v", err)
}