So for some reason, avast and AVG, is flagging my patcher as virus
Avast
Win64:TrojanX-gen [Trj]
AVG
Win64:TrojanX-gen [Trj]
Command is:
nuitka --mingw64 --windows-disable-console
App code is
import hashlib
import json
import os
import time
from pathlib import Path
from stat import S_ISDIR, S_ISREG
import sys
import logging
import subprocess
import traceback
import paramiko
import psutil
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QTextEdit, QLabel, QDesktopWidget,
QGraphicsDropShadowEffect
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer, QPropertyAnimation, QEasingCurve
from PyQt5.QtGui import QMovie, QLinearGradient, QColor, QPalette, QBrush
import ctypes
# Set up logging configuration
enable_logging = False
if enable_logging:
logging.basicConfig(filename='patcher.log', level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
else:
logging.getLogger().addHandler(logging.NullHandler())
def get_project_root() -> str:
return str(Path(__file__).parent)
class FileHashUtility:
def __init__(self, sftp_instance=None):
self.sftp_instance = sftp_instance
self.excluded_extensions = ['.rar']
self.excluded_files = ['.gitignore', 'generate_hash.py', "hash_values.json"]
def calculate_file_hash(self, filepath, local=False):
hasher = hashlib.md5()
if local:
with open(filepath, 'rb') as file:
for chunk in iter(lambda: file.read(4096), b''):
hasher.update(chunk)
else:
with self.sftp_instance.open(filepath, 'rb') as file:
for chunk in iter(lambda: file.read(4096), b''):
hasher.update(chunk)
return hasher.hexdigest()
def generate_hash_values(self, directory):
hash_values = {}
for root, dirs, files in os.walk(directory):
for filename in files:
filepath = os.path.join(root, filename)
if self.should_ignore(filepath):
continue
hash_value = self.calculate_file_hash(filepath, local=True)
hash_values[filepath.replace(directory, '')[1:].replace('\', '/')] = hash_value
return hash_values
def should_ignore(self, filepath):
filename = os.path.basename(filepath)
if filename in self.excluded_files:
return True
_, ext = os.path.splitext(filename)
if ext.lower() in self.excluded_extensions:
return True
return False
def save_to_json(self, hash_values, filename, save=False):
if save:
with open(filename, 'w') as json_file:
json.dump(hash_values, json_file, indent=4)
class DownloaderThread(QThread):
status_updated = pyqtSignal(str)
download_finished = pyqtSignal(bool, float) # Signal to indicate download finished and the elapsed time
def __init__(self, sftp_client, local_directory, remote_directory):
super().__init__()
self.sftp_client = sftp_client
self.local_directory = local_directory
self.remote_directory = remote_directory
def run(self):
start_time = time.time()
self.sftp_client.get_r_portable(self.remote_directory, self.local_directory, self.status_updated.emit)
elapsed_time = time.time() - start_time
self.download_finished.emit(True, elapsed_time)
class DownloaderUI(QWidget):
def __init__(self, sftp_client, remote_targer_dir):
super().__init__()
self.sftp_client = sftp_client
self.local_directory = os.getcwd() # Set the local directory to the current working directory
self.exe_path = os.getcwd() + '\metin2client.exe'
print(self.exe_path)
self.init_ui()
self.remote_targer_dir = remote_targer_dir
def init_ui(self):
# Set window properties
self.setWindowTitle("SFTP Downloader")
self.setGeometry(0, 0, 800, 600) # Set initial window size
# Create layout
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
# Create gradient background
self.setAutoFillBackground(True)
palette = self.palette()
gradient = QLinearGradient(0, 0, 0, self.height())
gradient.setColorAt(0.0, QColor(255, 165, 0)) # Orange
gradient.setColorAt(1.0, QColor(128, 0, 128)) # Purple
palette.setBrush(QPalette.Window, QBrush(gradient))
self.setPalette(palette)
# Add title label
title_label = QLabel("M2 - Patcher", self)
title_label.setAlignment(Qt.AlignCenter)
# Styling the label with a rounded, friendly font and cool color
title_label.setStyleSheet('QLabel { font-family: "Segoe UI", sans-serif; font-size: 20pt; font-weight: bold; color: white; text-decoration-color: rgba(255, 255, 255, 0.5); border-radius: 15px; padding: 10px; box-shadow: 0 0 10px rgba(0, 0, 20, 0.5); }')
title_label.resize(202, 202)
# Creating and setting the shadow effect
shadow_effect = QGraphicsDropShadowEffect()
shadow_effect.setOffset(5, 5)
shadow_effect.setBlurRadius(20)
shadow_effect.setColor(QColor(0, 0, 20, 127)) # Semi-transparent dark shadow
title_label.setGraphicsEffect(shadow_effect)
layout.addWidget(title_label, alignment=Qt.AlignCenter)
# Add preview window with styles
self.preview_window = QTextEdit(self)
self.preview_window.setReadOnly(True)
self.preview_window.setStyleSheet('QTextEdit { border: 0.3px solid black; border-radius: 20px; padding: 20px; background-color: rgba(255, 255, 255, 0.95); font-family: "Segoe UI", sans-serif; font-size: 9pt; color: #333; margin-top: 20px; }')
self.preview_window.setGeometry(50, 50, 500, 300)
# Creating and setting the shadow effect
shadow_effect = QGraphicsDropShadowEffect()
shadow_effect.setOffset(5, 5)
shadow_effect.setBlurRadius(15)
self.preview_window.setGraphicsEffect(shadow_effect)
self.preview_window.setFixedSize(int(self.width() * 0.8), int(self.height() * 0.8))
self.preview_window.setAlignment(Qt.AlignCenter)
# Center the preview_window horizontally within the layout
layout.addWidget(self.preview_window, alignment=Qt.AlignCenter)
# Add loading animation
self.loading_label = QLabel(self)
self.loading_label.setAlignment(Qt.AlignCenter)
self.loading_animation = QMovie("loading.gif") # Replace "loading.gif" with your loading animation file
self.loading_label.setMovie(self.loading_animation)
layout.addWidget(self.loading_label)
self.loading_label.hide()
# Add start button
self.start_button = QPushButton("Start Download", self)
self.start_button.clicked.connect(self.start_download)
self.start_button.setStyleSheet('QPushButton { background-color: #4b4453; color: white; border: 1.5px solid rgba(255, 255, 255, 1); border-radius: 17px; padding: 15px 30px; font-family: "Segoe UI", sans-serif; font-size: 10pt; font-weight: bold; height: 20px; box-shadow: 10px 10px 15px rgba(17, 14, 16, 0.8), inset 0 -3px 3px rgba(0,0,0,0.3); transition: background-color 0.5s ease-in-out, color 0.5s ease-in-out, transform 0.5s ease-in-out, box-shadow 0.5s ease-in-out; } QPushButton:hover { background-color: white; color: #4CAF50; transform: translateY(-2px); box-shadow: 10px 10px 15px rgba(17, 14, 16, 0.5); } QPushButton:pressed { box-shadow: inset 0 -3px 3px rgba(0,0,0,0.5); transform: translateY(0); }')
shadow_effect = QGraphicsDropShadowEffect()
shadow_effect.setOffset(5, 5) # Similar to CSS box-shadow offset
shadow_effect.setBlurRadius(15) # Similar to CSS box-shadow blur radius
shadow_effect.setColor(QColor(17, 14, 16, 204)) # Similar to CSS box-shadow color, semi-transparent
self.start_button.setGraphicsEffect(shadow_effect)
layout.addWidget(self.start_button, alignment=Qt.AlignCenter)
def center_window(self):
screen = QDesktopWidget().screenGeometry()
size = self.geometry()
self.move((screen.width() - size.width()) // 2, (screen.height() - size.height()) // 2)
def start_download(self):
try:
self.preview_window.clear()
if "metin2_client" in os.getcwd():
self.preview_window.append(
"<span style='font-size: 14pt;'>Starting download...</span><br>")
self.start_button.setEnabled(False)
self.loading_label.show()
self.loading_animation.start()
self.downloader_thread = DownloaderThread(self.sftp_client, self.local_directory, self.remote_targer_dir)
self.downloader_thread.status_updated.connect(self.update_preview)
self.downloader_thread.download_finished.connect(self.download_finished)
self.downloader_thread.start()
else:
self.preview_window.append(
"<span style='font-size: 16pt; color: red; font-weight: bold; font-family: Arial, "
"sans-serif;'>Please add the patcher in 'metin2_client' directory</span><br> "
)
self.preview_window.append(
"<span style='font-size: 12pt; color: red; font-weight: bold; font-family: Arial, "
"sans-serif;'>Proceeding to close patcher in 2 seconds</span><br> "
)
QTimer.singleShot(3000, self.close)
except Exception as e:
error_msg = f"Error occurred: {str(e)}"
traceback_str = traceback.format_exc()
self.status_updated.emit(error_msg)
self.download_finished.emit(False, 0)
logging.info("Error occurred during download")
def update_preview(self, status):
self.preview_window.append(status)
def download_finished(self, success, elapsed_time):
self.loading_animation.stop()
self.loading_label.hide()
self.start_button.setEnabled(False)
if success:
self.preview_window.append(f"Download completed in {elapsed_time:.2f} seconds.")
# Use QTimer.singleShot for UI update
QTimer.singleShot(4000, self.launch_exe)
else:
QTimer.singleShot(1000, lambda: self.preview_window.append("Download failed."))
def launch_exe(self):
if self.exe_path:
self.preview_window.append("Launched metin2")
os.startfile(self.exe_path)
self.preview_window.append(":MT2 client launched, preparing to exit...")
QTimer.singleShot(1000, self.close)
class M2_SFTP_Client:
def __init__(self, hostname, username, password, port=22):
self.hostname = hostname
self.username = username
self.password = password
self.port = port
self.sftp_instance = None
self.transport = None
def connect(self):
try:
# Create a transport object
self.transport = paramiko.Transport((self.hostname, self.port))
self.transport.connect(username=self.username, password=self.password)
# Create an SFTP client
self.sftp_instance = paramiko.SFTPClient.from_transport(self.transport)
logging.info("Connected to SFTP server successfully.")
except paramiko.AuthenticationException:
error_msg = "Authentication failed. Please check your credentials."
logging.error(error_msg)
print(error_msg)
except paramiko.SSHException as ssh_exception:
error_msg = f"SSH exception occurred"
logging.error(error_msg)
print(error_msg)
except Exception as e:
error_msg = f"An error occurred during server connection"
logging.error(error_msg)
print(error_msg)
def get_list_of_files_in_current_dir(self):
if self.sftp_instance:
current_dir = self.sftp_instance.listdir()
logging.info(f"Current working directory files list: {current_dir}")
return current_dir
else:
error_msg = "SFTP connection not established. Please connect first."
logging.error(error_msg)
print(error_msg)
return []
def change_working_dir(self, directory):
if self.sftp_instance:
self.sftp_instance.chdir(directory)
else:
error_msg = "SFTP connection not established. Please connect first."
logging.error(error_msg)
print(error_msg)
def get_current_directory(self):
if self.sftp_instance:
current_dir = self.sftp_instance.getcwd()
logging.info(f"Current working directory: {current_dir}")
else:
error_msg = "SFTP connection not established. Please connect first."
logging.error(error_msg)
print(error_msg)
def get_r_portable2(self, remotedir, localdir, status_callback):
hash_file_path = 'hash_values.json'
try:
# print("hash files is", hash_file_path)
# print(self.sftp_instance.getcwd())
# print(hash_file_path)
print('cur w dir', self.sftp_instance.getcwd())
with self.sftp_instance.open(hash_file_path, 'r') as hash_file:
remote_hashes = json.load(hash_file)
# print("remote_hashesn", json.dumps(remote_hashes, indent=4))
except FileNotFoundError:
status_callback("hash_values.json not found on the server. Unable to compare hashes.")
return
except Exception as e:
status_callback(f"Error occurred while reading hash_values.json: {e}")
return
print(remotedir)
for entry in self.sftp_instance.listdir_attr(remotedir):
print(entry)
remotepath = remotedir + "/" + entry.filename
localpath = os.path.join(localdir, entry.filename)
mode = entry.st_mode
if S_ISDIR(mode):
try:
os.mkdir(localpath)
except OSError:
pass
self.get_r_portable(remotepath, localpath, status_callback)
elif S_ISREG(mode):
print('filename', localpath.replace(get_project_root(), '').replace('\', '/'))
print("projhect r oot", get_project_root())
print("getcwd", os.getcwd())
remote_hash = remote_hashes.get(localpath.replace(os.getcwd(), "").replace('\', '/'))
if remote_hash:
# Check if the local file exists
if os.path.exists(localpath):
# Calculate the hash of the local file
local_hash = self.calculate_file_hash(localpath, local=True)
status_callback(f"Checking file {localpath}")
# Compare the hashes
if remote_hash == local_hash:
status_callback(f"File {localpath} is up to date. Skipping download.")
else:
status_callback(f"File {localpath} has a different hash. Downloading.")
self.sftp_instance.get(remotepath, localpath)
else:
status_callback(f"File {localpath} does not exist locally. Downloading.")
self.sftp_instance.get(remotepath, localpath)
else:
status_callback(f"No hash found for {entry.filename} in hash_values.json. Deleting.")
if "patcher-source" not in os.getcwd():
if os.path.exists(localpath):
os.remove(localpath)
pass
def get_r_portable3(self, remotedir, localdir, status_callback):
try:
if not self.sftp_instance:
status_callback("SFTP connection not established. Please connect first.")
return
hash_file_path = 'hash_values.json'
try:
with self.sftp_instance.open(hash_file_path, 'r') as hash_file:
remote_hashes = json.load(hash_file)
except FileNotFoundError:
status_callback("hash_values.json not found on the server.")
return
except Exception as e:
status_callback(f"Error occurred while reading hash_values.json: {e}")
return
# Removing the path prefix from remote hash keys
remote_hashes = {key.replace('/home/ftp_user/', ''): value for key, value in remote_hashes.items()}
# Generate local hashes
utility = FileHashUtility()
directory_path = os.getcwd()
local_hashes = utility.generate_hash_values(directory_path)
# Removing cwd prefix from local hash keys
local_hashes = {key.replace(os.getcwd() + '/', ''): value for key, value in local_hashes.items()}
# Download or delete files based on hash comparison
for local_file, local_hash in local_hashes.items():
remote_hash = remote_hashes.get(local_file)
if remote_hash:
if remote_hash != local_hash:
remote_file_path = os.path.join('/home/ftp_user', local_file)
status_callback(f"Updating local file {local_file}.")
self.sftp_instance.get(remote_file_path, os.path.join(os.getcwd(), local_file))
else:
status_callback(f"Removing local file {local_file} not present on server.")
try:
# os.remove(os.path.join(os.getcwd(), local_file))
pass
except Exception as ex:
pass
# Remove local files that are not in the remote hashes
for local_file in set(local_hashes.keys()) - set(remote_hashes.keys()):
status_callback(f"Removing outdated local file {local_file}.")
try:
pass
# os.remove(os.path.join(os.getcwd(), local_file))
except Exception:
pass
except Exception as e:
status_callback(str(e))
time.sleep(102300)
def delete_unwanted_file(self, path, callback):
exceptions = ['m2-patcher.exe']
try:
if 'm2-patcher.exe' not in path:
callback(f"Removing unnecesary file {path}")
os.remove(path)
except Exception as e:
pass
def get_r_portable(self, remotedir, localdir, status_callback, remove=True):
status_callback(f"Checking local files, please wait")
if not self.sftp_instance:
status_callback("SFTP connection not established. Please connect first.")
return
# Load remote hashes from the server
hash_file_path = 'hash_values.json'
try:
with self.sftp_instance.open(hash_file_path, 'r') as hash_file:
remote_hashes = json.load(hash_file)
except FileNotFoundError:
status_callback("hash_values.json not found on the server.")
return
except Exception as e:
status_callback(f"Error occurred while reading hash_values.json: {e}")
return
utility = FileHashUtility()
local_directory_path = os.getcwd()
local_hashes = utility.generate_hash_values(local_directory_path)
# local_hashes = {key.replace(os.getcwd() + '/', '').replace('\', ''): value for key, value in local_hashes.items()}
local_files = local_hashes.keys()
for remote_file, remote_hash in remote_hashes.items():
local_file_to_download = os.path.join(localdir, remote_file)
local_directory_path = os.path.dirname(local_file_to_download)
if not os.path.exists(local_directory_path):
os.makedirs(local_directory_path)
print(f"Created directory: {local_directory_path}")
if remote_file not in local_files:
print(remote_file)
print(local_files)
self.sftp_instance.get(remote_file, local_file_to_download)
status_callback(f"- Downloading {local_file_to_download}")
else:
if remote_hash != local_hashes[remote_file]:
try:
status_callback(f"- Removing outdated file {local_file_to_download}")
if remove:
os.remove(local_file_to_download)
except Exception:
pass
status_callback(f"- Replacing outdated file {local_file_to_download}")
self.sftp_instance.get(remote_file, local_file_to_download)
else:
status_callback(f"- OK {local_file_to_download}")
if remove:
[self.delete_unwanted_file(os.path.join(localdir, key), status_callback) for key in local_hashes.keys() if key not in remote_hashes.keys()]
def calculate_file_hash(self, filepath, local=False):
hasher = hashlib.md5()
if local:
with open(filepath, 'rb') as file:
for chunk in iter(lambda: file.read(4096), b''):
hasher.update(chunk)
else:
with self.sftp_instance.open(filepath, 'rb') as file:
for chunk in iter(lambda: file.read(4096), b''):
hasher.update(chunk)
return hasher.hexdigest()
def disconnect(self):
if self.sftp_instance:
self.sftp_instance.close()
if self.transport:
self.transport.close()
logging.info("Disconnected from SFTP server.")
else:
logging.error("SFTP connection not established.")
def status_callback(text):
print(text)
if __name__ == "__main__":
if 0 == 0:
try:
app = QApplication(sys.argv)
sftp_client = M2_SFTP_Client("62.171.164.192", "ftp_user", "]]9-82hD$pAMVI94D3D=TdCd@5I$D?]")
sftp_client.connect()
sftp_client.change_working_dir("/client")
print(sftp_client.get_list_of_files_in_current_dir())
downloader_ui = DownloaderUI(sftp_client, '.')
downloader_ui.show()
sys.exit(app.exec_())
except Exception as e:
pass
if 0 == 1:
sftp_client = M2_SFTP_Client("xc", "t", "rq")
sftp_client.connect()
sftp_client.change_working_dir("/client")
sftp_client.get_r_portable('', os.getcwd(), status_callback=status_callback, remove=False)
if 0 == 1:
print("Started generating hashes")
if os.path.exists("hash_values.json"):
os.remove("hash_values.json")
print("Removed hash_values.json")
utility = FileHashUtility()
directory_path = os.getcwd()
hash_values = utility.generate_hash_values(directory_path)
json_filename = "hash_values.json"
utility.save_to_json(hash_values, json_filename)
print(f"Hash values saved to {json_filename}.")
I’ve developed a Python patcher application for updating files from a remote server. However, I’m encountering an issue where antivirus software, specifically Avast and AVG, flag the executable generated by the application as a virus.
Problem:
The patcher application downloads files from a remote server, updates local files, and launches an executable. While the application doesn’t directly interact with the operating system or other programs in a malicious manner, it’s still being flagged by antivirus software.
Disable the antivirus or use pyinstaller. A prompt similar to what you presented would be:
pip install pyinstaller
pyinstaller –onefile –noconsole NameHere.py
Remove is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.