Caçando Macros do Office com Sysmon e Pandas.
Usando Pandas e Jupyter em todos os lugares
É engraçado como quando você fala sobre macros de escritório, nós da área de segurança sentimos um arrepio na espinha. Todos nós sabemos que existem várias maneiras de proteger uma empresa contra eles, mas uma coisa que tenho certeza é que, se dependesse de nós, todos preferiríamos que as macros fossem desativadas em nossa organização.
Para o bem ou para o mal, essas decisões têm que ser muito bem motivadas e, acima de tudo, o impacto na empresa tem que ser muito bem medido antes de qualquer decisão ser tomada.
Para auditar o uso de macros, não há muitas opções se não tivermos serviços em nuvem como o O365, mas tentaremos.
Vamos começar a aventura.
Primeira tentativa, extensão de arquivo.
A abordagem mais fácil pode ser pesquisar arquivos com macros em nossa organização pesquisando a extensão do arquivo, mas nunca é tão fácil.
Uma coisa que é importante saber, e muita gente não sabe, é que para um arquivo do Office conter macros ele deve ter extensões de arquivo muito específicas e outras extensões são incompatíveis com elas.
Por muitos anos, a Microsoft quis identificar documentos contendo macros com a letra “m” na extensão como docm, xlsm… e impossibilitando a execução de macros nas novas extensões contendo a letra “x” no final como docx, xlsx… mas como sempre com a Microsoft, a compatibilidade com versões anteriores funcionou aqui.
No formato de documento do Office 97–2003, podemos armazenar e executar macros, mantendo extensões simples como doc, xls… tornando impossível para os analistas de segurança saber se um documento pode conter macros ou não apenas usando a extensão do arquivo.
Segunda tentativa, regra Sygma e Sysmon
Após uma conversa com outros profissionais, eles me sugeriram uma regra Sygma escrita por Florian Roth . Nesta regra é possível ver que existem 3 bibliotecas que o office carrega quando o arquivo possui macros VBA.
- '*\VBE7.DLL*'
- '*\VBEUI.DLL*'
- '*\VBE7INTL.DLL*'
Isso parecia promissor se você tivesse o Sysmon ou pudesse implantá-lo.
<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>
Ocorreu-me testar a regra Sygma com diferentes arquivos de escritório e testar o que acontecia se esses arquivos fossem baixados ou não e esses foram os resultados.

Parece que nossa segunda abordagem também não é válida. Embora as bibliotecas VBE7.dll e VBEUI.dll sirvam para identificar arquivos com macros criadas localmente, quando um arquivo é baixado da internet, essas bibliotecas também são carregadas, mesmo que o arquivo não contenha macros.
O culpado por esse comportamento é o Mark-of-the-Web ( MOTW ) ou o fluxo de dados alternativo adicionado aos arquivos quando são baixados via navegador da Web, o que faz com que os arquivos sejam abertos em uma exibição protegida.
Como essa primeira tentativa não funcionou, pensei em tentar usar a mesma abordagem, mas com um escopo mais amplo, ou seja, olhar para todas as bibliotecas carregadas pelo Excel em cada uma dessas execuções e observar suas diferenças.
Terceira e última tentativa, Python e Pandas

Qualquer pessoa que tenha jogado com o Procmon e o carregamento de bibliotecas saberá que isso pode ser um pouco tedioso, pois cada execução do Excel carrega centenas de bibliotecas. Para nos ajudar nessa tarefa temos nosso terceiro protagonista Python com seu fiel amigo Jupyter .
Utilizaremos os eventos de carregamento Sysmon e DLL para encontrar diferenças entre as execuções do Office, desta forma poderemos identificar diferenças que possam indicar que um documento aberto continha ou não macros, independentemente de sua extensão.
Primeiro a regra Sysmon para monitorar as bibliotecas carregadas pelo arquivo Excel durante a abertura de um 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>

Para analisar os arquivos EVTX resultantes do Sysmon vamos usar a biblioteca PyEvtxParser , um velho conhecido que foi muito útil para mim no passado quando escrevi Grafiki . Aqui você encontra este Notebook no meu Github entre outros.
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
A próxima pergunta que nos fazemos é: haverá diferentes bibliotecas entre essas execuções que nos permitem determinar que um documento possui macros?
Para isso, o que vamos fazer é fazer uma comparação entre todas as medições, criando assim uma tabela comparativa que nos permitirá detectar anomalias. Na parte esquerda da tabela teremos a base do nosso comparativo, ou seja a parte “Quais bibliotecas contém esta execução” e na parte superior “Qual não contém esta execução”.
Para isso utilizaremos esta função simples que nos permite fazer um comparativo de DataFrames e manteremos apenas os que estiverem no primeiro 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
Neste ponto podemos concluir que esta forma não é válida para detectar documentos do Office com macros. Uma grande descoberta que pode nos poupar de gastar muito mais tempo implementando regras, analisando resultados e tirando conclusões sobre dados que acreditávamos serem confiáveis.
Bônus
Como a ideia com o Jupyter é gerar documentos reutilizáveis. Vamos implementar um pequeno menu que nos ajudará a ver detalhadamente cada uma das comparações.
Talvez neste caso não seja muito relevante mas este notebook é facilmente adaptável para monitorar outros tipos de execuções detectando por exemplo diferenças em conexões de rede, modificações de chave de registro, criação de pipes nomeados…
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)

Espero que tenham gostado e que, como eu, não confiem nos meus resultados e se atrevam a tentar por si mesmos, se fizerem e os resultados forem diferentes, por favor me avisem
Vejo você na próxima vez!