Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Schwachstellen und Sicherheitslücken

Schwachstellen und Sicherheitslücken

Slides from my talk about "security weaknesses and vulnerabilities" at PyCon.DE 2013 in Cologne.

Christian Heimes

October 15, 2013
Tweet

More Decks by Christian Heimes

Other Decks in Programming

Transcript

  1. whoami • Python developer (seit 2002, Zope, Plone, Python.NET) •

    Python Core Developer (seit 2007) • Python Security Response Team (seit 2011) • PEP 370, PEP 452, PEP 456 • Coverity Scan • sha3-Modul, XML Schwachstellen, SSL-Verbesserungen... • Entwickler und DevOps bei s<e>mantics GmbH / Aachen
  2. Was bedeutet „Sicherheit“ • Authentifizierung • Autorisierung • Accounting •

    Vertraulichkeit • Integrität / Unversehrtheit • Unleugbarkeit • Abstreitbarkeit • Zuverlässigkeit • Stabilität • Nachvollziehbarkeit • Bedrohungsanalyse • Auditierung
  3. Coverity Scan • 890 fixed • 108 intentional • 72

    false positives • Analyse seit 2006 • nur C-Code von CPython • Python 3.4 dev
  4. readline() import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("localhost", 12345)) s.listen(1)

    while 1: conn, addr = s.accept() print("Connection from {}".format(addr)) with conn.makefile() as f: print(f.readline()) conn.close()
  5. readline() $ echo "line" | nc localhost 12345 $ echo

    "line 2" | nc localhost 12345 $ ./line_server.py Connection from ('127.0.0.1', 56390) line Connection from ('127.0.0.1', 56391) line 2
  6. readline() fixed import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("localhost", 12345))

    s.listen(1) MAX_LINE = 1024 while 1: conn, addr = s.accept() print("Connection from {}".format(addr)) with conn.makefile() as f: line = f.readline(MAX_LINE + 1) if len(line) > MAX_LINE: raise ValueError("line too long") print(line) conn.close()
  7. CVE-2013-1752 • Issue #16037 httplib (Adrien Kunysz) • Issue #16038

    ftplib • Issue #16039 imaplib • Issue #16040 nntplib • Issue #16041 poplib • Issue #16042 smtplib behoben in Python 2.6.9 und zukünftige Versionen
  8. Python ssl - Module • Wrapper für OpenSSL • Keine

    Validierung von CA-Certs (Grundeinstellung) • geplant für Python 3.4 – CA Certificate Bundle von NSS (Mozilla) – Anbindung an CA-Store von Windows und Mac OS X – X.509 Typ, CA Chain API
  9. X509_check_ca() static int check_ca(const X509 *x) { /* keyUsage if

    present should allow cert signing */ if(ku_reject(x, KU_KEY_CERT_SIGN)) return 0; if(x->ex_flags & EXFLAG_BCONS) { if(x->ex_flags & EXFLAG_CA) return 1; /* If basicConstraints says not a CA then say so */ else return 0; } else { /* we support V1 roots for... uh, I don't really know why. */ if((x->ex_flags & V1_ROOT) == V1_ROOT) return 3; /* If key usage present it must have certSign so tolerate it */ else if (x->ex_flags & EXFLAG_KUSAGE) return 4; /* Older certificates could have Netscape-specific CA types */ else if (x->ex_flags & EXFLAG_NSCERT && x->ex_nscert & NS_ANY_CA) return 5; /* can this still be regarded a CA certificate? I doubt it */ return 0; } }
  10. Homoglyphischer Angriff >>> import unicodedata >>> for c in 'Руthοn':

    ... print(unicodedata.name(c)) ... CYRILLIC CAPITAL LETTER ER CYRILLIC SMALL LETTER U LATIN SMALL LETTER T LATIN SMALL LETTER H GREEK SMALL LETTER OMICRON LATIN SMALL LETTER N
  11. time of check / time of use Script mit Root-Rechten

    import os filename = 'example.txt' if not os.path.isfile(filename): # file does not exist, create it with open(filename, 'w') as f: f.write('example')
  12. time of check / time of use Script mit Root-Rechten

    import os filename = 'example.txt' if not os.path.isfile(filename): # file does not exist, create it with open(filename, 'w') as f: f.write('example') Angriff (unpriviligiert) $ ln -s /etc/passwd example.txt $ cat /etc/passwd example
  13. time of check / time of use Python 3.3+ import

    os filename = 'example.txt' try: with open(filename, 'x') as f: f.write('example') except FileExistsError: pass Python < 3.3 import os, errno filename = 'example.txt' flags = os.O_WRONLY flags |= os.O_CREAT | os.O_TRUNC flags |= os.O_EXCL try: fd = os.open(filename, flags) except OSError as e: if e.errno != errno.EEXIST: raise else: with os.fdopen(fd, 'w') as f: f.write('example')
  14. directory traversal attack import os.path download_dir = '/path/to/download/directory' def download_file(filename):

    fullpath = os.path.join(download_dir, filename) with open(fullpath) as f: return f.read()
  15. directory traversal attack import os.path download_dir = '/path/to/download/directory' def download_file(filename):

    fullpath = os.path.join(download_dir, filename) with open(fullpath) as f: return f.read() http://www.example.org/download.py?filename=./etc/passwd http://www.example.org/download.py?filename=../etc/passwd http://www.example.org/download.py?filename=../../etc/passwd http://www.example.org/download.py?filename=../../../etc/passwd http://www.example.org/download.py?filename=../../../../etc/passwd
  16. gettext Pluralformen Language: de Plural-Forms: nplurals=2; plural=n != 1; Language:

    fr Plural-Forms: nplurals=2; plural=n > 1; Language: ru Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : \ n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2; Language: DoS Plural-Forms: nplurals=0; plural=42**42**42;
  17. Abstract Syntax Tree >>> import ast >>> tree = ast.parse("a

    + b", mode="eval") >>> ast.dump(tree) "Expression(body=BinOp( left=Name(id='a', ctx=Load()), op=Add(), right=Name(id='b', ctx=Load())))"
  18. Abstract Syntax Tree >>> tree = ast.parse("result = [s.strip() for

    s in somestring]", ... mode="exec") >>> ast.dump(tree) "Module(body=[Assign(targets=[Name(id='result', ctx=Store())], value=ListComp( elt=Call( func=Attribute(value=Name(id='s', ctx=Load()), attr='strip', ctx=Load()), args=[], keywords=[], starargs=None, kwargs=None), generators=[comprehension( target=Name(id='s', ctx=Store()), iter=Name(id='somestring', ctx=Load()), ifs=[])]) )])"
  19. Abstract Syntax Tree >>> import ast >>> tree = ast.parse("a

    + b", mode="eval") >>> code = compile(tree, "<ast>", "eval") >>> eval(code, {"a": 2, "b": 3}) 5 >>> ast.literal_eval("(1, 2, 3)") (1, 2, 3) >>> ast.literal_eval("(1, 2, int(3))") Traceback (most recent call last): ... ValueError: malformed node or string: <_ast.Call object at ...>
  20. import ast class ASTValidator(ast.NodeVisitor): allowed = (ast.BinOp, ast.Add, ast.Name) def

    visit(self, node): if not isinstance(node, self.allowed): raise SyntaxError(node) return super().visit(node) def visit_Name(self, node): if node.id not in {"a", "b"}: raise SyntaxError(node) return node def adder(codestring, a, b): tree = ast.parse(codestring, mode="eval") ASTValidator().generic_visit(tree) code = compile(tree, "<ast>", mode="eval") return eval(code, {"a": a, "b": b} AST Validator
  21. Abstract Syntax Tree >>> from astvalidator import adder >>> adder("a

    + b", 2, 3) 5 >>> adder("a + b + b", 2, 3) 8 >>> adder("a + b + -b", 2, 3) Traceback (most recent call last): ... SyntaxError: <_ast.UnaryOp object at 0x7f2b1756c3d0>
  22. XML exploits • billion laughs / exponential entity expansion •

    quadratic blowup entity expansion • DTD & external entity expansion (remote and local) • attribute blowup / attribute hash collision attack • decompression bomb (gzip) • XPath injection attacks • XInclude <xi:include /> • XMLSchema-Import <xs:import /> • XSLT features wie xalan/redirect, xalan/java
  23. Lösung XML bomb protection for Python stdlib modules • defusedxml

    • defusedexpat • libexpat patch Pakete auf PyPI, vielleicht Bestandteil von Python 3.4.
  24. XPath injection attack XML = b""" <users> <user name="bob" password="secret"

    role="common" /> <user name="admin" password="hidden" role="master" /> </users> """ QUERY = "/users/user[@name='{name}' and @password='{password}']/@role" >>> tree = lxml.etree.fromstring(XML) >>> tree.xpath(QUERY.format(name='bob', password='secret')) ['common']
  25. XPath injection attack >>> tree.xpath(QUERY.format(name="admin' or ('1'='0", ... password="') and

    '1'='1")) ['common'] /users/user[@name='admin' or ('1'='0' and @password='') and '1'='1']/@role
  26. XPath injection attack SECURE_QUERY = "/users/user[@name=$name and @password=$password]/@role" >>> tree.xpath(SECURE_QUERY,

    name='bob', password='secret') ['common'] >>> tree.xpath(SECURE_QUERY, name="admin' or ('1'='1", ... password="') and '1'='1") []
  27. XSL(T) <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime" xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object" exclude-result-prefixes="rt ob"> <xsl:template match="/">

    <xsl:variable name="runtimeObject" select="rt:getRuntime()"/> <xsl:variable name="command" select="rt:exec($runtimeObject, &apos;/usr/bin/zenity --error --text XSLT_is_insecure&apos;)"/> <xsl:variable name="commandAsString" select="ob:toString($command)"/> <xsl:value-of select="$commandAsString"/> </xsl:template> </xsl:stylesheet>
  28. Hash collision attack 200.000 multi-collisions à 10 bytes roughly 2

    MB 40.000.000.000 string comparisons On a 1GHz machine, this is at least 40s (reasonable-sized attack strings only for 32 bits)
  29. Fowler-Noll-Vo Algorithmus def fnv(p): if len(p) == 0: return 0

    mask = 2 * sys.maxsize + 1 x = hashsecret.prefix x = (x ^ (ord(p[0]) << 7)) & mask for c in p: x = ((1000003 * x) ^ ord(c)) & mask x = (x ^ len(p)) & mask x = (x ^ hashsecret.suffix) & mask return x
  30. String-Vergleich def bytes__eq__(a, b): if a is b: return True

    if len(a) != len(b): return False return memcmp(a, b, len(a)) == 0 def memcmp(a, b, length): for i in range(length): diff = ord(a[i]) - ord(b[i]) if diff: return diff return 0
  31. Hash collision demo >>> def demo_hash(s): ... return sum(ord(c) for

    c in s) >>> demo_hash('de') # 100 + 101 201 >>> mask = dct.slots() - 1 >>> demo_hash('de') & mask # (100 + 101) & 7 1
  32. Lösungswege • Datenstruktur mit O(log n) Worst-Case – Trie (radix

    tree, patricia trie, marisa trie) – Binary Tree • Limitierung auf Applikationsebene • collision counting • kryptographische Hashfunktion
  33. PEP 456 Secure and interchangeable hash algorithm ersetzt Fowler-Noll-Vo durch

    Daniel J. Bernsteins SipHash24 cryptographic pseudo-random function / keyed hashing algorithm
  34. Kryptographie National Institute of Standards and Technology vs. National Security

    Agency The math is good, but math has no agency. Code has agency, and the code has been subverted. (Bruce Schneier)
  35. Bausteine • symmetric ciphers (block, stream) – AES, Blowfish, DES,

    3DES, IDEA, RC4, Salsa20 • cipher modes – ECB, CBC, CFB, OFB, CTR • authenticated encryption – EtM, E&M, MtE, GCM, CCM, OCB • padding – PKCS#7, bit padding, ANSI X.923
  36. Bausteine • cryptographic hash function – MD5, SHA-1, SHA-2 family,

    SHA-3 (Keccak) • message authentication code – HMAC, CMAC, Poly1305-AES • asymmetric encryption – RSA, ElGamal • public key schemes (PKCS#1) – RSAES-OAEP, RSASSA-PSS
  37. Bausteine • key agreement – Diffie-Hellman, ECDH, ECDHE • digital

    signature – DSA, ECDSA, ElGamal signature scheme • KDF / key stretching • CPRNG, nonce, IV • ASN.1, X.509, OIDs ...
  38. Schau mal Mama, ich benutze AES import os from PIL

    import Image from Crypto.Cipher import AES img = Image.open("image.png") key = os.urandom(16) cipher = AES.new(key) cdata = cipher.encrypt(img.tobytes()) encrypted = Image.frombytes(img.mode, img.size, cdata) encrypted.show()
  39. Merkle–Damgård Konstruktion • Algorithmus für kollisionssichere, kryptographischer Hash-Funktionen • MD5,

    SHA-1, SHA-2 Familie • Schwächen wie length extensions attack sha1(secret + message) → digest sha1(secret + message + padding + extension) → altdigest
  40. length extension attack >>> import hashlib >>> secret = 'pycon2013'

    >>> message = '3. Deutsche Python-Konferenz' >>> digest = hashlib.new('sha1', secret + message).hexdigest() >>> digest '63d678c849bd6a6c5e9d4966fe29e72fe9d45a0e' request.py?message=3.+Deutsche+Python-Konferenz& digest=63d678c849bd6a6c5e9d4966fe29e72fe9d45a0e >>> digest = hashlib.new('sha1', secret + qs['message']).hexdigest() >>> digest == qs['digest'] True
  41. length extension attack >>> ext = shaext(message, len(secret), digest) >>>

    ext.add("You have been hacked!") >>> new_message, new_digest = ext.final() >>> new_message '3. Deutsche Python-Konferenz\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x28 You have been hacked!' >>> new_digest '913ade7a09bb0506a5912169806f9bca2e93b506' >>> hashlib.new("sha1", secret + new_message).hexdigest() '913ade7a09bb0506a5912169806f9bca2e93b506'
  42. Lösung: MAC Algorithmus • Message Authentication Code – HMAC (keyed

    hash) – CMAC (block cipher MAC) HMAC(key, msg) = H(key + H(key + msg)) H((key ⊕ opad) + H((key ⊕ ipad) + message) opad = \x5c\x5c..., ipad = \x36\x36...
  43. random.random() • Mersenne Twister (MT19937) • schneller Algorithmus • optimiert

    für Monte-Carlo • nicht geeignet für Kryptographie – nach 624 Werten vollständig vorhersehbar
  44. cryptographic pseudo RNG • os.urandom() – /dev/urandom (Unix) – CryptGenRandom()

    (Windows) • ssl.RAND_pseudo_bytes() – Probleme mit fork() (issue #18747) • Dual Elliptic Curve Deterministic Random Bit Generator – NSA...
  45. 14.10.2013 Security Analysis of Pseudo-Random Number Generators with Input: /dev/random

    is not Robust Yevgeniy Dodis, David Pointcheval, Sylvain Ruhault, Damien Vergnaud, and Daniel Wichs http://eprint.iacr.org/2013/338.pdf
  46. Password Hashing Schlecht • MD5(password) • SHA1(salt + password) Gut

    • PBKDF2 • bcrypt • scrypt 348 Milliarden Hashes pro Sekunde (25 GPUs Stand 2012 für NTLM)
  47. PBKDF2 def pbkdf2_hmac_sha1(passord, salt, rounds, dklen=20): dk = b"" idx

    = 1 while len(dk) < dklen: rkey = prev = prf(password, salt + idx.to_bytes(4, "big")) for i in range(rounds - 1): prev = prf(password, prev) rkey = xor(rkey, prev) idx += 1 dk += rkey return dk[:dklen] def xor(a, b): return bytes(x ^ y for x, y in zip(a, b)) def prf(key, msg): return HMAC(key, msg, sha1).digest()
  48. pbkdf2_hmac() seit Samstag in Python 3.4 Issue #18582 (OpenSSL) Issue

    #19254 (Python hmac) bcrypt und scrypt sind in Planung...
  49. Password Hashing • salt mit os.urandom() erstellen • kein Applikations-Salt

    • hmac.compare_digest() verwenden • Rundenzahl mit Hardware skalieren – 100.000 Runden SHA-256 entsprechen ca. 200ms • Passwortlänge begrenzen (Django DoS) – OpenSSL, PyCrypto, pbkdf2.py • oder meine Implementation verwenden ;-)
  50. Timing Attacks def login(username, password): if username in user_database: if

    password == user_database[username]: return True return False
  51. Timing Attacks def login(username, password): if username in user_database: if

    password == user_database[username]: return True return False
  52. String-Vergleich def bytes__eq__(a, b): if a is b: return True

    if len(a) != len(b): return False return memcmp(a, b, len(a)) == 0 def memcmp(a, b, length): for i in range(length): diff = ord(a[i]) - ord(b[i]) if diff: return diff return 0
  53. constant timing comparison def constant_cmp(left, right): if len(left) != len(right):

    return False result = 0 for l, r in zip(left, right): result |= ord(l) ^ ord(r) return result == 0 Issue #15061 hmac.compare_digest() (ab Python 3.3)
  54. C Compiler #include <string.h> int get_secret_key(char *buffer); char* encrypt(const char

    *private, const char *msg, int msg_len); char* demo(const char *msg, int msg_len) { char secret[16]; char *output; get_secret_key(secret); output = encrypt(secret, msg, msg_len); /* wipe secret key from memory */ memset(secret, 0xaf, sizeof(secret)); return output; }
  55. $ clang -O0 -c memset_demo.c $ objdump -dr memset_demo.o 0000000000000000

    <demo>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 30 sub $0x30,%rsp 8: 48 8d 45 e0 lea -0x20(%rbp),%rax c: 48 89 7d f8 mov %rdi,-0x8(%rbp) 10: 89 75 f4 mov %esi,-0xc(%rbp) 13: 48 89 c7 mov %rax,%rdi 16: e8 00 00 00 00 callq 1b <demo+0x1b> 17: R_X86_64_PC32 get_secret_key-0x4 1b: 48 8d 7d e0 lea -0x20(%rbp),%rdi 1f: 48 8b 75 f8 mov -0x8(%rbp),%rsi 23: 8b 55 f4 mov -0xc(%rbp),%edx 26: 89 45 d4 mov %eax,-0x2c(%rbp) 29: e8 00 00 00 00 callq 2e <demo+0x2e> 2a: R_X86_64_PC32 encrypt-0x4 2e: be af 00 00 00 mov $0xaf,%esi 33: 48 ba 10 00 00 00 00 movabs $0x10,%rdx 3a: 00 00 00 3d: 48 8d 7d e0 lea -0x20(%rbp),%rdi 41: 48 89 45 d8 mov %rax,-0x28(%rbp) 45: e8 00 00 00 00 callq 4a <demo+0x4a> 46: R_X86_64_PC32 memset-0x4 4a: 48 8b 45 d8 mov -0x28(%rbp),%rax 4e: 48 83 c4 30 add $0x30,%rsp 52: 5d pop %rbp 53: c3 retq
  56. $ clang -O3 -c memset_demo.c $ objdump -dr memset_demo.o 0000000000000000

    <demo>: 0: 55 push %rbp 1: 41 56 push %r14 3: 53 push %rbx 4: 48 83 ec 10 sub $0x10,%rsp 8: 89 f5 mov %esi,%ebp a: 48 89 fb mov %rdi,%rbx d: 4c 8d 34 24 lea (%rsp),%r14 11: 4c 89 f7 mov %r14,%rdi 14: e8 00 00 00 00 callq 19 <demo+0x19> 15: R_X86_64_PC32 get_secret_key-0x4 19: 4c 89 f7 mov %r14,%rdi 1c: 48 89 de mov %rbx,%rsi 1f: 89 ea mov %ebp,%edx 21: e8 00 00 00 00 callq 26 <demo+0x26> 22: R_X86_64_PC32 encrypt-0x4 26: 48 83 c4 10 add $0x10,%rsp 2a: 5b pop %rbx 2b: 41 5e pop %r14 2d: 5d pop %rbp 2e: c3 retq
  57. Dead Code Elimination #include <string.h> int get_secret_key(char *buffer); char* encrypt(const

    char *private, const char *msg, int msg_len); char* demo(const char *msg, int msg_len) { char secret[16]; char *output; get_secret_key(secret); output = encrypt(secret, msg, msg_len); /* wipe secret key from memory */ memset(secret, 0xaf, sizeof(secret)); return output; }
  58. Memory Barrier #include <string.h> int get_secret_key(char *buffer); char* encrypt(const char

    *private, const char *msg, int msg_len); char* demo(const char *msg, int msg_len) { char secret[16]; char *output; get_secret_key(secret); output = encrypt(secret, msg, msg_len); /* wipe secret key from memory */ memset(secret, 0xaf, sizeof(secret)); asm volatile("nop" : : "r"(&secret) : "memory"); return output; }
  59. $ clang -O3 -c memset_demo.c $ objdump -dr memset_demo.o 0000000000000000

    <demo>: 0: 55 push %rbp 1: 41 56 push %r14 3: 53 push %rbx 4: 48 83 ec 10 sub $0x10,%rsp 8: 89 f5 mov %esi,%ebp a: 48 89 fb mov %rdi,%rbx d: 4c 8d 34 24 lea (%rsp),%r14 11: 4c 89 f7 mov %r14,%rdi 14: e8 00 00 00 00 callq 19 <demo+0x19> 15: R_X86_64_PC32 get_secret_key-0x4 19: 4c 89 f7 mov %r14,%rdi 1c: 48 89 de mov %rbx,%rsi 1f: 89 ea mov %ebp,%edx 21: e8 00 00 00 00 callq 26 <demo+0x26> 22: R_X86_64_PC32 encrypt-0x4 26: 48 b9 af af af af af movabs $0xafafafafafafafaf,%rcx 2d: af af af 30: 48 89 4c 24 08 mov %rcx,0x8(%rsp) 35: 48 89 0c 24 mov %rcx,(%rsp) 39: 90 nop 3a: 48 83 c4 10 add $0x10,%rsp 3e: 5b pop %rbx 3f: 41 5e pop %r14 41: 5d pop %rbp 42: c3 retq
  60. $ clang -O3 -c memset_demo.c $ objdump -dr memset_demo.o 0000000000000000

    <demo>: 0: 55 push %rbp 1: 41 56 push %r14 3: 53 push %rbx 4: 48 83 ec 10 sub $0x10,%rsp 8: 89 f5 mov %esi,%ebp a: 48 89 fb mov %rdi,%rbx d: 4c 8d 34 24 lea (%rsp),%r14 11: 4c 89 f7 mov %r14,%rdi 14: e8 00 00 00 00 callq 19 <demo+0x19> 15: R_X86_64_PC32 get_secret_key-0x4 19: 4c 89 f7 mov %r14,%rdi 1c: 48 89 de mov %rbx,%rsi 1f: 89 ea mov %ebp,%edx 21: e8 00 00 00 00 callq 26 <demo+0x26> 22: R_X86_64_PC32 encrypt-0x4 26: 48 b9 af af af af af movabs $0xafafafafafafafaf,%rcx 2d: af af af 30: 48 89 4c 24 08 mov %rcx,0x8(%rsp) 35: 48 89 0c 24 mov %rcx,(%rsp) 39: 90 nop 3a: 48 83 c4 10 add $0x10,%rsp 3e: 5b pop %rbx 3f: 41 5e pop %r14 41: 5d pop %rbp 42: c3 retq
  61. Coverity Scan Issue #18339 CID 486776 >>> import pickle >>>

    import io >>> unpickler = pickle.Unpickler(io.BytesIO()) >>> unpickler.memo = {-1: None} Speicherzugriffsfehler (Speicherabzug geschrieben)