Cazando Macros de Office con Sysmon y Pandas.

Dec 02 2022
Usando Pandas y Jupyter en todas partes Es curioso cómo cuando hablas de macros de oficina, a los que estamos en el campo de la seguridad nos da escalofríos. Todos sabemos que existen múltiples formas de proteger a una empresa contra ellos, pero de lo que estoy seguro es que, si fuera por nosotros, todos preferiríamos tener las macros deshabilitadas en nuestra organización.

Usando Pandas y Jupyter en todas partes

Es curioso cómo cuando hablas de macros de oficina, aquellos de nosotros en el campo de la seguridad sentimos un escalofrío en la columna vertebral. Todos sabemos que existen múltiples formas de proteger a una empresa contra ellos, pero de lo que estoy seguro es que, si fuera por nosotros, todos preferiríamos tener las macros deshabilitadas en nuestra organización.

Para bien o para mal, estas decisiones tienen que estar muy bien motivadas y, sobre todo, hay que medir muy bien el impacto en la empresa antes de tomar cualquier decisión.

Para auditar el uso de macros no hay muchas opciones si no disponemos de servicios en la nube como O365 pero lo intentaremos.

Comencemos la aventura.

Primer intento, extensión de archivo.

El enfoque fácil podría ser buscar en nuestra organización archivos con macros buscando la extensión del archivo, pero nunca es tan fácil.

Una cosa que es importante saber, y mucha gente no sabe, es que para que un archivo de Office contenga macros, debe tener extensiones de archivo muy específicas y otras extensiones son incompatibles con ellas.

Durante muchos años Microsoft ha querido identificar los documentos que contenían macros con la letra “m” en la extensión como docm, xlsm… e imposibilitando la ejecución de macros en las nuevas extensiones que contenían la letra “x” al final como docx, xlsx… pero como siempre con Microsoft, la compatibilidad con versiones anteriores hizo lo suyo aquí.

En el formato de documento de Office 97–2003, podemos almacenar y ejecutar macros mientras mantenemos extensiones simples como doc, xls... lo que hace imposible que los analistas de seguridad sepan si un documento puede contener macros o no solo usando la extensión de archivo.

Segundo intento, regla Sygma y Sysmon

Después de una conversación con otros profesionales, me sugieren una regla Sygma escrita por Florian Roth . En esta regla es posible ver que hay 3 bibliotecas que Office carga cuando un archivo tiene macros VBA.

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

Esto parecía prometedor si tiene Sysmon o puede implementarlo.

<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>

Se me ocurrió probar la regla Sygma con diferentes archivos de office y probar que pasaba si estos archivos se descargaban o no y estos eran los resultados.

Resultados.

Parece que nuestro segundo enfoque tampoco es válido. Aunque las bibliotecas VBE7.dll y VBEUI.dll sirven para identificar archivos con macros creadas localmente, cuando se descarga un archivo de Internet, estas bibliotecas también se cargan, incluso si el archivo no contiene macros.

El culpable de este comportamiento es Mark-of-the-Web ( MOTW ) o el flujo de datos alternativo agregado a los archivos cuando se descargan a través del navegador web, lo que hace que los archivos se abran en una vista protegida.

Como este primer intento no funcionó, pensé en intentar usar el mismo enfoque pero con un alcance más amplio, es decir, mirar todas las bibliotecas cargadas por Excel en cada una de estas ejecuciones y observar sus diferencias.

Tercer y último intento, Python y Pandas

Foto de Erik Mclean: https://www.pexels.com/es-es/foto/ciudad-carretera-hombre-arte-4065797/

Cualquiera que haya jugado con Procmon y la carga de bibliotecas sabrá que esto puede ser un poco tedioso ya que cada ejecución de Excel carga cientos de bibliotecas. Para ayudarnos en esta tarea contamos con nuestro tercer protagonista Python junto a su fiel amigo Jupyter .

Usaremos los eventos de carga de Sysmon y DLL para encontrar diferencias entre las ejecuciones de Office, de esta manera podríamos identificar diferencias que podrían indicar que un documento abierto contiene macros o no, independientemente de su extensión.

Primero, la regla Sysmon para monitorear las bibliotecas cargadas por el archivo de Excel durante la apertura de un documento.

<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

Para analizar los archivos EVTX resultantes de Sysmon vamos a utilizar la biblioteca PyEvtxParser , una vieja conocida que me resultó muy útil en el pasado cuando escribía Grafiki . Aquí puedes encontrar este Notebook en mi Github entre otros.

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 siguiente pregunta que nos hacemos es, ¿habrá diferentes bibliotecas entre estas ejecuciones que nos permitan determinar que un documento tiene macros?

Para ello lo que vamos a hacer es hacer una comparación entre todas las medidas creando así una tabla comparativa que nos permitirá detectar anomalías. En la parte izquierda de la tabla tendremos la base de nuestra comparativa, es decir la parte “Qué bibliotecas contiene esta ejecución” y en la parte superior “Cuál no contiene esta ejecución”.

Para ello utilizaremos esta sencilla función que nos permite hacer una comparativa de DataFrames y solo nos quedaremos con los que están en el primer operador.

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

Llegados a este punto podemos sacar la conclusión de que esta forma no es válida para detectar documentos de Office con macros. Un gran descubrimiento que nos puede ahorrar mucho más tiempo implementando reglas, analizando resultados y sacando conclusiones sobre datos que creíamos fiables.

Bono

Ya que la idea con Jupyter es generar documentos reutilizables. Vamos a implementar un pequeño menú que nos ayudará a ver en detalle cada una de las comparativas.

Quizás en este caso no sea muy relevante pero este portátil es fácilmente adaptable para monitorizar otro tipo de ejecuciones detectando por ejemplo diferencias en conexiones de red, modificaciones de claves de registro, creación de named pipes…

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)

Comparar menú

Espero que os haya gustado y que, como a mí, no os fiéis de mis resultados y os animéis a probarlo vosotros mismos, si lo hacéis y los resultados son diferentes, por favor, avísame

¡Hasta la próxima!