
Análisis de CVE-2016-3959 y un ataque de Prueba de Concepto contra un servidor SSH en Go.
Alex Mullins
9 de abril de 2016
Recientemente, se descubrió un error en la biblioteca criptográfica del Algoritmo de Firma Digital (DSA) para el lenguaje de programación Go. En este artículo revisaremos los detalles del error y cómo un atacante podría aprovecharlo para iniciar un ataque de denegación de servicio contra un servidor SSH estándar de Go que utiliza la biblioteca DSA subyacente para autenticar clientes.
La primera mención de esta vulnerabilidad apareció en una publicación en la Lista de Correo de Seguridad de Código Abierto (oss-sec) en http://seclists.org/oss-sec/2016/q2/11.
Go tiene un bucle infinito en varias rutinas de enteros grandes que hace que los programas Go sean vulnerables a ataques remotos de denegación de servicio. Los programas que utilizan autenticación de cliente HTTPS o las bibliotecas del servidor SSH de Go están expuestos a esta vulnerabilidad. Esto se está abordando en el siguiente CL: https://golang.org/cl/21533
-- Jason Buberel
En resumen, si esta vulnerabilidad fuera explotada, podría llevar a un bucle infinito en el código subyacente de la biblioteca BigNum. Esto consumirá recursos del sistema en términos de CPU y memoria y podría eventualmente hacer que el programa o el sistema mismo dejen de responder.
La declaración anterior dice que SSH junto con la autenticación de cliente HTTPS están afectados, pero después de examinar los paquetes crypto/tls y net/http de Go, eso parece incorrecto. La autenticación de cliente HTTPS puede usar esquemas de firma RSA o ECDSA, pero no DSA. Vea a continuación. Si me equivoco al respecto, por favor hágamelo saber.
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
}
Poco después de que apareciera esa publicación en la lista de correo oss-sec, se emitió un número CVE: CVE-2016-3959. Los mantenedores de Go tienen una solución lista para esto y aparecerá en las versiones 1.5.4 y 1.6.1 que se lanzarán el miércoles 13 de abril de 2016; https://groups.google.com/forum/#!topic/golang-nuts/MmSbFHLPo8g.
Para seguir los ejemplos de código en este artículo necesitará tener instalada la versión 1.6 de Go. Siga las instrucciones en https://golang.org/doc/install. Si desea descargar este documento y los ejemplos de código, también necesitará tener Git instalado. Siga las instrucciones en https://git-scm.com/book/en/v2/Getting-Started-Installing-Git. Para clonar el repositorio, ejecute el siguiente comando en una terminal:
$ go get github.com/alexmullins/dsa
Esto clonará el repositorio en su espacio de trabajo de Go.
La siguiente sección cubrirá los detalles de la vulnerabilidad.
Entonces, ¿qué es exactamente lo que está mal? Para responder eso, uno debe remontarse al anuncio original en la lista de correo oss-sec. No hay mucha información allí aparte de una explicación general del problema y un enlace a la corrección del código en https://golang.org/cl/21533. El mensaje de confirmación de ese cambio contiene lo siguiente:
crypto/dsa: eliminar PublicKey inválido temprano
Para PublicKey.P == 0, Verify fallará. Ni siquiera lo intentes.
--- Robert Griesemer
y el código corregido:
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
}
Para resumir el mensaje de confirmación y la corrección del código anterior: en Go 1.6 y versiones anteriores hay un error en la función Verify del paquete crypto/dsa. Si alguien llama a Verify con el parámetro de clave pública P establecido en 0, causará un bucle infinito en una de las declaraciones más adelante en la función Verify.
Un breve desvío para explicar DSA. DSA es un algoritmo de firma digital que utiliza criptografía asimétrica para firmar un mensaje que luego puede usarse para garantizar que el mensaje fue realmente enviado por el remitente/titular de la clave privada. Un ejemplo simple: Alicia envía un mensaje a Bob diciéndole dónde y cuándo deben encontrarse para almorzar. Bob, sin embargo, quiere asegurarse de que realmente sea Alicia quien le envió el mensaje y no otra persona. Para que esto funcione, Alicia firmará el mensaje con su clave privada y Bob podrá verificar la firma de Alicia con su clave pública que Bob conoce. Nadie más que el titular de la clave privada puede firmar un mensaje que luego pueda ser verificado por la clave pública correspondiente (esa es la idea al menos).
Para funcionar, DSA necesita 5 números grandes. Los primeros 3 números se conocen como los parámetros DSA P, Q y G. Estos definen el grupo subyacente y el generador del grupo. Estos números pueden crearse adecuadamente con una llamada a dsa.GenerateParameters().
type Parameters struct {
P, Q, G *big.Int
}
Los últimos dos números necesarios para DSA son la clave privada X y la clave pública correspondiente Y. Estos números también pueden crearse con una llamada a dsa.GenerateKey().
type PrivateKey struct {
PublicKey
X *big.Int
}
type PublicKey struct {
Parameters
Y *big.Int
}
Eso es todo lo que necesita saber sobre DSA para seguir adelante. Para más información, consulte el estándar NIST: http://csrc.nist.gov/publications/fips/fips186-3/fips_186-3.pdf o la página de Wikipedia: https://en.wikipedia.org/wiki/Digital_Signature_Algorithm.
Volviendo al tema; ¿dónde está el bucle infinito en la función Verify? Con un poco más de investigación encontrará que el código se cuelga en:
v := u1.Exp(pub.G, u1, pub.P)
Este es el comentario para el método 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 {
De lo anterior, puede ver que el comentario especifica que cuando m == 0, se exponenciará sin reducción modular. Cuando toma un número grande y lo exponencia con otro número grande, el resultado también será un número REALMENTE GRANDE. No estoy muy familiarizado con cómo funciona math/big, pero creo que eso es lo que está sucediendo aquí. Exp() está procesando esta exponenciación que tomará muchísimo tiempo en completarse (bien podría ser infinito).