Chasse aux macros Office avec Sysmon et Pandas.

Dec 02 2022
Utiliser Pandas et Jupyter partout C'est drôle comme quand on parle de macros bureautiques, ceux d'entre nous dans le domaine de la sécurité ont un frisson dans le dos. Nous savons tous qu'il existe plusieurs façons de sécuriser une entreprise contre eux, mais une chose dont je suis sûr est que, si cela ne tenait qu'à nous, nous préférerions tous que les macros soient désactivées dans notre organisation.

Utiliser Pandas et Jupyter partout

C'est drôle comme quand on parle de macros de bureau, ceux d'entre nous dans le domaine de la sécurité ont un frisson dans le dos. Nous savons tous qu'il existe plusieurs façons de sécuriser une entreprise contre eux, mais une chose dont je suis sûr est que, si cela ne tenait qu'à nous, nous préférerions tous que les macros soient désactivées dans notre organisation.

Pour le meilleur ou pour le pire, ces décisions doivent être très bien motivées et, surtout, l'impact sur l'entreprise doit être très bien mesuré avant qu'une décision puisse être prise.

Pour auditer l'utilisation des macros, il n'y a pas beaucoup d'options si nous n'avons pas de services cloud tels que O365 mais nous allons l'essayer.

Commençons l'aventure.

Premier essai, extension de fichier.

L'approche la plus simple peut consister à rechercher dans notre organisation des fichiers contenant des macros en recherchant l'extension de fichier, mais ce n'est jamais aussi simple.

Une chose qu'il est important de savoir, et que beaucoup de gens ne savent pas, c'est que pour qu'un fichier Office contienne des macros, il doit avoir des extensions de fichier très spécifiques et d'autres extensions sont incompatibles avec elles.

Pendant de nombreuses années, Microsoft a voulu identifier les documents contenant des macros avec la lettre « m » dans l'extension comme docm, xlsm… et rendant impossible l'exécution de macros dans les nouvelles extensions contenant la lettre « x » à la fin comme docx, xlsx… mais comme toujours avec Microsoft, la rétrocompatibilité a fait son effet ici.

Dans le format de document Office 97–2003, nous sommes autorisés à stocker et à exécuter des macros tout en conservant des extensions simples telles que doc, xls… ce qui rend impossible pour les analystes de la sécurité de savoir si un document peut contenir des macros ou non uniquement en utilisant l'extension de fichier.

Deuxième essai, règle Sygma et Sysmon

Après une conversation avec d'autres professionnels, ils me proposent une règle Sygma rédigée par Florian Roth . Dans cette règle, il est possible de voir qu'il y a 3 bibliothèques que le bureau charge lorsqu'un fichier contient des macros VBA.

  • '*\VBE7.DLL*'
  • '*\VBEUI.DLL*'
  • '*\VBE7INTL.DLL*'

Cela semblait prometteur si vous avez Sysmon ou si vous pouvez le déployer.

<RuleGroup name="" groupRelation="or">
  <ImageLoad onmatch="include">
    <Rule name="Potential Macro file opened" groupRelation="or">
      <ImageLoaded condition="end with">vbeintl.dll</ImageLoaded>
      <ImageLoaded condition="end with">vbe7.dll</ImageLoaded>
      <ImageLoaded condition="end with">vbeui.dll</ImageLoaded>
    </Rule>
  </ImageLoad>
</RuleGroup>
<RuleGroup name="" groupRelation="or">
  <RegistryEvent onmatch="include">
    <TargetObject name="T1060,RunKey" condition="contains">Documents\TrustRecords</TargetObject>
  </RegistryEvent>
</RuleGroup>

Il m'est venu à l'esprit de tester la règle Sygma avec différents fichiers bureautiques et de tester ce qui se passait si ces fichiers étaient téléchargés ou non et voici les résultats.

Résultats.

Il semble que notre deuxième approche ne soit pas valide non plus. Bien que les bibliothèques VBE7.dll et VBEUI.dll servent à identifier les fichiers avec des macros créées localement, lorsqu'un fichier est téléchargé depuis Internet, ces bibliothèques sont également chargées, même si le fichier ne contient pas de macros.

Le coupable de ce comportement est le Mark-of-the-Web ( MOTW ) ou le flux de données alternatif ajouté aux fichiers lorsqu'ils sont téléchargés via un navigateur Web, ce qui entraîne l'ouverture des fichiers dans une vue protégée.

Comme cette première tentative n'a pas fonctionné, j'ai pensé essayer d'utiliser la même approche mais avec une portée plus large, c'est-à-dire regarder toutes les bibliothèques chargées par Excel dans chacune de ces exécutions et observer leurs différences.

Troisième et dernier essai, Python et Pandas

Photo d'Erik Mclean : https://www.pexels.com/es-es/foto/ciudad-carretera-hombre-arte-4065797/

Quiconque a joué avec Procmon et le chargement des bibliothèques saura que cela peut être un peu fastidieux car chaque exécution d'Excel charge des centaines de bibliothèques. Pour nous aider dans cette tâche, nous avons notre troisième protagoniste Python avec son fidèle ami Jupyter .

Nous utiliserons les événements de chargement Sysmon et DLL pour trouver les différences entre les exécutions d'Office, de cette façon nous pourrions identifier les différences qui pourraient indiquer qu'un document ouvert contient ou non des macros, indépendamment de son extension.

D'abord la règle Sysmon pour surveiller les bibliothèques chargées par le fichier Excel lors de l'ouverture d'un document.

<RuleGroup name="" groupRelation="or">
  <ImageLoad onmatch="include">
    <Rule name="Image Loaded by Excel" groupRelation="or">
      <Image condition="end with">excel.exe</Image>
    </Rule>
  </ImageLoad>
</RuleGroup>

Makeameme https://makeameme.org/meme/PANDAS-PANDAS-EVERYWHERE

Pour analyser les fichiers EVTX résultants de Sysmon, nous allons utiliser la bibliothèque PyEvtxParser , une vieille connaissance qui m'a été très utile dans le passé lorsque j'ai écrit Grafiki . Ici vous pouvez trouver ce Notebook sur mon Github entre autres.

def evtx_folder_to_dataframes(directory):
    dataframes_list_seven = {}

    for filename in os.listdir(directory):
        f = os.path.join(directory, filename)
        if os.path.isfile(f):
            events_one_five = []
            events_seven = []
            a = open(f, 'rb')
            parser = PyEvtxParser(a)

            for record in parser.records_json():
                event = json.loads(record['data'])
                #Image loaded
                if event["Event"]["System"]["EventID"] == 7:
                    event_list = [event["Event"]["EventData"]["UtcTime"],
                    event["Event"]["System"]["EventID"],
                    event["Event"]["EventData"]["ImageLoaded"],
                    event["Event"]["EventData"]["FileVersion"],
                    event["Event"]["EventData"]["Description"],
                    event["Event"]["EventData"]["Product"],
                    event["Event"]["EventData"]["Company"],
                    event["Event"]["EventData"]["OriginalFileName"],
                    event["Event"]["EventData"]["Hashes"],
                    event["Event"]["EventData"]["Signed"],
                    event["Event"]["EventData"]["Signature"],
                    event["Event"]["EventData"]["SignatureStatus"]]
                    events_seven.append(event_list)
            name = filename.split("\\")[-1].split(".")[-2]

            df_7 = pd.DataFrame.from_records(events_seven,
                                   columns=['UtcTime','EventID', 'ImageLoaded', 'FileVersion', 'Description',
                                            'Product', 'Company', 'OriginalFileName', 'Hashes', 'Signed',
                                           'Signature', 'SignatureStatus'])
            dataframes_list_seven[name] = df_7
            
    return dataframes_list_seven

df_count = pd.DataFrame(index=list(dataframes_list_seven.keys()), columns=["Count"])
for e in list(dataframes_list_seven):
    df_count["Count"][e] = dataframes_list_seven[e]["ImageLoaded"].nunique()

      
                
DLL count results

La question suivante que nous nous posons est la suivante : y aura-t-il différentes bibliothèques entre ces exécutions qui nous permettront de déterminer qu'un document contient des macros ?

Pour ce faire, nous allons faire une comparaison entre toutes les mesures, créant ainsi un tableau comparatif qui nous permettra de détecter les anomalies. Dans la partie gauche du tableau nous aurons la base de notre comparatif, c'est-à-dire la partie "Quelles bibliothèques contiennent cette exécution" et dans la partie supérieure "Qui ne contient pas cette exécution".

Pour cela nous allons utiliser cette fonction simple qui nous permet de faire un comparatif des DataFrames et nous ne garderons que celles qui sont dans le premier opérateur.

def compare_df(df_in, df_not_in):
    list = df_in.merge(df_not_in.drop_duplicates(), 
                       on=['ImageLoaded'], 
                       how='left', 
                       indicator=True) 
    list_min = list[list['_merge'] == 'left_only']["ImageLoaded"].unique()
    return(list_min)

df = pd.DataFrame(index=list(dataframes_list_seven.keys()), columns=list(dataframes_list_seven.keys()))

for e in list(dataframes_list_seven.keys()):
    for i in list(dataframes_list_seven.keys()):
        list_min = compare_df(dataframes_list_seven[e], dataframes_list_seven[i])
        df[i][e] = len(list_min)

      
                
Comparation result

À ce stade, nous pouvons tirer la conclusion que cette méthode n'est pas valable pour détecter les documents Office avec des macros. Une belle découverte qui peut nous éviter de passer beaucoup plus de temps à mettre en place des règles, à analyser des résultats et à tirer des conclusions sur des données que nous pensions fiables.

Bonus

Car l'idée avec Jupyter est de générer des documents réutilisables. Nous allons mettre en place un petit menu qui nous aidera à voir en détail chacune des comparaisons.

Peut-être que dans ce cas ce n'est pas très pertinent mais ce notebook est facilement adaptable pour surveiller d'autres types d'exécutions en détectant par exemple des différences de connexions réseau, des modifications de clé de registre, la création de tubes nommés…

def compare_events():
    import ipywidgets as widgets
    from IPython.display import display, Markdown, clear_output
    pd.set_option('display.max_rows', 500)
    output = widgets.Output()
    dfs = list(dataframes_list_seven.keys())
    columns = dataframes_list_seven[list(dataframes_list_seven.keys())[0]].columns
    in_widget = widgets.Dropdown(
                        options=dfs,
                        description='Events in:',
                        disabled=False)
    not_in_widget = widgets.Dropdown(
                        options=dfs,
                        description='And not in:',
                        disabled=False)
    columns = widgets.Dropdown(
                        options=columns,
                        description='Column:',
                        disabled=False)
    button = widgets.Button(description=f'List')
    display(in_widget, not_in_widget, columns, button, output)

    def _click_function(_):
        with output:
            clear_output()
            list = dataframes_list_seven[in_widget.value].merge(dataframes_list_seven[not_in_widget.value].drop_duplicates(), 
                                                          on=['ImageLoaded'], 
                                                          how='left', 
                                                          indicator=True) 
            list_min = list[list['_merge'] == 'left_only'][columns.value].unique()
            display(len(list_min))
            display(pd.DataFrame(list_min).style.set_properties(**{'text-align': 'left'}))

    button.on_click(_click_function)

Comparer le menu

J'espère que cela vous a plu et que, comme moi, vous ne faites pas confiance à mes résultats et que vous osez l'essayer par vous-mêmes, si vous le faites et que les résultats sont différents, faites-le moi savoir

À la prochaine!