1- import itertools
21import logging
32import os
43import subprocess
4+ import time
55import zipfile
66from threading import Thread
77
@@ -88,12 +88,17 @@ class Evtx(Thread, Auxiliary):
8888 "Microsoft-Windows-DriverFrameworks-UserMode/Operational" ,
8989 ]
9090
91+ # Interval in seconds between periodic snapshots
92+ SNAPSHOT_INTERVAL = 30
93+
9194 def __init__ (self , options = None , config = None ):
9295 if options is None :
9396 options = {}
9497 Thread .__init__ (self )
9598 Auxiliary .__init__ (self , options , config )
9699 self .enabled = config .evtx
100+ self .do_run = True
101+ self .snapshot_count = 0
97102 self .startupinfo = subprocess .STARTUPINFO ()
98103 self .startupinfo .dwFlags |= subprocess .STARTF_USESHOWWINDOW
99104
@@ -181,27 +186,56 @@ def enable_advanced_logging(self):
181186 except Exception as err :
182187 log .error ("Cannot enable audit policy %s (%s) - %s" , description , guid , err )
183188
184- def collect_windows_logs (self ):
185- """Collect selected evtx files, as specified in self.windows_logs."""
186- logs_folder = None
189+ def export_windows_logs (self , output_zip ):
190+ """Export event logs using wevtutil epl (proper export, flushes buffers).
191+
192+ Unlike raw file copy, wevtutil epl ensures all buffered events are
193+ written and the exported file is a consistent snapshot.
194+
195+ """
196+ export_dir = os .path .join (os .environ .get ("TEMP" , "C:\\ Windows\\ Temp" ), "evtx_export" )
197+ os .makedirs (export_dir , exist_ok = True )
198+
199+ exported = []
200+ for channel in self .windows_logs :
201+ safe_name = channel .replace ("/" , "%4" ) + ".evtx"
202+ export_path = os .path .join (export_dir , safe_name )
203+ try :
204+ # Remove previous export if exists
205+ if os .path .exists (export_path ):
206+ os .unlink (export_path )
207+ cmd = f'wevtutil epl "{ channel } " "{ export_path } " /ow:true'
208+ result = subprocess .call (cmd , startupinfo = self .startupinfo ,
209+ timeout = 30 )
210+ if result == 0 and os .path .exists (export_path ):
211+ size = os .path .getsize (export_path )
212+ if size > 0 :
213+ exported .append ((export_path , safe_name ))
214+ except subprocess .TimeoutExpired :
215+ log .debug ("Timeout exporting %s" , channel )
216+ except Exception as err :
217+ log .debug ("Cannot export %s - %s" , channel , err )
218+
219+ if not exported :
220+ return
221+
222+ with zipfile .ZipFile (output_zip , "w" , zipfile .ZIP_DEFLATED ) as zip_obj :
223+ for export_path , safe_name in exported :
224+ try :
225+ zip_obj .write (export_path , safe_name )
226+ except Exception as err :
227+ log .debug ("Cannot add %s to zip - %s" , safe_name , err )
228+
229+ # Clean up exported files
230+ for export_path , _ in exported :
231+ try :
232+ os .unlink (export_path )
233+ except Exception :
234+ pass
187235 try :
188- logs_folder = "C:/windows/Sysnative/winevt/Logs"
189- os .listdir (logs_folder )
236+ os .rmdir (export_dir )
190237 except Exception :
191- logs_folder = "C:/Windows/System32/winevt/Logs"
192-
193- with zipfile .ZipFile (self .evtx_dump , "w" , zipfile .ZIP_DEFLATED ) as zip_obj :
194- for evtx_file_name , selected_evtx in itertools .product (os .listdir (logs_folder ), self .windows_logs ):
195- _selected_evtx = f"{ selected_evtx } .evtx"
196- _selected_evtx = _selected_evtx .replace ("/" , "%4" )
197- if _selected_evtx == evtx_file_name :
198- full_path = os .path .join (logs_folder , evtx_file_name )
199- if os .path .exists (full_path ):
200- log .debug ("Adding %s to zip dump" , full_path )
201- zip_obj .write (full_path , evtx_file_name )
202-
203- log .debug ("Uploading %s to host" , self .evtx_dump )
204- upload_to_host (self .evtx_dump , f"evtx/{ self .evtx_dump } " )
238+ pass
205239
206240 def wipe_windows_logs (self ):
207241 """Wipe sequentially Windows logs."""
@@ -213,16 +247,100 @@ def wipe_windows_logs(self):
213247 except Exception as err :
214248 log .error ("Module error - %s" , err )
215249
216- def run (self ):
217- if self .enabled :
218- self .enable_cmdline_logging ()
219- self .configure_log_sizes ()
220- self .enable_advanced_logging ()
250+ def take_snapshot (self ):
251+ """Export current logs to a local snapshot dir, then wipe.
252+
253+ Snapshots are kept locally on the guest and merged into a single
254+ evtx.zip at stop() time. This protects against malware clearing
255+ logs — each snapshot captures events since the last wipe.
256+ """
257+ self .snapshot_count += 1
258+ snapshot_dir = os .path .join (
259+ os .environ .get ("TEMP" , "C:\\ Windows\\ Temp" ),
260+ "evtx_snapshots" ,
261+ str (self .snapshot_count ),
262+ )
263+ os .makedirs (snapshot_dir , exist_ok = True )
264+
265+ try :
266+ for channel in self .windows_logs :
267+ safe_name = channel .replace ("/" , "%4" ) + ".evtx"
268+ export_path = os .path .join (snapshot_dir , safe_name )
269+ try :
270+ cmd = f'wevtutil epl "{ channel } " "{ export_path } " /ow:true'
271+ subprocess .call (cmd , startupinfo = self .startupinfo , timeout = 30 )
272+ except Exception :
273+ pass
274+
221275 self .wipe_windows_logs ()
222- return True
223- return False
276+ log .debug ("Took evtx snapshot %d" , self .snapshot_count )
277+ except Exception as err :
278+ log .error ("Failed to take evtx snapshot - %s" , err )
279+
280+ def run (self ):
281+ if not self .enabled :
282+ return False
283+
284+ self .enable_cmdline_logging ()
285+ self .configure_log_sizes ()
286+ self .enable_advanced_logging ()
287+ self .wipe_windows_logs ()
288+
289+ # Periodic snapshot loop — captures events even if malware wipes logs
290+ while self .do_run :
291+ for _ in range (self .SNAPSHOT_INTERVAL ):
292+ if not self .do_run :
293+ break
294+ time .sleep (1 )
295+ if self .do_run :
296+ self .take_snapshot ()
297+
298+ return True
224299
225300 def stop (self ):
226- if self .enabled :
227- self .collect_windows_logs ()
301+ self .do_run = False
302+ if not self .enabled :
303+ return True
304+
305+ # Take final snapshot of remaining events
306+ self .take_snapshot ()
307+
308+ # Merge all snapshots into a single evtx.zip.
309+ # Each snapshot is incremental (logs wiped after each), so we
310+ # include ALL snapshots. Since evtx files can't be concatenated,
311+ # we add each snapshot's evtx with a unique name (channel_N.evtx).
312+ snapshot_base = os .path .join (
313+ os .environ .get ("TEMP" , "C:\\ Windows\\ Temp" ),
314+ "evtx_snapshots" ,
315+ )
316+
317+ with zipfile .ZipFile (self .evtx_dump , "w" , zipfile .ZIP_DEFLATED ) as zip_obj :
318+ if os .path .isdir (snapshot_base ):
319+ for snap_name in sorted (os .listdir (snapshot_base ), key = lambda x : int (x ) if x .isdigit () else x ):
320+ snap_dir = os .path .join (snapshot_base , snap_name )
321+ if not os .path .isdir (snap_dir ):
322+ continue
323+ for evtx_file in os .listdir (snap_dir ):
324+ if not evtx_file .lower ().endswith (".evtx" ):
325+ continue
326+ full_path = os .path .join (snap_dir , evtx_file )
327+ if os .path .getsize (full_path ) == 0 :
328+ continue
329+ # Add with snapshot number prefix to avoid name collisions
330+ arc_name = f"{ snap_name } _{ evtx_file } "
331+ try :
332+ zip_obj .write (full_path , arc_name )
333+ except Exception :
334+ pass
335+
336+ if os .path .exists (self .evtx_dump ):
337+ upload_to_host (self .evtx_dump , f"evtx/{ self .evtx_dump } " )
338+
339+ # Clean up
340+ try :
341+ import shutil
342+ shutil .rmtree (snapshot_base , ignore_errors = True )
343+ except Exception :
344+ pass
345+
228346 return True
0 commit comments